diff --git a/src/browser/extension-relay-auth.test.ts b/src/browser/extension-relay-auth.test.ts index abc25765da1..3410e1566cd 100644 --- a/src/browser/extension-relay-auth.test.ts +++ b/src/browser/extension-relay-auth.test.ts @@ -3,6 +3,7 @@ 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"; @@ -51,6 +52,13 @@ describe("extension-relay-auth", () => { expect(tokenA1).not.toBe(TEST_GATEWAY_TOKEN); }); + it("accepts both relay-scoped and raw gateway tokens for compatibility", () => { + const tokens = resolveRelayAcceptedTokensForPort(18790); + expect(tokens).toContain(TEST_GATEWAY_TOKEN); + expect(tokens[0]).not.toBe(TEST_GATEWAY_TOKEN); + expect(tokens[0]).toBe(resolveRelayAuthTokenForPort(18790)); + }); + it("accepts authenticated openclaw relay probe responses", async () => { let seenToken: string | undefined; await withRelayServer( diff --git a/src/browser/extension-relay-auth.ts b/src/browser/extension-relay-auth.ts index 40de39ae746..86b79a5e976 100644 --- a/src/browser/extension-relay-auth.ts +++ b/src/browser/extension-relay-auth.ts @@ -27,14 +27,22 @@ function deriveRelayAuthToken(gatewayToken: string, port: number): string { return createHmac("sha256", gatewayToken).update(`${RELAY_TOKEN_CONTEXT}:${port}`).digest("hex"); } -export function resolveRelayAuthTokenForPort(port: number): string { +export function resolveRelayAcceptedTokensForPort(port: number): string[] { const gatewayToken = resolveGatewayAuthToken(); - if (gatewayToken) { - return deriveRelayAuthToken(gatewayToken, port); + if (!gatewayToken) { + throw new Error( + "extension relay requires gateway auth token (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)", + ); } - throw new Error( - "extension relay requires gateway auth token (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)", - ); + const relayToken = deriveRelayAuthToken(gatewayToken, port); + if (relayToken === gatewayToken) { + return [relayToken]; + } + return [relayToken, gatewayToken]; +} + +export function resolveRelayAuthTokenForPort(port: number): string { + return resolveRelayAcceptedTokensForPort(port)[0]; } export async function probeAuthenticatedOpenClawRelay(params: { diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index 0aae3307fcc..84a84af6f75 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -277,6 +277,23 @@ describe("chrome extension relay server", () => { ext.close(); }); + it("accepts raw gateway token for relay auth compatibility", async () => { + const port = await getFreePort(); + cdpUrl = `http://127.0.0.1:${port}`; + await ensureChromeExtensionRelayServer({ cdpUrl }); + + const versionRes = await fetch(`${cdpUrl}/json/version`, { + headers: { "x-openclaw-relay-token": TEST_GATEWAY_TOKEN }, + }); + expect(versionRes.status).toBe(200); + + const ext = new WebSocket( + `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(TEST_GATEWAY_TOKEN)}`, + ); + await waitForOpen(ext); + ext.close(); + }); + it( "tracks attached page targets and exposes them via CDP + /json/list", async () => { diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index a6687764b85..0036f47f263 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -7,6 +7,7 @@ import { isLoopbackAddress, isLoopbackHost } from "../gateway/net.js"; import { rawDataToString } from "../infra/ws.js"; import { probeAuthenticatedOpenClawRelay, + resolveRelayAcceptedTokensForPort, resolveRelayAuthTokenForPort, } from "./extension-relay-auth.js"; @@ -219,6 +220,7 @@ export async function ensureChromeExtensionRelayServer(opts: { } const relayAuthToken = resolveRelayAuthTokenForPort(info.port); + const relayAuthTokens = new Set(resolveRelayAcceptedTokensForPort(info.port)); let extensionWs: WebSocket | null = null; const cdpClients = new Set(); @@ -365,8 +367,8 @@ export async function ensureChromeExtensionRelayServer(opts: { const path = url.pathname; if (path.startsWith("/json")) { - const token = getHeader(req, RELAY_AUTH_HEADER); - if (!token || token !== relayAuthToken) { + const token = getHeader(req, RELAY_AUTH_HEADER)?.trim(); + if (!token || !relayAuthTokens.has(token)) { res.writeHead(401); res.end("Unauthorized"); return; @@ -489,7 +491,7 @@ export async function ensureChromeExtensionRelayServer(opts: { if (pathname === "/extension") { const token = getRelayAuthTokenFromRequest(req, url); - if (!token || token !== relayAuthToken) { + if (!token || !relayAuthTokens.has(token)) { rejectUpgrade(socket, 401, "Unauthorized"); return; } @@ -514,7 +516,7 @@ export async function ensureChromeExtensionRelayServer(opts: { if (pathname === "/cdp") { const token = getRelayAuthTokenFromRequest(req, url); - if (!token || token !== relayAuthToken) { + if (!token || !relayAuthTokens.has(token)) { rejectUpgrade(socket, 401, "Unauthorized"); return; }