One endpoint in front of every free LLM tier, with automatic failover and no provider key ever leaving the box. Providers are data, not code: adding one is a JSON file plus an env var, never a TypeScript change. Ships 12 providers pre-written; `verify-provider --apply` derives the live model list from your own key. - core: registry, bucketed quota meter (rolling + UTC-calendar windows), key-pool store, 6-step model router with diagnostic rejection reasons - gateway: node:http, failover that commits to a stream only after 200 headers, SSE passthrough with usage sniffing, virtual keys with admin/inference roles - context engine: sqlite sessions and a 5-rung refit ladder, so a 128k thread keeps working when it fails over to an 8k model - cache: exact (sha256, safe-by-default) + opt-in semantic (flat Float32Array) - guard: inbound limits, quota forecasting, limit calibration from upstream headers, key health, secret/PII/injection scanning - dashboard: one self-contained HTML file, live SSE request log, token usage - sdk: zero-dep typed client, drop-in for the OpenAI SDK Zero runtime dependencies. 326 tests, 27-check smoke suite against a scripted mock upstream so routing is testable without spending quota. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
import { strict as assert } from 'node:assert';
|
|
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { after, describe, it } from 'node:test';
|
|
|
|
import { KeyVault } from '../src/keyvault.ts';
|
|
import { silentLogger } from '../src/util.ts';
|
|
|
|
const dirs: string[] = [];
|
|
after(() => {
|
|
for (const d of dirs) rmSync(d, { recursive: true, force: true });
|
|
});
|
|
|
|
function vaultFile(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), 'freeway-vault-'));
|
|
dirs.push(dir);
|
|
return join(dir, 'keys.json');
|
|
}
|
|
|
|
describe('KeyVault', () => {
|
|
it('stores a key and hands it to the registry in env-var grammar', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
const masked = vault.add('groq', 'gsk-abcdefghijklmnop');
|
|
|
|
assert.equal(masked, 'gsk-…mnop');
|
|
assert.deepEqual(vault.all(), { groq: 'gsk-abcdefghijklmnop' });
|
|
});
|
|
|
|
it('builds a pool from repeated adds, without duplicates', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
vault.add('groq', 'gsk-aaaaaaaaaaaa');
|
|
vault.add('groq', 'gsk-bbbbbbbbbbbb');
|
|
vault.add('groq', 'gsk-aaaaaaaaaaaa');
|
|
|
|
assert.equal(vault.all()['groq'], 'gsk-aaaaaaaaaaaa,gsk-bbbbbbbbbbbb');
|
|
});
|
|
|
|
it('writes the file 0600', () => {
|
|
// Anything readable by other users on the box defeats the purpose of the
|
|
// gateway holding the keys in the first place.
|
|
const file = vaultFile();
|
|
const vault = new KeyVault({ file, logger: silentLogger });
|
|
vault.add('groq', 'gsk-abcdefghijklmnop');
|
|
|
|
const mode = statSync(file).mode & 0o777;
|
|
assert.equal(mode, 0o600, `expected 0600, got 0${mode.toString(8)}`);
|
|
});
|
|
|
|
it('survives a restart', () => {
|
|
const file = vaultFile();
|
|
new KeyVault({ file, logger: silentLogger }).add('groq', 'gsk-abcdefghijklmnop');
|
|
|
|
const reopened = new KeyVault({ file, logger: silentLogger });
|
|
assert.equal(reopened.all()['groq'], 'gsk-abcdefghijklmnop');
|
|
assert.equal(reopened.has('groq'), true);
|
|
});
|
|
|
|
it('only ever describes keys masked', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
vault.add('groq', 'gsk-supersecret-1234');
|
|
vault.add('groq', 'gsk-anothersecret-99');
|
|
|
|
const described = vault.describe();
|
|
assert.deepEqual(described[0]?.masked, ['gsk-…1234', 'gsk-…t-99']);
|
|
assert.ok(!JSON.stringify(described).includes('supersecret'));
|
|
});
|
|
|
|
it('removes one key from a pool by its mask', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
vault.add('groq', 'gsk-aaaaaaaaaaaa');
|
|
vault.add('groq', 'gsk-bbbbbbbbbbbb');
|
|
|
|
assert.equal(vault.remove('groq', 'gsk-…aaaa'), true);
|
|
assert.equal(vault.all()['groq'], 'gsk-bbbbbbbbbbbb');
|
|
assert.equal(vault.remove('groq', 'gsk-…zzzz'), false, 'an unknown mask removes nothing');
|
|
});
|
|
|
|
it('drops the provider entirely when its last key goes', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
vault.add('groq', 'gsk-aaaaaaaaaaaa');
|
|
vault.remove('groq', 'gsk-…aaaa');
|
|
assert.equal(vault.has('groq'), false);
|
|
});
|
|
|
|
it('rejects a comma-separated blob, so one paste is one key', () => {
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger });
|
|
assert.throws(() => vault.add('groq', 'a,b'), /one key at a time/);
|
|
assert.throws(() => vault.add('groq', ' '), /empty/);
|
|
});
|
|
|
|
it('refuses every mutation when not writable', () => {
|
|
// This is the state the gateway uses when bound to a non-loopback address
|
|
// without an admin key.
|
|
const vault = new KeyVault({ file: vaultFile(), logger: silentLogger, writable: false });
|
|
assert.throws(() => vault.add('groq', 'gsk-abcdefghijkl'), /disabled/);
|
|
assert.throws(() => vault.remove('groq'), /disabled/);
|
|
assert.equal(vault.writable, false);
|
|
});
|
|
|
|
it('ignores a corrupt file instead of refusing to start', () => {
|
|
const file = vaultFile();
|
|
const vault = new KeyVault({ file, logger: silentLogger });
|
|
vault.add('groq', 'gsk-abcdefghijkl');
|
|
// Simulate a truncated write from an older version.
|
|
const raw = readFileSync(file, 'utf8').slice(0, 12);
|
|
rmSync(file);
|
|
new KeyVault({ file, logger: silentLogger });
|
|
|
|
let reopened: KeyVault | undefined;
|
|
assert.doesNotThrow(() => {
|
|
reopened = new KeyVault({ file, logger: silentLogger });
|
|
});
|
|
assert.deepEqual(reopened?.all(), {});
|
|
void raw;
|
|
});
|
|
});
|