import { test } from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { checkIpRateLimit, getClientIp, sanitizeForensicHeader, } from "../../../../src/app/api/v1/relay/chat/completions/relaySecurity.ts"; import { getDbInstance } from "../../../../src/lib/db/core.ts"; import { getRelayLogs } from "../../../../src/lib/db/relayProxies.ts"; // ─── T-12 (#3932 PR-4): bifrost sidecar proxy route ────────────────────── // // We test the *contract* of the route by setting env before import and // calling the exported POST handler. The handler is module-scope configured // (BIFROST_BASE_URL is read at import time), so env must be set BEFORE the // dynamic import below. const ORIGINAL_BIFROST_BASE_URL = process.env.BIFROST_BASE_URL; const ORIGINAL_BIFROST_API_KEY = process.env.BIFROST_API_KEY; const ORIGINAL_BIFROST_OMNI_KEY = process.env.OMNIROUTE_BIFROST_KEY; const ORIGINAL_BIFROST_TIMEOUT = process.env.BIFROST_TIMEOUT_MS; const ORIGINAL_BIFROST_STREAMING = process.env.BIFROST_STREAMING_ENABLED; const ORIGINAL_FETCH = globalThis.fetch; function seedRelayToken(rawToken: string) { const id = `rl_test_${Date.now()}_${Math.random().toString(16).slice(2)}`; const now = Math.floor(Date.now() / 1000); getDbInstance() .prepare( ` INSERT INTO relay_tokens (id, name, token_hash, token_prefix, description, combo_id, allowed_models, max_tokens_per_request, max_requests_per_minute, max_requests_per_day, max_cost_per_day, enabled, created_at, updated_at, expires_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?) ` ) .run( id, "bifrost-sse-lifecycle", createHash("sha256").update(rawToken).digest("hex"), "rl_test", "", null, JSON.stringify(["*"]), 128000, 60, 10000, 0, now, now, null, "{}" ); return { id, rawToken }; } function restoreEnv() { if (ORIGINAL_BIFROST_BASE_URL === undefined) delete process.env.BIFROST_BASE_URL; else process.env.BIFROST_BASE_URL = ORIGINAL_BIFROST_BASE_URL; if (ORIGINAL_BIFROST_API_KEY === undefined) delete process.env.BIFROST_API_KEY; else process.env.BIFROST_API_KEY = ORIGINAL_BIFROST_API_KEY; if (ORIGINAL_BIFROST_OMNI_KEY === undefined) delete process.env.OMNIROUTE_BIFROST_KEY; else process.env.OMNIROUTE_BIFROST_KEY = ORIGINAL_BIFROST_OMNI_KEY; if (ORIGINAL_BIFROST_TIMEOUT === undefined) delete process.env.BIFROST_TIMEOUT_MS; else process.env.BIFROST_TIMEOUT_MS = ORIGINAL_BIFROST_TIMEOUT; if (ORIGINAL_BIFROST_STREAMING === undefined) delete process.env.BIFROST_STREAMING_ENABLED; else process.env.BIFROST_STREAMING_ENABLED = ORIGINAL_BIFROST_STREAMING; globalThis.fetch = ORIGINAL_FETCH; } // Case 1: BIFROST_BASE_URL unset. We test this first because the route's // module-scope `BIFROST_BASE_URL` would be empty for the entire test file. test("bifrost route: returns 503 + fallback header when BIFROST_BASE_URL is unset", async () => { delete process.env.BIFROST_BASE_URL; delete process.env.BIFROST_API_KEY; delete process.env.OMNIROUTE_BIFROST_KEY; delete process.env.BIFROST_TIMEOUT_MS; delete process.env.BIFROST_STREAMING_ENABLED; // Dynamic import after env is set so the module reads the empty value. const { POST } = await import( "../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts" ); const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "gpt-4", messages: [] }), }); const res = await POST(req); assert.equal(res.status, 503); assert.equal(res.headers.get("X-Bifrost-Fallback"), "/api/v1/relay/chat/completions"); const body = await res.json(); assert.match(String(body?.error?.message ?? ""), /Bifrost sidecar not configured/); restoreEnv(); }); // Case 2: BIFROST_BASE_URL set but no auth token in request. The route should // return 401 *before* trying to reach the gateway, so we don't need a mock fetch. test("bifrost route: returns 401 when BIFROST_BASE_URL is set but no auth token is provided", async () => { process.env.BIFROST_BASE_URL = "http://bifrost.test.local:8080"; delete process.env.BIFROST_API_KEY; delete process.env.OMNIROUTE_BIFROST_KEY; delete process.env.BIFROST_TIMEOUT_MS; delete process.env.BIFROST_STREAMING_ENABLED; // Use a fresh module instance by appending a cache-busting query string. // (Node's ESM cache is keyed by resolved URL, so a unique query bypasses it.) const { POST } = await import( `../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}` ); const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "gpt-4", messages: [{ role: "user", content: "hi" }] }), }); const res = await POST(req); assert.equal(res.status, 401); const body = await res.json(); assert.match(String(body?.error?.message ?? ""), /Missing relay token/); restoreEnv(); }); // Case 3: hashToken() is used internally. Verify the SHA-256 output shape so // downstream code that compares hashes doesn't silently break if the impl // changes. (This is a contract test, not a black-box test of the function.) test("bifrost route: relay token hashing matches the SHA-256 hex contract", () => { const token = "test-token-abc-123"; const hash = createHash("sha256").update(token).digest("hex"); assert.equal(hash.length, 64); // SHA-256 hex = 64 chars assert.match(hash, /^[0-9a-f]{64}$/); }); // Case 4: Validate the CORS preflight handler exists and responds with 204/200 test("bifrost route: OPTIONS responds with CORS headers", async () => { const { OPTIONS } = await import( `../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}` ); const res = await OPTIONS(); // handleCorsOptions() returns 204 No Content with the standard CORS // methods/headers. Access-Control-Allow-Origin is intentionally NOT set on // the route's response — src/middleware.ts (applyCorsHeaders) is the single // source of truth for which origin to echo, based on the allowlist in // src/server/cors/origins.ts. We assert the route ships its end of the // contract: status + methods/headers. Origin overlay is exercised by the // middleware tests. assert.ok(res.status === 200 || res.status === 204, `expected 200/204, got ${res.status}`); assert.ok( res.headers.get("Access-Control-Allow-Methods"), "missing Access-Control-Allow-Methods header" ); assert.ok( res.headers.get("Access-Control-Allow-Headers"), "missing Access-Control-Allow-Headers header" ); }); test("bifrost route: shared relay IP limiter blocks a repeated token from one IP", () => { const tokenId = `bifrost-ip-limit-${Date.now()}-${Math.random()}`; const ip = "203.0.113.77"; for (let i = 0; i < 30; i++) { assert.equal(checkIpRateLimit(tokenId, ip).allowed, true); } const blocked = checkIpRateLimit(tokenId, ip); assert.equal(blocked.allowed, false); assert.ok(blocked.resetIn >= 0 && blocked.resetIn <= 60); }); test("bifrost route: shared relay security helpers sanitize forwarded client metadata", () => { const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", { headers: { "x-forwarded-for": "198.51.100.8, 10.0.0.2", }, }); assert.equal(getClientIp(req), "198.51.100.8"); assert.equal(sanitizeForensicHeader("ua\r\nforged"), "ua forged"); }); test("bifrost route: records relay usage after SSE stream completion", async () => { process.env.BIFROST_BASE_URL = "http://bifrost.test.local:8080"; process.env.BIFROST_TIMEOUT_MS = "5000"; delete process.env.BIFROST_API_KEY; delete process.env.OMNIROUTE_BIFROST_KEY; delete process.env.BIFROST_STREAMING_ENABLED; const relayToken = seedRelayToken(`relay_bifrost_sse_${Date.now()}`); globalThis.fetch = async () => new Response( new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("data: {\"delta\":\"hi\"}\n\n")); controller.close(); }, }), { status: 200, headers: { "content-type": "text/event-stream" }, } ); const { POST } = await import( `../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}` ); const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", { method: "POST", headers: { authorization: `Bearer ${relayToken.rawToken}`, "content-type": "application/json", "x-request-id": "bifrost-sse-lifecycle-test", }, body: JSON.stringify({ model: "gpt-4", stream: true, messages: [{ role: "user", content: "hi" }], }), }); const res = await POST(req); assert.equal(res.status, 200); assert.equal(res.headers.get("X-Routed-By"), "bifrost"); assert.equal(getRelayLogs(relayToken.id, 10).length, 0); assert.match(await res.text(), /delta/); const logs = getRelayLogs(relayToken.id, 10); assert.equal(logs.length, 1); assert.equal(logs[0].status, "success"); assert.equal(logs[0].status_code, 200); assert.equal(logs[0].request_id, "bifrost-sse-lifecycle-test"); restoreEnv(); });