import { afterAll, beforeAll, describe, test, expect, mock } from "bun:test"; import type { GatewayConfig } from "../config.js"; import { initSigningKey, mintToken } from "../auth/token-service.js"; import { CURRENT_POLICY_EPOCH } from "../auth/policy.js"; import type { CredentialCache } from "../credential-cache.js"; import { createSpeechRelayUpgradeHandler, getSpeechRelayWebsocketHandlers, velayOriginForPlatformHost, type SpeechRelaySocketData, } from "../http/routes/speech-relay-websocket.js"; import { VELAY_FORWARDED_HEADER } from "../velay/bridge-utils.js"; const TEST_SIGNING_KEY = Buffer.from("test-signing-key-at-least-32-bytes-long"); initSigningKey(TEST_SIGNING_KEY); /** The daemon's self-minted service token — the only accepted principal. */ function mintDaemonServiceToken(): string { return mintToken({ aud: "vellum-gateway", sub: "svc:daemon:self", scope_profile: "gateway_service_v1", policy_epoch: CURRENT_POLICY_EPOCH, ttlSeconds: 300, }); } /** An actor edge token — valid signature, wrong principal for this path. */ function mintActorToken(): string { return mintToken({ aud: "vellum-gateway", sub: "actor:test-assistant:test-user", scope_profile: "actor_client_v1", policy_epoch: CURRENT_POLICY_EPOCH, ttlSeconds: 300, }); } function makeConfig(overrides: Partial = {}): GatewayConfig { return { assistantRuntimeBaseUrl: "http://localhost:7821", routingEntries: [], defaultAssistantId: undefined, unmappedPolicy: "reject", port: 7830, runtimeProxyRequireAuth: true, shutdownDrainMs: 5000, runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, maxWebhookPayloadBytes: 1048576, logFile: { dir: undefined, retentionDays: 30 }, velayBaseUrl: "https://velay.test", gatewayInternalBaseUrl: "http://127.0.0.1:7830", trustProxy: false, ...overrides, } as GatewayConfig; } function makeFakeServer(upgradeResult: boolean = true) { return { requestIP: mock(() => ({ address: "127.0.0.1", family: "IPv4", port: 54000, })), upgrade: mock(() => upgradeResult), } as unknown as import("bun").Server; } function makeCredentials(apiKey: string | undefined): CredentialCache { return { get: mock(async () => apiKey), } as unknown as CredentialCache; } async function bodyOf( res: Response, ): Promise<{ code: string; detail: string }> { return (await res.json()) as { code: string; detail: string }; } const TOKEN = mintDaemonServiceToken(); function makeKeyedCredentials( values: Record, ): CredentialCache { return { get: mock(async (key: string) => values[key]), } as unknown as CredentialCache; } describe("velay origin derivation", () => { // getPlatformBaseUrl falls back to this env var; pin it absent so the // missing-credential cases below are deterministic on dev machines. const savedPlatformUrl = process.env.VELLUM_PLATFORM_URL; beforeAll(() => { delete process.env.VELLUM_PLATFORM_URL; }); afterAll(() => { if (savedPlatformUrl !== undefined) { process.env.VELLUM_PLATFORM_URL = savedPlatformUrl; } }); test("velayOriginForPlatformHost follows the deployment naming convention", () => { expect(velayOriginForPlatformHost("platform.vellum.ai")).toBe( "https://velay.vellum.ai", ); expect(velayOriginForPlatformHost("staging-platform.vellum.ai")).toBe( "https://velay-staging.vellum.ai", ); expect(velayOriginForPlatformHost("dev-platform.vellum.ai")).toBe( "https://velay-dev.vellum.ai", ); expect(velayOriginForPlatformHost("localhost")).toBeNull(); expect(velayOriginForPlatformHost("example.com")).toBeNull(); expect( velayOriginForPlatformHost("staging-platform.vellum.ai.evil.com"), ).toBeNull(); }); test("derives the velay origin from the stored platform base URL when VELAY_BASE_URL is unset", async () => { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler( makeConfig({ velayBaseUrl: undefined }), "stt", { credentials: makeKeyedCredentials({ "credential/vellum/assistant_api_key": "vk-1", "credential/vellum/platform_base_url": "https://staging-platform.vellum.ai", }), }, ); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; expect(new URL(data.data.upstreamWsUrl).origin).toBe( "wss://velay-staging.vellum.ai", ); expect(new URL(data.data.upstreamHttpUrl).origin).toBe( "https://velay-staging.vellum.ai", ); }); test("falls back to prod velay for an off-convention or missing platform base URL", async () => { for (const platformBaseUrl of [ "http://localhost:8000", "not a url", undefined, ]) { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler( makeConfig({ velayBaseUrl: undefined }), "stt", { credentials: makeKeyedCredentials({ "credential/vellum/assistant_api_key": "vk-1", "credential/vellum/platform_base_url": platformBaseUrl, }), }, ); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = ( server.upgrade as unknown as { mock: { calls: unknown[][] } } ).mock.calls[0]![1] as { data: SpeechRelaySocketData }; expect(new URL(data.data.upstreamWsUrl).origin).toBe( "wss://velay.vellum.ai", ); } }); test("an explicit VELAY_BASE_URL wins over the stored platform base URL", async () => { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeKeyedCredentials({ "credential/vellum/assistant_api_key": "vk-1", "credential/vellum/platform_base_url": "https://staging-platform.vellum.ai", }), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; expect(new URL(data.data.upstreamWsUrl).origin).toBe("wss://velay.test"); }); }); describe("createSpeechRelayUpgradeHandler — gate", () => { test("upgrades a daemon service connection and strips the key param", async () => { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}&encoding=linear16&sample_rate=16000&channels=1`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); expect(server.upgrade).toHaveBeenCalledTimes(1); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; const upstream = new URL(data.data.upstreamWsUrl); expect(upstream.origin).toBe("wss://velay.test"); expect(upstream.pathname).toBe("/v1/speech/stt/stream"); expect(upstream.searchParams.get("encoding")).toBe("linear16"); expect(upstream.searchParams.get("sample_rate")).toBe("16000"); // The daemon's gateway token must never be forwarded to velay. expect(upstream.searchParams.has("key")).toBe(false); }); test("routes the tts operation to velay's tts path", async () => { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler(makeConfig(), "tts", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/tts/stream?key=${TOKEN}&encoding=linear16`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; expect(new URL(data.data.upstreamWsUrl).pathname).toBe( "/v1/speech/tts/stream", ); }); test("forwards the tts voice model to velay", async () => { // Regression: the daemon sends the selected managed voice as `model`; // a gateway that rejects it silently breaks voice selection (the // daemon's synthesis fails before velay ever sees the session). const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler(makeConfig(), "tts", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/tts/stream?key=${TOKEN}&encoding=linear16&sample_rate=16000&container=none&model=cjVigY5qzO86Huf0OWal`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; const upstream = new URL(data.data.upstreamWsUrl); expect(upstream.searchParams.get("model")).toBe("cjVigY5qzO86Huf0OWal"); }); test("rejects a missing token", async () => { const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), }); const req = new Request("http://127.0.0.1:7830/v1/speech/stt/stream", { headers: { upgrade: "websocket" }, }); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(401); expect((await bodyOf(res)).code).toBe("invalid_token"); }); test("rejects an actor token — daemon service principal only", async () => { const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${mintActorToken()}`, { headers: { upgrade: "websocket" } }, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(401); expect((await bodyOf(res)).code).toBe("invalid_token"); }); test("rejects non-allowlisted query parameters at the gateway boundary", async () => { const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}&encoding=linear16&model=nova-3`, { headers: { upgrade: "websocket" } }, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(400); expect(await bodyOf(res)).toEqual({ code: "invalid_request", detail: "unsupported query parameter: model", }); }); test("rejects velay-forwarded requests regardless of token", async () => { const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, { headers: { upgrade: "websocket", [VELAY_FORWARDED_HEADER]: "1" } }, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(403); expect((await bodyOf(res)).code).toBe("forbidden"); }); }); describe("createSpeechRelayUpgradeHandler — probe (non-upgrade GET)", () => { test("returns missing_platform_connection without a stored API key", async () => { const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials(undefined), }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(401); expect((await bodyOf(res)).code).toBe("missing_platform_connection"); }); test("forwards velay's JSON rejection with its status", async () => { const fetchImpl = mock( async () => new Response( JSON.stringify({ code: "insufficient_balance", detail: "empty" }), { status: 402 }, ), ) as unknown as typeof fetch; const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), fetchImpl, }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}&encoding=linear16`, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(402); expect(await bodyOf(res)).toEqual({ code: "insufficient_balance", detail: "empty", }); // The probe authenticates to velay with the assistant API key. const probeCall = (fetchImpl as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]!; expect(String(probeCall[0])).toContain( "https://velay.test/v1/speech/stt/stream", ); expect( (probeCall[1] as { headers: Record }).headers .Authorization, ).toBe("Api-Key vk-1"); }); test("returns upgrade_required when the gate passes", async () => { const fetchImpl = mock( async () => new Response("Upgrade Required", { status: 426 }), ) as unknown as typeof fetch; const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", { credentials: makeCredentials("vk-1"), fetchImpl, }); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`, ); const res = (await handler(req, makeFakeServer()))!; expect(res.status).toBe(426); expect((await bodyOf(res)).code).toBe("upgrade_required"); }); }); // --------------------------------------------------------------------------- // Websocket handlers — velay dial + frame pipe // --------------------------------------------------------------------------- type WsListener = (ev?: unknown) => void; class FakeUpstreamWebSocket { static instances: FakeUpstreamWebSocket[] = []; readyState = 0; // CONNECTING binaryType = ""; sent: (string | ArrayBuffer | Uint8Array)[] = []; closeCalled: { code?: number; reason?: string } | null = null; constructor( readonly url: string, readonly options?: { headers?: Record }, ) { FakeUpstreamWebSocket.instances.push(this); } private listeners = new Map(); addEventListener(type: string, listener: WsListener): void { const list = this.listeners.get(type) ?? []; list.push(listener); this.listeners.set(type, list); } send(data: string | ArrayBuffer | Uint8Array): void { this.sent.push(data); } close(code?: number, reason?: string): void { this.closeCalled = { code, reason }; this.readyState = 3; } emitOpen(): void { this.readyState = 1; for (const l of this.listeners.get("open") ?? []) l(); } emitMessage(data: string): void { for (const l of this.listeners.get("message") ?? []) l({ data }); } emitClose(code: number, reason = ""): void { this.readyState = 3; for (const l of this.listeners.get("close") ?? []) l({ code, reason }); } emitError(): void { for (const l of this.listeners.get("error") ?? []) l({}); } } class FakeDownstreamSocket { sent: (string | Uint8Array)[] = []; closeCalled: { code?: number; reason?: string } | null = null; data: SpeechRelaySocketData; constructor(data: SpeechRelaySocketData) { this.data = data; } send(data: string | Uint8Array): void { this.sent.push(data); } close(code?: number, reason?: string): void { this.closeCalled = { code, reason }; } } function makeSocketData( overrides: Partial = {}, ): SpeechRelaySocketData { return { wsType: "speech-relay", operation: "stt", upstreamWsUrl: "wss://velay.test/v1/speech/stt/stream?encoding=linear16", upstreamHttpUrl: "https://velay.test/v1/speech/stt/stream?encoding=linear16", deps: { credentials: makeCredentials("vk-1"), webSocketConstructor: FakeUpstreamWebSocket as unknown as SpeechRelaySocketData["deps"]["webSocketConstructor"], }, ...overrides, }; } const tick = () => new Promise((r) => setTimeout(r, 0)); describe("getSpeechRelayWebsocketHandlers", () => { test("dials velay with the Api-Key header and pipes frames both ways", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const ws = new FakeDownstreamSocket(makeSocketData()); await handlers.open(ws as never); const upstream = FakeUpstreamWebSocket.instances[0]!; expect(upstream.url).toBe( "wss://velay.test/v1/speech/stt/stream?encoding=linear16", ); expect(upstream.options?.headers?.Authorization).toBe("Api-Key vk-1"); // Frames sent before upstream opens are buffered, then flushed. handlers.message(ws as never, "early-audio"); expect(upstream.sent).toHaveLength(0); upstream.emitOpen(); expect(upstream.sent).toEqual(["early-audio"]); handlers.message(ws as never, "more-audio"); expect(upstream.sent).toEqual(["early-audio", "more-audio"]); upstream.emitMessage('{"type":"Results"}'); expect(ws.sent).toEqual(['{"type":"Results"}']); }); test("closes downstream with 1008 when pre-open buffered bytes exceed the cap", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const ws = new FakeDownstreamSocket(makeSocketData()); await handlers.open(ws as never); // The velay leg never opens; pump binary frames up to the 1 MiB cap. const frame = new Uint8Array(64 * 1024); for (let i = 0; i < 16; i++) { handlers.message(ws as never, frame); } expect(ws.closeCalled).toBeNull(); // The next frame exceeds the cap. handlers.message(ws as never, frame); expect(ws.closeCalled).toEqual({ code: 1008, reason: "Buffer overflow" }); }); test("forwards upstream close codes and reasons to the daemon", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const ws = new FakeDownstreamSocket(makeSocketData()); await handlers.open(ws as never); const upstream = FakeUpstreamWebSocket.instances[0]!; upstream.emitOpen(); upstream.emitMessage( '{"type":"velay_error","code":"session_duration_exceeded","detail":""}', ); upstream.emitClose(1000, "session_duration_exceeded"); // The velay_error frame passes through untouched, then the close. expect(ws.sent[0]).toContain("session_duration_exceeded"); expect(ws.closeCalled).toEqual({ code: 1000, reason: "session_duration_exceeded", }); }); test("sets arraybuffer binary mode on the upstream socket", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const ws = new FakeDownstreamSocket(makeSocketData()); await handlers.open(ws as never); expect(FakeUpstreamWebSocket.instances[0]!.binaryType).toBe("arraybuffer"); }); test("a pre-open close waits for the probe's velay_error before closing", async () => { // A rejected handshake can emit close (or error+close) before the async // probe resolves; the raw close must not beat the synthesized frame. FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const fetchImpl = mock( async () => new Response( JSON.stringify({ code: "insufficient_balance", detail: "empty" }), { status: 402 }, ), ) as unknown as typeof fetch; const data = makeSocketData(); data.deps.fetchImpl = fetchImpl; const ws = new FakeDownstreamSocket(data); await handlers.open(ws as never); const upstream = FakeUpstreamWebSocket.instances[0]!; upstream.emitError(); upstream.emitClose(1006); await tick(); // One synthesized frame, one close — the double-fire is coalesced. expect(ws.sent).toHaveLength(1); expect(JSON.parse(ws.sent[0] as string).code).toBe("insufficient_balance"); expect(ws.closeCalled?.code).toBe(1011); expect( (fetchImpl as unknown as { mock: { calls: unknown[] } }).mock.calls, ).toHaveLength(1); }); test("a failed velay dial synthesizes a velay_error from the HTTP probe", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const fetchImpl = mock( async () => new Response(JSON.stringify({ code: "invalid_key", detail: "bad" }), { status: 401, }), ) as unknown as typeof fetch; const data = makeSocketData(); data.deps.fetchImpl = fetchImpl; const ws = new FakeDownstreamSocket(data); await handlers.open(ws as never); FakeUpstreamWebSocket.instances[0]!.emitError(); await tick(); expect(ws.sent).toHaveLength(1); expect(JSON.parse(ws.sent[0] as string)).toEqual({ type: "velay_error", code: "invalid_key", detail: "bad", }); expect(ws.closeCalled?.code).toBe(1011); }); test("missing API key closes with a synthesized velay_error, no dial", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const data = makeSocketData(); data.deps.credentials = makeCredentials(undefined); const ws = new FakeDownstreamSocket(data); await handlers.open(ws as never); expect(FakeUpstreamWebSocket.instances).toHaveLength(0); expect(JSON.parse(ws.sent[0] as string).code).toBe( "missing_platform_connection", ); expect(ws.closeCalled?.code).toBe(1011); }); test("a ws:// relay base still yields a fetchable probe URL", async () => { const server = makeFakeServer(); const handler = createSpeechRelayUpgradeHandler( makeConfig({ velayBaseUrl: "wss://velay.test" } as never), "stt", { credentials: makeCredentials("vk-1") }, ); const req = new Request( `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}&encoding=linear16`, { headers: { upgrade: "websocket" } }, ); expect(await handler(req, server)).toBeUndefined(); const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } }) .mock.calls[0]![1] as { data: SpeechRelaySocketData }; expect(data.data.upstreamWsUrl.startsWith("wss://velay.test/")).toBe(true); expect(data.data.upstreamHttpUrl.startsWith("https://velay.test/")).toBe( true, ); }); test("a daemon close during the credential read prevents the velay dial", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); let releaseRead: (v: string) => void = () => {}; const data = makeSocketData(); data.deps.credentials = { get: () => new Promise((r) => (releaseRead = r)), } as unknown as CredentialCache; const ws = new FakeDownstreamSocket(data); const opening = handlers.open(ws as never); // The daemon hangs up while the CES/keychain read is still pending. handlers.close(ws as never, 1001, "going away"); releaseRead("vk-1"); await opening; // No upstream session was dialed — nothing left to close would have // torn it down. expect(FakeUpstreamWebSocket.instances).toHaveLength(0); }); test("daemon close is forwarded to velay", async () => { FakeUpstreamWebSocket.instances = []; const handlers = getSpeechRelayWebsocketHandlers(); const ws = new FakeDownstreamSocket(makeSocketData()); await handlers.open(ws as never); const upstream = FakeUpstreamWebSocket.instances[0]!; upstream.emitOpen(); handlers.close(ws as never, 1000, "client done"); expect(upstream.closeCalled).toEqual({ code: 1000, reason: "client done" }); }); });