openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
249 lines (248 loc) • 11 kB
JavaScript
import { t as hasExactKeys } from "./strict-json-DsGFhwfH.mjs";
import crypto from "node:crypto";
//#region extensions/browser/chrome-extension/modules/relay-auth-v2-crypto.js
const RELAY_AUTH_LABEL = "openclaw.browser-relay.auth";
const textEncoder = new TextEncoder();
function canonicalRelayAuthProofBytes(proofKind, fields, clientProof) {
if (proofKind !== "server" && proofKind !== "client" && proofKind !== "accept") throw new Error("invalid relay auth proof kind");
const values = [
RELAY_AUTH_LABEL,
2,
proofKind,
fields.keyId,
fields.instanceId,
fields.sessionId,
fields.clientNonce,
fields.serverNonce,
fields.issuedAtMs,
fields.expiresAtMs,
fields.role,
fields.transport,
fields.method,
fields.resource,
fields.flow
];
if (proofKind === "accept") {
if (typeof clientProof !== "string") throw new Error("accept proof requires clientProof");
values.push(clientProof);
}
return textEncoder.encode(JSON.stringify(values));
}
//#endregion
//#region extensions/browser/src/browser/extension-relay/auth-v2-crypto.ts
const KEY_HEX_PATTERN = /^[0-9a-f]{64}$/u;
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
function decodeRelayKey(keyHex) {
if (!KEY_HEX_PATTERN.test(keyHex)) throw new Error("browser relay key must be 32 lowercase-hex bytes");
return Buffer.from(keyHex, "hex");
}
function isCanonicalBase64UrlBytes(value, bytes) {
if (typeof value !== "string" || !BASE64URL_PATTERN.test(value)) return false;
try {
const decoded = Buffer.from(value, "base64url");
return decoded.length === bytes && decoded.toString("base64url") === value;
} catch {
return false;
}
}
function isBase64UrlText(value) {
return BASE64URL_PATTERN.test(value);
}
function relayKeyIdFromHex(keyHex) {
return crypto.createHash("sha256").update(decodeRelayKey(keyHex)).digest("base64url").slice(0, 22);
}
function randomRelayNonce() {
return crypto.randomBytes(32).toString("base64url");
}
function randomRelayId() {
return crypto.randomBytes(16).toString("base64url");
}
function createRelayProof(keyHex, proofKind, fields, clientProof) {
const hmac = crypto.createHmac("sha256", decodeRelayKey(keyHex));
if (proofKind === "accept" && !isCanonicalBase64UrlBytes(clientProof, 32)) throw new Error("accept proof requires a 32-byte client proof");
return hmac.update(canonicalRelayAuthProofBytes(proofKind, fields, clientProof)).digest("base64url");
}
function verifyRelayProof(keyHex, proofKind, fields, candidate, clientProof) {
if (!isCanonicalBase64UrlBytes(candidate, 32)) return false;
const expected = Buffer.from(createRelayProof(keyHex, proofKind, fields, clientProof), "base64url");
const actual = Buffer.from(candidate, "base64url");
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}
//#endregion
//#region extensions/browser/src/browser/extension-relay/auth-v2.ts
const BROWSER_RELAY_EXTENSION_SUBPROTOCOL = "openclaw-extension-relay.v2";
const BROWSER_RELAY_AUTH_CHALLENGE_PATH = "/_openclaw/relay/auth/v2/challenge";
const BROWSER_RELAY_AUTH_COMPLETE_PATH = "/_openclaw/relay/auth/v2/complete";
const BROWSER_RELAY_CHALLENGE_TTL_MS = 1e4;
const MAX_PENDING_AUTH_CONNECTIONS = 128;
const MAX_PENDING_AUTH_CONNECTIONS_PER_SOURCE = 32;
const MAX_AUTHENTICATED_CONNECTIONS = 128;
const MAX_REPLAY_ENTRIES = 1024;
const MAX_REPLAY_ENTRIES_PER_SOURCE = 256;
const LOOPBACK_AUTH_SOURCE = "loopback";
function normalizeAuthSource(source) {
const normalized = source.trim().toLowerCase();
if (normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized) || /^::ffff:127(?:\.\d{1,3}){3}$/.test(normalized)) return LOOPBACK_AUTH_SOURCE;
return normalized || "unknown";
}
function parseRelayAuthHello(record) {
if (!hasExactKeys(record, [
"type",
"v",
"keyId",
"clientNonce"
]) || record.type !== "auth.hello" || record.v !== 2 || typeof record.keyId !== "string" || record.keyId.length !== 22 || !isBase64UrlText(record.keyId) || !isCanonicalBase64UrlBytes(record.clientNonce, 32)) return null;
return record;
}
function parseRelayAuthResponse(record) {
if (!hasExactKeys(record, [
"type",
"v",
"sessionId",
"clientProof"
]) || record.type !== "auth.response" || record.v !== 2 || !isCanonicalBase64UrlBytes(record.sessionId, 16) || !isCanonicalBase64UrlBytes(record.clientProof, 32)) return null;
return record;
}
function parseRelayHttpChallengeRequest(record) {
if (!hasExactKeys(record, [
"v",
"keyId",
"clientNonce",
"role",
"transport",
"method",
"resource",
"flow"
]) || record.v !== 2 || typeof record.keyId !== "string" || record.keyId.length !== 22 || !isBase64UrlText(record.keyId) || !isCanonicalBase64UrlBytes(record.clientNonce, 32) || record.role !== "cdp" || record.transport !== "connection" || !(record.flow === "cdp" && record.method === "SEQUENCE" && record.resource === "/json/version -> /cdp" || record.flow === "json-list" && record.method === "GET" && record.resource === "/json/list")) return null;
return record;
}
function parseRelayHttpCompleteRequest(record) {
if (!hasExactKeys(record, [
"v",
"sessionId",
"clientProof"
]) || record.v !== 2 || !isCanonicalBase64UrlBytes(record.sessionId, 16) || !isCanonicalBase64UrlBytes(record.clientProof, 32)) return null;
return record;
}
function parseExtensionRelayResource(rawUrl, expectedPath) {
const url = URL.parse(rawUrl, "http://127.0.0.1");
if (!url || url.pathname !== expectedPath || url.hash) return null;
if ([...url.searchParams.entries()].some(([key]) => key !== "profile") || url.searchParams.getAll("profile").length > 1) return null;
const profile = url.searchParams.get("profile");
if (profile !== null && !/^[a-z0-9-]+$/u.test(profile)) return null;
return profile === null ? expectedPath : `${expectedPath}?profile=${encodeURIComponent(profile)}`;
}
var BrowserRelayAuthV2Authority = class {
constructor(keyHex) {
this.keyHex = keyHex;
this.instanceId = randomRelayId();
this.challenges = /* @__PURE__ */ new Map();
this.pendingConnections = /* @__PURE__ */ new Map();
this.authenticatedConnections = /* @__PURE__ */ new Map();
this.replay = /* @__PURE__ */ new Map();
this.disposed = false;
this.keyId = relayKeyIdFromHex(keyHex);
}
registerPendingConnection(binding, onInvalidate, source) {
const normalizedSource = normalizeAuthSource(source);
let pendingForSource = 0;
for (const pending of this.pendingConnections.values()) pendingForSource += pending.source === normalizedSource ? 1 : 0;
if (this.disposed || this.pendingConnections.has(binding) || this.authenticatedConnections.has(binding) || pendingForSource >= MAX_PENDING_AUTH_CONNECTIONS_PER_SOURCE || this.pendingConnections.size >= MAX_PENDING_AUTH_CONNECTIONS) return false;
this.pendingConnections.set(binding, {
onInvalidate,
source: normalizedSource
});
return true;
}
registerAuthenticatedConnection(binding, onInvalidate) {
if (this.disposed || this.pendingConnections.has(binding) || this.authenticatedConnections.has(binding) || this.authenticatedConnections.size >= MAX_AUTHENTICATED_CONNECTIONS) return false;
this.authenticatedConnections.set(binding, onInvalidate);
return true;
}
releaseConnection(binding) {
this.pendingConnections.delete(binding);
this.authenticatedConnections.delete(binding);
for (const [sessionId, challenge] of this.challenges) if (challenge.binding === binding) this.challenges.delete(sessionId);
}
issueChallenge(binding, hello, expected, nowMs = Date.now()) {
const pending = this.pendingConnections.get(binding);
if (this.disposed || !pending || hello.keyId !== this.keyId || this.challenges.size >= MAX_PENDING_AUTH_CONNECTIONS) return null;
const expiresAtMs = nowMs + BROWSER_RELAY_CHALLENGE_TTL_MS;
let entriesForSource = 0;
for (const [candidate, entry] of this.replay) if (entry.expiresAtMs < nowMs) this.replay.delete(candidate);
else entriesForSource += entry.source === pending.source ? 1 : 0;
const replayKey = `${this.keyId}:${hello.clientNonce}`;
if (this.replay.has(replayKey) || entriesForSource >= MAX_REPLAY_ENTRIES_PER_SOURCE || this.replay.size >= MAX_REPLAY_ENTRIES) return null;
this.replay.set(replayKey, {
expiresAtMs,
source: pending.source
});
const fields = {
keyId: this.keyId,
instanceId: this.instanceId,
sessionId: randomRelayId(),
clientNonce: hello.clientNonce,
serverNonce: randomRelayNonce(),
issuedAtMs: nowMs,
expiresAtMs,
...expected
};
this.challenges.set(fields.sessionId, {
binding,
fields
});
return {
type: "auth.challenge",
v: 2,
...fields,
serverProof: createRelayProof(this.keyHex, "server", fields)
};
}
completeChallenge(binding, response, nowMs = Date.now()) {
const challenge = this.challenges.get(response.sessionId);
if (!challenge || challenge.binding !== binding || this.disposed) return null;
this.challenges.delete(response.sessionId);
if (nowMs > challenge.fields.expiresAtMs || !verifyRelayProof(this.keyHex, "client", challenge.fields, response.clientProof)) return null;
const pending = this.pendingConnections.get(binding);
if (!pending || this.authenticatedConnections.size >= MAX_AUTHENTICATED_CONNECTIONS) return null;
this.pendingConnections.delete(binding);
this.authenticatedConnections.set(binding, pending.onInvalidate);
return {
fields: challenge.fields,
ok: {
type: "auth.ok",
v: 2,
sessionId: challenge.fields.sessionId,
acceptProof: createRelayProof(this.keyHex, "accept", challenge.fields, response.clientProof)
}
};
}
dispose() {
if (this.disposed) return;
this.disposed = true;
const invalidators = [...Array.from(this.pendingConnections.values(), ({ onInvalidate }) => onInvalidate), ...this.authenticatedConnections.values()];
this.pendingConnections.clear();
this.authenticatedConnections.clear();
this.challenges.clear();
this.replay.clear();
for (const invalidate of invalidators) invalidate();
}
};
let activeAuthority = null;
/** One process-wide replay/session authority for the current host key. */
function getBrowserRelayAuthV2Authority(keyHex) {
if (activeAuthority?.keyHex === keyHex) return activeAuthority.authority;
activeAuthority?.authority.dispose();
const authority = new BrowserRelayAuthV2Authority(keyHex);
activeAuthority = {
keyHex,
authority
};
return authority;
}
function invalidateBrowserRelayAuthV2Authority() {
activeAuthority?.authority.dispose();
activeAuthority = null;
}
//#endregion
export { verifyRelayProof as _, getBrowserRelayAuthV2Authority as a, parseRelayAuthHello as c, parseRelayHttpCompleteRequest as d, createRelayProof as f, relayKeyIdFromHex as g, randomRelayNonce as h, BROWSER_RELAY_EXTENSION_SUBPROTOCOL as i, parseRelayAuthResponse as l, randomRelayId as m, BROWSER_RELAY_AUTH_COMPLETE_PATH as n, invalidateBrowserRelayAuthV2Authority as o, isCanonicalBase64UrlBytes as p, BROWSER_RELAY_CHALLENGE_TTL_MS as r, parseExtensionRelayResource as s, BROWSER_RELAY_AUTH_CHALLENGE_PATH as t, parseRelayHttpChallengeRequest as u, RELAY_AUTH_LABEL as v };