openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
210 lines (209 loc) • 9.17 kB
JavaScript
import "./utils-P__uGsPB.js";
import { t as sleep } from "./sleep-D7nua6TP.js";
import { _ as resolveGatewayPort } from "./paths-D2sRr1a_.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import "./http-body-D3IMwTJJ.js";
import { r as readResponseTextSnippet } from "./http-response-body-CwT_cCNz.js";
import { l as CONTROL_UI_OWNER_BOOTSTRAP_PROFILE_HINT, s as CONTROL_UI_BOOTSTRAP_PROFILE_FRAGMENT_PARAM } from "./zod-schema-DnirJMRA.js";
import "./config-Cs0XXL3x.js";
import { t as createGatewayCredentialPlan } from "./credential-planner-DAMjVc9j.js";
import { o as isSameProcessSpecificIpv4WithLoopbackListeners } from "./ports-format-D-ReVpaa.js";
import { n as inspectPortUsage } from "./ports-inspect-CHTlDwBh.js";
import { t as resolveGatewayInteractiveSurfaceAuth } from "./auth-surface-resolution-BmpOVonk.js";
import { r as normalizeTlsFingerprint } from "./client-address-utils-DUCasR_v.js";
import { t as inspectGatewayTlsCertificate } from "./gateway-BtIixcdW.js";
import { n as resolveControlUiLinks } from "./control-ui-links-BY4F5DYV.js";
import { r as fetchConfiguredLocalOriginWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js";
import "./control-ui-assets-Ia-uwq-Z.js";
import "./control-ui-contract-zYW4RcpK.js";
import { j as CONTROL_UI_OWNER_BOOTSTRAP_PROFILE, s as issueDeviceBootstrapToken } from "./device-bootstrap-DRFMRCcr.js";
import "./onboard-helpers-BDY-6Z-t.js";
//#region src/commands/control-ui-handoff.ts
const CONTROL_UI_DOCUMENT_REQUEST_TIMEOUT_MS = 2e3;
const CONTROL_UI_DOCUMENT_ERROR_MAX_BYTES = 2048;
/** One canonical target keeps advertised identity, local browser delivery, and document probes distinct. */
async function resolveControlUiHandoffTarget(params) {
const { config, env } = params;
const port = resolveGatewayPort(config, env);
const bind = config.gateway?.bind ?? "loopback";
const basePath = config.gateway?.controlUi?.basePath;
const customBindHost = config.gateway?.customBindHost;
const tlsConfig = config.gateway?.tls;
const tlsEnabled = tlsConfig?.enabled === true;
const credentialPlan = createGatewayCredentialPlan({
config,
env
});
const resolvedAuth = await resolveGatewayInteractiveSurfaceAuth({
config,
env,
surface: "local"
});
const authMode = config.gateway?.auth?.mode ?? (credentialPlan.localPassword.value || credentialPlan.envPassword ? "password" : "token");
const gatewayAuthHandoff = authMode === "password" && !credentialPlan.localPassword.hasSecretRef ? resolvedAuth.password : void 0;
const customBindIsWildcard = bind === "custom" && customBindHost?.trim() === "0.0.0.0";
const browserBind = bind === "lan" || customBindIsWildcard || !tlsEnabled && (bind === "tailnet" || bind === "custom") ? "loopback" : bind;
const configuredLinks = resolveControlUiLinks({
port,
bind,
customBindHost,
basePath,
tlsEnabled
});
const links = browserBind === bind ? configuredLinks : resolveControlUiLinks({
port,
bind: browserBind,
customBindHost,
basePath,
tlsEnabled
});
const configuredHost = new URL(configuredLinks.wsUrl).hostname;
const loopbackAliasHost = browserBind === "loopback" && (bind === "tailnet" || bind === "custom") && configuredHost !== "127.0.0.1" && configuredHost !== "0.0.0.0" ? configuredHost : void 0;
const documentUrl = new URL(links.httpUrl);
documentUrl.hostname = "127.0.0.1";
documentUrl.username = "";
documentUrl.password = "";
documentUrl.search = "";
documentUrl.hash = "";
const token = credentialPlan.localToken.value ?? credentialPlan.envToken ?? "";
const includeTokenInUrl = Boolean(token) && !credentialPlan.localToken.hasSecretRef;
return {
port,
basePath,
bind,
links,
authMode,
gatewayAuthHandoff,
includeTokenInUrl,
dashboardUrl: includeTokenInUrl ? `${links.httpUrl}#token=${encodeURIComponent(token)}` : links.httpUrl,
documentUrl: documentUrl.toString(),
probeUrl: loopbackAliasHost ? configuredLinks.wsUrl : links.wsUrl,
loopbackAliasHost,
tlsConfig
};
}
/** Keep shared browser credentials on the same process proven by the configured Gateway endpoint. */
async function hasVerifiedControlUiLoopbackAlias(target) {
if (!target.loopbackAliasHost) return true;
const portUsage = await inspectPortUsage(target.port).catch(() => void 0);
return Boolean(portUsage && isSameProcessSpecificIpv4WithLoopbackListeners(portUsage.listeners, target.port, target.loopbackAliasHost));
}
/** Mint the Control-UI-scoped one-time device grant immediately before actual delivery. */
async function issueControlUiBrowserHandoff(httpUrl) {
const issued = await issueDeviceBootstrapToken({ profile: CONTROL_UI_OWNER_BOOTSTRAP_PROFILE });
return {
browserUrl: `${httpUrl}#${new URLSearchParams({
bootstrapToken: issued.token,
[CONTROL_UI_BOOTSTRAP_PROFILE_FRAGMENT_PARAM]: CONTROL_UI_OWNER_BOOTSTRAP_PROFILE_HINT
}).toString()}`,
expiresAtMs: issued.expiresAtMs
};
}
/** Wait only for an explicitly preparing dashboard; fail immediately for terminal HTTP states. */
async function waitForControlUiDocument(params) {
const now = params.deps?.now ?? Date.now;
const sleepFor = params.deps?.sleep ?? sleep;
const deadline = now() + (params.timeoutMs ?? 6e5);
let tlsFingerprint;
let tlsConnect;
if (params.tlsConfig?.enabled === true) try {
const certificate = await inspectGatewayTlsCertificate(params.tlsConfig);
if (!certificate.ok) return {
ready: false,
reason: certificate.error
};
tlsFingerprint = certificate.value.fingerprintSha256;
const expectedFingerprint = tlsFingerprint;
tlsConnect = {
ca: certificate.value.cert,
allowPartialTrustChain: true,
checkServerIdentity: (_hostname, peerCertificate) => normalizeTlsFingerprint(peerCertificate.fingerprint256 ?? "") === expectedFingerprint ? void 0 : /* @__PURE__ */ new Error("Gateway TLS certificate fingerprint mismatch.")
};
} catch (error) {
return {
ready: false,
reason: error instanceof Error ? error.message : "Gateway TLS certificate is unavailable."
};
}
let pendingReported = false;
const origin = new URL(params.url).origin;
const requestDocument = async (method, remainingMs) => await (params.deps?.fetch ?? fetchConfiguredLocalOriginWithSsrFGuard)({
url: params.url,
configuredLocalOriginBaseUrl: origin,
policy: { allowedOrigins: [origin] },
maxRedirects: 0,
timeoutMs: Math.min(CONTROL_UI_DOCUMENT_REQUEST_TIMEOUT_MS, remainingMs),
capture: false,
auditContext: "gateway-control-ui-readiness",
...tlsConnect ? { dispatcherPolicy: {
mode: "direct",
connect: tlsConnect
} } : {},
init: {
method,
headers: {
Accept: "text/html",
"Accept-Encoding": "identity"
}
}
});
while (true) {
const remainingMs = deadline - now();
if (remainingMs <= 0) return {
ready: false,
reason: "Control UI assets did not finish preparing in time."
};
try {
const request = await requestDocument("HEAD", remainingMs);
let pendingDelayMs;
try {
const mediaType = request.response.headers.get("content-type")?.split(";", 1)[0]?.trim();
if (request.response.status === 200 && mediaType?.toLowerCase() === "text/html") return {
ready: true,
...tlsFingerprint ? { tlsFingerprint } : {}
};
const retryAfter = request.response.headers.get("retry-after");
if (request.response.status !== 503 || !retryAfter || params.waitForPending === false) {
let detail;
if (request.response.status === 503 && !retryAfter) {
const diagnostic = await requestDocument("GET", Math.max(1, deadline - now())).catch(() => void 0);
if (diagnostic) try {
const diagnosticType = diagnostic.response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
if (diagnostic.response.status === 503 && diagnosticType === "text/plain") {
const snippet = await readResponseTextSnippet(diagnostic.response, {
maxBytes: CONTROL_UI_DOCUMENT_ERROR_MAX_BYTES,
maxChars: CONTROL_UI_DOCUMENT_ERROR_MAX_BYTES,
timeoutMs: CONTROL_UI_DOCUMENT_REQUEST_TIMEOUT_MS
});
detail = snippet ? sanitizeTerminalText(snippet) : void 0;
}
} finally {
await diagnostic.release();
}
}
return {
ready: false,
reason: request.response.status === 503 && retryAfter ? "Control UI assets are still preparing." : detail || `Control UI dashboard is unavailable (HTTP ${request.response.status}).`,
status: request.response.status
};
}
const retryAfterSeconds = Number(retryAfter);
pendingDelayMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : 1e3;
} finally {
await request.release();
}
if (!pendingReported) {
pendingReported = true;
params.onPending?.();
}
await sleepFor(Math.min(pendingDelayMs, Math.max(0, deadline - now())));
} catch (error) {
return {
ready: false,
reason: `Control UI dashboard is unavailable: ${error instanceof Error ? error.message : String(error)}`
};
}
}
}
//#endregion
export { waitForControlUiDocument as i, issueControlUiBrowserHandoff as n, resolveControlUiHandoffTarget as r, hasVerifiedControlUiLoopbackAlias as t };