Freeway: self-hosted OpenAI-compatible gateway for free-tier providers

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>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-08-16 22:41:49 -05:00
commit cb985dcf9c
86 files changed
+16350

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
/**
* Regression tests for the security sweep.
*
* Each of these encodes a vulnerability that was real, so a future refactor
* cannot quietly reintroduce it.
*/
import { strict as assert } from 'node:assert';
import { once } from 'node:events';
import { mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
import type { Server } from 'node:http';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { after, describe, it } from 'node:test';
import { ContextStore, ExactCache, Registry, Router, Store, Tokenizer, parseConfig, silentLogger, type FreewayConfig } from '@freeway/core';
import { createGateway } from '../src/server.ts';
import { createMockUpstream } from '../../../scripts/mock-upstream.js';
const cleanups: (() => unknown)[] = [];
after(async () => {
for (const c of cleanups.reverse()) await c();
});
interface Mock { url: string; close(): Promise<unknown> }
interface Harness {
base: string;
context: ContextStore;
dataDir: string;
}
async function harness(over: Partial<FreewayConfig> = {}): Promise<Harness> {
const upstream = (await createMockUpstream({ name: 'up' })) as Mock;
cleanups.push(() => upstream.close());
const dir = mkdtempSync(join(tmpdir(), 'freeway-sec-'));
const dataDir = mkdtempSync(join(tmpdir(), 'freeway-sec-data-'));
cleanups.push(() => rmSync(dir, { recursive: true, force: true }), () => rmSync(dataDir, { recursive: true, force: true }));
writeFileSync(join(dir, 'up.json'), JSON.stringify({
id: 'up', priority: 10, baseUrl: upstream.url,
auth: { type: 'bearer', envKeys: ['UP_KEY'] }, limits: { rpm: 1000 },
models: [{ id: 'up-chat', alias: ['fast'], context: 128000, caps: ['chat'] }],
}));
const registry = new Registry({ dir, env: { UP_KEY: 'uk-000000000001' }, logger: silentLogger });
const store = new Store({ file: null, logger: silentLogger });
const { config } = parseConfig({}, {});
Object.assign(config, over);
const router = new Router(registry, store, config);
const context = new ContextStore(join(dataDir, 'freeway.db'));
const cache = new ExactCache({ file: join(dataDir, 'cache.db'), mode: 'safe', ttlMs: 3_600_000, maxEntries: 50 });
cleanups.push(() => context.close(), () => cache.close());
const server: Server = createGateway({
registry, store, router, config, logger: silentLogger,
contextStore: context, tokenizer: new Tokenizer(), cache,
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const addr = server.address();
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
cleanups.push(() => new Promise((r) => server.close(() => r(undefined))));
return { base: `http://127.0.0.1:${port}`, context, dataDir };
}
const APP = 'fw-app-key';
const ADMIN = 'fw-admin-key';
const authed = (key: string) => ({ 'content-type': 'application/json', authorization: `Bearer ${key}` });
// ---------------------------------------------------------------------------
describe('C1 — an inference key is not an admin key', () => {
const cfg = { keys: [APP], adminKey: ADMIN, host: '0.0.0.0' };
it('refuses an app key on every /api route', async () => {
const h = await harness(cfg);
const routes: [string, string][] = [
['GET', '/api/providers'], ['GET', '/api/logs'], ['GET', '/api/sessions'],
['GET', '/api/alerts'], ['GET', '/api/cache'], ['GET', '/api/stats'], ['GET', '/api/events'],
['POST', '/api/reload'], ['POST', '/api/providers/up/enabled'],
['POST', '/api/providers/up/key'], ['POST', '/api/providers/up/probe'],
['DELETE', '/api/cache'],
];
for (const [method, path] of routes) {
const res = await fetch(`${h.base}${path}`, { method, headers: authed(APP), ...(method === 'GET' ? {} : { body: '{}' }) });
assert.equal(res.status, 403, `${method} ${path} should be forbidden to an app key`);
assert.equal(((await res.json()) as { error: { code: string } }).error.code, 'admin_required');
}
});
it('still lets the app key do inference', async () => {
const h = await harness(cfg);
const res = await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: authed(APP),
body: JSON.stringify({ model: 'fast', messages: [{ role: 'user', content: 'hi' }] }),
});
assert.equal(res.status, 200);
});
it('lets the admin key through', async () => {
const h = await harness(cfg);
assert.equal((await fetch(`${h.base}/api/stats`, { headers: authed(ADMIN) })).status, 200);
});
it('refuses /api entirely on a public bind with no admin key', async () => {
// Otherwise an exposed gateway with only inference keys hands them the box.
const h = await harness({ keys: [APP], adminKey: null, host: '0.0.0.0' });
const res = await fetch(`${h.base}/api/logs`, { headers: authed(APP) });
assert.equal(res.status, 403);
assert.match(((await res.json()) as { error: { message: string } }).error.message, /FREEWAY_ADMIN_KEY/);
});
it('stays convenient on loopback with no admin key', async () => {
const h = await harness({ keys: [APP], adminKey: null, host: '127.0.0.1' });
assert.equal((await fetch(`${h.base}/api/stats`, { headers: authed(APP) })).status, 200);
});
});
describe('C2 — CORS is an allowlist, never a wildcard', () => {
it('sends no CORS headers by default', async () => {
const h = await harness({ keys: [APP] });
const res = await fetch(`${h.base}/v1/models`, { headers: { ...authed(APP), origin: 'https://evil.example' } });
assert.equal(res.headers.get('access-control-allow-origin'), null);
});
it('refuses an origin that is not on the list', async () => {
const h = await harness({ keys: [APP], corsOrigins: ['https://app.example'] });
const res = await fetch(`${h.base}/v1/models`, { headers: { ...authed(APP), origin: 'https://evil.example' } });
assert.equal(res.headers.get('access-control-allow-origin'), null);
});
it('echoes only a listed origin, and varies on it', async () => {
const h = await harness({ keys: [APP], corsOrigins: ['https://app.example'] });
const res = await fetch(`${h.base}/v1/models`, { headers: { ...authed(APP), origin: 'https://app.example' } });
assert.equal(res.headers.get('access-control-allow-origin'), 'https://app.example');
assert.match(res.headers.get('vary') ?? '', /Origin/);
});
it('rejects "*" in configuration rather than honouring it', () => {
const { config, issues } = parseConfig({ corsOrigins: ['*', 'https://ok.example'] }, {});
assert.deepEqual(config.corsOrigins, ['https://ok.example']);
assert.ok(issues.some((i) => i.path === 'corsOrigins'));
});
});
describe('C3 — sessions belong to the key that made them', () => {
it('refuses to read another key’s session', async () => {
const h = await harness({ keys: [APP, 'fw-other-key'], adminKey: ADMIN });
await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: authed(APP),
body: JSON.stringify({ model: 'fast', session: 'private', messages: [{ role: 'user', content: 'my bank pin is 1234' }] }),
});
const res = await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: authed('fw-other-key'),
body: JSON.stringify({ model: 'fast', session: 'private', messages: [{ role: 'user', content: 'what did I say?' }] }),
});
assert.equal(res.status, 403);
assert.equal(((await res.json()) as { error: { code: string } }).error.code, 'session_forbidden');
});
it('lets the owner continue its own session', async () => {
const h = await harness({ keys: [APP], adminKey: ADMIN });
const body = (content: string) => JSON.stringify({ model: 'fast', session: 'mine', messages: [{ role: 'user', content }] });
assert.equal((await fetch(`${h.base}/v1/chat/completions`, { method: 'POST', headers: authed(APP), body: body('one') })).status, 200);
assert.equal((await fetch(`${h.base}/v1/chat/completions`, { method: 'POST', headers: authed(APP), body: body('two') })).status, 200);
assert.equal(h.context.messages('mine').length, 4);
});
it('records the owner, masked', async () => {
const h = await harness({ keys: [APP], adminKey: ADMIN });
await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: authed(APP),
body: JSON.stringify({ model: 'fast', session: 'owned', messages: [{ role: 'user', content: 'hi' }] }),
});
const owner = h.context.info('owned')?.owner;
assert.ok(owner, 'session should be claimed');
assert.ok(!owner.includes('app-key'), 'the owner must be a mask, not the key');
});
});
describe('H2 — live log subscriptions are capped', () => {
it('refuses beyond the cap instead of accumulating listeners', async () => {
const h = await harness();
const controllers: AbortController[] = [];
let refused = 0;
for (let i = 0; i < 20; i++) {
const controller = new AbortController();
controllers.push(controller);
const res = await fetch(`${h.base}/api/events`, { signal: controller.signal });
if (res.status === 503) {
refused += 1;
break;
}
// Start reading so the subscription actually registers server-side.
void res.body?.getReader().read();
}
for (const c of controllers) c.abort();
assert.ok(refused > 0, 'an unbounded number of subscribers was accepted');
});
});
describe('H3 — response headers cannot be poisoned by input', () => {
it('survives a session id full of illegal header bytes', async () => {
// These used to reach writeHead raw and throw, turning a good response
// into a remote 500.
const h = await harness();
for (const id of ['sess\r\nX-Injected: yes', 'sess→arrow', 'sessnull']) {
const res = await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ model: 'fast', session: id, messages: [{ role: 'user', content: 'hi' }] }),
});
assert.equal(res.status, 200, `session id ${JSON.stringify(id)} should not break the response`);
assert.equal(res.headers.get('x-injected'), null, 'no header was injected');
}
});
});
describe('M1 — data at rest is not world-readable', () => {
it('creates the sqlite files 0600', async () => {
const h = await harness();
await fetch(`${h.base}/v1/chat/completions`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ model: 'fast', session: 's', temperature: 0, messages: [{ role: 'user', content: 'hi' }] }),
});
for (const file of ['freeway.db', 'cache.db']) {
const mode = statSync(join(h.dataDir, file)).mode & 0o777;
assert.equal(mode, 0o600, `${file} is 0${mode.toString(8)}, expected 0600`);
}
});
});
describe('M2 — key comparison leaks neither content nor length', () => {
it('rejects keys of every length identically', async () => {
const h = await harness({ keys: ['fw-the-real-key-abcdefghijk'] });
for (const wrong of ['x', 'fw-', 'fw-the-real-key-abcdefghij', 'fw-the-real-key-abcdefghijkX', 'x'.repeat(500)]) {
const res = await fetch(`${h.base}/v1/models`, { headers: { authorization: `Bearer ${wrong}` } });
assert.equal(res.status, 401, `"${wrong.slice(0, 12)}…" should be rejected`);
}
assert.equal((await fetch(`${h.base}/v1/models`, { headers: authed('fw-the-real-key-abcdefghijk') })).status, 200);
});
});
describe('still no way to reach a raw provider key', () => {
it('keeps provider keys out of every admin response', async () => {
const h = await harness({ keys: [APP], adminKey: ADMIN });
for (const path of ['/api/providers', '/api/stats', '/api/logs', '/api/alerts']) {
const text = await (await fetch(`${h.base}${path}`, { headers: authed(ADMIN) })).text();
assert.ok(!text.includes('uk-000000000001'), `${path} leaked a provider key`);
}
});
});