feat:
- added retry logic with exponential backoff - per request base_url setting - configurable key_dir - protocol downgrade protection - public secure memory API
This commit is contained in:
parent
3b1792e613
commit
76b2a284d5
5 changed files with 220 additions and 37 deletions
|
|
@ -10,6 +10,8 @@ import { ChatCompletionRequest, ChatCompletionResponse } from '../types/api';
|
||||||
export class SecureChatCompletion {
|
export class SecureChatCompletion {
|
||||||
private client: SecureCompletionClient;
|
private client: SecureCompletionClient;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
|
/** Stored config used to spin up a temporary per-request instance when base_url is overridden */
|
||||||
|
private readonly _config: ChatCompletionConfig;
|
||||||
|
|
||||||
constructor(config: ChatCompletionConfig = {}) {
|
constructor(config: ChatCompletionConfig = {}) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -22,8 +24,11 @@ export class SecureChatCompletion {
|
||||||
keyRotationInterval,
|
keyRotationInterval,
|
||||||
keyRotationDir,
|
keyRotationDir,
|
||||||
keyRotationPassword,
|
keyRotationPassword,
|
||||||
|
maxRetries,
|
||||||
|
keyDir,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
|
this._config = config;
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
this.client = new SecureCompletionClient({
|
this.client = new SecureCompletionClient({
|
||||||
routerUrl: baseUrl,
|
routerUrl: baseUrl,
|
||||||
|
|
@ -34,6 +39,8 @@ export class SecureChatCompletion {
|
||||||
...(keyRotationInterval !== undefined && { keyRotationInterval }),
|
...(keyRotationInterval !== undefined && { keyRotationInterval }),
|
||||||
...(keyRotationDir !== undefined && { keyRotationDir }),
|
...(keyRotationDir !== undefined && { keyRotationDir }),
|
||||||
...(keyRotationPassword !== undefined && { keyRotationPassword }),
|
...(keyRotationPassword !== undefined && { keyRotationPassword }),
|
||||||
|
...(maxRetries !== undefined && { maxRetries }),
|
||||||
|
...(keyDir !== undefined && { keyDir }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,18 +50,19 @@ export class SecureChatCompletion {
|
||||||
* Supports additional NOMYO-specific fields:
|
* Supports additional NOMYO-specific fields:
|
||||||
* - `security_tier`: "standard" | "high" | "maximum" — controls hardware routing
|
* - `security_tier`: "standard" | "high" | "maximum" — controls hardware routing
|
||||||
* - `api_key`: per-request API key override (takes precedence over constructor key)
|
* - `api_key`: per-request API key override (takes precedence over constructor key)
|
||||||
|
* - `base_url`: per-request router URL override (creates a temporary client for
|
||||||
|
* this single call, matching the Python SDK's `create(base_url=...)` behaviour)
|
||||||
*/
|
*/
|
||||||
async create(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
async create(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
||||||
const payloadId = generateUUID();
|
const payloadId = generateUUID();
|
||||||
|
|
||||||
// Extract NOMYO-specific fields that must not go into the encrypted payload
|
// Extract NOMYO-specific fields that must not go into the encrypted payload
|
||||||
const { security_tier, api_key, ...payload } = request as ChatCompletionRequest & {
|
const { security_tier, api_key, base_url, ...payload } = request as ChatCompletionRequest & {
|
||||||
security_tier?: string;
|
security_tier?: string;
|
||||||
api_key?: string;
|
api_key?: string;
|
||||||
|
base_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiKey = api_key ?? this.apiKey;
|
|
||||||
|
|
||||||
if (!payload.model) {
|
if (!payload.model) {
|
||||||
throw new Error('Missing required field: model');
|
throw new Error('Missing required field: model');
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +70,29 @@ export class SecureChatCompletion {
|
||||||
throw new Error('Missing or invalid required field: messages');
|
throw new Error('Missing or invalid required field: messages');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.client.sendSecureRequest(
|
const response = await this.client.sendSecureRequest(
|
||||||
payload,
|
payload,
|
||||||
payloadId,
|
payloadId,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ export class SecureCompletionClient {
|
||||||
private keyRotationTimer?: ReturnType<typeof setInterval>;
|
private keyRotationTimer?: ReturnType<typeof setInterval>;
|
||||||
private readonly keyRotationDir?: string;
|
private readonly keyRotationDir?: string;
|
||||||
private readonly keyRotationPassword?: string;
|
private readonly keyRotationPassword?: string;
|
||||||
|
private readonly maxRetries: number;
|
||||||
|
private readonly keyDir: string;
|
||||||
private _isHttps: boolean = true;
|
private _isHttps: boolean = true;
|
||||||
|
|
||||||
// Promise-based mutex: serialises concurrent ensureKeys() calls
|
// Promise-based mutex: serialises concurrent ensureKeys() calls
|
||||||
|
|
@ -88,6 +90,8 @@ export class SecureCompletionClient {
|
||||||
keyRotationInterval = 86400000, // 24 hours
|
keyRotationInterval = 86400000, // 24 hours
|
||||||
keyRotationDir,
|
keyRotationDir,
|
||||||
keyRotationPassword,
|
keyRotationPassword,
|
||||||
|
maxRetries = 2,
|
||||||
|
keyDir = 'client_keys',
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
this.debugMode = debug;
|
this.debugMode = debug;
|
||||||
|
|
@ -95,6 +99,8 @@ export class SecureCompletionClient {
|
||||||
this.keyRotationInterval = keyRotationInterval;
|
this.keyRotationInterval = keyRotationInterval;
|
||||||
this.keyRotationDir = keyRotationDir;
|
this.keyRotationDir = keyRotationDir;
|
||||||
this.keyRotationPassword = keyRotationPassword;
|
this.keyRotationPassword = keyRotationPassword;
|
||||||
|
this.maxRetries = maxRetries;
|
||||||
|
this.keyDir = keyDir;
|
||||||
this.keySize = keySize;
|
this.keySize = keySize;
|
||||||
this.allowHttp = allowHttp;
|
this.allowHttp = allowHttp;
|
||||||
this.secureMemory = secureMemory;
|
this.secureMemory = secureMemory;
|
||||||
|
|
@ -243,29 +249,29 @@ export class SecureCompletionClient {
|
||||||
private async _doEnsureKeys(): Promise<void> {
|
private async _doEnsureKeys(): Promise<void> {
|
||||||
if (this.keyManager.hasKeys()) return;
|
if (this.keyManager.hasKeys()) return;
|
||||||
|
|
||||||
// Try to load keys from default location (Node.js only)
|
// Try to load keys from the configured directory (Node.js only)
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
try {
|
try {
|
||||||
const fs = require('fs').promises as { access: (p: string) => Promise<void> };
|
const fs = require('fs').promises as { access: (p: string) => Promise<void> };
|
||||||
const path = require('path') as { join: (...p: string[]) => string };
|
const path = require('path') as { join: (...p: string[]) => string };
|
||||||
|
|
||||||
const privateKeyPath = path.join('client_keys', 'private_key.pem');
|
const privateKeyPath = path.join(this.keyDir, 'private_key.pem');
|
||||||
const publicKeyPath = path.join('client_keys', 'public_key.pem');
|
const publicKeyPath = path.join(this.keyDir, 'public_key.pem');
|
||||||
|
|
||||||
await fs.access(privateKeyPath);
|
await fs.access(privateKeyPath);
|
||||||
await fs.access(publicKeyPath);
|
await fs.access(publicKeyPath);
|
||||||
|
|
||||||
await this.loadKeys(privateKeyPath, publicKeyPath);
|
await this.loadKeys(privateKeyPath, publicKeyPath);
|
||||||
if (this.debugMode) console.log('Loaded existing keys from client_keys/');
|
if (this.debugMode) console.log(`Loaded existing keys from ${this.keyDir}/`);
|
||||||
return;
|
return;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
if (this.debugMode) console.log('No existing keys found, generating new keys...');
|
if (this.debugMode) console.log(`No existing keys found in ${this.keyDir}/, generating new keys...`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.generateKeys({
|
await this.generateKeys({
|
||||||
saveToFile: typeof window === 'undefined',
|
saveToFile: typeof window === 'undefined',
|
||||||
keyDir: 'client_keys',
|
keyDir: this.keyDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,6 +445,22 @@ export class SecureCompletionClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate version and algorithm to prevent downgrade attacks
|
||||||
|
const SUPPORTED_VERSION = '1.0';
|
||||||
|
const SUPPORTED_ALGORITHM = 'hybrid-aes256-rsa4096';
|
||||||
|
if (packageData.version !== SUPPORTED_VERSION) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported protocol version: '${String(packageData.version)}'. ` +
|
||||||
|
`Expected: '${SUPPORTED_VERSION}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (packageData.algorithm !== SUPPORTED_ALGORITHM) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported encryption algorithm: '${String(packageData.algorithm)}'. ` +
|
||||||
|
`Expected: '${SUPPORTED_ALGORITHM}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const encryptedPayload = packageData.encrypted_payload as Record<string, unknown>;
|
const encryptedPayload = packageData.encrypted_payload as Record<string, unknown>;
|
||||||
if (typeof encryptedPayload !== 'object' || encryptedPayload === null) {
|
if (typeof encryptedPayload !== 'object' || encryptedPayload === null) {
|
||||||
throw new Error('Invalid encrypted_payload: must be an object');
|
throw new Error('Invalid encrypted_payload: must be an object');
|
||||||
|
|
@ -515,6 +537,9 @@ export class SecureCompletionClient {
|
||||||
/**
|
/**
|
||||||
* Send a secure chat completion request to the router.
|
* Send a secure chat completion request to the router.
|
||||||
*
|
*
|
||||||
|
* Retries on transient errors (429, 500, 502, 503, 504, network errors)
|
||||||
|
* with exponential backoff matching the Python SDK's `max_retries` behaviour.
|
||||||
|
*
|
||||||
* @param securityTier Optional routing tier: "standard" | "high" | "maximum"
|
* @param securityTier Optional routing tier: "standard" | "high" | "maximum"
|
||||||
*/
|
*/
|
||||||
async sendSecureRequest(
|
async sendSecureRequest(
|
||||||
|
|
@ -545,8 +570,6 @@ export class SecureCompletionClient {
|
||||||
|
|
||||||
await this.ensureKeys();
|
await this.ensureKeys();
|
||||||
|
|
||||||
const encryptedPayload = await this.encryptPayload(payload);
|
|
||||||
|
|
||||||
const publicKeyPem = await this.keyManager.getPublicKeyPEM();
|
const publicKeyPem = await this.keyManager.getPublicKeyPEM();
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'X-Payload-ID': payloadId,
|
'X-Payload-ID': payloadId,
|
||||||
|
|
@ -565,31 +588,86 @@ export class SecureCompletionClient {
|
||||||
const url = `${this.routerUrl}/v1/chat/secure_completion`;
|
const url = `${this.routerUrl}/v1/chat/secure_completion`;
|
||||||
if (this.debugMode) console.log(`Target URL: ${url}`);
|
if (this.debugMode) console.log(`Target URL: ${url}`);
|
||||||
|
|
||||||
let response: { statusCode: number; body: ArrayBuffer };
|
// Retry loop — mirrors Python SDK's max_retries + exponential backoff.
|
||||||
try {
|
// The payload is re-encrypted on every attempt so each attempt gets a
|
||||||
response = await this.httpClient.post(url, {
|
// fresh AES key and nonce (the HTTP client zeros the buffer after write).
|
||||||
headers,
|
let lastError: Error = new APIConnectionError('Request failed');
|
||||||
body: encryptedPayload,
|
|
||||||
timeout: this.requestTimeout,
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
||||||
});
|
if (attempt > 0) {
|
||||||
} catch (error) {
|
const delaySec = Math.pow(2, attempt - 1); // 1 s, 2 s, 4 s, …
|
||||||
if (error instanceof Error) {
|
if (this.debugMode) {
|
||||||
if (error.message === 'Request timeout') {
|
console.warn(
|
||||||
throw new APIConnectionError('Connection to server timed out');
|
`Retrying request (attempt ${attempt}/${this.maxRetries}) ` +
|
||||||
|
`after ${delaySec}s...`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new APIConnectionError(`Failed to connect to router: ${error.message}`);
|
await new Promise<void>(resolve => setTimeout(resolve, delaySec * 1000));
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
|
// Re-encrypt each attempt (throws non-retryable errors like SecurityError
|
||||||
|
// or DisposedError — let those propagate immediately)
|
||||||
|
const encryptedPayload = await this.encryptPayload(payload);
|
||||||
|
|
||||||
|
let response: { statusCode: number; body: ArrayBuffer };
|
||||||
|
try {
|
||||||
|
response = await this.httpClient.post(url, {
|
||||||
|
headers,
|
||||||
|
body: encryptedPayload,
|
||||||
|
timeout: this.requestTimeout,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Network / timeout errors from the HTTP client
|
||||||
|
let connError: APIConnectionError;
|
||||||
|
if (error instanceof Error) {
|
||||||
|
connError = error.message === 'Request timeout'
|
||||||
|
? new APIConnectionError('Connection to server timed out')
|
||||||
|
: new APIConnectionError(`Failed to connect to router: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
connError = new APIConnectionError('Failed to connect to router: unknown error');
|
||||||
|
}
|
||||||
|
lastError = connError;
|
||||||
|
if (attempt < this.maxRetries) {
|
||||||
|
if (this.debugMode) console.warn(`Network error on attempt ${attempt}: ${connError.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debugMode) console.log(`HTTP Status: ${response.statusCode}`);
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return await this.decryptResponse(response.body, payloadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = this.buildErrorFromResponse(response);
|
||||||
|
|
||||||
|
if (this.isRetryableError(err) && attempt < this.maxRetries) {
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.warn(`Got retryable status ${response.statusCode}: retrying...`);
|
||||||
|
}
|
||||||
|
lastError = err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debugMode) console.log(`HTTP Status: ${response.statusCode}`);
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
/**
|
||||||
return await this.decryptResponse(response.body, payloadId);
|
* Return true for errors that warrant a retry (transient failures).
|
||||||
}
|
* Non-retryable errors (auth, bad request, forbidden, etc.) propagate immediately.
|
||||||
|
*/
|
||||||
// Map HTTP error status codes to typed errors
|
private isRetryableError(error: Error): boolean {
|
||||||
throw this.buildErrorFromResponse(response);
|
if (error instanceof APIConnectionError) return true;
|
||||||
|
if (error instanceof RateLimitError) return true;
|
||||||
|
if (error instanceof ServerError) return true;
|
||||||
|
if (error instanceof ServiceUnavailableError) return true;
|
||||||
|
// 502 Bad Gateway and 504 Gateway Timeout fall through as generic APIError
|
||||||
|
if (error instanceof APIError && (error.statusCode === 502 || error.statusCode === 504)) return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,46 @@
|
||||||
/**
|
/**
|
||||||
* Secure memory interface and context manager
|
* Secure memory interface, context manager, and public API.
|
||||||
*
|
*
|
||||||
* IMPORTANT: This is a pure JavaScript implementation that provides memory zeroing only.
|
* IMPORTANT: This is a pure JavaScript implementation that provides memory zeroing only.
|
||||||
* OS-level memory locking (mlock) is NOT implemented in this version.
|
* OS-level memory locking (mlock) is NOT implemented in this version.
|
||||||
*
|
*
|
||||||
* For production use, consider implementing a native addon for true memory locking.
|
* For production use, consider implementing a native addon for true memory locking.
|
||||||
* See SECURITY.md for details on memory protection limitations.
|
* See SECURITY.md for details on memory protection limitations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ProtectionInfo } from '../../types/crypto';
|
import { ProtectionInfo } from '../../types/crypto';
|
||||||
|
|
||||||
|
// ─── Global secure-memory state ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Module-level flag, mirrors Python's global _secure_memory.enabled. */
|
||||||
|
let _globalSecureMemoryEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable secure memory operations globally.
|
||||||
|
* Affects new SecureByteContext instances created without an explicit `useSecure` argument.
|
||||||
|
* Existing client instances are unaffected (they pass `useSecure` explicitly).
|
||||||
|
* Mirrors Python's `disable_secure_memory()`.
|
||||||
|
*/
|
||||||
|
export function disableSecureMemory(): void {
|
||||||
|
_globalSecureMemoryEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-enable secure memory operations globally.
|
||||||
|
* Mirrors Python's `enable_secure_memory()`.
|
||||||
|
*/
|
||||||
|
export function enableSecureMemory(): void {
|
||||||
|
_globalSecureMemoryEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return information about the memory protection capabilities available on this
|
||||||
|
* platform/runtime. Mirrors Python's `get_memory_protection_info()`.
|
||||||
|
*/
|
||||||
|
export function getMemoryProtectionInfo(): ProtectionInfo {
|
||||||
|
return createSecureMemory().getProtectionInfo();
|
||||||
|
}
|
||||||
|
|
||||||
export interface SecureMemory {
|
export interface SecureMemory {
|
||||||
/**
|
/**
|
||||||
* Zero memory (fill with zeros)
|
* Zero memory (fill with zeros)
|
||||||
|
|
@ -24,15 +55,19 @@ export interface SecureMemory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Secure byte context manager
|
* Secure byte context manager.
|
||||||
* Ensures memory is zeroed even if an exception occurs (similar to Python's context manager)
|
* Ensures memory is zeroed even if an exception occurs (analogous to Python's
|
||||||
|
* `secure_bytearray()` context manager and `SecureBuffer` class).
|
||||||
|
*
|
||||||
|
* When `useSecure` is omitted, the module-level global flag set by
|
||||||
|
* `disableSecureMemory()` / `enableSecureMemory()` is consulted.
|
||||||
*/
|
*/
|
||||||
export class SecureByteContext {
|
export class SecureByteContext {
|
||||||
private data: ArrayBuffer;
|
private data: ArrayBuffer;
|
||||||
private secureMemory: SecureMemory;
|
private secureMemory: SecureMemory;
|
||||||
private useSecure: boolean;
|
private useSecure: boolean;
|
||||||
|
|
||||||
constructor(data: ArrayBuffer, useSecure: boolean = true) {
|
constructor(data: ArrayBuffer, useSecure: boolean = _globalSecureMemoryEnabled) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.useSecure = useSecure;
|
this.useSecure = useSecure;
|
||||||
this.secureMemory = createSecureMemory();
|
this.secureMemory = createSecureMemory();
|
||||||
|
|
|
||||||
10
src/index.ts
10
src/index.ts
|
|
@ -13,3 +13,13 @@ export * from './types/crypto';
|
||||||
|
|
||||||
// Export errors
|
// Export errors
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
|
|
||||||
|
// Secure memory public API — mirrors Python's get_memory_protection_info(),
|
||||||
|
// disable_secure_memory(), enable_secure_memory(), and SecureBuffer/secure_bytearray()
|
||||||
|
export {
|
||||||
|
getMemoryProtectionInfo,
|
||||||
|
disableSecureMemory,
|
||||||
|
enableSecureMemory,
|
||||||
|
SecureByteContext,
|
||||||
|
createSecureMemory,
|
||||||
|
} from './core/memory/secure';
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,20 @@ export interface ClientConfig {
|
||||||
|
|
||||||
/** Password to encrypt rotated private key files */
|
/** Password to encrypt rotated private key files */
|
||||||
keyRotationPassword?: string;
|
keyRotationPassword?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory to load/save RSA keys on startup.
|
||||||
|
* If the directory contains an existing key pair it is loaded; otherwise a
|
||||||
|
* new pair is generated and saved there. Default: 'client_keys'.
|
||||||
|
* Matches the Python SDK's `key_dir` constructor parameter.
|
||||||
|
*/
|
||||||
|
keyDir?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of retries on retryable errors (429, 500, 502, 503, 504,
|
||||||
|
* network errors). Uses exponential backoff (1 s, 2 s, 4 s, …). Default: 2.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyGenOptions {
|
export interface KeyGenOptions {
|
||||||
|
|
@ -83,4 +97,19 @@ export interface ChatCompletionConfig {
|
||||||
|
|
||||||
/** Password to encrypt rotated private key files */
|
/** Password to encrypt rotated private key files */
|
||||||
keyRotationPassword?: string;
|
keyRotationPassword?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory to load/save RSA keys on startup.
|
||||||
|
* If the directory contains an existing key pair it is loaded; otherwise a
|
||||||
|
* new pair is generated and saved there.
|
||||||
|
* Omit (or set to undefined) to use the default 'client_keys/' directory.
|
||||||
|
* Matches the Python SDK's `key_dir` constructor parameter.
|
||||||
|
*/
|
||||||
|
keyDir?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of retries on retryable errors (429, 500, 502, 503, 504,
|
||||||
|
* network errors). Uses exponential backoff (1 s, 2 s, 4 s, …). Default: 2.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue