2026-01-17 12:02:08 +01:00
|
|
|
/**
|
|
|
|
|
* OpenAI-compatible secure chat completion API
|
|
|
|
|
* Provides a drop-in replacement for OpenAI's ChatCompletion API with end-to-end encryption
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-04 11:30:44 +01:00
|
|
|
import { SecureCompletionClient, generateUUID } from '../core/SecureCompletionClient';
|
2026-01-17 12:02:08 +01:00
|
|
|
import { ChatCompletionConfig } from '../types/client';
|
|
|
|
|
import { ChatCompletionRequest, ChatCompletionResponse } from '../types/api';
|
|
|
|
|
|
|
|
|
|
export class SecureChatCompletion {
|
|
|
|
|
private client: SecureCompletionClient;
|
|
|
|
|
private apiKey?: string;
|
2026-04-16 15:36:20 +02:00
|
|
|
/** Stored config used to spin up a temporary per-request instance when base_url is overridden */
|
|
|
|
|
private readonly _config: ChatCompletionConfig;
|
2026-01-17 12:02:08 +01:00
|
|
|
|
|
|
|
|
constructor(config: ChatCompletionConfig = {}) {
|
|
|
|
|
const {
|
2026-04-01 13:38:45 +02:00
|
|
|
baseUrl = 'https://api.nomyo.ai:12435',
|
2026-01-17 12:02:08 +01:00
|
|
|
allowHttp = false,
|
|
|
|
|
apiKey,
|
|
|
|
|
secureMemory = true,
|
fix:
Added DisposedError
Wrapped zeroMemory() in its own try/catch in finally
Generic error message; fixed ArrayBufferLike TypeScript type issue
Generic error message; password/salt/IV wrapped in SecureByteContext
Password ≥8 chars enforced; zeroKeys(); rotateKeys(); debug-gated logs; TS type fix
Zero source ArrayBuffer after req.write()
Added timeout, debug, keyRotationInterval, keyRotationDir, keyRotationPassword
dispose(), assertNotDisposed(), startKeyRotationTimer(), rotateKeys(); Promise-mutex on ensureKeys(); new URL() validation; CR/LF API key check; server error detail truncation; response schema validation; all console.log behind debugMode
Propagates new config fields; dispose()
Tests for dispose, timer, header injection, URL validation, error sanitization, debug flag
Tests for generic error messages, password validation, zeroKeys()
2026-04-01 14:28:05 +02:00
|
|
|
timeout,
|
|
|
|
|
debug,
|
|
|
|
|
keyRotationInterval,
|
|
|
|
|
keyRotationDir,
|
|
|
|
|
keyRotationPassword,
|
2026-04-16 15:36:20 +02:00
|
|
|
maxRetries,
|
|
|
|
|
keyDir,
|
2026-01-17 12:02:08 +01:00
|
|
|
} = config;
|
|
|
|
|
|
2026-04-16 15:36:20 +02:00
|
|
|
this._config = config;
|
2026-01-17 12:02:08 +01:00
|
|
|
this.apiKey = apiKey;
|
|
|
|
|
this.client = new SecureCompletionClient({
|
|
|
|
|
routerUrl: baseUrl,
|
|
|
|
|
allowHttp,
|
|
|
|
|
secureMemory,
|
fix:
Added DisposedError
Wrapped zeroMemory() in its own try/catch in finally
Generic error message; fixed ArrayBufferLike TypeScript type issue
Generic error message; password/salt/IV wrapped in SecureByteContext
Password ≥8 chars enforced; zeroKeys(); rotateKeys(); debug-gated logs; TS type fix
Zero source ArrayBuffer after req.write()
Added timeout, debug, keyRotationInterval, keyRotationDir, keyRotationPassword
dispose(), assertNotDisposed(), startKeyRotationTimer(), rotateKeys(); Promise-mutex on ensureKeys(); new URL() validation; CR/LF API key check; server error detail truncation; response schema validation; all console.log behind debugMode
Propagates new config fields; dispose()
Tests for dispose, timer, header injection, URL validation, error sanitization, debug flag
Tests for generic error messages, password validation, zeroKeys()
2026-04-01 14:28:05 +02:00
|
|
|
...(timeout !== undefined && { timeout }),
|
|
|
|
|
...(debug !== undefined && { debug }),
|
|
|
|
|
...(keyRotationInterval !== undefined && { keyRotationInterval }),
|
|
|
|
|
...(keyRotationDir !== undefined && { keyRotationDir }),
|
|
|
|
|
...(keyRotationPassword !== undefined && { keyRotationPassword }),
|
2026-04-16 15:36:20 +02:00
|
|
|
...(maxRetries !== undefined && { maxRetries }),
|
|
|
|
|
...(keyDir !== undefined && { keyDir }),
|
2026-01-17 12:02:08 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-04 11:30:44 +01:00
|
|
|
* Create a chat completion (matches OpenAI API).
|
|
|
|
|
*
|
|
|
|
|
* Supports additional NOMYO-specific fields:
|
|
|
|
|
* - `security_tier`: "standard" | "high" | "maximum" — controls hardware routing
|
|
|
|
|
* - `api_key`: per-request API key override (takes precedence over constructor key)
|
2026-04-16 15:36:20 +02:00
|
|
|
* - `base_url`: per-request router URL override (creates a temporary client for
|
|
|
|
|
* this single call, matching the Python SDK's `create(base_url=...)` behaviour)
|
2026-01-17 12:02:08 +01:00
|
|
|
*/
|
|
|
|
|
async create(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
2026-03-04 11:30:44 +01:00
|
|
|
const payloadId = generateUUID();
|
2026-01-17 12:02:08 +01:00
|
|
|
|
2026-03-04 11:30:44 +01:00
|
|
|
// Extract NOMYO-specific fields that must not go into the encrypted payload
|
2026-04-16 15:36:20 +02:00
|
|
|
const { security_tier, api_key, base_url, ...payload } = request as ChatCompletionRequest & {
|
2026-03-04 11:30:44 +01:00
|
|
|
security_tier?: string;
|
|
|
|
|
api_key?: string;
|
2026-04-16 15:36:20 +02:00
|
|
|
base_url?: string;
|
2026-03-04 11:30:44 +01:00
|
|
|
};
|
2026-01-17 12:02:08 +01:00
|
|
|
|
|
|
|
|
if (!payload.model) {
|
|
|
|
|
throw new Error('Missing required field: model');
|
|
|
|
|
}
|
|
|
|
|
if (!payload.messages || !Array.isArray(payload.messages)) {
|
|
|
|
|
throw new Error('Missing or invalid required field: messages');
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 15:36:20 +02:00
|
|
|
const apiKey = api_key ?? this.apiKey;
|
|
|
|
|
|
|
|
|
|
// Per-request base_url: spin up a temporary client for this one call,
|
|
|
|
|
// inheriting all other config from the current instance.
|
|
|
|
|
if (base_url !== undefined) {
|
|
|
|
|
const tempInstance = new SecureChatCompletion({
|
|
|
|
|
...this._config,
|
|
|
|
|
baseUrl: base_url,
|
|
|
|
|
apiKey: this.apiKey,
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const response = await tempInstance.client.sendSecureRequest(
|
|
|
|
|
payload,
|
|
|
|
|
payloadId,
|
|
|
|
|
apiKey,
|
|
|
|
|
security_tier
|
|
|
|
|
);
|
|
|
|
|
return response as unknown as ChatCompletionResponse;
|
|
|
|
|
} finally {
|
|
|
|
|
tempInstance.dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 12:02:08 +01:00
|
|
|
const response = await this.client.sendSecureRequest(
|
|
|
|
|
payload,
|
|
|
|
|
payloadId,
|
2026-03-04 11:30:44 +01:00
|
|
|
apiKey,
|
|
|
|
|
security_tier
|
2026-01-17 12:02:08 +01:00
|
|
|
);
|
|
|
|
|
|
2026-03-04 11:30:44 +01:00
|
|
|
return response as unknown as ChatCompletionResponse;
|
2026-01-17 12:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Async alias for create() (for compatibility with OpenAI SDK)
|
|
|
|
|
*/
|
|
|
|
|
async acreate(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
|
|
|
|
return this.create(request);
|
|
|
|
|
}
|
fix:
Added DisposedError
Wrapped zeroMemory() in its own try/catch in finally
Generic error message; fixed ArrayBufferLike TypeScript type issue
Generic error message; password/salt/IV wrapped in SecureByteContext
Password ≥8 chars enforced; zeroKeys(); rotateKeys(); debug-gated logs; TS type fix
Zero source ArrayBuffer after req.write()
Added timeout, debug, keyRotationInterval, keyRotationDir, keyRotationPassword
dispose(), assertNotDisposed(), startKeyRotationTimer(), rotateKeys(); Promise-mutex on ensureKeys(); new URL() validation; CR/LF API key check; server error detail truncation; response schema validation; all console.log behind debugMode
Propagates new config fields; dispose()
Tests for dispose, timer, header injection, URL validation, error sanitization, debug flag
Tests for generic error messages, password validation, zeroKeys()
2026-04-01 14:28:05 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release resources: stop key rotation timer and zero in-memory key material.
|
|
|
|
|
*/
|
|
|
|
|
dispose(): void {
|
|
|
|
|
this.client.dispose();
|
|
|
|
|
}
|
2026-01-17 12:02:08 +01:00
|
|
|
}
|