openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
441 lines (440 loc) • 14.4 kB
JavaScript
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { a as READ_SCOPE } from "./operator-scopes-Dw7Gu2cA.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js";
import { s as isLoopbackHost } from "./net-DbNPs6Xm.js";
import { o as classifyGatewayConnectFailure } from "./connect-error-details-DaOfGNN8.js";
import { o as readMissingScopeError } from "./gateway-error-details-w0nAGBBp.js";
import { i as loadDeviceAuthTokenReadOnly, s as loadOriginDeviceTokenReadOnly } from "./device-auth-store-Bu3kSvQA.js";
import { c as resolveSafeTimeoutDelayMs } from "./timeouts-D2XMKe-X.js";
import { t as gatewayOriginScope } from "./gateway-origin-scope-D4zHFrov.js";
import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-B1nULpha.js";
import { t as GatewayClient } from "./client-I-RoP1Al.js";
import { r as isGatewayProtocolResponseError } from "./protocol-request-BMUN1re6.js";
import { t as GatewayClientRequestError } from "./request-error-BSFClQsA.js";
import { n as normalizeEdgeAuthHeadersConfig, r as resolveEdgeAuthHeaders, t as gatewayEdgeAuthValueForTarget } from "./edge-auth-DAIxzz6z.js";
import "./method-scopes-K6J_UQGL.js";
import { randomUUID } from "node:crypto";
//#region src/gateway/probe.ts
const MIN_PROBE_TIMEOUT_MS = 250;
const OPERATOR_READ_SCOPE = "operator.read";
const OPERATOR_WRITE_SCOPE = "operator.write";
const OPERATOR_ADMIN_SCOPE = "operator.admin";
const DEVICE_IDENTITY_REQUIRED_CLOSE_CODE = 1008;
const DEVICE_IDENTITY_REQUIRED_CLOSE_REASON = "device identity required";
const DEVICE_REQUIRED_PROBE_FAILURE_THRESHOLD = 3;
const DEVICE_REQUIRED_PROBE_TTL_MS = 3e5;
const PROBE_CLIENT_STOP_TIMEOUT_MS = 1e3;
const deviceRequiredProbeCache = /* @__PURE__ */ new Map();
function clampProbeTimeoutMs(timeoutMs) {
return resolveSafeTimeoutDelayMs(timeoutMs, { minMs: MIN_PROBE_TIMEOUT_MS });
}
function formatProbeCloseError(close) {
return `gateway closed (${close.code}): ${close.reason}`;
}
function resolveDeviceRequiredProbeCacheKey(url) {
try {
return new URL(url).href;
} catch {
return url;
}
}
function isDeviceIdentityRequiredClose(close) {
return close?.code === DEVICE_IDENTITY_REQUIRED_CLOSE_CODE && close.reason.trim().toLowerCase() === DEVICE_IDENTITY_REQUIRED_CLOSE_REASON;
}
function hasProbeAuth(auth) {
return Boolean(auth?.token?.trim() || auth?.password?.trim());
}
function resolveProbeDeviceAuthScope(url) {
try {
return isLoopbackHost(new URL(url).hostname) ? void 0 : gatewayOriginScope(url);
} catch {
return;
}
}
function shouldShortCircuitDeviceRequiredProbe(cacheKey, nowMs) {
const entry = deviceRequiredProbeCache.get(cacheKey);
if (!entry) return false;
if (nowMs - entry.firstFailureAtMs >= DEVICE_REQUIRED_PROBE_TTL_MS) {
deviceRequiredProbeCache.delete(cacheKey);
return false;
}
return entry.failures >= DEVICE_REQUIRED_PROBE_FAILURE_THRESHOLD;
}
function noteDeviceRequiredProbeFailure(cacheKey, nowMs) {
const existing = deviceRequiredProbeCache.get(cacheKey);
if (!existing || nowMs - existing.firstFailureAtMs >= DEVICE_REQUIRED_PROBE_TTL_MS) {
deviceRequiredProbeCache.set(cacheKey, {
failures: 1,
firstFailureAtMs: nowMs
});
return;
}
existing.failures += 1;
}
function clearDeviceRequiredProbeFailures(cacheKey) {
deviceRequiredProbeCache.delete(cacheKey);
}
function emptyProbeAuth() {
return {
role: null,
scopes: [],
capability: "unknown"
};
}
function emptyProbeServer() {
return {
version: null,
connId: null
};
}
function makeDeviceRequiredShortCircuitResult(url) {
const close = {
code: DEVICE_IDENTITY_REQUIRED_CLOSE_CODE,
reason: DEVICE_IDENTITY_REQUIRED_CLOSE_REASON,
hint: "probe short-circuited by recent device-required rejections"
};
return {
ok: false,
url,
connectLatencyMs: null,
error: formatProbeCloseError(close),
close,
auth: emptyProbeAuth(),
server: emptyProbeServer(),
health: null,
status: null,
presence: null,
configSnapshot: null
};
}
function resolveProbeAuthSummary(params) {
const scopes = Array.isArray(params.scopes) ? params.scopes : [];
return {
role: params.role ?? null,
scopes,
capability: resolveGatewayProbeCapability({
auth: { scopes },
authMetadataPresent: params.authMetadataPresent,
connectErrorDetails: params.connectErrorDetails,
error: params.error,
close: params.close,
verifiedRead: params.verifiedRead,
connectLatencyMs: params.connectLatencyMs
})
};
}
function resolveGatewayProbeCapability(params) {
if (classifyGatewayConnectFailure({
details: params.connectErrorDetails,
reason: params.close?.reason,
message: params.error
}).kind === "pairing-required") return "pairing_pending";
const scopes = Array.isArray(params.auth?.scopes) ? params.auth.scopes : [];
if (scopes.includes(OPERATOR_ADMIN_SCOPE)) return "admin_capable";
if (scopes.includes(OPERATOR_WRITE_SCOPE)) return "write_capable";
if (scopes.includes(OPERATOR_READ_SCOPE) || params.verifiedRead === true) return "read_only";
if (params.connectLatencyMs != null && params.authMetadataPresent === true) return "connected_no_operator_scope";
return "unknown";
}
async function probeGateway(opts) {
const startedAt = Date.now();
const instanceId = randomUUID();
let connectLatencyMs = null;
let connectError = null;
let connectErrorDetails = null;
let gatewayReached = false;
let close = null;
let auth = emptyProbeAuth();
let server = emptyProbeServer();
let authMetadataPresent = false;
const detailLevel = opts.includeDetails === false ? "none" : opts.detailLevel ?? "full";
const deviceAuthScope = opts.suppressStoredDeviceAuth ? void 0 : opts.originScopedDeviceAuth ? gatewayOriginScope(opts.url) : resolveProbeDeviceAuthScope(opts.url);
const deviceIdentity = await (async () => {
try {
if (!URL.canParse(opts.url)) return null;
const { loadDeviceIdentityIfPresent } = await import("./device-identity-IBUe6KAS.js");
const identity = loadDeviceIdentityIfPresent({ env: opts.env });
if (!identity) return null;
return (opts.suppressStoredDeviceAuth ? null : deviceAuthScope ? loadOriginDeviceTokenReadOnly({
gatewayScope: deviceAuthScope,
deviceId: identity.deviceId,
role: "operator",
env: opts.env
}) : loadDeviceAuthTokenReadOnly({
deviceId: identity.deviceId,
role: "operator",
env: opts.env
})) ? identity : null;
} catch {
return null;
}
})();
const cacheKey = resolveDeviceRequiredProbeCacheKey(opts.url);
const cacheEligible = deviceIdentity == null && !hasProbeAuth(opts.auth);
if (cacheEligible && shouldShortCircuitDeviceRequiredProbe(cacheKey, Date.now())) return makeDeviceRequiredShortCircuitResult(opts.url);
const initialProbeTimeoutMs = clampProbeTimeoutMs(opts.timeoutMs);
const edgeAuthConfig = normalizeEdgeAuthHeadersConfig(gatewayEdgeAuthValueForTarget({
config: opts.config ?? {},
targetUrl: opts.url
}));
const edgeAuthHeaders = await resolveEdgeAuthHeaders({
config: opts.config ?? {},
value: edgeAuthConfig,
targetUrl: opts.url,
env: opts.env ?? process.env
});
return await new Promise((resolve) => {
let settled = false;
let timer = null;
let onAbort;
const startAbort = new AbortController();
const clearProbeTimer = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
const armProbeTimer = (onTimeout, timeoutMs = initialProbeTimeoutMs) => {
clearProbeTimer();
timer = setTimeout(onTimeout, resolveSafeTimeoutDelayMs(timeoutMs));
};
const settle = (result) => {
if (settled) return;
settled = true;
if (onAbort) {
opts.signal?.removeEventListener("abort", onAbort);
onAbort = void 0;
}
startAbort.abort();
clearProbeTimer();
(async () => {
try {
await client.stopAndWait({ timeoutMs: PROBE_CLIENT_STOP_TIMEOUT_MS });
} catch {
client.stop();
}
if (result.ok) clearDeviceRequiredProbeFailures(cacheKey);
else if (cacheEligible && result.gatewayReached && isDeviceIdentityRequiredClose(result.close)) noteDeviceRequiredProbeFailure(cacheKey, Date.now());
const { connectErrorDetails: resultConnectErrorDetails, ...rest } = result;
resolve({
url: opts.url,
...rest,
...resultConnectErrorDetails != null ? { connectErrorDetails: resultConnectErrorDetails } : {}
});
})();
};
const settleProbe = (params) => {
settle({
ok: params.ok,
...gatewayReached ? { gatewayReached: true } : {},
connectLatencyMs,
error: params.error,
...params.missingScopeErrorDetails ? { missingScopeErrorDetails: params.missingScopeErrorDetails } : {},
connectErrorDetails,
close,
auth: resolveProbeAuthSummary({
role: auth.role,
scopes: auth.scopes,
authMetadataPresent,
connectErrorDetails,
error: params.error,
close,
verifiedRead: params.verifiedRead,
connectLatencyMs
}),
server,
health: params.health,
status: params.status,
presence: params.presence,
configSnapshot: params.configSnapshot
});
};
const client = new GatewayClient({
url: opts.url,
...deviceAuthScope ? { deviceAuthScope } : {},
token: opts.auth?.token,
password: opts.auth?.password,
edgeAuthHeaders,
tlsFingerprint: opts.tlsFingerprint || (opts.url.trim() === opts.config?.gateway?.remote?.url?.trim() ? opts.config?.gateway?.remote?.tlsFingerprint : void 0),
preauthHandshakeTimeoutMs: opts.preauthHandshakeTimeoutMs,
env: opts.env,
scopes: [READ_SCOPE],
clientName: GATEWAY_CLIENT_NAMES.CLI,
clientVersion: "dev",
mode: GATEWAY_CLIENT_MODES.PROBE,
sharedStateMode: "read-only",
instanceId,
deviceIdentity,
onConnectError: (err) => {
connectError = formatErrorMessage(err);
connectErrorDetails = err instanceof GatewayClientRequestError ? err.details : null;
gatewayReached ||= isGatewayProtocolResponseError(err);
},
onClose: (code, reason, info) => {
close = {
code,
reason
};
gatewayReached ||= isGatewayProtocolResponseError(info?.connectError);
if (connectLatencyMs == null) {
if (info?.transportValidated === true) connectLatencyMs = Date.now() - startedAt;
settleProbe({
ok: false,
error: connectError || formatProbeCloseError(close),
health: null,
status: null,
presence: null,
configSnapshot: null
});
}
},
onHelloOk: (hello) => {
gatewayReached = true;
(async () => {
connectLatencyMs = Date.now() - startedAt;
authMetadataPresent = typeof hello?.auth === "object" && hello.auth !== null;
server = {
version: typeof hello?.server?.version === "string" ? hello.server.version : null,
...typeof hello?.server?.buildId === "string" ? { buildId: hello.server.buildId } : {},
connId: typeof hello?.server?.connId === "string" ? hello.server.connId : null
};
auth = resolveProbeAuthSummary({
role: typeof hello?.auth?.role === "string" ? hello.auth.role : null,
scopes: Array.isArray(hello?.auth?.scopes) ? hello.auth.scopes.filter((scope) => typeof scope === "string") : [],
authMetadataPresent
});
if (detailLevel === "none") {
settleProbe({
ok: true,
error: null,
verifiedRead: false,
health: null,
status: null,
presence: null,
configSnapshot: null
});
return;
}
armProbeTimer(() => {
settleProbe({
ok: false,
error: "timeout",
health: null,
status: null,
presence: null,
configSnapshot: null
});
});
try {
if (detailLevel === "presence") {
const presence = await client.request("system-presence");
settleProbe({
ok: true,
error: null,
verifiedRead: true,
health: null,
status: null,
presence: Array.isArray(presence) ? presence : null,
configSnapshot: null
});
return;
}
if (detailLevel === "config") {
const configSnapshot = await client.request("config.get", {});
settleProbe({
ok: true,
error: null,
verifiedRead: true,
health: null,
status: null,
presence: null,
configSnapshot
});
return;
}
const [health, status, presence, configSnapshot] = await Promise.all([
client.request("health"),
client.request("status"),
client.request("system-presence"),
client.request("config.get", {})
]);
settleProbe({
ok: true,
error: null,
verifiedRead: true,
health,
status,
presence: Array.isArray(presence) ? presence : null,
configSnapshot
});
} catch (err) {
const error = formatErrorMessage(err);
const missingScopeErrorDetails = readMissingScopeError(err);
settleProbe({
ok: false,
error,
...missingScopeErrorDetails ? { missingScopeErrorDetails } : {},
health: null,
status: null,
presence: null,
configSnapshot: null
});
}
})();
}
});
if (opts.signal) {
onAbort = () => {
settleProbe({
ok: false,
error: "aborted",
health: null,
status: null,
presence: null,
configSnapshot: null
});
};
opts.signal.addEventListener("abort", onAbort, { once: true });
if (opts.signal.aborted) onAbort();
}
if (settled) return;
armProbeTimer(() => {
const error = connectError ? `connect failed: ${connectError}` : "timeout";
settleProbe({
ok: false,
error,
health: null,
status: null,
presence: null,
configSnapshot: null
});
});
startGatewayClientWhenEventLoopReady(client, {
timeoutMs: initialProbeTimeoutMs,
signal: startAbort.signal
}).then((readiness) => {
if (settled || readiness.ready || readiness.aborted) return;
settleProbe({
ok: false,
error: "timeout",
health: null,
status: null,
presence: null,
configSnapshot: null
});
}).catch((err) => {
if (settled) return;
connectError = formatErrorMessage(err);
settleProbe({
ok: false,
error: connectError,
health: null,
status: null,
presence: null,
configSnapshot: null
});
});
});
}
//#endregion
export { probeGateway as n, resolveProbeAuthSummary as r, clampProbeTimeoutMs as t };