import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { probeAuthenticatedOpenClawRelay, resolveRelayAcceptedTokensForPort, resolveRelayAuthTokenForPort, } from "./extension-relay-auth.js"; import { getFreePort } from "./test-port.js"; async function withRelayServer( handler: (req: IncomingMessage, res: ServerResponse) => void, run: (params: { port: number }) => Promise, ) { const port = await getFreePort(); const server = createServer(handler); await new Promise((resolve, reject) => { server.listen(port, "127.0.0.1", () => resolve()); server.once("error", reject); }); try { const actualPort = (server.address() as AddressInfo).port; await run({ port: actualPort }); } finally { await new Promise((resolve) => server.close(() => resolve())); } } function handleNonVersionRequest(req: IncomingMessage, res: ServerResponse): boolean { if (req.url?.startsWith("/json/version")) { return false; } res.writeHead(404); res.end("not found"); return true; } async function probeRelay(baseUrl: string, relayAuthToken: string): Promise { return await probeAuthenticatedOpenClawRelay({ baseUrl, relayAuthHeader: "x-openclaw-relay-token", relayAuthToken, }); } describe("extension-relay-auth", () => { const TEST_GATEWAY_TOKEN = "test-gateway-token"; let prevGatewayToken: string | undefined; beforeEach(() => { prevGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; process.env.OPENCLAW_GATEWAY_TOKEN = TEST_GATEWAY_TOKEN; }); afterEach(() => { if (prevGatewayToken === undefined) { delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { process.env.OPENCLAW_GATEWAY_TOKEN = prevGatewayToken; } }); it("derives deterministic relay tokens per port", async () => { const tokenA1 = await resolveRelayAuthTokenForPort(18790); const tokenA2 = await resolveRelayAuthTokenForPort(18790); const tokenB = await resolveRelayAuthTokenForPort(18791); expect(tokenA1).toBe(tokenA2); expect(tokenA1).not.toBe(tokenB); expect(tokenA1).not.toBe(TEST_GATEWAY_TOKEN); }); it("accepts both relay-scoped and raw gateway tokens for compatibility", async () => { const tokens = await resolveRelayAcceptedTokensForPort(18790); expect(tokens).toContain(TEST_GATEWAY_TOKEN); expect(tokens[0]).not.toBe(TEST_GATEWAY_TOKEN); expect(tokens[0]).toBe(await resolveRelayAuthTokenForPort(18790)); }); it("accepts authenticated openclaw relay probe responses", async () => { let seenToken: string | undefined; await withRelayServer( (req, res) => { if (handleNonVersionRequest(req, res)) { return; } const header = req.headers["x-openclaw-relay-token"]; seenToken = Array.isArray(header) ? header[0] : header; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ Browser: "OpenClaw/extension-relay" })); }, async ({ port }) => { const token = await resolveRelayAuthTokenForPort(port); const ok = await probeRelay(`http://127.0.0.1:${port}`, token); expect(ok).toBe(true); expect(seenToken).toBe(token); }, ); }); it("rejects unauthenticated probe responses", async () => { await withRelayServer( (req, res) => { if (handleNonVersionRequest(req, res)) { return; } res.writeHead(401); res.end("Unauthorized"); }, async ({ port }) => { const ok = await probeRelay(`http://127.0.0.1:${port}`, "irrelevant"); expect(ok).toBe(false); }, ); }); it("rejects probe responses with wrong browser identity", async () => { await withRelayServer( (req, res) => { if (handleNonVersionRequest(req, res)) { return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ Browser: "FakeRelay" })); }, async ({ port }) => { const ok = await probeRelay(`http://127.0.0.1:${port}`, "irrelevant"); expect(ok).toBe(false); }, ); }); });