openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
112 lines (111 loc) • 6.45 kB
JavaScript
import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js";
import { r as capturePluginRegistryLifecycleEpoch, s as isPluginRegistryLifecycleEpochActive } from "./registry-lifecycle-BozndFXl.js";
import { createHmac, randomBytes } from "node:crypto";
//#region src/gateway/board-view-ticket.ts
const BOARD_HTTP_PATH_PREFIX = "/__openclaw__/board/";
const BOARD_VIEW_TICKET_TTL_MS = 12e5;
const BOARD_VIEW_TICKET_SCOPE = "board-widget-view";
const BOARD_VIEW_TICKET_MAX_LENGTH = 2048;
const ticketSecret = randomBytes(32);
const ticketAuthorities = /* @__PURE__ */ new WeakMap();
var BoardGatewayUnavailableError = class extends Error {
constructor() {
super("dashboard unavailable");
this.name = "BoardGatewayUnavailableError";
}
};
function signTicketPayload(payload, secret) {
return createHmac("sha256", secret).update(`${BOARD_VIEW_TICKET_SCOPE}\0${payload}`).digest("base64url");
}
function isValidClaims(value) {
if (!value || typeof value !== "object") return false;
const claims = value;
return typeof claims.sessionKey === "string" && claims.sessionKey.length > 0 && claims.sessionKey.length <= 512 && (claims.agentId === void 0 || typeof claims.agentId === "string" && claims.agentId.length > 0 && claims.agentId.length <= 64) && typeof claims.name === "string" && claims.name.length > 0 && claims.name.length <= 64 && Number.isSafeInteger(claims.revision) && (claims.revision ?? 0) >= 1 && typeof claims.viewGeneration === "string" && /^[a-f0-9]{32}$/u.test(claims.viewGeneration) && typeof claims.authorityGeneration === "string" && /^[A-Za-z0-9_-]{32}$/u.test(claims.authorityGeneration) && Number.isSafeInteger(claims.expiresAtMs) && typeof claims.nonce === "string" && /^[A-Za-z0-9_-]{32}$/u.test(claims.nonce) && (claims.pluginFrame === void 0 || typeof claims.pluginFrame === "object" && typeof claims.pluginFrame.pluginKind === "string" && claims.pluginFrame.pluginKind.length <= 128 && typeof claims.pluginFrame.scopedHostUrl === "string" && claims.pluginFrame.scopedHostUrl.length <= 1024);
}
function captureBoardViewTicketAuthority(input) {
let currentContext;
try {
currentContext = input.resolveGatewayContext();
} catch {
throw new BoardGatewayUnavailableError();
}
if (currentContext !== input.gatewayContext || input.gatewayContext.resolveGatewayContext !== input.resolveGatewayContext) throw new BoardGatewayUnavailableError();
const methodRegistry = input.gatewayContext.getGatewayMethodRegistry?.();
const pluginRegistryEpoch = input.pluginRegistry ? capturePluginRegistryLifecycleEpoch(input.pluginRegistry) : void 0;
if (input.pluginRegistry && !pluginRegistryEpoch) throw new BoardGatewayUnavailableError();
const existing = ticketAuthorities.get(input.gatewayContext);
if (existing?.resolveGatewayContext === input.resolveGatewayContext && existing.methodRegistry === methodRegistry && existing.pluginRegistry === input.pluginRegistry && existing.pluginRegistryEpoch === pluginRegistryEpoch) return existing;
const authority = {
...input,
generation: randomBytes(24).toString("base64url"),
...methodRegistry ? { methodRegistry } : {},
...pluginRegistryEpoch ? { pluginRegistryEpoch } : {}
};
ticketAuthorities.set(input.gatewayContext, authority);
return authority;
}
function requireBoardViewTicketAuthority(claims, gatewayContext) {
const authority = gatewayContext ? ticketAuthorities.get(gatewayContext) : void 0;
let currentContext;
try {
currentContext = authority?.resolveGatewayContext();
} catch {
throw new BoardGatewayUnavailableError();
}
if (!gatewayContext || !authority || authority.generation !== claims.authorityGeneration || authority.gatewayContext !== gatewayContext || currentContext !== gatewayContext || gatewayContext.resolveGatewayContext !== authority.resolveGatewayContext || authority.methodRegistry && gatewayContext.getGatewayMethodRegistry?.() !== authority.methodRegistry || authority.pluginRegistry && (!authority.pluginRegistryEpoch || !isPluginRegistryLifecycleEpochActive(authority.pluginRegistry, authority.pluginRegistryEpoch))) throw new BoardGatewayUnavailableError();
return authority;
}
function createBoardViewTicket(params) {
const nowMs = params.nowMs ?? Date.now();
const authority = captureBoardViewTicketAuthority(params.authority);
const claims = {
sessionKey: params.sessionKey,
...params.agentId ? { agentId: params.agentId } : {},
name: params.name,
revision: params.revision,
viewGeneration: params.viewGeneration,
authorityGeneration: authority.generation,
expiresAtMs: nowMs + BOARD_VIEW_TICKET_TTL_MS,
nonce: randomBytes(24).toString("base64url"),
...params.pluginFrame ? { pluginFrame: params.pluginFrame } : {}
};
if (!Number.isSafeInteger(nowMs) || !isValidClaims(claims)) throw new Error("invalid board view ticket binding");
const payload = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
return {
ticket: `v1.${payload}.${signTicketPayload(payload, ticketSecret)}`,
expiresAtMs: claims.expiresAtMs
};
}
function verifyBoardViewTicket(value, options = {}) {
const nowMs = options.nowMs ?? Date.now();
if (!Number.isSafeInteger(nowMs) || value.length > BOARD_VIEW_TICKET_MAX_LENGTH) return;
const parts = value.split(".");
if (parts.length !== 3 || parts[0] !== "v1") return;
const [, payload, signature] = parts;
if (!payload || !signature) return;
const expectedSignature = signTicketPayload(payload, ticketSecret);
if (!safeEqualSecret(signature, expectedSignature)) return;
let claims;
try {
claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
} catch {
return;
}
if (!isValidClaims(claims) || claims.expiresAtMs <= nowMs) return;
return claims;
}
function resolveAuthorizedBoardViewTicketClaims(value, options = {}) {
const claims = verifyBoardViewTicket(value, options);
if (!claims) return;
try {
requireBoardViewTicketAuthority(claims, options.gatewayContext);
return claims;
} catch {
return;
}
}
function buildBoardWidgetFrameUrl(params) {
return `${BOARD_HTTP_PATH_PREFIX}${encodeURIComponent(params.sessionKey)}/${encodeURIComponent(params.name)}/index.html?bt=${encodeURIComponent(params.ticket)}`;
}
//#endregion
export { createBoardViewTicket as a, verifyBoardViewTicket as c, buildBoardWidgetFrameUrl as i, BOARD_VIEW_TICKET_TTL_MS as n, requireBoardViewTicketAuthority as o, BoardGatewayUnavailableError as r, resolveAuthorizedBoardViewTicketClaims as s, BOARD_HTTP_PATH_PREFIX as t };