Files
openclaw/extensions/browser/chrome-extension/modules/relay-core.test.ts
Peter Steinberger 654029892d feat(browser): pair the Chrome extension directly to a remote gateway (#101127)
* feat(browser): direct extension→gateway relay path for remote Chrome (#53599)

Let the OpenClaw Chrome extension pair directly to a remote gateway over
wss:// with no OpenClaw node host on the browser machine — the managed-hosting
path from #53599 (extension is the only thing installed on the laptop).

- Gateway route /browser/extension registered by the browser plugin with
  auth:"plugin" + no nodeCapability, so the gateway does not pre-enforce token
  auth (browser WebSockets cannot send an Authorization header). The upgrade
  handler self-validates the host-local relay secret from ?token=, origin-checks
  chrome-extension://, resolves the extension profile, then attaches the socket
  to the same ExtensionRelayBridge the loopback relay uses. All CDP synthesis,
  tab-group scoping, and the in-process Playwright /cdp client are unchanged.
- `openclaw browser extension pair --gateway-url wss://host` prints a
  wss://host/browser/extension#<secret> string; the path ends in /extension so
  the extension's existing pairing parser accepts it with zero extension code
  changes.
- relay-server: extract attachExtensionWebSocket + export requestToken /
  isAllowedExtensionOrigin / EXTENSION_RELAY_MAX_PAYLOAD_BYTES so loopback and
  gateway paths share one bind + one frame cap.
- runtime-lifecycle: dispose the shared gateway WebSocketServer on shutdown.
- docs: three remote topologies (same host / direct-to-gateway / via node host).

Coverage: 6 unit tests for the handler's path/503/403/404/401/attach branches.
The full extension→bridge→CDP→Chrome loop over /browser/extension was live-proven
with a real Chrome + the built extension. The real gateway upgrade→handleUpgrade
dispatch for an auth:"plugin" unprotected route is verified against core
(server-http.ts, plugins-http.ts, route-auth.ts).

* fix(browser): harden remote extension pairing
2026-07-07 04:31:52 +01:00

66 lines
2.3 KiB
TypeScript

// Pure-logic tests for the OpenClaw Chrome extension. Runs under the
// extension-browser vitest glob (extensions/browser/**/*.test.ts).
import { describe, expect, it } from "vitest";
import {
buildRelayWsProtocols,
nearestGroupColor,
parsePairingString,
reconnectDelayMs,
} from "./relay-core.js";
describe("parsePairingString", () => {
it("parses a valid pairing string the CLI emits", () => {
const parsed = parsePairingString("ws://127.0.0.1:18797/extension#deadbeefcafe");
expect(parsed).toEqual({
relayUrl: "ws://127.0.0.1:18797/extension",
token: "deadbeefcafe",
});
});
it("round-trips with the CLI pairing format", () => {
const port = 18797;
const token = "abc123";
const pairing = `ws://127.0.0.1:${port}/extension#${token}`;
const parsed = parsePairingString(pairing);
if (!parsed) {
throw new Error("expected pairing string to parse");
}
expect(parsed.relayUrl).toBe(`ws://127.0.0.1:${port}/extension`);
expect(buildRelayWsProtocols(parsed.token)).toEqual([
"openclaw-extension-relay",
`openclaw-extension-token.${token}`,
]);
});
it("rejects malformed strings", () => {
expect(parsePairingString("")).toBeNull();
expect(parsePairingString("http://127.0.0.1/extension#tok")).toBeNull();
expect(parsePairingString("ws://127.0.0.1/other#tok")).toBeNull();
expect(parsePairingString("ws://127.0.0.1/extension#")).toBeNull();
expect(parsePairingString("ws://127.0.0.1/extension")).toBeNull();
});
});
describe("reconnectDelayMs", () => {
it("backs off exponentially and caps at 30s", () => {
expect(reconnectDelayMs(0)).toBe(1000);
expect(reconnectDelayMs(1)).toBe(2000);
expect(reconnectDelayMs(4)).toBe(16_000);
expect(reconnectDelayMs(5)).toBe(30_000);
expect(reconnectDelayMs(50)).toBe(30_000);
});
});
describe("nearestGroupColor", () => {
it("maps hex accents to Chrome tab-group color names", () => {
expect(nearestGroupColor("#FF4500")).toBe("orange");
expect(nearestGroupColor("#00AA00")).toBe("green");
expect(nearestGroupColor("#4285F4")).toBe("blue");
});
it("falls back to orange for invalid input", () => {
expect(nearestGroupColor("not-a-color")).toBe("orange");
expect(nearestGroupColor(undefined)).toBe("orange");
});
});