|
Some checks failed
NYX Security Scan / nyx-scan (pull_request) Failing after 5m51s
BREAKING CHANGE: keyDir now defaults to null (ephemeral) instead of
'client_keys'. Clients that relied on keys persisting across restarts
must now pass keyDir explicitly.
The Python SDK defaults to key_dir=None: a key pair is generated in
memory for the session and never written to disk. The JS port defaulted
to 'client_keys' and always persisted, so merely constructing a client
wrote an RSA private key into the working directory. That is a weaker
default than the client it ports, and one users never asked for.
- keyDir?: string | null, defaulting to undefined. null and undefined
both mean ephemeral, matching Python's None.
- Persistent mode is unchanged when keyDir is set: load the existing
pair from that directory, otherwise generate and save one there.
- Browsers are always ephemeral; they have no filesystem.
Key rotation follows the same rule. It previously hardcoded
'client_keys' as its fallback directory, so an ephemeral client would
have started writing private keys to disk on the first rotation tick.
Rotated keys are now persisted only where keyDir or keyRotationDir is
explicitly configured.
Tests assert the intent (that saveKeys is never called) rather than
probing the filesystem, since a leftover client_keys/ from the old
default would otherwise make them pass or fail for the wrong reason.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|---|---|---|
| .forgejo/workflows | ||
| doc | ||
| examples | ||
| native | ||
| src | ||
| tests | ||
| .gitignore | ||
| CONTRIBUTING.md | ||
| jest.config.js | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| renovate.json | ||
| rollup.config.mjs | ||
| tsconfig.json | ||
NOMYO.js — Secure JavaScript Chat Client
OpenAI-compatible secure chat client with end-to-end encryption for NOMYO Inference Endpoints
- All prompts and responses are automatically encrypted and decrypted
- Hybrid encryption: AES-256-GCM payload + RSA-OAEP-SHA256 key exchange, 4096-bit keys
- Drop-in replacement for OpenAI's ChatCompletion API
- Works in both Node.js and browsers
Quick Start
Installation
npm install nomyo-js
Basic Usage (Node.js)
import { SecureChatCompletion } from 'nomyo-js';
const client = new SecureChatCompletion({
apiKey: process.env.NOMYO_API_KEY,
});
const response = await client.create({
model: 'Qwen/Qwen3-0.6B',
messages: [{ role: 'user', content: 'Hello!' }],
temperature: 0.7,
});
console.log(response.choices[0].message.content);
client.dispose();
Basic Usage (Browser)
<script type="module">
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.js';
const client = new SecureChatCompletion({
baseUrl: 'https://api.nomyo.ai',
apiKey: 'your-api-key',
});
const response = await client.create({
model: 'Qwen/Qwen3-0.6B',
messages: [{ role: 'user', content: 'What is 2+2?' }],
});
console.log(response.choices[0].message.content);
</script>
Documentation
Full documentation is in the doc/ directory:
- Getting Started — walkthrough for new users
- API Reference — complete constructor options, methods, types, and error classes
- Models — available models and selection guide
- Security Guide — encryption, memory protection, key management, compliance
- Rate Limits — limits, automatic retry behaviour, batch throttling
- Examples — 12+ code examples for common scenarios
- Troubleshooting — error reference and debugging tips
Security Features
Hybrid Encryption
- Payload encryption: AES-256-GCM (authenticated encryption)
- Key exchange: RSA-OAEP-SHA256
- Key size: 4096-bit RSA keys
- Scope: All communication is end-to-end encrypted
Key Management
- Ephemeral by default: A fresh key pair is generated in memory on first use and never written to disk. Set
keyDirto persist and reuse keys across runs (Node.js only); existing keys in that directory are reloaded automatically. - Password protection: Optional AES-encrypted private key files (minimum 8 characters).
- Secure permissions: Private key files saved at
0600(owner-only). - Auto-rotation: Keys rotate every 24 hours by default (configurable via
keyRotationInterval). - Explicit lifecycle: Call
dispose()to zero in-memory key material and stop the rotation timer.
Secure Memory
The library wraps all intermediate sensitive buffers (AES keys, plaintext payload, decrypted bytes) in SecureByteContext, which zeroes them in a finally block immediately after use.
Pure JavaScript cannot lock pages to prevent OS swapping (mlock). For environments where swap-file exposure is unacceptable, install the optional nomyo-native addon. Check the current protection level:
import { getMemoryProtectionInfo } from 'nomyo-js';
const info = getMemoryProtectionInfo();
// Without addon: { method: 'zero-only', canLock: false }
// With addon: { method: 'mlock', canLock: true }
Security Tiers
Pass security_tier per request to route inference to increasingly isolated hardware:
| Tier | Hardware | Use case |
|---|---|---|
"standard" |
GPU | General secure inference |
"high" |
CPU/GPU balanced | Sensitive business data |
"maximum" |
CPU only | HIPAA PHI, classified data |
const response = await client.create({
model: 'Qwen/Qwen3-0.6B',
messages: [{ role: 'user', content: 'Patient record summary...' }],
security_tier: 'maximum',
});
Usage Examples
With API Key
const client = new SecureChatCompletion({ apiKey: process.env.NOMYO_API_KEY });
Error Handling
import {
SecureChatCompletion,
AuthenticationError,
RateLimitError,
ForbiddenError,
} from 'nomyo-js';
try {
const response = await client.create({ model: 'Qwen/Qwen3-0.6B', messages: [...] });
} catch (err) {
if (err instanceof AuthenticationError) console.error('Check API key:', err.message);
else if (err instanceof RateLimitError) console.error('Rate limit hit:', err.message);
else if (err instanceof ForbiddenError) console.error('Model/tier mismatch:', err.message);
else throw err;
}
Per-Request Router Override
Send a single request to a different router without changing the main client:
const response = await client.create({
model: 'Qwen/Qwen3-0.6B',
messages: [{ role: 'user', content: 'Hello from secondary router' }],
base_url: 'https://secondary.nomyo.ai:12435', // temporary — main client unchanged
});
Tool / Function Calling
const response = await client.create({
model: 'Qwen/Qwen3-0.6B',
messages: [{ role: 'user', content: "What's the weather in Paris?" }],
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather information for a location',
parameters: {
type: 'object',
properties: { location: { type: 'string' } },
required: ['location'],
},
},
},
],
tool_choice: 'auto',
});
Thinking Models
const response = await client.create({
model: 'LiquidAI/LFM2.5-1.2B-Thinking',
messages: [{ role: 'user', content: 'Is 9.9 larger than 9.11?' }],
});
const { content, reasoning_content } = response.choices[0].message;
console.log('Reasoning:', reasoning_content);
console.log('Answer:', content);
Resource Management
const client = new SecureChatCompletion({ apiKey: process.env.NOMYO_API_KEY });
try {
const response = await client.create({ model: 'Qwen/Qwen3-0.6B', messages: [...] });
console.log(response.choices[0].message.content);
} finally {
client.dispose(); // zeros key material, stops rotation timer
}
Local Development (HTTP)
const client = new SecureChatCompletion({
baseUrl: 'http://localhost:12435',
allowHttp: true, // required — also prints a visible warning
});
API Reference
SecureChatCompletion — Constructor Options
new SecureChatCompletion(config?: ChatCompletionConfig)
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
'https://api.nomyo.ai' |
NOMYO router URL. Must be HTTPS in production. |
allowHttp |
boolean |
false |
Allow HTTP connections. Local development only. |
apiKey |
string |
undefined |
Bearer token for Authorization header. |
secureMemory |
boolean |
true |
Zero sensitive buffers immediately after use. |
timeout |
number |
60000 |
Request timeout in milliseconds. |
debug |
boolean |
false |
Print verbose logging to the console. |
keyDir |
string | null |
null |
Directory to load/save RSA keys on startup. Omit for ephemeral in-memory keys that are never written to disk. Node.js only. |
keyRotationInterval |
number |
86400000 |
Auto-rotate keys every N ms. 0 disables rotation. |
keyRotationDir |
string |
keyDir |
Directory for rotated key files. Rotated keys are only persisted when keyDir or keyRotationDir is set. Node.js only. |
keyRotationPassword |
string |
undefined |
Password for encrypted rotated key files. |
maxRetries |
number |
2 |
Extra retry attempts on 429/5xx/network errors. Exponential backoff (1 s, 2 s, …). |
Methods
create(request): Promise<ChatCompletionResponse>— send an encrypted chat completionacreate(request): Promise<ChatCompletionResponse>— alias forcreate()dispose(): void— zero key material and stop the rotation timer
create() Request Fields
All standard OpenAI fields (model, messages, temperature, top_p, max_tokens, stop, n, tools, tool_choice, user, frequency_penalty, presence_penalty, logit_bias) plus:
| Field | Description |
|---|---|
security_tier |
"standard" | "high" | "maximum" — hardware isolation level |
api_key |
Per-request API key override |
base_url |
Per-request router URL override — creates a temporary client, used once, then disposed |
SecureCompletionClient — Constructor Options
Lower-level client. All options above apply, with these differences:
| Option | Type | Default | Description |
|---|---|---|---|
routerUrl |
string |
'https://api.nomyo.ai' |
Base URL (baseUrl is renamed here) |
keySize |
2048 | 4096 |
4096 |
RSA modulus length |
Methods
generateKeys(options?)— generate a new RSA key pairloadKeys(privateKeyPath, publicKeyPath?, password?)— load existing PEM filesfetchServerPublicKey()— fetch the server's RSA public keyencryptPayload(payload)— encrypt a request payloaddecryptResponse(encrypted, payloadId)— decrypt a response bodysendSecureRequest(payload, payloadId, apiKey?, securityTier?)— full encrypt → POST → decrypt cycledispose()— zero key material and stop rotation timer
Secure Memory Public API
import {
getMemoryProtectionInfo,
disableSecureMemory,
enableSecureMemory,
SecureByteContext,
} from 'nomyo-js';
| Export | Description |
|---|---|
getMemoryProtectionInfo() |
Returns { method, canLock, isPlatformSecure, details? } |
disableSecureMemory() |
Disable global secure-memory zeroing |
enableSecureMemory() |
Re-enable global secure-memory zeroing |
SecureByteContext |
Low-level buffer wrapper — zeros in finally block |
Error Classes
import {
AuthenticationError, InvalidRequestError, RateLimitError,
ForbiddenError, ServerError, ServiceUnavailableError,
APIConnectionError, SecurityError, DisposedError, APIError,
} from 'nomyo-js';
| Class | HTTP | Thrown when |
|---|---|---|
AuthenticationError |
401 | Invalid or missing API key |
InvalidRequestError |
400 | Malformed request |
ForbiddenError |
403 | Model not allowed for the security tier |
RateLimitError |
429 | Rate limit exceeded (after all retries) |
ServerError |
500 | Internal server error (after all retries) |
ServiceUnavailableError |
503 | Backend unavailable (after all retries) |
APIError |
varies | Other HTTP errors |
APIConnectionError |
— | Network failure or timeout (after all retries) |
SecurityError |
— | HTTPS not used, header injection, or crypto failure |
DisposedError |
— | Method called after dispose() |
Platform Support
Node.js
- Minimum: Node.js 22.22.2+
- Recommended: Node.js 22 LTS or later
- Key storage: File system (
keyDirdirectory, defaultclient_keys/)
Browsers
- Supported: Chrome 37+, Firefox 34+, Safari 11+, Edge 79+
- Key storage: In-memory only (not persisted)
- Limitation: File-based key operations (
keyDir,loadKeys) are not available
Security Best Practices
- Always use HTTPS (
allowHttpisfalseby default) - Load API key from an environment variable, never hardcode it
- Use password-protected key files (
keyRotationPassword) - Store keys outside the project directory and outside version control
- Add
client_keys/and*.pemto.gitignore - Call
dispose()when the client is no longer needed - Use
security_tier: 'maximum'for HIPAA PHI or classified data - Consider the
nomyo-nativeaddon if swap-file exposure is unacceptable
License
See LICENSE file.