mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 13:41:37 +00:00
* 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
66 lines
2.3 KiB
TypeScript
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");
|
|
});
|
|
});
|