Files
browser/extension/background-utils.js
pastilhasandClaude Opus 5 3346b4c853 extension: normalise the address, and always https
Pasting the address you use for Officer produced http://https://platform.officer.dev:18792/ — the
scheme was taken as part of the hostname and http:// prepended anyway. It failed as "Relay not
reachable", which reads as the relay being down rather than the URL being mangled.

normalizeHost strips a scheme, a path and credentials, keeps IPv6 brackets, and lifts a port out of
the address so pasting host:9000 moves the port field instead of dropping it. The field is rewritten
with the result, so what is shown is what is stored.

The scheme is now always https, and wss for the socket, whatever was typed. The relay carries a
pairing token on every request and drives a browser someone is signed into; plaintext because a field
was left blank is not a default worth having.

CONSEQUENCE, stated rather than discovered: the relay itself still serves plain HTTP on 18792, so the
extension cannot connect until it is behind TLS. That is a proxy host away and it is the right shape —
the raw port was never reachable from anywhere but the tailnet anyway.

The unreachable message now says what was tried and that the port is normally private, instead of
"verify the server address", which is true and useless when the address is fine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:47:31 +00:00

54 lines
1.9 KiB
JavaScript

// Based on OpenClaw Browser Extension Relay (MIT License)
// https://github.com/nicepkg/openclaw
export function reconnectDelayMs(
attempt,
opts = { baseMs: 1000, maxMs: 30000, jitterMs: 1000, random: Math.random },
) {
const baseMs = Number.isFinite(opts.baseMs) ? opts.baseMs : 1000;
const maxMs = Number.isFinite(opts.maxMs) ? opts.maxMs : 30000;
const jitterMs = Number.isFinite(opts.jitterMs) ? opts.jitterMs : 1000;
const random = typeof opts.random === "function" ? opts.random : Math.random;
const safeAttempt = Math.max(0, Number.isFinite(attempt) ? attempt : 0);
const backoff = Math.min(baseMs * 2 ** safeAttempt, maxMs);
return backoff + Math.max(0, jitterMs) * random();
}
export async function deriveRelayToken(gatewayToken, port) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
enc.encode(gatewayToken),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
"HMAC",
key,
enc.encode(`officer-browser-relay-v1:${port}`),
);
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
export async function buildRelayWsUrl(host, port, gatewayToken) {
const token = String(gatewayToken || "").trim();
if (!token) {
throw new Error(
"Missing relay token in extension settings (chrome.storage.local.relayToken)",
);
}
// wss, always — the matching half of the options page forcing https. The token is on the query string
// because a WebSocket handshake from an extension cannot set headers, so plaintext here would put a
// live credential on the wire in full view.
return `wss://${host}:${port}/extension?token=${encodeURIComponent(token)}`;
}
export function isRetryableReconnectError(err) {
const message = err instanceof Error ? err.message : String(err || "");
if (message.includes("Missing relay token")) {
return false;
}
return true;
}