openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
3,402 lines • 159 kB
JavaScript
import { j as resolveIntegerOption } from "./number-coercion-CLj0HTDM.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { u as normalizeSortedUniqueTrimmedStringList, y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { i as getOrCreatePromise, n as createLazyPromise } from "./lazy-promise-DGqyc4Y4.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { r as racePromiseWithAbortSignal } from "./abort-signal-D2k14JsD.js";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { B as hasInternalDiagnosticEventInterest, C as createChildDiagnosticTraceContext, M as parseDiagnosticTraceparent, N as runWithDiagnosticTraceContext, O as getActiveDiagnosticTraceContext, c as emitTrustedSecurityEvent, o as emitTrustedDiagnosticEvent, t as areDiagnosticsEnabledForProcess, w as createDiagnosticTraceContext } from "./diagnostic-events-Cwe92uV3.js";
import { l as resolveRuntimeServiceVersion, s as resolveRuntimeServiceBuildId } from "./version-v1kuAkGj.js";
import { n as sha256Base64Url, o as sha256HexPrefixCore } from "./crypto-digest-C4hqTb_e.js";
import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-BaQikjTR.js";
import { t as getActivePluginGatewayNodePolicyRegistry } from "./runtime-state-Ccxn1fO9.js";
import { c as WRITE_SCOPE, r as PAIRING_SCOPE, t as ADMIN_SCOPE } from "./operator-scopes-Dw7Gu2cA.js";
import { a as hasGatewayClientCap, n as GATEWAY_CLIENT_IDS, r as GATEWAY_CLIENT_MODES, t as GATEWAY_CLIENT_CAPS } from "./client-info-B1bPgeKr.js";
import "./io-bdCzpGWJ.js";
import { i as runUtf8CommandWithTimeout } from "./exec-BIE-3oLG.js";
import { a as isLoopbackAddress, c as isPrivateOrLoopbackAddress, d as isTrustedProxyAddress, i as isLocalishHost } from "./net-DbNPs6Xm.js";
import { c as AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, o as AUTH_RATE_LIMIT_SCOPE_NODE_PAIRING } from "./auth-rate-limit-C2k5aTEA.js";
import { n as PROXY_ATTRIBUTION_REQUIRED_REASON } from "./ingress-attribution-C3JDfhmX.js";
import { n as isGatewayHostBrowserOrigin, r as normalizeChromeExtensionOrigin } from "./origin-check-BQMysGh3.js";
import { n as withSerializedRateLimitAttempt, t as withSerializedCredentialFallbackAttempt } from "./rate-limit-attempt-serialization-CvMQJrpm.js";
import { h as resolveDeviceAuthConnectErrorDetailCode, i as buildPairingConnectErrorMessage, m as resolveAuthConnectErrorDetailCode, n as buildPairingConnectCloseReason, r as buildPairingConnectErrorDetails, t as ConnectErrorDetailCodes } from "./connect-error-details-DaOfGNN8.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import "./version-CwNT1gaY.js";
import { a as normalizeDevicePublicKeyBase64Url, t as deriveDeviceIdFromPublicKey } from "./device-identity-J83pn_rP.js";
import { r as roleScopesAllow } from "./operator-scope-compat-iV7_Lmth.js";
import { t as resolveGatewayPublicOrigin } from "./gateway-public-origin-BcHLka2A.js";
import { t as normalizeControlUiBasePath } from "./control-ui-shared-BiO6QP54.js";
import { i as gatewayStartupUnavailableDetails, n as GATEWAY_STARTUP_CLOSE_REASON, r as GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, t as GATEWAY_STARTUP_CLOSE_CODE } from "./startup-unavailable-D0-EeFjq.js";
import { r as normalizeDeviceMetadataForAuth } from "./device-auth-na9vtJo1.js";
import { t as rawDataToString } from "./websocket-data-2vBvd4uX.js";
import { t as resolveGatewayClientPlatformIdentity } from "./gateway-client-platform-h2QbDjYZ.js";
import { Ed as GATEWAY_SUSPEND_UNAVAILABLE_REASON, Td as GATEWAY_RESTART_UNAVAILABLE_REASON, _g as formatValidationErrors, it as validateConnectParams, lr as validateRequestFrame, nh as GATEWAY_SERVER_CAPS } from "./src-BiL5aQto.js";
import { d as isCoreGatewayMethodClassified } from "./method-scopes-K6J_UQGL.js";
import { D as tryBeginGatewayRootWorkAdmission, E as tryBeginGatewayRestartStartupRootWorkAdmission, b as runWithGatewayIndependentRootWorkAdmission, c as getGatewaySuspendAdmissionPhase, u as isGatewayRestartDraining, y as runOutsideGatewayRootWorkAdmission } from "./gateway-work-admission-R1IpuDim.js";
import { i as isGatewayCliClient, l as isWebchatClient, n as isBrowserOperatorUiClient, r as isEphemeralGatewayClient, s as isOperatorUiClient, t as isBrowserCopilotClient } from "./message-channel-BQrhwUEA.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { a as MAX_PAYLOAD_BYTES, i as MAX_BUFFERED_BYTES, o as MAX_PREAUTH_PAYLOAD_BYTES, s as TICK_INTERVAL_MS } from "./server-constants-BrVEC7RW.js";
import { i as ensureProfileForTailscaleIdentity, m as hasMultipleSessionSharingIdentities, n as ensureGatewayOwnerProfile, p as getUserProfileDisplay, r as ensureProfileForEmail, t as adoptTailscaleProfileAvatar, v as classifyTailscaleLogin } from "./user-profiles-4AB7AmiH.js";
import { i as verifyAgentRuntimeIdentityToken } from "./agent-runtime-identity-token-DoLEaEV9.js";
import { n as isOperatorApprovalRuntimeToken } from "./operator-approval-runtime-token-BASQugN0.js";
import { n as resolveControlUiLinkLocation } from "./control-ui-link-base-D3CcSxxk.js";
import { l as resolveOperatorRolePolicyForProfile, p as authenticatedProfileUnavailableError } from "./operator-role-policy-wsr1DeJv.js";
import { i as TALK_PTT_COMMANDS, n as IOS_WATCH_RELAY_COMMANDS, r as PLATFORM_DEFAULTS, t as DEFAULT_DANGEROUS_NODE_COMMANDS } from "./node-command-policy-H4FpS0aA.js";
import { a as listControlUiPluginTabs, n as shouldUseGatewayOwnerProfile, o as listControlUiPluginWidgetKinds, r as resolveHostAccountName, t as createAuthenticatedGitHubIdentitySync } from "./github-user-identity-DaoVKeEh.js";
import { i as upsertPresence } from "./system-presence-Y_GcutUC.js";
import { A as CLOUD_WORKER_PAIRING_SETUP_BOOTSTRAP_PROFILE, H as resolveBootstrapProfileScopesForRoles, I as deviceBootstrapProfilesEqual, L as isMobilePairingSetupBootstrapProfile, R as isNodePairingSetupBootstrapProfile, V as resolveBootstrapProfileScopesForRole, a as getBoundDeviceBootstrapContext, d as redeemDeviceBootstrapTokenProfile, f as restoreGenericDeviceBootstrapToken, h as verifyDeviceBootstrapToken, j as CONTROL_UI_OWNER_BOOTSTRAP_PROFILE, k as BOOTSTRAP_HANDOFF_OPERATOR_SCOPES, o as getBoundDeviceBootstrapProfile, z as isVoiceNodePairingSetupBootstrapProfile } from "./device-bootstrap-DRFMRCcr.js";
import { c as listDevicePairing, d as pruneSupersededSilentPairedDevices, h as requestDevicePairing, i as hasEffectivePairedDeviceRole, n as getPairedDevice, s as listApprovedPairedDeviceRoles, u as listEffectivePairedDeviceRoles, v as updatePairedDeviceMetadata } from "./device-pairing-DFIQp3ZY.js";
import { d as releaseNodePairingCleanupClaim, n as beginNodePairingConnect, p as requestNodePairing, r as finalizeNodePairingCleanupClaim, s as recordPairedNodeConnection, t as approveNodePairing } from "./device-pairing-node-DiTk_P80.js";
import { n as normalizeNodeApprovalSurfaceList } from "./node-pairing-surface-zCblzr6w.js";
import { i as recordRemoteNodeInfo, o as refreshRemoteNodeBins } from "./remote-D-j1AEXQ.js";
import { t as rawDataByteLength } from "./ws-C3ckvj65.js";
import { n as logRejectedLargePayload } from "./diagnostic-payload-B51qzY4j.js";
import { t as resolveEffectiveComputerUseDescriptor } from "./node-computer-use-descriptor-D_tS4bop.js";
import { r as serializeEventPayload } from "./node-registry-DspP5BRY.js";
import { n as invalidateGatewayPolicyClient, r as registerGatewayPolicyResponse } from "./ws-policy-close-CZEENNh9.js";
import { f as resolvePluginNodeCapabilityExpiresAtMs, m as setClientPluginNodeCapability, o as indexPluginNodeCapabilitySurfaces, r as buildPluginNodeCapabilityScopedHostUrl, s as mintPluginNodeCapabilityToken } from "./plugin-node-capability-CIHQ6nIw.js";
import { n as logWs, t as formatForLog } from "./ws-log-Dkg9wKNU.js";
import { n as ensureDeviceToken, s as verifyDeviceToken } from "./device-pairing-tokens-GP7d0q7h.js";
import { n as approveDevicePairing, t as approveBootstrapDevicePairing } from "./device-pairing-approval-LumnRImV.js";
import { n as ControlUiGitHubError } from "./control-ui-github-api-CCpVJnCA.js";
import { t as resolveSharedGatewaySessionGeneration } from "./ws-shared-generation-DZTjxt6J.js";
import { O as allowedSessionVisibilities } from "./session-sharing-B7MI8hNo.js";
import { n as loadVoiceWakeConfig } from "./server-utils-BUZaV07G.js";
import { r as resolveChatAttachmentPolicy } from "./chat-attachment-policy-F0KcWQR6.js";
import { t as getTailscalePublishedOrigin } from "./tailscale-published-origin-BvNdSLZR.js";
import { n as parseGatewayRole, r as roleCanSkipDeviceIdentity } from "./role-policy-B0ZcgXv3.js";
import { n as classifyGatewayStaleInstall } from "./stale-install-EFmAbT2d.js";
import { t as loadVoiceWakeRoutingConfig } from "./voicewake-routing-i9PtlWt7.js";
import { r as prepareGatewayLocalUserIngress, t as attachGatewayLocalUserIngress } from "./local-user-ingress-o2_EOOV8.js";
import { a as canReadDetailedUpdateMetadata } from "./events-DFzVQFw-.js";
import { n as getHealthCache, r as getHealthVersion, t as buildGatewaySnapshot } from "./health-state-BQ5pY3AF.js";
import { n as clearRemovedNodeRuntimeState, s as reconcileRevokedDeviceWorker } from "./node-runtime-state-Cp7qgqDe.js";
import { t as captureAuthenticatedNodePairingState } from "./device-pairing-node-state-DxNt2jjv.js";
import { t as resolveLocalNodeId } from "./local-id-BhWlYr_Q.js";
import { t as broadcastPresenceSnapshot } from "./presence-events-DoThnKcO.js";
import { t as buildAuthenticatedPresenceUser } from "./authenticated-presence-user-FnQxM2p0.js";
import { a as scheduleNodeConnectionNotification, o as retireDeviceTokenClients, r as resolveGatewayWsBrowserOrigin, t as checkGatewayWsBrowserOrigin } from "./ws-origin-policy-DhLBST96.js";
import { a as buildHandshakeAuthLogKey, i as HandshakeAuthLogLimiter, o as shouldLimitMissingCredentialAuthLog, r as scheduleGatewayRequestStart, t as prepareGatewayReceiverHandoff } from "./request-start-CJxBGjAY.js";
import { t as truncateCloseReason } from "./close-reason-C0VVCJld.js";
import { a as consumeSetupHandoff, c as isNativeAppUiClient, d as resolvePairingLocality, f as resolveUnauthorizedHandshakeContext, h as shouldSkipLocalBackendSelfPairing, i as confirmSetupHandoffDelivery, l as resolveDeviceSignaturePayloadVersion, m as shouldPreserveLocalCliSharedAuthScopes, n as broadcastSetupHandoffCompletion, o as resolveConnectAuthDecision, p as shouldAllowSilentLocalPairing, r as broadcastSetupHandoffDeliveryUncertain, s as resolveConnectAuthState, t as reconcileNodePairingOnConnect, u as resolveHandshakeBrowserSecurityContext } from "./node-connect-reconcile-CD0BmRmz.js";
import { isDeepStrictEqual } from "node:util";
import net from "node:net";
import os from "node:os";
import { performance } from "node:perf_hooks";
//#region src/gateway/node-pairing-auto-approve.ts
/** Remote auto-approval may grant only the fresh, capability-free node request it proved. */
function isScopelessNodePairingRequest(request) {
return request.isRepair !== true && (request.scopes ?? []).length === 0 && (request.role === void 0 || request.role === "node") && (request.roles ?? []).every((role) => role === "node");
}
/** Classifies how the gateway learned the client IP for node auto-approval. */
function resolveNodePairingClientIpSource(params) {
if (!params.reportedClientIp) return "none";
if (!params.hasProxyHeaders || !params.remoteIsTrustedProxy) return "direct";
return params.remoteIsLoopback ? "loopback-trusted-proxy" : "trusted-proxy";
}
/**
* Shared floor for every non-interactive node pairing approval (trusted-CIDR,
* SSH-verified): only a fresh, scopeless, non-browser `role: node` request
* with a directly attributable client IP qualifies. Upgrades and spoofable
* loopback trusted-proxy header paths always stay on the manual prompt.
*/
function isEligibleFreshNodePairingRequest(params) {
if (params.existingPairedDevice) return false;
if (params.role !== "node") return false;
if (params.reason !== "not-paired") return false;
if (params.scopes.length > 0) return false;
if (params.hasBrowserOriginHeader || params.isControlUi || params.isWebchat) return false;
if (params.reportedClientIpSource === "none" || params.reportedClientIpSource === "loopback-trusted-proxy") return false;
return Boolean(params.reportedClientIp);
}
/** Returns true when a node pairing request can be auto-approved by trusted CIDR policy. */
function shouldAutoApproveNodePairingFromTrustedCidrs(params) {
if (!isEligibleFreshNodePairingRequest(params) || !params.reportedClientIp) return false;
const autoApproveCidrs = params.autoApproveCidrs?.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
if (!autoApproveCidrs || autoApproveCidrs.length === 0) return false;
return isTrustedProxyAddress(params.reportedClientIp, autoApproveCidrs);
}
//#endregion
//#region src/gateway/server/ws-connection/request-diagnostics.ts
var GatewayRpcDiagnostics = class {
constructor(method) {
this.method = method;
this.startedAt = performance.now();
this.trace = getActiveDiagnosticTraceContext();
this.handlerStarted = false;
this.deliveryFailureRecorded = false;
this.dispatchFinished = false;
this.responseState = "none";
this.emit({
type: "gateway.rpc",
method,
phase: "received"
});
}
emit(event) {
runWithDiagnosticTraceContext(this.trace, () => emitTrustedDiagnosticEvent(event));
}
bindTrace(trace = getActiveDiagnosticTraceContext()) {
this.trace = trace;
}
startQueue() {
this.queueStartedAt = performance.now();
}
finishQueue() {
if (this.queueStartedAt !== void 0) this.queueWaitMs = performance.now() - this.queueStartedAt;
}
response(outcome) {
const sent = outcome === "ok" || outcome === "error";
if (sent ? this.responseState === "sent" : this.deliveryFailureRecorded) return;
if (sent) this.responseState = "sent";
else {
this.deliveryFailureRecorded = true;
if (this.responseState !== "sent") this.responseState = outcome;
}
this.emit({
type: "gateway.rpc",
method: this.method,
phase: "response",
outcome,
durationMs: performance.now() - this.startedAt
});
}
async runHandler(invoke) {
const startedAt = performance.now();
this.handlerStarted = true;
let outcome = "returned";
try {
await invoke();
} catch (error) {
outcome = "threw";
throw error;
} finally {
this.emit({
type: "gateway.rpc",
method: this.method,
phase: "handler",
outcome,
durationMs: performance.now() - startedAt,
admissionMs: startedAt - this.startedAt
});
}
}
finish(outcome) {
if (this.dispatchFinished) return;
this.dispatchFinished = true;
this.emit({
type: "gateway.rpc",
method: this.method,
phase: "dispatch",
outcome: outcome === "returned" && !this.handlerStarted ? "rejected" : outcome,
durationMs: performance.now() - this.startedAt,
...this.queueWaitMs !== void 0 ? { queueWaitMs: this.queueWaitMs } : {},
response: this.responseState
});
}
};
function createGatewayRpcDiagnostics(method, getMethodRegistry, extraHandlers) {
if (!areDiagnosticsEnabledForProcess() || !hasInternalDiagnosticEventInterest("gateway.rpc")) return;
return new GatewayRpcDiagnostics(isCoreGatewayMethodClassified(method) ? method : getMethodRegistry?.().getHandler(method) || Object.hasOwn(extraHandlers, method) ? "other" : "unknown");
}
//#endregion
//#region src/gateway/server/ws-connection/unauthorized-flood-guard.ts
const DEFAULT_CLOSE_AFTER = 10;
const DEFAULT_LOG_EVERY = 100;
/** Counts unauthorized failures and decides when to log or close the socket. */
var UnauthorizedFloodGuard = class {
constructor(options) {
this.count = 0;
this.suppressedSinceLastLog = 0;
this.closeAfter = resolveIntegerOption(options?.closeAfter, DEFAULT_CLOSE_AFTER, { min: 1 });
this.logEvery = resolveIntegerOption(options?.logEvery, DEFAULT_LOG_EVERY, { min: 1 });
}
registerUnauthorized() {
this.count += 1;
const shouldClose = this.count > this.closeAfter;
if (!(this.count === 1 || this.count % this.logEvery === 0 || shouldClose)) {
this.suppressedSinceLastLog += 1;
return {
shouldClose,
shouldLog: false,
count: this.count,
suppressedSinceLastLog: 0
};
}
const suppressedSinceLastLog = this.suppressedSinceLastLog;
this.suppressedSinceLastLog = 0;
return {
shouldClose,
shouldLog: true,
count: this.count,
suppressedSinceLastLog
};
}
reset() {
this.count = 0;
this.suppressedSinceLastLog = 0;
}
};
/** Identifies role-auth failures that should feed the flood guard. */
function isUnauthorizedRoleError(error) {
if (!error) return false;
return error.code === ErrorCodes.INVALID_REQUEST && typeof error.message === "string" && error.message.startsWith("unauthorized role:");
}
//#endregion
//#region src/gateway/server/ws-connection/authenticated-request-dispatch.ts
const loadGatewayServerMethods = createLazyPromise(() => import("./authenticated-request-dispatch.server-methods.runtime.js"));
const DEVICE_CREDENTIAL_INVALIDATING_METHODS = /* @__PURE__ */ new Set([
"device.pair.remove",
"device.token.rotate",
"device.token.revoke",
"node.pair.remove"
]);
function createGatewayAuthenticatedRequestDispatcher(params) {
const { connId, getRequiredSharedGatewaySessionGeneration, extraHandlers, getMethodRegistry, buildRequestContext, send, close, isClosed, setCloseCause, logGateway } = params.handler;
const unauthorizedFloodGuard = new UnauthorizedFloodGuard();
let deviceCredentialMutationBarrier;
const closeInvalidatedClient = (client, method) => {
if (!client.invalidated) return false;
const reason = client.invalidatedReason ?? "invalidated";
setCloseCause("client-invalidated", {
reason,
method
});
invalidateGatewayPolicyClient(client, {
reason,
code: 4001,
message: `client invalidated: ${reason}`,
close: () => close(4001, `client invalidated: ${reason}`)
});
return true;
};
const dispatch = async (parsed, client, frameBytes, admission) => {
if (!validateRequestFrame(parsed)) {
send({
type: "res",
id: parsed?.id ?? "invalid",
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, `invalid request frame: ${formatValidationErrors(validateRequestFrame.errors)}`)
});
return;
}
const req = parsed;
const diagnostics = createGatewayRpcDiagnostics(req.method, getMethodRegistry, extraHandlers);
logWs("in", "req", {
connId,
id: req.id,
method: req.method
});
const context = buildRequestContext();
const hasCurrentClientAuthority = () => {
if (closeInvalidatedClient(client, req.method)) return false;
const requiredGeneration = client.usesSharedGatewayAuth ? getRequiredSharedGatewaySessionGeneration?.() : void 0;
if (requiredGeneration !== void 0 && client.sharedGatewaySessionGeneration !== requiredGeneration) {
setCloseCause("gateway-auth-rotated", {
authGenerationStale: true,
method: req.method
});
invalidateGatewayPolicyClient(client, {
reason: "gateway-auth-changed",
code: 4001,
message: "gateway auth changed",
close: () => close(4001, "gateway auth changed")
});
return false;
}
return true;
};
const respond = (ok, payload, error, meta) => {
if (!policyResponse?.pending && !hasCurrentClientAuthority()) {
diagnostics?.response("suppressed");
return;
}
try {
let responseOk = ok;
let responseError = error;
let sendResult = send({
type: "res",
id: req.id,
ok,
payload,
error
});
if (sendResult.kind === "serialization") {
const detail = formatForLog(sendResult.error);
logGateway.error(`response serialization failed method=${req.method}: ${detail}`);
responseOk = false;
responseError = errorShape(ErrorCodes.UNAVAILABLE, "response serialization failed");
sendResult = send({
type: "res",
id: req.id,
ok: responseOk,
error: responseError
});
}
diagnostics?.response(sendResult.kind === "sent" ? responseOk ? "ok" : "error" : "unavailable");
const unauthorizedRoleError = isUnauthorizedRoleError(responseError);
let logMeta = meta;
if (unauthorizedRoleError) {
const unauthorizedDecision = unauthorizedFloodGuard.registerUnauthorized();
if (unauthorizedDecision.suppressedSinceLastLog > 0) logMeta = {
...logMeta,
suppressedUnauthorizedResponses: unauthorizedDecision.suppressedSinceLastLog
};
if (!unauthorizedDecision.shouldLog) return;
if (unauthorizedDecision.shouldClose) {
setCloseCause("repeated-unauthorized-requests", {
unauthorizedCount: unauthorizedDecision.count,
method: req.method
});
queueMicrotask(() => close(1008, "repeated unauthorized calls"));
}
logMeta = {
...logMeta,
unauthorizedCount: unauthorizedDecision.count
};
} else unauthorizedFloodGuard.reset();
logWs("out", "res", {
connId,
id: req.id,
ok: responseOk,
method: req.method,
errorCode: responseError?.code,
errorMessage: responseError?.message,
...logMeta
});
} finally {
policyResponse?.finish();
}
};
const agentRuntimeIdentity = client.internal?.agentRuntimeIdentity;
const hasCurrentRuntimeAuthority = () => {
if (agentRuntimeIdentity && context.validateAgentRuntimeApprovalAuthority?.(agentRuntimeIdentity) !== true) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "agent runtime authority is no longer active"));
setCloseCause("agent-runtime-authority-closed", { method: req.method });
close(4001, "agent runtime authority closed");
return false;
}
return true;
};
const respondWithAuthority = (ok, payload, error, meta) => {
if (hasCurrentRuntimeAuthority()) respond(ok, payload, error, meta);
};
const policyResponse = registerGatewayPolicyResponse(req.method, client, respondWithAuthority);
const executeRequest = async () => {
diagnostics?.bindTrace();
let entry;
const credentialMutationBarrier = deviceCredentialMutationBarrier;
const requestController = req.method === "sessions.companion.ask" || req.method === "node.invoke" && client.connect.client.id === GATEWAY_CLIENT_IDS.CLI && client.connect.client.mode === GATEWAY_CLIENT_MODES.CLI ? new AbortController() : void 0;
const cancelRequest = () => requestController?.abort();
if (requestController) client.socket.once("close", cancelRequest);
let dispatchOutcome = "returned";
try {
entry = context.requestEntryLifetime?.enter({
req,
client,
context
});
if (credentialMutationBarrier) {
await racePromiseWithAbortSignal(credentialMutationBarrier, context.requestEntryLifetime?.signal).catch(() => void 0);
if (context.requestEntryLifetime?.signal.aborted) {
respondWithAuthority(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "gateway closing before request dispatch", { retryable: true }));
return;
}
if (isClosed()) return;
}
if (!hasCurrentClientAuthority() || !hasCurrentRuntimeAuthority()) return;
const { handleGatewayRequest } = await loadGatewayServerMethods();
entry?.assertOpen();
if (client.connect.role === "operator") {
diagnostics?.startQueue();
const start = scheduleGatewayRequestStart(frameBytes);
if (!start) {
respondWithAuthority(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "gateway request start capacity exceeded", { retryable: true }));
return;
}
await start;
diagnostics?.finishQueue();
}
entry?.assertOpen();
if (requestController?.signal.aborted || !hasCurrentClientAuthority() || !hasCurrentRuntimeAuthority()) return;
await runOutsideGatewayRootWorkAdmission(() => handleGatewayRequest({
req,
respond: respondWithAuthority,
client,
isWebchatConnect: params.isWebchatConnect,
extraHandlers,
methodRegistry: getMethodRegistry?.(),
context,
...admission ? { admission } : {},
requestEntry: entry,
...requestController ? { signal: requestController.signal } : {}
}, diagnostics));
} catch (err) {
dispatchOutcome = "threw";
logGateway.error(`request handler failed: ${formatForLog(err)}`);
const staleInstall = classifyGatewayStaleInstall(err);
respondWithAuthority(false, void 0, staleInstall?.error ?? errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
} finally {
policyResponse?.finish();
diagnostics?.finish(requestController?.signal.aborted ? "cancelled" : dispatchOutcome);
entry?.release();
if (requestController) client.socket.off("close", cancelRequest);
}
};
const upstreamTrace = parseDiagnosticTraceparent(req.traceparent);
const dispatchRequest = () => upstreamTrace ? runWithDiagnosticTraceContext(createChildDiagnosticTraceContext(upstreamTrace), executeRequest) : executeRequest();
const requestDispatch = client.connect.role === "node" ? params.handler.nodeLifecycleDispatch.dispatch(req.method, dispatchRequest) : dispatchRequest();
if (DEVICE_CREDENTIAL_INVALIDATING_METHODS.has(req.method)) {
const barrier = requestDispatch.finally(() => {
if (deviceCredentialMutationBarrier === barrier) deviceCredentialMutationBarrier = void 0;
});
deviceCredentialMutationBarrier = barrier;
}
await requestDispatch;
};
return { dispatch };
}
//#endregion
//#region src/gateway/server/ws-connection/connect-admission.ts
function hasCredential(value) {
return typeof value === "string" && value.trim().length > 0;
}
function isStartupNodeConnect(connectParams) {
return connectParams.role === "node" && connectParams.client.mode === GATEWAY_CLIENT_MODES.NODE;
}
/** Exact first-connect shape emitted by `openclaw connect` for a setup-code node. */
function isStartupNodeBootstrapConnect(connectParams) {
const auth = connectParams.auth;
const device = connectParams.device;
return isStartupNodeConnect(connectParams) && connectParams.client.id === GATEWAY_CLIENT_IDS.NODE_HOST && Array.isArray(connectParams.scopes) && connectParams.scopes.length === 0 && Boolean(device?.id.trim() && device.publicKey.trim()) && hasCredential(auth?.bootstrapToken) && !hasCredential(auth?.token) && !hasCredential(auth?.deviceToken) && !hasCredential(auth?.password) && !hasCredential(auth?.approvalRuntimeToken) && !hasCredential(auth?.agentRuntimeIdentityToken);
}
async function rejectGatewayStartupConnect(context) {
const { close } = context.handler;
const { frame, markHandshakeFailure, sendFrame } = context;
markHandshakeFailure(GATEWAY_STARTUP_PENDING_CLOSE_CAUSE);
await sendFrame({
type: "res",
id: frame.id,
ok: false,
error: errorShape(ErrorCodes.UNAVAILABLE, "gateway starting; retry shortly", {
retryable: true,
retryAfterMs: 500,
details: gatewayStartupUnavailableDetails()
})
}).catch(() => {});
queueMicrotask(() => close(GATEWAY_STARTUP_CLOSE_CODE, GATEWAY_STARTUP_CLOSE_REASON));
}
async function rejectUnavailableProfileConnect(context, error) {
const failure = authenticatedProfileUnavailableError(error instanceof ControlUiGitHubError && error.statusCode === 429 ? "GitHub is rate limiting profile verification. Retry shortly; if this continues, ask a gateway administrator to check the GitHub API credential." : void 0, error instanceof ControlUiGitHubError ? error.retryAfterMs : void 0);
context.markHandshakeFailure("authenticated-profile-unavailable");
context.sendHandshakeErrorResponse(ErrorCodes.UNAVAILABLE, failure.message, failure);
await context.releasePendingNodePairingCleanup();
context.handler.close(1013, truncateCloseReason(failure.message));
}
function applyConnectionScopeCap(params) {
const header = params.upgradeReq.headers["x-openclaw-scopes"];
const rawHeader = Array.isArray(header) ? header[0] : header;
if (rawHeader === void 0) return params.scopes;
const declaredScopes = new Set(rawHeader.split(",").map((scope) => scope.trim()).filter((scope) => scope.length > 0));
return declaredScopes.size === 0 ? [] : params.scopes.filter((scope) => declaredScopes.has(scope));
}
function resolveEffectiveConnectionScopes(params) {
const verifiedIdentity = params.verifiedIdentity;
let identityScopes = [];
if (params.role === "operator" && verifiedIdentity) {
const exactIdentityScopes = params.identityScopes?.[verifiedIdentity];
identityScopes = exactIdentityScopes ?? [];
if (exactIdentityScopes === void 0 && verifiedIdentity.includes("@")) {
const normalizedIdentity = verifiedIdentity.toLowerCase();
identityScopes = Object.entries(params.identityScopes ?? {}).find(([identity]) => identity.includes("@") && identity.toLowerCase() === normalizedIdentity)?.[1] ?? [];
}
}
const scopes = applyConnectionScopeCap({
scopes: [.../* @__PURE__ */ new Set([...params.deviceScopes, ...identityScopes])],
upgradeReq: params.upgradeReq
});
return {
scopes,
addedIdentityScopes: identityScopes.filter((scope) => scopes.includes(scope) && !roleScopesAllow({
role: "operator",
requestedScopes: [scope],
allowedScopes: params.deviceScopes
}))
};
}
function rejectGatewayConnectOrigin(context, reason) {
const message = "origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)";
context.markHandshakeFailure("origin-mismatch", {
origin: context.handler.requestOrigin ?? "n/a",
host: context.handler.requestHost ?? "n/a",
reason
});
context.sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, message, { details: {
code: ConnectErrorDetailCodes.CONTROL_UI_ORIGIN_NOT_ALLOWED,
reason
} });
context.handler.close(1008, truncateCloseReason(message));
}
/** Recheck live authority after awaited work, before granting credentials or registering a client. */
function resolveGatewayConnectPolicyFailure(context, state) {
if (state.sessionUsesSharedGatewayAuth && context.handler.getRequiredSharedGatewaySessionGeneration && state.sessionSharedGatewaySessionGeneration !== context.handler.getRequiredSharedGatewaySessionGeneration()) return { kind: "auth" };
if (context.browserOrigin) {
const originCheck = checkGatewayWsBrowserOrigin(context.browserOrigin, getRuntimeConfig());
if (!originCheck.ok) return {
kind: "origin",
reason: originCheck.reason
};
}
}
async function admitGatewayConnect(context) {
const { connId, remoteAddr, remotePort, requestHost, requestOrigin, close, isStartupPending, logGateway, logWsControl, originCheckMetrics } = context.handler;
const { connectParams, configSnapshot, peerLabel, browserOrigin, clientLabel, markHandshakeFailure, sendHandshakeErrorResponse, isWebchatConnect } = context;
const isNodeClient = isStartupNodeConnect(connectParams);
const startupPending = isStartupPending?.() === true;
if (startupPending && !isNodeClient) {
await rejectGatewayStartupConnect(context);
return;
}
const { minProtocol, maxProtocol } = connectParams;
const supportsCurrentProtocol = maxProtocol >= 4 && minProtocol <= 4;
const supportsProbeRestartProtocol = connectParams.client.mode === GATEWAY_CLIENT_MODES.PROBE && maxProtocol >= 3 && minProtocol <= 4;
const supportsPreviousNodeProtocol = isNodeClient && maxProtocol >= 3 && minProtocol <= 3;
const usesLegacyNodeProtocol = !supportsCurrentProtocol && supportsPreviousNodeProtocol;
if (!supportsCurrentProtocol && !supportsProbeRestartProtocol && !supportsPreviousNodeProtocol) {
markHandshakeFailure("protocol-mismatch", {
minProtocol,
maxProtocol,
expectedProtocol: 4,
minimumProbeProtocol: 3
});
logWsControl.warn(`protocol mismatch conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} remotePort=${remotePort ?? "?"} client=${formatForLog(clientLabel)} ${connectParams.client.mode} v${formatForLog(connectParams.client.version)} min=${minProtocol} max=${maxProtocol} expected=4 probeMin=3 instance=${formatForLog(connectParams.client.instanceId ?? "n/a")}`);
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "protocol mismatch", { details: {
code: ConnectErrorDetailCodes.PROTOCOL_MISMATCH,
clientMinProtocol: minProtocol,
clientMaxProtocol: maxProtocol,
expectedProtocol: 4,
minimumProbeProtocol: 3
} });
close(1002, "protocol mismatch");
return;
}
const roleRaw = connectParams.role ?? "operator";
const role = parseGatewayRole(roleRaw);
if (!role) {
markHandshakeFailure("invalid-role", { role: roleRaw });
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "invalid role");
close(1008, "invalid role");
return;
}
const scopes = Array.isArray(connectParams.scopes) ? connectParams.scopes : [];
connectParams.role = role;
connectParams.scopes = scopes;
const isBrowserCopilot = isBrowserCopilotClient(connectParams.client);
const browserCopilotOrigin = isBrowserCopilot ? normalizeChromeExtensionOrigin(requestOrigin ?? void 0) : void 0;
if (isBrowserCopilot && (connectParams.client.mode !== GATEWAY_CLIENT_MODES.UI || !hasGatewayClientCap(connectParams.caps, GATEWAY_CLIENT_CAPS.RUN_TOOL_BINDINGS) || !hasGatewayClientCap(connectParams.caps, GATEWAY_CLIENT_CAPS.SESSION_SCOPED_EVENTS))) {
const message = "browser copilot requires ui mode with run-tool-bindings and session-scoped-events capabilities";
markHandshakeFailure("invalid-client", {
client: connectParams.client.id,
mode: connectParams.client.mode
});
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, message);
close(1008, truncateCloseReason(message));
return;
}
if (isBrowserCopilot && !browserCopilotOrigin) {
const message = "browser copilot requires a canonical Chrome extension origin";
markHandshakeFailure("origin-mismatch", {
origin: requestOrigin ?? "n/a",
client: connectParams.client.id
});
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, message, { details: {
code: ConnectErrorDetailCodes.CONTROL_UI_ORIGIN_NOT_ALLOWED,
reason: "invalid browser copilot origin"
} });
close(1008, truncateCloseReason(message));
return;
}
const isControlUi = isOperatorUiClient(connectParams.client) && !isBrowserCopilot;
const isBrowserOperatorUi = isBrowserOperatorUiClient(connectParams.client);
const isWebchat = isWebchatConnect(connectParams);
const isNativeAppUi = isNativeAppUiClient(connectParams.client);
if (browserOrigin) {
const originCheck = checkGatewayWsBrowserOrigin(browserOrigin, configSnapshot);
if (!originCheck.ok) {
rejectGatewayConnectOrigin(context, originCheck.reason);
return;
}
if (originCheck.matchedBy === "host-header-fallback") {
originCheckMetrics.hostHeaderFallbackAccepted += 1;
logWsControl.warn(`security warning: websocket origin accepted via Host-header fallback conn=${connId} count=${originCheckMetrics.hostHeaderFallbackAccepted} host=${requestHost ?? "n/a"} origin=${requestOrigin ?? "n/a"}`);
logGateway.warn("security metric: gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback accepted a websocket connect request");
}
}
return {
minProtocol,
maxProtocol,
usesLegacyNodeProtocol,
role,
scopes,
isControlUi,
isBrowserOperatorUi,
isWebchat,
isNativeAppUi,
startupPending
};
}
//#endregion
//#region src/gateway/server/ws-connection/auth-messages.ts
const SETUP_CODE_REJECTED_MESSAGE = "unauthorized: setup code invalid, expired, revoked, or already used (create a new code; review `openclaw devices list`)";
/** Formats a client-specific auth failure message without exposing secret values. */
function formatGatewayAuthFailureMessage(params) {
const { authMode, authProvided, reason, client, isLocalClient } = params;
const isCli = isGatewayCliClient(client);
const isControlUi = isOperatorUiClient(client);
const isWebchat = isWebchatClient(client);
if (client?.mode === "node" && reason?.startsWith("trusted_proxy_missing_header_")) return "gateway rejected this node: trusted-proxy identity-header authentication is required and no usable machine credential was accepted; run `openclaw doctor` on the Gateway";
const uiHint = "open the dashboard URL and paste the token in Control UI settings";
const missingUiTokenHint = "paste in Control UI settings or openclaw doctor --generate-gateway-token; restart";
const tokenHint = isCli ? isLocalClient ? "use this gateway's gateway.auth.token or pair the device" : "set gateway.remote.token to match gateway.auth.token" : isControlUi || isWebchat ? uiHint : "provide gateway auth token";
const passwordHint = isCli ? isLocalClient ? "use this gateway's gateway.auth.password" : "set gateway.remote.password to match gateway.auth.password" : isControlUi || isWebchat ? "enter the password in Control UI settings" : "provide gateway auth password";
switch (reason) {
case "token_missing": return `unauthorized: gateway token missing (${isControlUi || isWebchat ? missingUiTokenHint : tokenHint})`;
case "token_mismatch": return `unauthorized: gateway token mismatch (${tokenHint})`;
case "token_missing_config": return "unauthorized: gateway token not configured on gateway (set gateway.auth.token)";
case "password_missing": return `unauthorized: gateway password missing (${passwordHint})`;
case "password_mismatch": return `unauthorized: gateway password mismatch (${passwordHint})`;
case "password_missing_config": return "unauthorized: gateway password not configured on gateway (set gateway.auth.password)";
case "bootstrap_token_invalid": return SETUP_CODE_REJECTED_MESSAGE;
case "tailscale_user_missing": return "unauthorized: tailscale identity missing (use Tailscale Serve auth or gateway token/password)";
case "tailscale_proxy_missing": return "unauthorized: tailscale proxy headers missing (use Tailscale Serve or gateway token/password)";
case "tailscale_whois_failed": return "unauthorized: tailscale identity check failed (use Tailscale Serve auth or gateway token/password)";
case "tailscale_user_mismatch": return "unauthorized: tailscale identity mismatch (use Tailscale Serve auth or gateway token/password)";
case "rate_limited": return "unauthorized: too many failed authentication attempts (retry later)";
case PROXY_ATTRIBUTION_REQUIRED_REASON: return "unauthorized: proxy client attribution is required (configure gateway.trustedProxies narrowly and make the proxy overwrite or safely rebuild forwarded client headers)";
case "device_token_mismatch": return "unauthorized: device token mismatch (rotate/reissue device token)";
case "scope_mismatch": return "unauthorized: device token scope mismatch (re-pair or approve scope upgrade)";
}
if (authMode === "token" && authProvided === "none") return `unauthorized: gateway token missing (${tokenHint})`;
if (authMode === "token" && authProvided === "device-token") return "unauthorized: device token rejected (pair/repair this device, or provide gateway token)";
if (authProvided === "bootstrap-token") return SETUP_CODE_REJECTED_MESSAGE;
if (authMode === "password" && authProvided === "none") return `unauthorized: gateway password missing (${passwordHint})`;
return "unauthorized";
}
//#endregion
//#region src/gateway/server/ws-connection/connect-auth-security.ts
function hashGatewaySecurityId(value) {
const normalized = value?.trim();
if (!normalized) return;
return `sha256:${sha256HexPrefixCore(normalized, 12)}`;
}
function emitGatewayAuthSecurityEvent(params) {
emitTrustedSecurityEvent({
category: "auth",
action: params.action,
outcome: params.outcome,
severity: params.severity,
actor: {
kind: params.role === "node" ? "node" : "operator",
...params.deviceId ? { deviceIdHash: hashGatewaySecurityId(params.deviceId) } : {},
role: params.role
},
target: {
kind: "gateway",
name: "websocket"
},
policy: {
id: "gateway.websocket-auth",
decision: params.outcome === "success" ? "allow" : "deny",
...params.reason ? { reason: params.reason } : {}
},
control: {
id: "gateway.ws.connect",
family: "auth"
},
...params.reason ? { reason: params.reason } : {},
attributes: {
auth_mode: params.authMode,
auth_method: params.authMethod ?? "unknown",
auth_provided: params.authProvided ?? "unknown",
client_mode: params.clientMode ?? "unknown",
has_device_identity: Boolean(params.deviceId),
scope_count: params.scopes.length,
...params.rateLimited !== void 0 ? { rate_limited: params.rateLimited } : {}
}
});
}
//#endregion
//#region src/gateway/server/ws-connection/connect-device-metadata.ts
function resolvePairedAccessScopes(device) {
const scopes = Array.isArray(device?.approvedScopes) ? device.approvedScopes : Array.isArray(device?.scopes) ? device.scopes : [];
return normalizeSortedUniqueTrimmedStringList(scopes);
}
function isSetupCodeMobileBootstrapClient(client) {
const platform = normalizeDeviceMetadataForAuth(client.platform);
const deviceFamily = normalizeDeviceMetadataForAuth(client.deviceFamily);
if (client.id === GATEWAY_CLIENT_IDS.ANDROID_APP) return /^android(?:\s|$)/u.test(platform) && deviceFamily === "android";
if (client.id === GATEWAY_CLIENT_IDS.IOS_APP) return /^(?:ios|ipados)(?:\s|$)/u.test(platform) && /^(?:iphone|ipad|ios)$/u.test(deviceFamily);
return false;
}
/** Embedded voice nodes must prove the canonical node-host and ESP32 metadata tuple. */
function isSetupCodeVoiceNodeBootstrapClient(client) {
const platform = normalizeDeviceMetadataForAuth(client.platform);
const deviceFamily = normalizeDeviceMetadataForAuth(client.deviceFamily);
return client.id === GATEWAY_CLIENT_IDS.NODE_HOST && /^esp32(?:\s|$)/u.test(platform) && deviceFamily === "esp32";
}
/** Match a closed setup profile to the client metadata class allowed to redeem it silently. */
function isSetupCodeHandoffBootstrapClient(params) {
return isMobilePairingSetupBootstrapProfile(params.profile) && isSetupCodeMobileBootstrapClient(params.client) || isNodePairingSetupBootstrapProfile(params.profile) && params.client.id === GATEWAY_CLIENT_IDS.NODE_HOST || isVoiceNodePairingSetupBootstrapProfile(params.profile) && isSetupCodeVoiceNodeBootstrapClient(params.client);
}
/** Match the exact host-issued browser-owner profile and its closed requested scope set. */
function isControlUiOwnerBootstrapProfile(params) {
const { profile, requestedScopes } = params;
return Boolean(profile && deviceBootstrapProfilesEqual(profile, CONTROL_UI_OWNER_BOOTSTRAP_PROFILE) && deviceBootstrapProfilesEqual({
roles: ["operator"],
scopes: requestedScopes,
purpose: CONTROL_UI_OWNER_BOOTSTRAP_PROFILE.purpose
}, CONTROL_UI_OWNER_BOOTSTRAP_PROFILE));
}
function isControlUiOperatorBootstrapProfile(params) {
const { profile, requestedScopes } = params;
if (isControlUiOwnerBootstrapProfile(params)) return true;
if (!profile || profile.purpose !== "control-ui") return false;
if (profile.roles.length !== 1 || profile.roles[0] !== "operator") return false;
if (!profile.scopes.every((scope) => BOOTSTRAP_HANDOFF_OPERATOR_SCOPES.includes(scope))) return false;
return roleScopesAllow({
role: "operator",
requestedScopes,
allowedScopes: profile.scopes
});
}
function isMobileNodeBootstrapConnect(params) {
return params.role === "node" && params.scopes.length === 0 && !params.isControlUi && !params.isBrowserOperatorUi && !params.isWebchat && params.clientMode === GATEWAY_CLIENT_MODES.NODE;
}
function pairedDeviceAllowsBootstrapRole(params) {
return hasEffectivePairedDeviceRole(params.device, params.role) && roleScopesAllow({
role: params.role,
requestedScopes: resolveBootstrapProfileScopesForRole(params.role, params.profile.scopes, params.profile.purpose),
allowedScopes: resolvePairedAccessScopes(params.device)
});
}
function pairedDeviceAllowsBootstrapProfile(params) {
const device = params.device;
return Boolean(device && device.publicKey === params.devicePublicKey && params.profile.roles.every((role) => pairedDeviceAllowsBootstrapRole({
device,
profile: params.profile,
role
})));
}
function pairedDeviceAllowsBootstrapOperator(params) {
const device = params.device;
return Boolean(device && device.publicKey === params.devicePublicKey && pairedDeviceAllowsBootstrapRole({
device,
profile: params.profile,
role: "operator"
}));
}
function resolvePinnedClientMetadata(params) {
function resolveNativeAppPlatformFamily(clientId, value) {
if (clientId === GATEWAY_CLIENT_IDS.IOS_APP && /^(?:ios|ipados)(?:\s|$)/.test(value)) return "ios-family";
if (clientId === GATEWAY_CLIENT_IDS.ANDROID_APP && /^android(?:\s|$)/.test(value)) return "android";
if (clientId === GATEWAY_CLIENT_IDS.WATCHOS_APP && /^watchos \d+(?:\.\d+){0,2}$/.test(value)) return "watchos";
if (clientId === GATEWAY_CLIENT_IDS.MACOS_APP && /^macos \d+(?:\.\d+){0,2}$/.test(value)) return "macos";
}
const claimedPlatform = normalizeDeviceMetadataForAuth(params.claimedPlatform);
const claimedDeviceFamily = normalizeDeviceMetadataForAuth(params.claimedDeviceFamily);
const pairedPlatform = normalizeDeviceMetadataForAuth(params.pairedPlatform);
const pairedDeviceFamily = normalizeDeviceMetadataForAuth(params.pairedDeviceFamily);
const hasPinnedPlatform = pairedPlatform !== "";
const hasPinnedDeviceFamily = pairedDeviceFamily !== "";
const pairedRuntimeIdentity = resolveGatewayClientPlatformIdentity(pairedPlatform);
const isRuntimePlatformPin = params.clientId === GATEWAY_CLIENT_IDS.NODE_HOST && params.clientMode === GATEWAY_CLIENT_MODES.NODE && hasPinnedPlatform && claimedPlatform !== "" && resolveGatewayClientPlatformIdentity(claimedPlatform).platform === pairedRuntimeIdentity.platform || !hasPinnedDeviceFamily && pairedRuntimeIdentity.platform !== pairedPlatform && claimedPlatform === pairedRuntimeIdentity.platform && claimedDeviceFamily === normalizeDeviceMetadataForAuth(pairedRuntimeIdentity.deviceFamily);
const isNodeHostUsingMacAppPlatformPin = params.clientId === GATEWAY_CLIENT_IDS.NODE_HOST && params.clientMode === GATEWAY_CLIENT_MODES.NODE && (claimedPlatform === "darwin" || claimedPlatform === "macos") && /^macos \d+(?:\.\d+){0,2}$/.test(pairedPlatform);
const claimedNativeAppPlatformFamily = resolveNativeAppPlatformFamily(params.clientId, claimedPlatform);
const pairedNativeAppPlatformFamily = resolveNativeAppPlatformFamily(params.clientId, pairedPlatform);
const isNativeAppPlatformVersionRefresh = hasPinnedPlatform && claimedPlatform !== "" && claimedPlatform !== pairedPlatform && (claimedNativeAppPlatformFamily !== void 0 && claimedNativeAppPlatformFamily === pairedNativeAppPlatformFamily || params.clientId === GATEWAY_CLIENT_IDS.MACOS_APP && claimedNativeAppPlatformFamily === "macos" && (pairedPlatform === "darwin" || pairedPlatform === "macos"));
const platformMismatch = hasPinnedPlatform && claimedPlatform !== pairedPlatform && !isRuntimePlatformPin && !isNodeHostUsingMacAppPlatformPin && !isNativeAppPlatformVersionRefresh;
const deviceFamilyMismatch = hasPinnedDeviceFamily && claimedDeviceFamily !== pairedDeviceFamily;
const pinnedPlatform = isRuntimePlatformPin ? pairedRuntimeIdentity.platform : claimedPlatform === pairedPlatform ? params.pairedPlatform : isNodeHostUsingMacAppPlatformPin ? params.pairedPlatform : isNativeAppPlatformVersionRefresh ? params.claimedPlatform : void 0;
return {
platformMismatch,
deviceFamilyMismatch,
pinnedPlatform: hasPinnedPlatform ? pinnedPlatform : void 0,
pinnedDeviceFamily: hasPinnedDeviceFamily ? params.pairedDeviceFamily : void 0,
...isNativeAppPlatformVersionRefresh ? { refreshPairedPlatform: params.claimedPlatform } : {}
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-device-proof.ts
const DEVICE_SIGNATURE_SKEW_MS = 12e4;
function verifyGatewayConnectDeviceProof(context, params) {
const { device, resolvedAuth, authMethod, role, scopes } = params;
if (!device) return {
ok: true,
devicePublicKey: null,
deviceAuthPayloadVersion: null
};
const { frame, connectParams } = context;
const { send, close, setHandshakeState, setCloseCause } = context.handler;
const rejectDeviceAuthInvalid = (reason, message) => {
emitGatewayAuthSecurityEvent({
action: "gateway.auth.failed",
outcome: "denied",
severity: "medium",
authMode: resolvedAuth.mode,
authMethod,
authProvided: "device-signature",
role,
scopes,
clientMode: connectParams.client.mode,
deviceId: device.id,
reason
});
setHandshakeState("failed");
setCloseCause("device-auth-invalid", {
reason,
client: connectParams.client.id,
deviceId: device.id
});
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, message, { details: {
code: resolveDeviceAuthConnectErrorDetailCode(reason),
reason
} })
});
close(1008, message);
};
const derivedId = deriveDeviceIdFromPublicKey(device.publicKey);
if (!derivedId || derivedId !== device.id) {
rejectDeviceAuthInvalid("device-id-mismatch", "device identity mismatch");
return { ok: false };
}
const signedAt = device.signedAt;
if (typeof signedAt !== "number" || Math.abs(Date.now() - signedAt) > DEVICE_SIGNATURE_SKEW_MS) {
rejectDeviceAuthInvalid("device-signature-stale", "device signature expired");
return { ok: false };
}
const providedNonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
if (!providedNonce) {
rejectDeviceAuthInvalid("device-nonce-missing", "device nonce required");
return { ok: false };
}
if (providedNonce !== context.handler.connectNonce) {
rejectDeviceAuthInvalid("device-nonce-mismatch", "device nonce mismatch");
return { ok: false };
}
const payloadVersion = resolveDeviceSignaturePayloadVersion({
device,
connectParams,
role,
scopes,
signedAtMs: signedAt,
nonce: providedNonce
});
if (!payloadVersion) {
rejectDeviceAuthInvalid("device-signature", "device signature invalid");
return { ok: false };
}
const devicePublicKey = normalizeDevicePublicKeyBase64Url(device.publicKey);
if (!devicePublicKey) {
rejectDeviceAuthInvalid("device-public-key", "device public key invalid");
return { ok: false };
}
return {
ok: true,
devicePublicKey,
deviceAuthPayloadVersion: payloadVersion
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-policy.ts
function shouldSkipControlUiPairing(params) {
if (params.isControlUi && params.role === "operator" && params.authMethod === "tailscale" && params.device) return "tailscale-device";
if (params.isControlUi && params.role === "operator" && params.authMode === "none") return "auth-none";
return null;
}
function isTrustedProxyControlUiOperatorAuth(params) {
return params.isControlUi && params.role === "operator" && params.authMode === "trusted-proxy" && params.authOk && params.authMethod === "trusted-proxy";
}
function shouldClearUnboundScopesForMissingDeviceIdentity(params) {
return params.decision.kind !== "allow" || params.authMethod === "token" || params.authMethod === "password" || params.authMethod === "trusted-proxy";
}
function evaluateMissingDeviceIdentity(params) {
if (params.hasDeviceIdentity) return { kind: "allow" };
if (params.isControlUi && params.trustedProxyAuthOk) return { kind: "allow" };
if (params.localBackendSelfPairingOk && params.role === "operator") return { kind: "allow" };
if (params.isControlUi) return { kind: "reject-control-ui-insecure-auth" };
if (roleCanSkipDeviceIdentity(params.role, params.sharedAuthOk)) return { kind: "allow" };
if (!params.authOk && params.hasSharedAuth) return { kind: "reject-unauthorized" };
return { kind: "reject-device-required" };
}
//#endregion
//#region src/gateway/server/ws-connection/connect-auth.ts
const unauthorizedHandshakeLogLimiter = new HandshakeAuthLogLimiter();
async function authenticateGatewayConnect(context) {
const hasCredentialFallback = Boolean(context.connectParams.auth?.deviceToken || context.connectParams.device && context.connectParams.auth?.token);
if (!context.authRateLimiter || !hasCredentialFallback) return await authenticateGatewayConnectCore(context);
return await withSerializedCredentialFallbackAttempt({
limiter: context.authRateLimiter,
ip: context.browserRateLimitClientIp,
run: async () => await authenticateGatewayConnectCore(context)
});
}
async function authenticateGatewayConnectCore(context) {
const { upgradeReq, connId, remoteAddr, remotePort, localAddr, localPort, requestHost, requestOrigin, requestUserAgent, getResolvedAuth, getRequiredSharedGatewaySessionGeneration, advanceHandshakePhase, setCloseCause, close, logWsControl } = context.handler;
const { connectParams, trustedProxies, allowRealIpFallback, peerLabel, hasProxyHeaders, isLocalClient, hasBrowserOriginHeader, browserRateLimitClientIp, authRateLimiter, clientLabel, markHandshakeFailure, sendHandshakeErrorResponse } = context;
const resolvedAuth = getResolvedAuth();
const hasRequestedScopes = Array.isArray(connectParams.scopes);
const admission = await admitGatewayConnect(context);
if (!admission) return;
let { scopes } = admission;
const { minProtocol, maxProtocol, usesLegacyNodeProtocol, role, isControlUi, isBrowserOperatorUi, isWebchat, isNativeAppUi, startupPending } = admission;
const startupBootstrapConnect = startupPending && isStartupNodeBootstrapConnect(connectParams);
const deviceRaw = connectParams.device;
const hasTokenAuth = Boolean(connectParams.auth?.token);
const hasPasswordAuth = Boolean(connectParams.auth?.password);
const hasSharedAuth = hasTokenAuth || hasPasswordAuth;
const device = deviceRaw;
const hasBootstrapProof = Boolean(connectParams.auth?.bootstrapToken);
const hasDeviceTokenProof = Boolean(connectParams.auth?.deviceToken);
if (hasSharedAuth || hasBootstrapProof || hasDeviceTokenProof || Boolean(device)) advanceHandshakePhase("auth_credentials_received");
const connectAuthState = await resolveConnectAuthState({
resolvedAuth,
connectAuth: connectParams.auth,
hasDeviceIdentity: Boolean(device),
req: upgradeReq,
trustedProxies,
allowRealIpFallback,
rateLimiter: authRateLimiter,
clientIp: browserRateLimitClientIp
});
const { sharedAuthOk, pendingSharedAuthFailure, bootstrapTokenCandidate, deviceTokenCandidate, deviceTokenCandidateSource } = connectAuthState;
let { authResult, authOk, authMethod } = connectAuthState;
let rejectedPendingSharedAuthFailure = pendingSharedAuthFailure;
const settleRejectedSharedAuthFailure = async () => {
if (!rejectedPendingSharedAuthFailure) return;
rejectedPendingSharedAuthFailure = false;
await authRateLimiter?.recordFailureAndDelay(browserRateLimitClientIp, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
};
const rejectUnauthorized = (failedAuth) => {
const { authProvided, canRetryWithDeviceToken, recommendedNextStep } = resolveUnauthorizedHandshakeContext({
connectAuth: connectParams.auth,
failedAuth,
hasDeviceIdentity: Boolean(device)
});
emitGatewayAuthSecurityEvent({
action: "gateway.auth.failed",
outcome: "denied",
severity: failedAuth.rateLimited ? "high" : "medium",
authMode: resolvedAuth.mode,
authMethod: failedAuth.method ?? authMethod,
authProvided,
role,
scopes,
clientMode: connectParams.client.mode,
deviceId: device?.id,
reason: failedAuth.reason ?? "unknown",
rateLimited: failedAuth.rateLimited === true
});
markHandshakeFailure("unauthorized", {
authMode: resolvedAuth.mode,
authProvided,
authReason: failedAuth.reason,
allowTailscale: resolvedAuth.allowTailscale,
peer: peerLabel,
remoteAddr,
remotePort,
localAddr,
localPort,
role,
scopeCount: scopes.length,
hasDeviceIdentity: Boolean(device)
});
const authMessage = formatGatewayAuthFailureMessage({
authMode: resolvedAuth.mode,
authProvided,
reason: failedAuth.reason,
client: connectParams.client,
isLocalClient
});
const authLogDecision = shouldLimitMissingCredentialAuthLog({
reason: failedAuth.reason,
authProvided
}) ? unauthorizedHandshakeLogLimiter.register(buildHandshakeAuthLogKey({
reason: failedAuth.reason,
remoteAddr,
client: clientLabel,
mode: connectParams.client.mode,
authProvided
})) : {
shouldLog: true,
suppressedSinceLastLog: 0
};
if (authLogDecision.shouldLog) {
const suppressedText = authLogDecision.suppressedSinceLastLog > 0 ? ` suppressed=${authLogDecision.suppressedSinceLastLog}` : "";
logWsControl.warn(`unauthorized conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} client=${formatForLog(clientLabel)} ${connectParams.client.mode} v${formatForLog(connectParams.client.version)} role=${role} scopes=${scopes.length} auth=${authProvided} device=${device ? "yes" : "no"} platform=${formatForLog(connectParams.client.platform)} instance=${formatForLog(connectParams.client.instanceId ?? "n/a")} host=${formatForLog(requestHost ?? "n/a")} origin=${formatForLog(requestOrigin ?? "n/a")} ua=${formatForLog(requestUserAgent ?? "n/a")} reason=${failedAuth.reason ?? "unknown"} guidance=${formatForLog(authMessage)}${suppressedText}`);
}
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, authMessage, {
...failedAuth.rateLimited === true ? {
retryable: true,
...failedAuth.retryAfterMs !== void 0 ? { retryAfterMs: failedAuth.retryAfterMs } : {}
} : {},
details: {
code: resolveAuthConnectErrorDetailCode(failedAuth.reason),
authReason: failedAuth.reason,
canRetryWithDeviceToken,
recommendedNextStep
}
});
close(1008, truncateCloseReason(authMessage));
};
const clearUnboundScopes = () => {
if (scopes.length > 0) {
scopes = [];
connectParams.scopes = scopes;
}
};
let pairingLocality = resolvePairingLocality({
connectParams,
isLocalClient,
requestHost,
requestOrigin,
remoteAddress: remoteAddr,
hasProxyHeaders,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
let skipLocalBackendSelfPairing = shouldSkipLocalBackendSelfPairing({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
let preserveLocalCliSharedAuthScopes = shouldPreserveLocalCliSharedAuthScopes({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
const handleMissingDeviceIdentity = () => {
const trustedProxyAuthOk = isTrustedProxyControlUiOperatorAuth({
isControlUi,
role,
authMode: resolvedAuth.mode,
authOk,
authMethod
});
const decision = evaluateMissingDeviceIdentity({
hasDeviceIdentity: Boolean(device),
role,
isControlUi,
trustedProxyAuthOk,
localBackendSelfPairingOk: skipLocalBackendSelfPairing,
sharedAuthOk,
authOk,
hasSharedAuth,
isLocalClient
});
if (!device && !skipLocalBackendSelfPairing && !preserveLocalCliSharedAuthScopes && shouldClearUnboundScopesForMissingDeviceIdentity({
decision,
authMethod
})) clearUnboundScopes();
if (decision.kind === "allow") return true;
if (decision.kind === "reject-control-ui-insecure-auth") {
const errorMessage = "control ui requires device identity (use HTTPS or localhost secure context)";
markHandshakeFailure("control-ui-insecure-auth", { insecureAuthConfigured: false });
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage, { details: { code: ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED } });
close(1008, errorMessage);
return false;
}
if (decision.kind === "reject-unauthorized") {
rejectUnauthorized(authResult);
return false;
}
markHandshakeFailure("device-required");
sendHandshakeErrorResponse(ErrorCodes.NOT_PAIRED, "device identity required", { details: { code: ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED } });
close(1008, "device identity required");
return false;
};
if (startupPending && !device) {
await settleRejectedSharedAuthFailure();
await rejectGatewayStartupConnect(context);
return;
}
if (!handleMissingDeviceIdentity()) {
await settleRejectedSharedAuthFailure();
return;
}
const deviceProof = verifyGatewayConnectDeviceProof(context, {
device,
resolvedAuth,
authMethod,
role,
scopes
});
if (!deviceProof.ok) {
await settleRejectedSharedAuthFailure();
return;
}
const authDecision = await resolveConnectAuthDecision({
state: {
authResult,
authOk,
authMethod,
sharedAuthOk,
pendingSharedAuthFailure,
bootstrapTokenCandidate,
deviceTokenCandidate,
deviceTokenCandidateSource
},
hasDeviceIdentity: Boolean(device),
deviceId: device?.id,
publicKey: device?.publicKey,
role,
scopes,
requireBootstrapToken: startupBootstrapConnect,
rateLimiter: authRateLimiter,
clientIp: browserRateLimitClientIp,
async verifyBootstrapToken({ deviceId, publicKey, token, role: roleLocal, scopes: scopesLocal }) {
return await verifyDeviceBootstrapToken({
deviceId,
publicKey,
token,
role: roleLocal,
scopes: scopesLocal
});
},
async verifyDeviceToken(paramsLocal) {
return await verifyDeviceToken({
...paramsLocal,
requiredSharedGatewaySessionGeneration: getRequiredSharedGatewaySessionGeneration?.()
});
}
});
({authResult, authOk, authMethod} = authDecision);
const deviceTokenSharedGatewaySessionGeneration = authDecision.deviceTokenSharedGatewaySessionGeneration;
pairingLocality = resolvePairingLocality({
connectParams,
isLocalClient,
requestHost,
requestOrigin,
remoteAddress: remoteAddr,
hasProxyHeaders,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
skipLocalBackendSelfPairing = shouldSkipLocalBackendSelfPairing({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
preserveLocalCliSharedAuthScopes = shouldPreserveLocalCliSharedAuthScopes({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod
});
if (!authOk) {
if (startupPending && bootstrapTokenCandidate) {
await rejectGatewayStartupConnect(context);
return;
}
rejectUnauthorized(authResult);
return;
}
const boundBootstrapContext = authMethod === "bootstrap-token" && bootstrapTokenCandidate && device ? await getBoundDeviceBootstrapContext({
token: bootstrapTokenCandidate,
deviceId: device.id,
publicKey: device.publicKey
}) : null;
if (startupPending && authMethod === "bootstrap-token" && !startupBootstrapConnect) {
await rejectGatewayStartupConnect(context);
return;
}
if (startupBootstrapConnect) {
const setupId = boundBootstrapContext?.setupId?.trim();
const isCloudWorkerProfile = Boolean(boundBootstrapContext && deviceBootstrapProfilesEqual(boundBootstrapContext.profile, CLOUD_WORKER_PAIRING_SETUP_BOOTSTRAP_PROFILE));
let pendingSetup = false;
if (isCloudWorkerProfile && setupId && device) try {
pendingSetup = context.handler.isPendingWorkerNodeSetup?.(setupId, device.id) === true;
} catch {
pendingSetup = false;
}
if (!isCloudWorkerProfile || !pendingSetup) {
await rejectGatewayStartupConnect(context);
return;
}
}
advanceHandshakePhase("auth_validated");
const issuedBootstrapProfile = boundBootstrapContext?.profile ?? null;
const usesSharedGatewayAuth = authMethod === "token" || authMethod === "password" || authMethod === "trusted-proxy";
const sharedGatewaySessionGeneration = usesSharedGatewayAuth ? resolveSharedGatewaySessionGeneration(resolvedAuth, trustedProxies) : void 0;
const controlUiBootstrapSharedGatewaySessionGeneration = authMethod === "bootstrap-token" && isControlUi && role === "operator" && isControlUiOperatorBootstrapProfile({
profile: issuedBootstrapProfile,
requestedScopes: scopes
}) ? getRequiredSharedGatewaySessionGeneration?.() : void 0;
const sessionUsesSharedGatewayAuth = usesSharedGatewayAuth || deviceTokenSharedGatewaySessionGeneration !== void 0 || controlUiBootstrapSharedGatewaySessionGeneration !== void 0;
const sessionSharedGatewaySessionGeneration = sharedGatewaySessionGeneration ?? deviceTokenSharedGatewaySessionGeneration ?? controlUiBootstrapSharedGatewaySessionGeneration;
if (sessionUsesSharedGatewayAuth) {
const requiredSharedGatewaySessionGeneration = getRequiredSharedGatewaySessionGeneration?.();
if (requiredSharedGatewaySessionGeneration !== void 0 && sessionSharedGatewaySessionGeneration !== requiredSharedGatewaySessionGeneration) {
setCloseCause("gateway-auth-rotated", { authGenerationStale: true });
close(4001, "gateway auth changed");
return;
}
}
const handoffBootstrapProfile = null;
const trustedProxyAuthOk = isTrustedProxyControlUiOperatorAuth({
isControlUi,
role,
authMode: resolvedAuth.mode,
authOk,
authMethod
});
if (trustedProxyAuthOk) {
scopes = applyConnectionScopeCap({
scopes,
upgradeReq
});
connectParams.scopes = scopes;
}
const controlUiPairingKind = shouldSkipControlUiPairing({
isControlUi,
device,
role,
authMode: resolvedAuth.mode,
authMethod
});
return {
resolvedAuth,
minProtocol,
maxProtocol,
usesLegacyNodeProtocol,
role,
scopes,
hasRequestedScopes,
isControlUi,
isBrowserOperatorUi,
isWebchat,
isNativeAppUi,
startupPending,
device,
devicePublicKey: deviceProof.devicePublicKey,
deviceAuthPayloadVersion: deviceProof.deviceAuthPayloadVersion,
hasTokenAuth,
hasPasswordAuth,
bootstrapTokenCandidate,
deviceTokenSharedGatewaySessionGeneration,
authResult,
authOk,
authMethod,
pairingLocality,
usesSharedGatewayAuth,
sessionUsesSharedGatewayAuth,
sessionSharedGatewaySessionGeneration,
issuedBootstrapProfile,
handoffBootstrapProfile,
trustedProxyAuthOk,
controlUiPairingKind,
skipLocalBackendSelfPairing,
rejectUnauthorized
};
}
//#endregion
//#region src/gateway/device-pairing-prune.ts
/**
* After a silent auto-approval, retire older silent pairings of the same client
* cluster. Ephemeral state dirs mint a fresh deviceId per run and every run
* re-pairs silently, so without this the paired-device list grows without bound
* (dozens of stale operator/node records per host).
*/
async function pruneSupersededSilentPairingsAfterApproval(params) {
const { context } = params;
const pruned = await pruneSupersededSilentPairedDevices({
deviceId: params.deviceId,
baseDir: params.baseDir,
nowMs: params.nowMs,
isDeviceConnected: (deviceId) => context.hasConnectedClientsForDevice?.(deviceId) ?? false
});
for (const entry of pruned) {
context.logGateway.info(`device pairing pruned superseded silent pairing device=${entry.deviceId} roles=${entry.roles.join(",") || "none"}`);
if (entry.roles.includes("node")) clearRemovedNodeRuntimeState({
nodeId: entry.deviceId,
context
});
context.invalidateClientsForDevice?.(entry.deviceId, { reason: "device-pair-removed" });
await reconcileRevokedDeviceWorker(context, entry.deviceId);
if (entry.roles.includes("node")) context.broadcast("node.pair.resolved", {
requestId: "",
nodeId: entry.deviceId,
decision: "removed",
ts: Date.now()
}, { dropIfSlow: true });
context.disconnectClientsForDevice?.(entry.deviceId);
}
return pruned;
}
//#endregion
//#region src/gateway/node-legacy-protocol-filter.ts
const BUILT_IN_NODE_COMMANDS = /* @__PURE__ */ new Set([
...Object.values(PLATFORM_DEFAULTS).flat(),
...DEFAULT_DANGEROUS_NODE_COMMANDS,
...TALK_PTT_COMMANDS,
...IOS_WATCH_RELAY_COMMANDS
]);
const LEGACY_NODE_HOST_DESKTOP_METADATA = {
darwin: {
platform: "macos",
deviceFamily: "Mac"
},
linux: {
platform: "linux",
deviceFamily: "Linux"
},
macos: {
platform: "macos",
deviceFamily: "Mac"
},
win32: {
platform: "windows",
deviceFamily: "Windows"
},
windows: {
platform: "windows",
deviceFamily: "Windows"
}
};
/** Normalizes desktop aliases used by protocol-v3-compatible node hosts. */
function normalizeNodeHostCompatibilityMetadata(client) {
if (client.id !== GATEWAY_CLIENT_IDS.NODE_HOST || client.mode !== GATEWAY_CLIENT_MODES.NODE) return client;
if (!Object.hasOwn(LEGACY_NODE_HOST_DESKTOP_METADATA, client.platform)) return client;
const metadata = LEGACY_NODE_HOST_DESKTOP_METADATA[client.platform];
const deviceFamily = client.deviceFamily?.trim();
if (deviceFamily && deviceFamily.toLowerCase() !== metadata.deviceFamily.toLowerCase()) return client;
if (client.platform === metadata.platform && deviceFamily) return client;
return {
...client,
platform: metadata.platform,
deviceFamily: deviceFamily || metadata.deviceFamily
};
}
function filterLegacyNodeProtocolFeatures(params) {
const registry = getActivePluginGatewayNodePolicyRegistry();
if (!registry) return {
caps: [...params.caps],
commands: [...params.commands]
};
const pluginIds = /* @__PURE__ */ new Set([...registry.nodeHostCommands.map((entry) => entry.pluginId), ...registry.nodeInvokePolicies.map((entry) => entry.pluginId)]);
const pluginCaps = /* @__PURE__ */ new Set([...params.pluginSurfaces, ...pluginIds]);
const isPluginOnly = (command) => !BUILT_IN_NODE_COMMANDS.has(command);
const pluginCommands = /* @__PURE__ */ new Set([...registry.nodeHostCommands.map((entry) => entry.command.command).filter(isPluginOnly), ...registry.nodeInvokePolicies.flatMap((entry) => entry.policy.commands).filter(isPluginOnly)]);
return {
caps: params.caps.filter((cap) => !pluginCaps.has(cap)),
commands: params.commands.filter((command) => !pluginCommands.has(command))
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-device-tokens.ts
async function issueGatewayConnectDeviceTokens(params) {
const { state, scopes, hasApprovedDeviceBaseline } = params;
const { role, device, isBrowserOperatorUi, isWebchat, trustedProxyAuthOk, sessionUsesSharedGatewayAuth, sessionSharedGatewaySessionGeneration, deviceTokenSharedGatewaySessionGeneration, handoffBootstrapProfile } = state;
const sharedGatewayAuthIssuer = sessionSharedGatewaySessionGeneration && (deviceTokenSharedGatewaySessionGeneration !== void 0 || sessionUsesSharedGatewayAuth && (isBrowserOperatorUi || isWebchat)) ? {
kind: "shared-gateway-auth",
generation: sessionSharedGatewaySessionGeneration
} : void 0;
const issuedDeviceGrant = !trustedProxyAuthOk && device && hasApprovedDeviceBaseline ? await ensureDeviceToken({
deviceId: device.id,
role,
scopes,
issuer: sharedGatewayAuthIssuer,
isIssuanceCurrent: params.isIssuanceCurrent
}) : null;
const bootstrapDeviceTokens = [];
if (issuedDeviceGrant) bootstrapDeviceTokens.push({
deviceToken: issuedDeviceGrant.token,
role: issuedDeviceGrant.role,
scopes: issuedDeviceGrant.scopes,
issuedAtMs: issuedDeviceGrant.rotatedAtMs ?? issuedDeviceGrant.createdAtMs
});
if (device && handoffBootstrapProfile) for (const bootstrapRole of handoffBootstrapProfile.roles) {
if (bootstrapDeviceTokens.some((entry) => entry.role === bootstrapRole)) continue;
const bootstrapRoleScopes = bootstrapRole === "operator" ? resolveBootstrapProfileScopesForRole(bootstrapRole, handoffBootstrapProfile.scopes, handoffBootstrapProfile.purpose) : [];
const extraToken = await ensureDeviceToken({
deviceId: device.id,
role: bootstrapRole,
scopes: bootstrapRoleScopes,
isIssuanceCurrent: params.isIssuanceCurrent
});
if (!extraToken) continue;
bootstrapDeviceTokens.push({
deviceToken: extraToken.token,
role: extraToken.role,
scopes: extraToken.scopes,
issuedAtMs: extraToken.rotatedAtMs ?? extraToken.createdAtMs
});
}
return {
deviceToken: issuedDeviceGrant,
bootstrapDeviceTokens
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-existing-device.ts
async function authorizeExistingGatewayDevice(params) {
const { context, state, paired, devicePublicKey, clientAccessMetadata, requirePairing } = params;
const { connectParams, hasBrowserOriginHeader, reportedClientIp } = context;
const { connId, logGateway } = context.handler;
const { role, scopes, device, deviceAuthPayloadVersion, authMethod, bootstrapTokenCandidate, pairingLocality, isControlUi, isBrowserOperatorUi, isWebchat, isNativeAppUi } = state;
let { handoffBootstrapProfile } = params;
const claimedPlatform = connectParams.client.platform;
const pairedPlatform = paired.platform;
const claimedDeviceFamily = connectParams.client.deviceFamily;
const pairedDeviceFamily = paired.deviceFamily;
const metadataPinning = resolvePinnedClientMetadata({
clientId: connectParams.client.id,
clientMode: connectParams.client.mode,
claimedPlatform,
claimedDeviceFamily,
pairedPlatform,
pairedDeviceFamily
});
const { platformMismatch, deviceFamilyMismatch } = metadataPinning;
if (platformMismatch || deviceFamilyMismatch) {
if (!shouldAllowSilentLocalPairing({
locality: pairingLocality,
hasBrowserOriginHeader,
isControlUi,
isWebchat,
isNativeAppUi,
authMethod,
reason: "metadata-upgrade"
})) logGateway.warn(`security audit: device metadata upgrade requested reason=metadata-upgrade device=${device?.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} payload=${deviceAuthPayloadVersion ?? "unknown"} claimedPlatform=${claimedPlatform ?? "<none>"} pinnedPlatform=${pairedPlatform ?? "<none>"} claimedDeviceFamily=${claimedDeviceFamily ?? "<none>"} pinnedDeviceFamily=${pairedDeviceFamily ?? "<none>"} client=${connectParams.client.id} conn=${connId}`);
if (!await requirePairing("metadata-upgrade", paired)) return {
ok: false,
handoffBootstrapProfile
};
} else {
if (metadataPinning.pinnedPlatform) connectParams.client.platform = metadataPinning.pinnedPlatform;
if (metadataPinning.pinnedDeviceFamily) connectParams.client.deviceFamily = metadataPinning.pinnedDeviceFamily;
}
const pairedRoles = listEffectivePairedDeviceRoles(paired);
const pairedScopes = resolvePairedAccessScopes(paired);
if (!pairedRoles.includes(role)) {
if (!await requirePairing("role-upgrade", paired)) return {
ok: false,
handoffBootstrapProfile
};
}
if (scopes.length > 0) {
if (!(pairedScopes.length > 0 && roleScopesAllow({
role,
requestedScopes: scopes,
allowedScopes: pairedScopes
}))) {
if (!await requirePairing("scope-upgrade", paired)) return {
ok: false,
handoffBootstrapProfile
};
}
}
const retryBootstrapHandoffProfile = authMethod === "bootstrap-token" && bootstrapTokenCandidate && isMobileNodeBootstrapConnect({
role,
scopes,
isControlUi,
isBrowserOperatorUi,
isWebchat,
clientMode: connectParams.client.mode
}) && device ? await getBoundDeviceBootstrapProfile({
token: bootstrapTokenCandidate,
deviceId: device.id,
publicKey: devicePublicKey
}) : null;
if (retryBootstrapHandoffProfile && isSetupCodeHandoffBootstrapClient({
profile: retryBootstrapHandoffProfile,
client: connectParams.client
})) {
const retryBootstrapOperatorScopes = resolveBootstrapProfileScopesForRole("operator", retryBootstrapHandoffProfile.scopes, retryBootstrapHandoffProfile.purpose);
if (!(pairedRoles.includes("operator") && roleScopesAllow({
role: "operator",
requestedScopes: retryBootstrapOperatorScopes,
allowedScopes: pairedScopes
})) && !await requirePairing("scope-upgrade", paired)) return {
ok: false,
handoffBootstrapProfile
};
if (pairedDeviceAllowsBootstrapOperator({
device: device ? await getPairedDevice(device.id) : null,
devicePublicKey,
profile: retryBootstrapHandoffProfile
})) handoffBootstrapProfile = retryBootstrapHandoffProfile;
}
if (device) await updatePairedDeviceMetadata(device.id, {
...clientAccessMetadata,
...metadataPinning.refreshPairedPlatform ? { platform: metadataPinning.refreshPairedPlatform } : {}
});
return {
ok: true,
handoffBootstrapProfile
};
}
//#endregion
//#region src/gateway/node-pairing-ssh-verify.runtime.ts
const MAX_PROBE_OUTPUT_BYTES = 65536;
const REMOTE_IDENTITY_COMMAND = "sh -lc 'openclaw node identity --json'";
/** Read the node device identity back from the pairing host over SSH. */
async function runNodeIdentityProbe(params) {
const args = [
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=5",
"-o",
"NumberOfPasswordPrompts=0",
"-o",
"PreferredAuthentications=publickey",
"-o",
"StrictHostKeyChecking=yes",
"-o",
"UpdateHostKeys=no",
"-a",
"-x",
"-o",
"ForwardAgent=no",
"-o",
"ForwardX11=no",
"-o",
"ForwardX11Trusted=no",
"-o",
"ClearAllForwardings=yes",
"-o",
"ExitOnForwardFailure=yes",
"-p",
String(params.port ?? 22)
];
if (params.identity?.trim()) args.push("-i", params.identity.trim(), "-o", "IdentitiesOnly=yes");
args.push("--", `${params.user}@${params.host}`, REMOTE_IDENTITY_COMMAND);
try {
const result = await runUtf8CommandWithTimeout(["ssh", ...args], {
maxOutputBytes: MAX_PROBE_OUTPUT_BYTES,
outputCapture: "head",
timeoutMs: Math.max(250, params.timeoutMs)
});
if (result.termination === "timeout") return { status: "timeout" };
if (result.code === 0) return {
status: "ok",
stdout: result.stdout
};
return {
status: "failed",
code: result.code,
stderr: result.stderr
};
} catch (error) {
return {
status: "spawn-error",
message: error instanceof Error ? error.message : String(error)
};
}
}
//#endregion
//#region src/gateway/node-pairing-ssh-verify.ts
const DEFAULT_TIMEOUT_MS = 7e3;
const FAILURE_COOLDOWN_MS = 6e4;
const MISMATCH_COOLDOWN_MS = 3e5;
const MAX_CONCURRENT_PROBES = 4;
const MAX_COOLDOWN_ENTRIES = 512;
function resolveProcessUser() {
try {
const user = os.userInfo().username.trim();
if (user) return user;
} catch {}
return (process.env.USER ?? process.env.USERNAME)?.trim() || void 0;
}
/** Normalize the enabled-by-default config union into a probe policy, or null when off. */
function resolveNodePairingSshVerifyPolicy(raw) {
if (raw === false) return null;
const cfg = typeof raw === "object" && raw !== null ? raw : {};
const user = cfg.user?.trim() || resolveProcessUser();
if (!user) return null;
const cidrs = cfg.cidrs?.map((entry) => entry.trim()).filter((entry) => entry.length > 0).toSorted();
return {
user,
identity: cfg.identity?.trim() || void 0,
timeoutMs: typeof cfg.timeoutMs === "number" && Number.isFinite(cfg.timeoutMs) && cfg.timeoutMs > 0 ? cfg.timeoutMs : DEFAULT_TIMEOUT_MS,
cidrs: cidrs && cidrs.length > 0 ? cidrs : void 0
};
}
function normalizeProbeHost(reportedClientIp) {
const trimmed = reportedClientIp.trim();
const host = trimmed.toLowerCase().startsWith("::ffff:") ? trimmed.slice(7) : trimmed;
if (host.includes("%") || net.isIP(host) === 0) return null;
return host;
}
/**
* Resolve whether this pairing request qualifies for an SSH verification probe.
* Shares the fresh-node eligibility floor with trusted-CIDR auto-approval and
* additionally bounds the probe target: default private/CGNAT ranges only, so
* a token holder cannot use the gateway as an SSH probe primitive against
* arbitrary public addresses.
*/
function planNodePairingSshVerify(params) {
const policy = resolveNodePairingSshVerifyPolicy(params.config);
if (!policy) return null;
if (!isEligibleFreshNodePairingRequest(params.eligibility) || !params.eligibility.reportedClientIp) return null;
if (!(policy.cidrs ? isTrustedProxyAddress(params.eligibility.reportedClientIp, policy.cidrs) : isPrivateOrLoopbackAddress(params.eligibility.reportedClientIp) && !isLoopbackAddress(params.eligibility.reportedClientIp))) return null;
const host = normalizeProbeHost(params.eligibility.reportedClientIp);
return host ? {
policy,
host
} : null;
}
function parseRemoteIdentity(stdout) {
for (const line of stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed.startsWith("{")) continue;
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed.deviceId === "string" && typeof parsed.publicKey === "string") return {
deviceId: parsed.deviceId.trim(),
publicKey: parsed.publicKey.trim()
};
} catch {}
}
return null;
}
const inFlightByKey = /* @__PURE__ */ new Map();
const cooldownExpiryByKey = /* @__PURE__ */ new Map();
function probeKey(plan, deviceId, publicKey) {
return JSON.stringify([
plan.host,
deviceId,
publicKey,
plan.policy.user,
plan.policy.identity,
plan.policy.timeoutMs,
plan.policy.cidrs
]);
}
function pruneCooldowns(nowMs) {
for (const [key, expiry] of cooldownExpiryByKey) if (expiry <= nowMs) cooldownExpiryByKey.delete(key);
pruneMapToMaxSize(cooldownExpiryByKey, MAX_COOLDOWN_ENTRIES);
}
/**
* Start an SSH verification for one pending pairing request. Returns null when
* the probe should not run right now (cooldown or concurrency cap) so callers
* keep the default manual-approval reconnect behavior. A reconnect that lands
* while the probe is still running gets `alreadyInFlight: true`: the client
* must keep its retry hint, but only the connection that started the probe
* owns the approval work.
*/
function startNodePairingSshVerify(params) {
const nowMs = params.nowMs ?? Date.now();
pruneCooldowns(nowMs);
const key = probeKey(params.plan, params.expectedDeviceId, params.expectedPublicKey);
const inFlight = inFlightByKey.get(key);
if (inFlight) return {
done: inFlight,
alreadyInFlight: true
};
if ((cooldownExpiryByKey.get(key) ?? 0) > nowMs) return null;
if (inFlightByKey.size >= MAX_CONCURRENT_PROBES) return null;
const probe = params.probe ?? runNodeIdentityProbe;
const done = (async () => {
const result = await probe({
user: params.plan.policy.user,
host: params.plan.host,
identity: params.plan.policy.identity,
timeoutMs: params.plan.policy.timeoutMs
});
if (result.status !== "ok") {
cooldownExpiryByKey.set(key, Date.now() + FAILURE_COOLDOWN_MS);
return {
ok: false,
reason: "probe-failed"
};
}
const remote = parseRemoteIdentity(result.stdout);
if (!remote) {
cooldownExpiryByKey.set(key, Date.now() + FAILURE_COOLDOWN_MS);
return {
ok: false,
reason: "identity-unreadable"
};
}
const expectedKey = normalizeDevicePublicKeyBase64Url(params.expectedPublicKey);
const remoteKey = normalizeDevicePublicKeyBase64Url(remote.publicKey);
if (!(Boolean(expectedKey) && expectedKey === remoteKey && remote.deviceId === params.expectedDeviceId)) {
cooldownExpiryByKey.set(key, Date.now() + MISMATCH_COOLDOWN_MS);
return {
ok: false,
reason: "identity-mismatch"
};
}
return {
ok: true,
user: params.plan.policy.user,
host: params.plan.host
};
})();
return {
done: getOrCreatePromise(inFlightByKey, key, () => done, { evictOnSettled: true }),
alreadyInFlight: false
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-node-pairing-ssh.ts
function startGatewayNodePairingSshApproval(params) {
const { context, state, pairing, existingPairedDevice, devicePublicKey, clientAccessMetadata } = params;
const { connectParams, configSnapshot, reportedClientIp, reportedClientIpSource, hasBrowserOriginHeader, runDetachedConnectWork } = context;
const { connId, buildRequestContext, logGateway } = context.handler;
const { device, role, scopes, isControlUi, isWebchat } = state;
if (!device || pairing.request.silent === true) return false;
if (!isScopelessNodePairingRequest(pairing.request)) return false;
const eligibility = {
existingPairedDevice: Boolean(existingPairedDevice),
role,
reason: params.reason,
scopes,
hasBrowserOriginHeader,
isControlUi,
isWebchat,
reportedClientIpSource,
reportedClientIp
};
const sshVerifyPlan = planNodePairingSshVerify({
config: configSnapshot.gateway?.nodes?.pairing?.sshVerify,
eligibility
});
const sshVerify = sshVerifyPlan ? startNodePairingSshVerify({
plan: sshVerifyPlan,
expectedDeviceId: device.id,
expectedPublicKey: devicePublicKey
}) : null;
if (sshVerifyPlan && sshVerify && !sshVerify.alreadyInFlight) {
const pendingRequestId = pairing.request.requestId;
runDetachedConnectWork(async () => {
const outcome = await sshVerify.done;
if (!outcome.ok) {
logGateway.info(`node pairing ssh-verify did not approve device=${device.id} host=${sshVerifyPlan.host} reason=${outcome.reason}`);
return;
}
const approvedBySsh = await approveDevicePairing(pendingRequestId, {
callerScopes: scopes,
accessMetadata: clientAccessMetadata,
approvedVia: "ssh-verified",
isApprovalCurrent: ({ pending, existing }) => {
const currentConfig = getRuntimeConfigSnapshot();
if (!currentConfig || pending.deviceId !== device.id || pending.publicKey !== devicePublicKey || !isScopelessNodePairingRequest(pending)) return false;
return isDeepStrictEqual(sshVerifyPlan, planNodePairingSshVerify({
config: currentConfig.gateway?.nodes?.pairing?.sshVerify,
eligibility: {
...eligibility,
existingPairedDevice: Boolean(existing),
scopes: pending.scopes ?? []
}
}));
}
});
if (approvedBySsh?.status !== "approved") {
const reason = approvedBySsh?.status === "forbidden" ? approvedBySsh.reason : "request superseded or already resolved";
logGateway.info(`node pairing ssh-verify approval skipped device=${device.id} (${reason})`);
return;
}
logGateway.info(`security audit: device pairing ssh-verified auto-approve device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} sshUser=${outcome.user} client=${connectParams.client.id} conn=${connId}`);
buildRequestContext().broadcast("device.pair.resolved", {
requestId: pendingRequestId,
deviceId: device.id,
decision: "approved",
ts: Date.now()
}, { dropIfSlow: true });
}, (error) => {
logGateway.warn(`node pairing ssh-verify failed device=${device.id}: ${String(error)}`);
});
}
return Boolean(sshVerifyPlan && sshVerify);
}
//#endregion
//#region src/gateway/server/ws-connection/connect-pairing-approval-plan.ts
const DEFAULT_TRUSTED_PROXY_DEVICE_AUTO_APPROVE_SCOPES = [
"operator.read",
"operator.write",
"operator.approvals",
"operator.questions"
];
function resolveTrustedProxyDeviceAutoApproveScopes(params) {
const configuredScopes = normalizeSortedUniqueTrimmedStringList(params.configuredScopes ?? [...DEFAULT_TRUSTED_PROXY_DEVICE_AUTO_APPROVE_SCOPES]);
if (!params.hasRequestedScopes) return configuredScopes;
const configured = new Set(configuredScopes);
const requestedScopes = normalizeSortedUniqueTrimmedStringList(params.requestedScopes);
if (params.configuredScopes === void 0) requestedScopes.push("operator.questions");
return normalizeSortedUniqueTrimmedStringList(requestedScopes).filter((scope) => configured.has(scope));
}
/** Reused at planning and synchronous approval commit so config changes revoke the same policy. */
function resolveLocalPairingApproval(params) {
const { reason, existingPairedDevice, state, configSnapshot, scopes } = params;
const { role, isControlUi, isWebchat, isNativeAppUi, authMethod, pairingLocality } = state;
if (!(existingPairedDevice && role !== "operator") && shouldAllowSilentLocalPairing({
autoApproveLocal: configSnapshot.gateway?.nodes?.pairing?.autoApproveLocal,
locality: pairingLocality,
hasBrowserOriginHeader: params.hasBrowserOriginHeader,
isControlUi,
isWebchat,
isNativeAppUi,
authMethod,
reason
})) return "silent";
return shouldAutoApproveNodePairingFromTrustedCidrs({
existingPairedDevice: Boolean(existingPairedDevice),
role,
reason,
scopes,
hasBrowserOriginHeader: params.hasBrowserOriginHeader,
isControlUi,
isWebchat,
reportedClientIpSource: params.reportedClientIpSource,
reportedClientIp: params.reportedClientIp,
autoApproveCidrs: configSnapshot.gateway?.nodes?.pairing?.autoApproveCidrs
}) ? "trusted-cidr" : null;
}
async function resolvePairingApprovalPlan(params) {
const { reason, existingPairedDevice, state, connectParams, configSnapshot, scopes } = params;
const { role, isControlUi, isBrowserOperatorUi, isWebchat, isNativeAppUi, authMethod, authResult, bootstrapTokenCandidate } = state;
const localApproval = resolveLocalPairingApproval(params);
const trustedProxyAutoApproveConfig = configSnapshot.gateway?.auth?.trustedProxy?.deviceAutoApprove;
const trustedProxyUser = authResult.user?.trim();
const isTrustedProxySameKeyUpgrade = reason === "scope-upgrade" && existingPairedDevice?.publicKey === params.devicePublicKey;
const trustedProxyAutoApproveScopes = (reason === "not-paired" && !existingPairedDevice || isTrustedProxySameKeyUpgrade) && role === "operator" && (isBrowserOperatorUi || isWebchat || isNativeAppUi) && authMethod === "trusted-proxy" && Boolean(trustedProxyUser) && trustedProxyAutoApproveConfig?.enabled === true ? params.connectionScopeCap(resolveTrustedProxyDeviceAutoApproveScopes({
requestedScopes: scopes,
hasRequestedScopes: params.hasRequestedScopes,
configuredScopes: trustedProxyAutoApproveConfig?.scopes
})) : null;
const isSetupCodeMobileNodeConnect = isMobileNodeBootstrapConnect({
role,
scopes,
isControlUi,
isBrowserOperatorUi,
isWebchat,
clientMode: connectParams.client.mode
});
const boundBootstrapProfile = authMethod === "bootstrap-token" && bootstrapTokenCandidate && (reason === "not-paired" && !existingPairedDevice && (isSetupCodeMobileNodeConnect || isControlUi && role === "operator") || reason === "scope-upgrade" && Boolean(existingPairedDevice) && (isSetupCodeMobileNodeConnect || isControlUi && role === "operator")) ? await getBoundDeviceBootstrapProfile({
token: bootstrapTokenCandidate,
deviceId: params.deviceId,
publicKey: params.devicePublicKey
}) : null;
const allowSetupCodeHandoffBootstrapPairing = boundBootstrapProfile !== null && isSetupCodeMobileNodeConnect && isSetupCodeHandoffBootstrapClient({
profile: boundBootstrapProfile,
client: connectParams.client
});
const setupCodeHandoffBootstrapProfile = allowSetupCodeHandoffBootstrapPairing ? boundBootstrapProfile : null;
const allowControlUiOwnerBootstrapPairing = reason === "scope-upgrade" && isControlUiOwnerBootstrapProfile({
profile: boundBootstrapProfile,
requestedScopes: scopes
});
const allowControlUiOperatorBootstrapPairing = reason === "not-paired" && isControlUiOperatorBootstrapProfile({
profile: boundBootstrapProfile,
requestedScopes: scopes
}) || allowControlUiOwnerBootstrapPairing;
const controlUiOperatorBootstrapProfile = allowControlUiOperatorBootstrapPairing ? boundBootstrapProfile : null;
const bootstrapPairingRoles = setupCodeHandoffBootstrapProfile ? uniqueStrings([role, ...setupCodeHandoffBootstrapProfile.roles]) : controlUiOperatorBootstrapProfile ? ["operator"] : void 0;
const bootstrapPairingScopes = setupCodeHandoffBootstrapProfile ? resolveBootstrapProfileScopesForRoles(bootstrapPairingRoles ?? [], setupCodeHandoffBootstrapProfile.scopes, setupCodeHandoffBootstrapProfile.purpose) : controlUiOperatorBootstrapProfile ? resolveBootstrapProfileScopesForRole("operator", controlUiOperatorBootstrapProfile.scopes, controlUiOperatorBootstrapProfile.purpose) : void 0;
return {
silent: localApproval !== null || allowSetupCodeHandoffBootstrapPairing || allowControlUiOperatorBootstrapPairing,
localApproval,
trustedProxyAutoApproveScopes,
trustedProxyUser,
isTrustedProxySameKeyUpgrade,
allowSetupCodeHandoffBootstrapPairing,
allowControlUiOwnerBootstrapPairing,
bootstrapApprovalProfile: setupCodeHandoffBootstrapProfile ?? controlUiOperatorBootstrapProfile,
bootstrapPairingRoles,
bootstrapPairingScopes
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-device-pairing.ts
async function authorizeGatewayConnectDevice(context, state) {
const { connId, buildRequestContext, close, send, setHandshakeState, setCloseCause, logGateway, requestOrigin } = context.handler;
const { frame, connectParams, configSnapshot, reportedClientIp, reportedClientIpSource, hasBrowserOriginHeader } = context;
let { scopes } = state;
let { handoffBootstrapProfile } = state;
const { role, device, devicePublicKey, authMethod, authResult, hasRequestedScopes, skipLocalBackendSelfPairing, controlUiPairingKind } = state;
const isConnectAuthorizationCurrent = () => resolveGatewayConnectPolicyFailure(context, state) === void 0;
const failPairingHandshake = (params) => {
const { message, details, closeCause, closeReason } = params;
setHandshakeState("failed");
if (closeCause) setCloseCause(closeCause.cause, closeCause.meta);
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(ErrorCodes.NOT_PAIRED, message, details ? { details } : void 0)
});
close(1008, truncateCloseReason(closeReason ?? message));
};
const roleConfiguredHumanOperator = role === "operator" && Boolean(configSnapshot.gateway?.roles);
if (roleConfiguredHumanOperator && !(authMethod === "token" || authMethod === "password") && !authResult.user?.trim()) {
failPairingHandshake({
message: "operator role policies require a verified user identity for this authentication method; reconnect through the trusted proxy or Tailscale, or use the shared gateway token/password",
details: { code: ConnectErrorDetailCodes.AUTH_VERIFIED_USER_REQUIRED }
});
return;
}
let hasServerApprovedDeviceTokenBaseline = false;
let pairedClientId;
let pairedBrowserOrigin;
connectParams.client = normalizeNodeHostCompatibilityMetadata(connectParams.client);
const browserCopilotOrigin = isBrowserCopilotClient(connectParams.client) ? normalizeChromeExtensionOrigin(requestOrigin) : void 0;
if (device && devicePublicKey) {
const formatAuditList = (items) => normalizeSortedUniqueTrimmedStringList(items).join(",") || "<none>";
const logUpgradeAudit = (reason, currentRoles, currentScopes) => logGateway.warn(`security audit: device access upgrade requested reason=${reason} device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} roleFrom=${formatAuditList(currentRoles)} roleTo=${role} scopesFrom=${formatAuditList(currentScopes)} scopesTo=${formatAuditList(scopes)} client=${connectParams.client.id} conn=${connId}`);
const clientPairingMetadata = {
displayName: connectParams.client.displayName,
platform: connectParams.client.platform,
deviceFamily: connectParams.client.deviceFamily,
clientId: connectParams.client.id,
clientMode: connectParams.client.mode,
...browserCopilotOrigin ? { browserOrigin: browserCopilotOrigin } : {},
role,
scopes,
remoteIp: reportedClientIp
};
const clientAccessMetadata = {
displayName: connectParams.client.displayName,
remoteIp: reportedClientIp,
lastSeenAtMs: Date.now(),
lastSeenReason: "connect"
};
const requirePairing = async (reason, existingPairedDevice = null) => {
const pairingStateAllowsRequestedAccess = (pairedCandidate, requestedScopes = scopes) => pairedCandidate?.publicKey === devicePublicKey && hasEffectivePairedDeviceRole(pairedCandidate, role) && roleScopesAllow({
role,
requestedScopes,
allowedScopes: resolvePairedAccessScopes(pairedCandidate)
});
const pairingPlanParams = {
reason,
existingPairedDevice,
state,
connectParams,
configSnapshot,
hasBrowserOriginHeader,
reportedClientIp,
reportedClientIpSource,
deviceId: device.id,
devicePublicKey,
scopes,
hasRequestedScopes,
connectionScopeCap: (capped) => applyConnectionScopeCap({
scopes: capped,
upgradeReq: context.handler.upgradeReq
})
};
const plan = await resolvePairingApprovalPlan(pairingPlanParams);
if (reason === "scope-upgrade" && plan.isTrustedProxySameKeyUpgrade && plan.trustedProxyAutoApproveScopes !== null) {
const livePaired = await getPairedDevice(device.id);
if (livePaired && pairingStateAllowsRequestedAccess(livePaired, plan.trustedProxyAutoApproveScopes)) {
const livePairedScopes = resolvePairedAccessScopes(livePaired);
scopes = normalizeSortedUniqueTrimmedStringList([...scopes, ...plan.trustedProxyAutoApproveScopes].filter((scope) => roleScopesAllow({
role,
requestedScopes: [scope],
allowedScopes: livePairedScopes
})));
connectParams.scopes = scopes;
return true;
}
}
if (reason === "role-upgrade" || reason === "scope-upgrade") logUpgradeAudit(reason, existingPairedDevice ? listEffectivePairedDeviceRoles(existingPairedDevice) : void 0, existingPairedDevice ? resolvePairedAccessScopes(existingPairedDevice) : void 0);
const pairing = await requestDevicePairing({
deviceId: device.id,
publicKey: devicePublicKey,
...clientPairingMetadata,
scopes,
...plan.bootstrapPairingRoles ? {
roles: plan.bootstrapPairingRoles,
scopes: plan.bootstrapPairingScopes ?? []
} : {},
silent: plan.silent
});
const trustedProxyApprovalScopes = pairing.request.isRepair !== true || plan.isTrustedProxySameKeyUpgrade ? plan.trustedProxyAutoApproveScopes : null;
const requestContext = buildRequestContext();
const supersededResolvedAt = Date.now();
for (const superseded of pairing.superseded ?? []) requestContext.broadcast("device.pair.resolved", {
requestId: superseded.requestId,
deviceId: superseded.deviceId,
decision: "rejected",
ts: supersededResolvedAt
}, { dropIfSlow: true });
let approved;
let resolvedByConcurrentApproval = false;
let recoveryRequestId;
const resolveLivePendingRequestId = async () => {
const pendingList = await listDevicePairing();
const exactPending = pendingList.pending.find((pending) => pending.requestId === pairing.request.requestId);
if (exactPending) return exactPending.requestId;
return pendingList.pending.find((pending) => pending.deviceId === device.id && pending.publicKey === devicePublicKey)?.requestId;
};
const inlineApprovalAttempted = trustedProxyApprovalScopes !== null || pairing.request.silent === true;
if (inlineApprovalAttempted) {
if (trustedProxyApprovalScopes !== null) approved = await approveDevicePairing(pairing.request.requestId, {
callerScopes: trustedProxyApprovalScopes,
accessMetadata: clientAccessMetadata,
approvedVia: "trusted-proxy",
autoApproveNewDeviceScopes: trustedProxyApprovalScopes,
isApprovalCurrent: isConnectAuthorizationCurrent
});
else if (plan.bootstrapApprovalProfile) approved = await approveBootstrapDevicePairing(pairing.request.requestId, plan.bootstrapApprovalProfile, {
accessMetadata: clientAccessMetadata,
isApprovalCurrent: isConnectAuthorizationCurrent,
onTokensReplaced: (deviceId, roles) => retireDeviceTokenClients(requestContext, deviceId, roles, "device-token-rotated")
});
else if (plan.localApproval) approved = await approveDevicePairing(pairing.request.requestId, {
callerScopes: uniqueStrings([...scopes, ...existingPairedDevice ? resolvePairedAccessScopes(existingPairedDevice) : []]),
accessMetadata: clientAccessMetadata,
approvedVia: plan.localApproval,
isApprovalCurrent: ({ pending, existing }) => {
const currentConfig = getRuntimeConfigSnapshot();
if (!currentConfig || !isConnectAuthorizationCurrent() || pending.deviceId !== device.id || pending.publicKey !== devicePublicKey || plan.localApproval === "trusted-cidr" && !isScopelessNodePairingRequest(pending)) return false;
return resolveLocalPairingApproval({
...pairingPlanParams,
configSnapshot: currentConfig,
existingPairedDevice: existing ?? null,
scopes: pending.scopes ?? []
}) === plan.localApproval;
}
});
if (approved?.status === "approved") {
if (trustedProxyApprovalScopes !== null) {
scopes = trustedProxyApprovalScopes;
connectParams.scopes = scopes;
}
if (plan.bootstrapApprovalProfile) handoffBootstrapProfile = plan.bootstrapApprovalProfile;
if (trustedProxyApprovalScopes !== null && plan.trustedProxyUser) logGateway.warn(`security audit: trusted-proxy operator device auto-approved user=${formatForLog(plan.trustedProxyUser)} device=${formatForLog(approved.device.deviceId.slice(0, 12))} scopes=${formatAuditList(scopes)}`);
else logGateway.info(`device pairing auto-approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`);
requestContext.broadcast("device.pair.resolved", {
requestId: pairing.request.requestId,
deviceId: approved.device.deviceId,
decision: "approved",
ts: Date.now()
}, { dropIfSlow: true });
if (!plan.allowSetupCodeHandoffBootstrapPairing) try {
await pruneSupersededSilentPairingsAfterApproval({
deviceId: approved.device.deviceId,
context: requestContext
});
} catch (error) {
logGateway.warn(`device pairing prune failed device=${approved.device.deviceId} error=${String(error)}`);
}
} else {
const pairedAfterConcurrentApproval = await getPairedDevice(device.id);
resolvedByConcurrentApproval = plan.bootstrapApprovalProfile ? pairedDeviceAllowsBootstrapProfile({
device: pairedAfterConcurrentApproval,
devicePublicKey,
profile: plan.bootstrapApprovalProfile
}) : pairingStateAllowsRequestedAccess(pairedAfterConcurrentApproval);
let requestStillPending = false;
if (!resolvedByConcurrentApproval) {
recoveryRequestId = await resolveLivePendingRequestId();
requestStillPending = recoveryRequestId === pairing.request.requestId;
}
if (requestStillPending) requestContext.broadcast("device.pair.requested", pairing.request, { dropIfSlow: true });
}
} else if (pairing.created) requestContext.broadcast("device.pair.requested", pairing.request, { dropIfSlow: true });
const sshVerifyStarted = startGatewayNodePairingSshApproval({
context,
state: {
...state,
scopes,
handoffBootstrapProfile
},
pairing,
existingPairedDevice,
devicePublicKey,
clientAccessMetadata,
reason
});
recoveryRequestId = await resolveLivePendingRequestId();
if (!(inlineApprovalAttempted && (approved?.status === "approved" || resolvedByConcurrentApproval))) {
const exposeApprovedAccess = existingPairedDevice?.publicKey === devicePublicKey;
const approvedRoles = exposeApprovedAccess ? listApprovedPairedDeviceRoles(existingPairedDevice) : [];
const approvedScopes = exposeApprovedAccess ? resolvePairedAccessScopes(existingPairedDevice) : [];
const retryWhileDetachedApprovalPending = authMethod === "bootstrap-token" && reason === "not-paired" && role === "node" && scopes.length === 0 && !existingPairedDevice || sshVerifyStarted;
failPairingHandshake({
message: buildPairingConnectErrorMessage(reason),
details: buildPairingConnectErrorDetails({
reason,
requestId: recoveryRequestId,
...retryWhileDetachedApprovalPending ? {
recommendedNextStep: "wait_then_retry",
retryable: true,
pauseReconnect: false
} : {},
deviceId: device.id,
requestedRole: role,
requestedScopes: scopes,
...approvedRoles.length > 0 ? { approvedRoles } : {},
...approvedScopes.length > 0 ? { approvedScopes } : {}
}),
closeCause: {
cause: "pairing-required",
meta: {
deviceId: device.id,
...recoveryRequestId ? { requestId: recoveryRequestId } : {},
reason
}
},
closeReason: buildPairingConnectCloseReason({
reason,
requestId: recoveryRequestId
})
});
return false;
}
return true;
};
const paired = await getPairedDevice(device.id);
const isPaired = paired?.publicKey === devicePublicKey;
if (state.startupPending && !isStartupNodeBootstrapConnect(connectParams) && (!paired || !isPaired || !hasEffectivePairedDeviceRole(paired, "node") || !paired.nodeSurface)) {
await rejectGatewayStartupConnect(context);
return;
}
if (skipLocalBackendSelfPairing || controlUiPairingKind === "auth-none") {
if (isPaired) {
pairedClientId = paired.clientId;
pairedBrowserOrigin = paired.browserOrigin;
hasServerApprovedDeviceTokenBaseline = true;
await updatePairedDeviceMetadata(device.id, clientAccessMetadata);
} else if (controlUiPairingKind === "auth-none" || skipLocalBackendSelfPairing && authMethod !== "device-token") hasServerApprovedDeviceTokenBaseline = true;
} else if (!isPaired) {
if (controlUiPairingKind === null) {
if (!await requirePairing("not-paired", paired)) return;
const approvedDevice = await getPairedDevice(device.id);
pairedClientId = approvedDevice?.publicKey === devicePublicKey ? approvedDevice.clientId : void 0;
pairedBrowserOrigin = approvedDevice?.publicKey === devicePublicKey ? approvedDevice.browserOrigin : void 0;
hasServerApprovedDeviceTokenBaseline = true;
} else hasServerApprovedDeviceTokenBaseline = true;
} else {
pairedClientId = paired.clientId;
pairedBrowserOrigin = paired.browserOrigin;
hasServerApprovedDeviceTokenBaseline = true;
const existingDevice = await authorizeExistingGatewayDevice({
context,
state: {
...state,
scopes,
handoffBootstrapProfile
},
paired,
devicePublicKey,
clientAccessMetadata,
handoffBootstrapProfile,
requirePairing
});
if (!existingDevice.ok) return;
handoffBootstrapProfile = existingDevice.handoffBootstrapProfile;
}
}
const browserCopilotIdentityMismatch = pairedClientId !== connectParams.client.id && (isBrowserCopilotClient(connectParams.client) || isBrowserCopilotClient({ id: pairedClientId }));
const browserCopilotOriginMismatch = isBrowserCopilotClient(connectParams.client) && (!pairedBrowserOrigin || !browserCopilotOrigin || pairedBrowserOrigin !== browserCopilotOrigin);
if (browserCopilotIdentityMismatch || browserCopilotOriginMismatch) {
failPairingHandshake({ message: "browser copilot requires a dedicated paired device identity" });
return;
}
const { deviceToken, bootstrapDeviceTokens } = roleConfiguredHumanOperator && authResult.user?.trim() ? {
deviceToken: null,
bootstrapDeviceTokens: []
} : await issueGatewayConnectDeviceTokens({
state: {
...state,
scopes,
handoffBootstrapProfile
},
scopes,
hasApprovedDeviceBaseline: hasServerApprovedDeviceTokenBaseline,
isIssuanceCurrent: isConnectAuthorizationCurrent
});
return {
...state,
scopes,
handoffBootstrapProfile,
deviceToken,
bootstrapDeviceTokens
};
}
//#endregion
//#region src/gateway/control-ui-identity.ts
/** Advertise browser identity at the ingress that actually authenticates people. */
function resolveControlUiIdentity(cfg, auth) {
if (cfg.gateway?.controlUi?.enabled === false) return;
let origin;
let signal;
if (auth.mode === "trusted-proxy") origin = resolveGatewayPublicOrigin(cfg);
else if (auth.mode !== "none" && auth.allowTailscale) {
const published = getTailscalePublishedOrigin();
if (published?.mode === "serve") {
origin = published.origin;
signal = published.signal;
}
}
if (!origin?.startsWith("https://")) return;
return {
url: `${origin}${normalizeControlUiBasePath(cfg.gateway?.controlUi?.basePath)}/`,
signal
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-hello.ts
async function sendGatewayHello(context, state, pluginSurfaceUrls, authenticatedUserProfileId) {
const { connId, bootId, nodeReapprovalCoordinator, gatewayMethods, events, buildRequestContext, refreshHealthSnapshot, close, advanceHandshakePhase, setCloseCause, logGateway, logHealth } = context.handler;
const { frame, connectParams, sendFrame, pendingNodePairingCleanup, releasePendingNodePairingCleanup } = context;
const { resolvedAuth, role, scopes, device, devicePublicKey, hasTokenAuth, hasPasswordAuth, bootstrapTokenCandidate, authResult, authMethod, sessionSharedGatewaySessionGeneration, issuedBootstrapProfile, handoffBootstrapProfile, deviceToken, bootstrapDeviceTokens } = state;
const authenticatedPrincipal = authResult.user ? authenticatedUserProfileId ?? authResult.user : void 0;
const recoveryScopeMaterial = authenticatedPrincipal ? [
"principal",
authenticatedPrincipal,
device?.id ?? ""
] : deviceToken?.token ? ["device-token", deviceToken.token] : sessionSharedGatewaySessionGeneration ? [
"shared-auth",
sessionSharedGatewaySessionGeneration,
device?.id ?? ""
] : device?.id ? ["device", device.id] : void 0;
const recoveryScope = role === "operator" && recoveryScopeMaterial ? sha256Base64Url(JSON.stringify(recoveryScopeMaterial)) : void 0;
const canMigrateRecovery = role === "operator" && !authenticatedPrincipal && Boolean(deviceToken);
const snapshot = buildGatewaySnapshot({
client: context.handler.getClient(),
includeSensitive: scopes.includes(ADMIN_SCOPE),
includeUpdateDetails: canReadDetailedUpdateMetadata(role, scopes),
revisionProjector: buildRequestContext().configRevisionProjector
});
const cachedHealth = getHealthCache();
if (cachedHealth) {
snapshot.health = cachedHealth;
snapshot.stateVersion.health = getHealthVersion();
}
const controlUiTabs = listControlUiPluginTabs(scopes, { requireGatewayAuthGrant: resolvedAuth.mode !== "none" });
const controlUiWidgetKinds = listControlUiPluginWidgetKinds(scopes);
const controlUiLocation = resolveControlUiLinkLocation(context.configSnapshot);
const controlUiBuildSource = context.configSnapshot.gateway?.controlUi?.root ? "configured" : "bundled";
const serverBuildId = resolveRuntimeServiceBuildId();
const helloOk = {
type: "hello-ok",
protocol: 4,
server: {
version: resolveRuntimeServiceVersion(process.env),
...serverBuildId ? { buildId: serverBuildId } : {},
bootId,
controlUiBuildSource,
connId
},
features: {
methods: gatewayMethods,
events,
capabilities: [
GATEWAY_SERVER_CAPS.BOARD_WIDGET_PUT_CANVAS_DOC,
GATEWAY_SERVER_CAPS.CHAT_SEND_ROUTING_CONTRACT,
GATEWAY_SERVER_CAPS.GATEWAY_RESTART_TARGET_SAFE,
GATEWAY_SERVER_CAPS.NODE_WORKER_BUNDLE_RETENTION,
GATEWAY_SERVER_CAPS.NODE_WORKER_BUNDLE_STATUS,
GATEWAY_SERVER_CAPS.NODE_WORKER_ENVIRONMENT_SESSION,
GATEWAY_SERVER_CAPS.NODE_WORKER_PORTAL_STREAM,
GATEWAY_SERVER_CAPS.SESSION_SCOPED_CHAT_METADATA,
GATEWAY_SERVER_CAPS.SESSION_UNREAD_ACK_CONTRACT,
GATEWAY_SERVER_CAPS.SESSION_GOAL_START,
GATEWAY_SERVER_CAPS.SESSION_SETTINGS_CONTRACT,
GATEWAY_SERVER_CAPS.SESSION_SETTINGS_CAS,
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_WIZARD_CANCEL,
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_SETUP_MODEL_REF,
GATEWAY_SERVER_CAPS.TASK_SUGGESTIONS_ACCEPT_MODES
]
},
snapshot,
...controlUiLocation ? { controlUiUrl: `${controlUiLocation.origin}${controlUiLocation.basePath}` } : {},
...controlUiTabs.length > 0 ? { controlUiTabs } : {},
...controlUiWidgetKinds.length > 0 ? { controlUiWidgetKinds } : {},
...Object.keys(pluginSurfaceUrls).length > 0 ? { pluginSurfaceUrls } : {},
auth: {
role,
scopes,
...recoveryScope ? { recoveryScope } : {},
...canMigrateRecovery ? { recoveryMigrationAllowed: true } : {},
...deviceToken ? {
deviceToken: deviceToken.token,
issuedAtMs: deviceToken.rotatedAtMs ?? deviceToken.createdAtMs,
...bootstrapDeviceTokens.length > 1 ? { deviceTokens: bootstrapDeviceTokens.slice(1) } : {}
} : {}
},
policy: {
maxPayload: MAX_PAYLOAD_BYTES,
maxBufferedBytes: MAX_BUFFERED_BYTES,
tickIntervalMs: TICK_INTERVAL_MS,
attachments: resolveChatAttachmentPolicy(context.configSnapshot),
allowedSessionVisibilities: allowedSessionVisibilities(context.configSnapshot),
hasMultipleSessionSharingIdentities: hasMultipleSessionSharingIdentities()
}
};
advanceHandshakePhase("hello_payload_prepared");
let bootstrapHandoff;
if (authMethod === "bootstrap-token" && bootstrapTokenCandidate && device) try {
if (handoffBootstrapProfile || issuedBootstrapProfile) {
const redemption = await redeemDeviceBootstrapTokenProfile({
token: bootstrapTokenCandidate,
role,
scopes
});
if (handoffBootstrapProfile || redemption.fullyRedeemed) {
const consumed = await consumeSetupHandoff({
token: bootstrapTokenCandidate,
deviceId: device.id,
pairedDeviceMatches: (paired) => paired?.publicKey === devicePublicKey
});
if (!consumed) {
await releasePendingNodePairingCleanup();
setCloseCause("bootstrap-token-consume-failed");
close();
return;
}
bootstrapHandoff = consumed;
}
}
} catch (err) {
logGateway.warn(`bootstrap token post-connect bookkeeping failed device=${device.id}: ${formatForLog(err)}`);
await releasePendingNodePairingCleanup();
setCloseCause("bootstrap-token-consume-failed", { error: formatForLog(err) });
close();
return;
}
try {
if (role === "operator") {
const identity = resolveControlUiIdentity(context.configSnapshot, resolvedAuth);
if (identity) {
snapshot.controlUiIdentityUrl = identity.url;
if (identity.signal && !context.handler.isClosed()) {
const signal = identity.signal;
const withdraw = () => {
setCloseCause("browser-identity-route-withdrawn");
close(1012, "browser identity route changed");
};
signal.addEventListener("abort", withdraw, { once: true });
context.handler.socket.once("close", () => signal.removeEventListener("abort", withdraw));
}
}
}
snapshot.suspension = { phase: getGatewaySuspendAdmissionPhase() };
await sendFrame({
type: "res",
id: frame.id,
ok: true,
payload: helloOk
});
} catch (err) {
if (bootstrapHandoff) {
if (bootstrapHandoff.completion) try {
broadcastSetupHandoffDeliveryUncertain({
handoff: bootstrapHandoff,
broadcast: buildRequestContext().broadcast
});
} catch (broadcastError) {
logGateway.warn(`setup delivery-uncertain broadcast failed device=${device?.id ?? "unknown"}: ${formatForLog(broadcastError)}`);
}
else try {
await restoreGenericDeviceBootstrapToken({ record: bootstrapHandoff.record });
} catch (restoreError) {
logGateway.warn(`generic bootstrap token restore after hello-send failure failed device=${device?.id ?? "unknown"}: ${formatForLog(restoreError)}`);
}
}
await releasePendingNodePairingCleanup();
setCloseCause("hello-send-failed", { error: formatForLog(err) });
close();
return;
}
if (bootstrapHandoff) try {
const confirmedHandoff = await confirmSetupHandoffDelivery({ handoff: bootstrapHandoff });
if (confirmedHandoff) broadcastSetupHandoffCompletion({
handoff: confirmedHandoff,
broadcast: buildRequestContext().broadcast
});
else broadcastSetupHandoffDeliveryUncertain({
handoff: bootstrapHandoff,
broadcast: buildRequestContext().broadcast
});
} catch (err) {
logGateway.warn(`setup completion confirmation failed device=${device?.id ?? "unknown"}: ${formatForLog(err)}`);
try {
broadcastSetupHandoffDeliveryUncertain({
handoff: bootstrapHandoff,
broadcast: buildRequestContext().broadcast
});
} catch {}
}
let authProvided = authMethod;
if (authMethod !== "device-token" && authMethod !== "bootstrap-token") {
if (hasPasswordAuth) authProvided = "password";
else if (hasTokenAuth) authProvided = "token";
}
emitGatewayAuthSecurityEvent({
action: "gateway.auth.succeeded",
outcome: "success",
severity: "low",
authMode: resolvedAuth.mode,
authMethod,
authProvided,
role,
scopes,
clientMode: connectParams.client.mode,
deviceId: device?.id
});
advanceHandshakePhase("ready");
if (role === "node") {
const requestContext = buildRequestContext();
const nodeId = connectParams.device?.id ?? connectParams.client.id;
const nodeSession = requestContext.nodeRegistry.get(nodeId);
const pairingGeneration = nodeSession?.pairingGeneration;
if (nodeSession?.connId === connId && pairingGeneration) try {
const connection = await recordPairedNodeConnection(nodeSession.nodeId, nodeSession.connectedAtMs, void 0, {
nodeId: nodeSession.nodeId,
key: pairingGeneration
});
if (!connection.recorded) logGateway.warn(`failed to record last connect for ${nodeSession.nodeId}: not paired`);
else {
const currentSession = requestContext.nodeRegistry.getForPairingGeneration(nodeSession.nodeId, pairingGeneration);
if (currentSession) scheduleNodeConnectionNotification(requestContext.nodeRegistry, currentSession, { isFirstConnection: connection.firstConnection });
}
} catch (err) {
logGateway.warn(`failed to record last connect for ${nodeSession.nodeId}: ${formatForLog(err)}`);
}
}
if (pendingNodePairingCleanup.value) {
const requestContext = buildRequestContext();
const cleanupClaim = pendingNodePairingCleanup.value;
pendingNodePairingCleanup.value = void 0;
try {
const resolvedPairings = nodeReapprovalCoordinator ? await nodeReapprovalCoordinator.finalizeCleanup(cleanupClaim) : await finalizeNodePairingCleanupClaim(cleanupClaim);
const resolvedAt = Date.now();
for (const resolved of resolvedPairings) requestContext.broadcast("node.pair.resolved", {
requestId: resolved.requestId,
nodeId: resolved.nodeId,
decision: "rejected",
ts: resolvedAt
}, { dropIfSlow: true });
} catch (error) {
logGateway.warn(`failed to clear stale pending pairings for ${cleanupClaim.nodeId}: ${formatForLog(error)}`);
}
}
logWs("out", "hello-ok", {
connId,
methods: gatewayMethods.length,
events: events.length,
presence: snapshot.presence.length,
stateVersion: snapshot.stateVersion.presence
});
refreshHealthSnapshot({ probe: false }).catch((err) => logHealth.error(`post-connect health refresh failed: ${formatErrorMessage(err)}`));
const client = context.handler.getClient();
if (client?.presenceKey && !client.invalidated && client.socket.readyState === 1 && !context.handler.isClosed()) broadcastPresenceSnapshot(buildRequestContext());
}
//#endregion
//#region src/gateway/server/ws-connection/connect-node-session.ts
var NodePairingRateLimitError = class extends Error {
constructor(retryAfterMs) {
super("node pairing rate limited");
this.retryAfterMs = retryAfterMs;
}
};
async function requestNodePairingFromConnect(params) {
if (params.pairedReconnect) return params.reapprovalCoordinator ? await params.reapprovalCoordinator.request({
input: params.input,
cleanupClaim: params.cleanupClaim
}) : await requestNodePairing(params.input);
if (!params.rateLimiter) return await requestNodePairing(params.input);
return await withSerializedRateLimitAttempt({
ip: params.clientIp,
scope: AUTH_RATE_LIMIT_SCOPE_NODE_PAIRING,
run: async () => {
const rateCheck = params.rateLimiter?.check(params.clientIp, AUTH_RATE_LIMIT_SCOPE_NODE_PAIRING);
if (rateCheck && !rateCheck.allowed) throw new NodePairingRateLimitError(rateCheck.retryAfterMs);
const result = await requestNodePairing(params.input);
params.rateLimiter?.recordFailure(params.clientIp, AUTH_RATE_LIMIT_SCOPE_NODE_PAIRING);
return result;
}
});
}
async function prepareGatewayNodeConnect(context, state) {
if (state.role !== "node") return true;
const { pluginNodeCapabilities = [], nodeReapprovalCoordinator, buildRequestContext, logGateway } = context.handler;
const { connectParams, reportedClientIp, authRateLimiter, browserRateLimitClientIp, pendingNodePairingCleanup, releasePendingNodePairingCleanup, broadcastNodePairingResult } = context;
const { device, devicePublicKey, usesLegacyNodeProtocol, rejectUnauthorized } = state;
const nodeId = connectParams.device?.id ?? connectParams.client.id;
const nodePairingSnapshot = await beginNodePairingConnect(nodeId);
const pairedNode = nodePairingSnapshot.pairedNode;
pendingNodePairingCleanup.value = nodePairingSnapshot.cleanupClaim;
const pairedDeviceForSurface = device && devicePublicKey ? await getPairedDevice(device.id) : null;
const deviceApprovedVia = pairedDeviceForSurface?.publicKey === devicePublicKey ? pairedDeviceForSurface?.approvedVia : void 0;
const deviceApprovedNonInteractively = deviceApprovedVia === "silent" || deviceApprovedVia === "ssh-verified" || deviceApprovedVia === "bootstrap";
let reconciliation;
try {
reconciliation = await reconcileNodePairingOnConnect({
cfg: getRuntimeConfig(),
connectParams,
pairedNode,
reportedClientIp,
initialSurfaceSilent: deviceApprovedNonInteractively,
requestPairing: async (input) => {
return await requestNodePairingFromConnect({
input,
rateLimiter: authRateLimiter,
clientIp: browserRateLimitClientIp,
pairedReconnect: pairedNode !== null,
cleanupClaim: pendingNodePairingCleanup.value,
reapprovalCoordinator: nodeReapprovalCoordinator
});
}
});
} catch (error) {
await releasePendingNodePairingCleanup();
if (error instanceof NodePairingRateLimitError) {
rejectUnauthorized({
ok: false,
reason: "rate_limited",
rateLimited: true,
retryAfterMs: error.retryAfterMs
});
return false;
}
throw error;
}
if ((deviceApprovedVia === "ssh-verified" || deviceApprovedVia === "bootstrap") && !pairedNode && reconciliation.pendingPairing) {
const surfaceRequestId = reconciliation.pendingPairing.request.requestId;
const approvedSurface = await approveNodePairing(surfaceRequestId, { callerScopes: [
ADMIN_SCOPE,
PAIRING_SCOPE,
WRITE_SCOPE
] });
if (approvedSurface && "node" in approvedSurface) {
logGateway.info(`security audit: node capability surface ${deviceApprovedVia} auto-approve node=${reconciliation.nodeId} commands=${reconciliation.declaredCommands.join(",") || "<none>"}`);
buildRequestContext().broadcast("node.pair.resolved", {
requestId: surfaceRequestId,
nodeId: reconciliation.nodeId,
decision: "approved",
ts: Date.now()
}, { dropIfSlow: true });
reconciliation = {
...reconciliation,
effectiveCaps: reconciliation.declaredCaps,
effectiveCommands: reconciliation.declaredCommands,
effectivePermissions: reconciliation.declaredPermissions,
pendingPairing: void 0,
shouldClearPendingPairings: true
};
}
}
if (!reconciliation.shouldClearPendingPairings) await releasePendingNodePairingCleanup();
if (reconciliation.pendingPairing) broadcastNodePairingResult(reconciliation.pendingPairing);
const nodeConnectParams = connectParams;
nodeConnectParams.declaredCaps = reconciliation.declaredCaps;
nodeConnectParams.declaredCommands = reconciliation.declaredCommands;
nodeConnectParams.withheldCommands = reconciliation.withheldCommands;
nodeConnectParams.declaredComputerUse = reconciliation.declaredComputerUse;
nodeConnectParams.declaredPermissions = reconciliation.declaredPermissions;
const pluginSurfaces = pluginNodeCapabilities.map((surface) => surface.surface);
const declaredFeatures = {
caps: normalizeNodeApprovalSurfaceList(connectParams.caps),
commands: normalizeNodeApprovalSurfaceList(connectParams.commands)
};
const sessionCeiling = usesLegacyNodeProtocol ? filterLegacyNodeProtocolFeatures({
...declaredFeatures,
pluginSurfaces
}) : declaredFeatures;
nodeConnectParams.sessionCapsCeiling = sessionCeiling.caps;
nodeConnectParams.sessionCommandsCeiling = sessionCeiling.commands;
const effectiveFeatures = usesLegacyNodeProtocol ? filterLegacyNodeProtocolFeatures({
caps: reconciliation.effectiveCaps,
commands: reconciliation.effectiveCommands,
pluginSurfaces
}) : {
caps: reconciliation.effectiveCaps,
commands: reconciliation.effectiveCommands
};
connectParams.caps = effectiveFeatures.caps;
connectParams.commands = effectiveFeatures.commands;
connectParams.computerUse = resolveEffectiveComputerUseDescriptor({
commands: effectiveFeatures.commands,
declared: reconciliation.declaredComputerUse
});
connectParams.permissions = reconciliation.effectivePermissions;
return true;
}
//#endregion
//#region src/gateway/server/ws-connection/connect-user-profile.ts
function resolveAuthenticatedProfile(profileId, updatedAt) {
const { id, displayName, avatarRevision, hasAvatar } = getUserProfileDisplay(profileId);
return {
profileId: id,
displayName,
avatarRevision,
hasAvatar,
updatedAt
};
}
async function resolveGatewayConnectUserProfile(params) {
const profile = params.ownerProfileExpected ? ensureGatewayOwnerProfile(await resolveHostAccountName()) : params.resolveAuthenticatedGitHubIdentity ? await params.resolveAuthenticatedGitHubIdentity() : params.authResult.tailscaleIdentity ? ensureProfileForTailscaleIdentity(params.authResult.tailscaleIdentity) : ensureProfileForEmail(params.authenticatedUserId);
return resolveAuthenticatedProfile("profileId" in profile ? profile.profileId : profile.id, profile.updatedAt);
}
//#endregion
//#region src/gateway/server/ws-connection/control-ui-build-admission.ts
/**
* The Gateway owns bundled same-origin UI admission. Browser code still owns
* reload, but an older document must never become an RPC-capable session.
*
* The exemptions are deliberate, not gaps (ui/AGENTS.md "Gateway Coupling"):
* configured roots serve an independently built artifact the Gateway owns no
* matching build identity for, "dev" is the ui:dev sentinel, and cross-origin
* stays exempt because build ids embed the build timestamp — version-identical
* source-built installs carry different ids, so equality would reject matched
* pairings, and "reload" cannot remediate a client the Gateway did not serve.
* Exempted skew fails visibly at the first missing method instead.
*/
function resolveControlUiBuildMismatch(params) {
const gatewayBuildId = params.gatewayBuildId?.trim();
const clientBuildId = params.clientBuildId?.trim();
if (params.clientId !== GATEWAY_CLIENT_IDS.CONTROL_UI || params.configuredControlUiRoot || !gatewayBuildId || clientBuildId === "dev" || !isGatewayHostBrowserOrigin({
requestHost: params.requestHost,
origin: params.requestOrigin
}) || clientBuildId === gatewayBuildId) return null;
return {
gatewayBuildId,
clientBuildId: clientBuildId || null
};
}
//#endregion
//#region src/gateway/server/ws-connection/connect-session.ts
/** Match production release versions (YYYY.M.PATCH or YYYY.M.PATCH-beta.N). */
const RELEASED_VERSION_RE = /^\d{4}\.\d+\.\d+/;
function isReleasedVersion(version) {
return RELEASED_VERSION_RE.test(version);
}
async function attachAuthenticatedGatewayConnect(context, state) {
const { socket, connId, remoteAddr, pluginSurfaceBaseUrl, pluginNodeCapabilities = [], buildRequestContext, close, isClosed, clearHandshakeTimer, setClient, setHandshakeState, advanceHandshakePhase, setCloseCause, logGateway, logWsControl, requestHost, requestOrigin } = context.handler;
const { connectParams, isLocalClient, reportedClientIp, runDetachedConnectWork, isWebchatConnect, clientLabel, clientMeta, markHandshakeFailure, sendHandshakeErrorResponse, releasePendingNodePairingCleanup } = context;
const { minProtocol, maxProtocol, usesLegacyNodeProtocol, role, scopes: deviceScopes, device, devicePublicKey, deviceToken, authResult, authMethod, pairingLocality, sessionUsesSharedGatewayAuth, sessionSharedGatewaySessionGeneration } = state;
if (!await prepareGatewayNodeConnect(context, state)) return;
let nodePairingAdmission;
if (role === "node") {
const nodeId = device?.id ?? connectParams.client.id;
const authenticatedNodeToken = authMethod === "device-token" ? normalizeOptionalString(connectParams.auth?.deviceToken ?? connectParams.auth?.token) : deviceToken?.token;
if (!device || !devicePublicKey || !authenticatedNodeToken) {
const message = "authenticated node pairing identity unavailable";
markHandshakeFailure("node-pairing-generation-changed", {});
sendHandshakeErrorResponse(ErrorCodes.NOT_PAIRED, message);
await releasePendingNodePairingCleanup();
close(1008, truncateCloseReason(message));
return;
}
const authenticatedNodePairing = {
nodeId,
publicKey: devicePublicKey,
token: authenticatedNodeToken
};
const admittedPairingState = await captureAuthenticatedNodePairingState(authenticatedNodePairing);
if (!admittedPairingState) {
const message = "node pairing changed during connect";
markHandshakeFailure("node-pairing-generation-changed", device?.id ? { deviceId: device.id } : {});
sendHandshakeErrorResponse(ErrorCodes.NOT_PAIRED, message);
await releasePendingNodePairingCleanup();
close(1008, truncateCloseReason(message));
return;
}
nodePairingAdmission = {
...admittedPairingState,
authenticated: authenticatedNodePairing
};
}
const shouldTrackPresence = !isEphemeralGatewayClient(connectParams.client);
const clientId = connectParams.client.id;
const instanceId = connectParams.client.instanceId;
const presenceKey = shouldTrackPresence ? role === "node" ? device?.id ?? instanceId ?? connId : connId : void 0;
const authenticatedUserId = normalizeOptionalString(authResult.user);
const authenticatedUserIsTailscaleProvider = (authResult.tailscaleIdentity ? classifyTailscaleLogin(authResult.tailscaleIdentity.login) : void 0)?.kind === "provider";
const resolveAuthenticatedGitHubIdentity = createAuthenticatedGitHubIdentitySync({
authResult,
authConfig: context.configSnapshot.gateway?.auth,
requestHeaders: context.handler.upgradeReq.headers
});
const rolesConfigured = Boolean(context.configSnapshot.gateway?.roles);
const sharedSecretOperatorOwner = role === "operator" && (authMethod === "token" || authMethod === "password");
const ownerProfileExpected = shouldTrackPresence && shouldUseGatewayOwnerProfile({
role,
authenticatedUserId,
authMethod,
rolesConfigured
});
let authenticatedUserProfile;
if (ownerProfileExpected || authenticatedUserId && (!resolveAuthenticatedGitHubIdentity || rolesConfigured)) try {
authenticatedUserProfile = await resolveGatewayConnectUserProfile({
ownerProfileExpected,
authenticatedUserId,
authResult,
resolveAuthenticatedGitHubIdentity
});
} catch (error) {
logWsControl.warn(`user profile resolution failed conn=${connId} user=${formatForLog(authenticatedUserId)}: ${formatForLog(error)}`);
if (!ownerProfileExpected && rolesConfigured && role === "operator" && !sharedSecretOperatorOwner) {
await rejectUnavailableProfileConnect(context, error);
return;
}
}
const effectiveScopes = resolveEffectiveConnectionScopes({
role,
deviceScopes,
verifiedIdentity: authenticatedUserId,
identityScopes: context.configSnapshot.gateway?.auth?.identityScopes,
upgradeReq: context.handler.upgradeReq
});
const rolePolicy = role === "operator" && !sharedSecretOperatorOwner ? resolveOperatorRolePolicyForProfile(authenticatedUserProfile?.profileId, context.configSnapshot) : void 0;
const scopes = rolePolicy ? effectiveScopes.scopes.filter((scope) => roleScopesAllow({
role: "operator",
requestedScopes: [scope],
allowedScopes: rolePolicy.scopes
})) : effectiveScopes.scopes;
state.scopes = scopes;
connectParams.scopes = scopes;
const addedIdentityScopes = effectiveScopes.addedIdentityScopes.filter((scope) => scopes.includes(scope));
if (authenticatedUserId && addedIdentityScopes.length > 0) logGateway.warn(`security audit: identity scope grant elevated connection identity=${formatForLog(authenticatedUserId)} addedScopes=${addedIdentityScopes.join(",")} conn=${connId}`);
if (isClosed()) {
await releasePendingNodePairingCleanup();
setCloseCause("connect-aborted-before-register", {
...clientMeta,
auth: authMethod
});
return;
}
const pluginSurfaceUrls = {};
const pluginNodeCapabilitySurfaces = indexPluginNodeCapabilitySurfaces(pluginNodeCapabilities);
const pendingPluginNodeCapabilities = [];
const effectiveNodeCaps = role === "node" ? new Set(connectParams.caps ?? []) : void 0;
if (pluginSurfaceBaseUrl && !usesLegacyNodeProtocol) for (const pluginCapabilitySurface of Object.values(pluginNodeCapabilitySurfaces)) {
if (effectiveNodeCaps && !effectiveNodeCaps.has(pluginCapabilitySurface.surface)) continue;
const capability = mintPluginNodeCapabilityToken();
const expiresAtMs = resolvePluginNodeCapabilityExpiresAtMs(pluginCapabilitySurface);
if (expiresAtMs === void 0) continue;
const scopedUrl = buildPluginNodeCapabilityScopedHostUrl(pluginSurfaceBaseUrl, capability) ?? pluginSurfaceBaseUrl;
pluginSurfaceUrls[pluginCapabilitySurface.surface] = scopedUrl;
pendingPluginNodeCapabilities.push({
surface: pluginCapabilitySurface,
capability,
expiresAtMs
});
}
const isTrustedApprovalRuntime = pairingLocality !== "remote" && scopes.includes("operator.approvals") && connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT && connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND && isOperatorApprovalRuntimeToken(connectParams.auth?.approvalRuntimeToken);
const agentRuntimeIdentityProof = connectParams.auth?.agentRuntimeIdentityToken;
const canAcceptAgentRuntimeIdentity = pairingLocality !== "remote" && connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT && connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND;
let trustedAgentRuntimeIdentity;
if (typeof agentRuntimeIdentityProof === "string") {
if (!canAcceptAgentRuntimeIdentity) {
const message = "agent runtime identity token is only accepted from local backend gateway clients";
markHandshakeFailure("agent-runtime-identity-untrusted-client", {
client: connectParams.client.id,
mode: connectParams.client.mode,
pairingLocality
});
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, message);
close(1008, truncateCloseReason(message));
return;
}
trustedAgentRuntimeIdentity = await verifyAgentRuntimeIdentityToken(agentRuntimeIdentityProof);
if (!trustedAgentRuntimeIdentity) {
const message = "invalid agent runtime identity token";
markHandshakeFailure("agent-runtime-identity-invalid", {
client: connectParams.client.id,
mode: connectParams.client.mode,
pairingLocality
});
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, message);
close(1008, message);
return;
}
}
const controlUiBuildMismatch = resolveControlUiBuildMismatch({
clientId: connectParams.client.id,
clientBuildId: connectParams.client.buildId,
gatewayBuildId: resolveRuntimeServiceBuildId(),
configuredControlUiRoot: context.configSnapshot.gateway?.controlUi?.root,
requestHost,
requestOrigin
});
if (controlUiBuildMismatch) {
const message = "protocol mismatch: Control UI updated; reload this page to continue";
markHandshakeFailure("control-ui-build-mismatch", {
clientBuildId: controlUiBuildMismatch.clientBuildId ?? "legacy",
gatewayBuildId: controlUiBuildMismatch.gatewayBuildId
});
sendHandshakeErrorResponse(ErrorCodes.UNAVAILABLE, message, {
retryable: false,
details: {
code: ConnectErrorDetailCodes.PROTOCOL_MISMATCH,
gatewayBuildId: controlUiBuildMismatch.gatewayBuildId,
reloadRequired: true
}
});
logWsControl.warn(`control ui build rejected conn=${connId} clientBuild=${formatForLog(controlUiBuildMismatch.clientBuildId ?? "legacy")} gatewayBuild=${formatForLog(controlUiBuildMismatch.gatewayBuildId)}; reload required`);
await releasePendingNodePairingCleanup();
close(1008, truncateCloseReason(message));
return;
}
const controlUiAdmin = role === "operator" && authMethod !== void 0 && authMethod !== "none" && connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI && scopes.includes("operator.admin");
const internal = {
...isLocalClient ? { isLocalClient: true } : {},
...controlUiAdmin ? { controlUiAdmin: true } : {},
...isTrustedApprovalRuntime ? { approvalRuntime: true } : {},
...trustedAgentRuntimeIdentity ? { agentRuntimeIdentity: trustedAgentRuntimeIdentity } : {},
...sharedSecretOperatorOwner ? { operatorRoleActor: { kind: "system" } } : {}
};
const prepareLocalUserIngress = (profile = authenticatedUserProfile) => prepareGatewayLocalUserIngress({
authMethod,
authenticatedUserExpected: Boolean(authenticatedUserId) || ownerProfileExpected,
...profile ? { profile: {
profileId: profile.profileId,
displayName: profile.displayName
} } : {},
...device?.id ? { pairedDeviceId: device.id } : {},
isLocalClient
});
const localUserIngress = prepareLocalUserIngress();
if (usesLegacyNodeProtocol) logWsControl.warn(`legacy node protocol accepted conn=${connId} client=${formatForLog(clientLabel)} v${formatForLog(connectParams.client.version)} min=${minProtocol} max=${maxProtocol} current=4; upgrade recommended`);
clearHandshakeTimer();
const nextClient = {
socket,
connect: connectParams,
connId,
connectionKind: "gateway",
isDeviceTokenAuth: authMethod === "device-token",
pairedClientId: isBrowserCopilotClient(connectParams.client) ? connectParams.client.id : void 0,
usesSharedGatewayAuth: sessionUsesSharedGatewayAuth,
sharedGatewaySessionGeneration: sessionSharedGatewaySessionGeneration,
presenceKey,
...authenticatedUserId ? { authenticatedUserId } : {},
...authenticatedUserIsTailscaleProvider ? { authenticatedUserIsTailscaleProvider: true } : {},
...authenticatedUserProfile ? { authenticatedUserProfile } : {},
clientIp: reportedClientIp,
...context.browserOrigin ? { browserOrigin: context.browserOrigin } : {},
...Object.keys(internal).length > 0 ? { internal } : {},
...Object.keys(pluginSurfaceUrls).length > 0 ? { pluginSurfaceUrls } : {},
...Object.keys(pluginNodeCapabilitySurfaces).length > 0 ? { pluginNodeCapabilitySurfaces } : {}
};
attachGatewayLocalUserIngress(nextClient, localUserIngress);
const attachAuthenticatedProfile = (profileId, updatedAt) => {
if (isClosed() || context.handler.getClient() !== nextClient || nextClient.invalidated || socket.readyState !== 1) return;
const profile = resolveAuthenticatedProfile(profileId, updatedAt);
if (nextClient.authenticatedUserProfile) Object.assign(nextClient.authenticatedUserProfile, profile);
else nextClient.authenticatedUserProfile = profile;
attachGatewayLocalUserIngress(nextClient, prepareLocalUserIngress(nextClient.authenticatedUserProfile));
const { profileId: id, ...display } = profile;
buildRequestContext().refreshConnectedUserProfile?.({
id,
...display
});
};
if (resolveAuthenticatedGitHubIdentity) nextClient.authenticatedGitHubIdentitySync = async () => {
const result = await resolveAuthenticatedGitHubIdentity();
attachAuthenticatedProfile(result.profileId, result.updatedAt);
return result;
};
for (const entry of pendingPluginNodeCapabilities) setClientPluginNodeCapability({
client: nextClient,
surface: entry.surface,
capability: entry.capability,
expiresAtMs: entry.expiresAtMs
});
if (role === "node" && isLocalClient) {
const localNodeId = await resolveLocalNodeId();
if (localNodeId && device?.id === localNodeId) {
const gatewayVersion = resolveRuntimeServiceVersion(process.env);
const clientVersion = connectParams.client.version;
if (clientVersion && gatewayVersion && clientVersion !== gatewayVersion && isReleasedVersion(gatewayVersion) && isReleasedVersion(clientVersion)) {
logWsControl.info(`node version mismatch conn=${connId} client=${formatForLog(clientLabel)} clientVersion=${formatForLog(clientVersion)} gatewayVersion=${gatewayVersion}; closing for supervisor restart`);
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "client version mismatch", { details: {
code: ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH,
clientVersion,
gatewayVersion
} });
await releasePendingNodePairingCleanup();
close(1008, "client version mismatch");
return;
}
}
}
const admittedNodePairing = role === "node" ? nodePairingAdmission : void 0;
if (admittedNodePairing) {
const currentPairingState = await captureAuthenticatedNodePairingState(admittedNodePairing.authenticated);
if (!currentPairingState || currentPairingState.identity.key !== admittedNodePairing.identity.key || currentPairingState.generation?.key !== admittedNodePairing.generation?.key) {
const message = "node pairing changed during connect";
markHandshakeFailure("node-pairing-generation-changed", { deviceId: admittedNodePairing.identity.nodeId });
sendHandshakeErrorResponse(ErrorCodes.NOT_PAIRED, message);
await releasePendingNodePairingCleanup();
close(1008, truncateCloseReason(message));
return;
}
}
const policyFailure = resolveGatewayConnectPolicyFailure(context, state);
if (policyFailure) {
await releasePendingNodePairingCleanup();
if (policyFailure.kind === "auth") {
setCloseCause("gateway-auth-rotated", { authGenerationStale: true });
close(4001, "gateway auth changed");
} else rejectGatewayConnectOrigin(context, policyFailure.reason);
return;
}
const handoffReceiver = prepareGatewayReceiverHandoff(socket, role);
if (!handoffReceiver) {
const message = "unsupported Gateway WebSocket receiver";
markHandshakeFailure("unsupported-websocket-receiver", {});
sendHandshakeErrorResponse(ErrorCodes.UNAVAILABLE, message);
await releasePendingNodePairingCleanup();
close(1011, message);
return;
}
if (!setClient(nextClient)) {
await releasePendingNodePairingCleanup();
setCloseCause("connect-aborted-before-register", {
...clientMeta,
auth: authMethod
});
return;
}
handoffReceiver();
setHandshakeState("connected");
advanceHandshakePhase("session_attached");
logWs("in", "connect", {
connId,
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
version: connectParams.client.version,
mode: connectParams.client.mode,
clientId,
platform: connectParams.client.platform,
auth: authMethod
});
if (authenticatedUserId) logWsControl.info(`authenticated user connected conn=${connId} user=${formatForLog(authenticatedUserId)}`);
if (isWebchatConnect(connectParams)) {
const clientBuildId = connectParams.client.buildId?.trim();
logWsControl.info(`webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} build=${formatForLog(clientBuildId ?? "legacy")}`);
}
const currentAuthenticatedPresenceUser = () => nextClient.authenticatedGitHubIdentitySync && !nextClient.authenticatedUserProfile ? void 0 : buildAuthenticatedPresenceUser({
authenticatedUserId,
authenticatedUserIsTailscaleProvider,
authenticatedUserProfile: nextClient.authenticatedUserProfile
});
if (presenceKey) {
const authenticatedPresenceUser = currentAuthenticatedPresenceUser();
upsertPresence(presenceKey, {
host: connectParams.client.displayName ?? connectParams.client.id ?? os.hostname(),
ip: isLocalClient ? void 0 : reportedClientIp,
version: connectParams.client.version,
platform: connectParams.client.platform,
deviceFamily: connectParams.client.deviceFamily,
modelIdentifier: connectParams.client.modelIdentifier,
timeZone: connectParams.client.timeZone,
mode: connectParams.client.mode,
deviceId: device?.id,
roles: [role],
scopes,
instanceId: role === "node" ? device?.id ?? instanceId : instanceId,
...authenticatedPresenceUser ? { user: authenticatedPresenceUser } : {},
reason: "connect"
});
}
if (admittedNodePairing) {
const pairingGeneration = admittedNodePairing.generation?.key;
const requestContext = buildRequestContext();
const nodeSession = requestContext.nodeRegistry.register(nextClient, {
remoteIp: reportedClientIp,
pairingIdentity: admittedNodePairing.identity.key,
approvedSurface: admittedNodePairing.approvedSurface,
...pairingGeneration ? { pairingGeneration } : {}
});
recordRemoteNodeInfo({
nodeId: nodeSession.nodeId,
connId: nodeSession.connId,
displayName: nodeSession.displayName,
platform: nodeSession.platform,
deviceFamily: nodeSession.deviceFamily,
commands: nodeSession.commands,
remoteIp: nodeSession.remoteIp,
pairingGeneration: nodeSession.pairingGeneration
});
runDetachedConnectWork(async () => {
await refreshRemoteNodeBins({
nodeId: nodeSession.nodeId,
platform: nodeSession.platform,
deviceFamily: nodeSession.deviceFamily,
commands: nodeSession.commands,
cfg: getRuntimeConfig(),
readinessDelayMs: 5e3
});
}, (err) => logGateway.warn(`remote bin probe failed for ${nodeSession.nodeId}: ${formatForLog(err)}`));
const sendConnectSnapshot = async (event, payload) => {
if (pairingGeneration) {
await requestContext.nodeRegistry.sendEventRawForPairingGeneration(nodeSession.nodeId, pairingGeneration, event, serializeEventPayload(payload));
return;
}
await requestContext.nodeRegistry.sendEventForPairingIdentity({
nodeId: nodeSession.nodeId,
connId: nodeSession.connId,
pairingIdentity: admittedNodePairing.identity.key,
event,
payload
});
};
runDetachedConnectWork(async () => {
const cfg = await loadVoiceWakeConfig();
await sendConnectSnapshot("voicewake.changed", { triggers: cfg.triggers });
}, (err) => logGateway.warn(`voicewake snapshot failed for ${nodeSession.nodeId}: ${formatForLog(err)}`));
runDetachedConnectWork(async () => {
const routing = await loadVoiceWakeRoutingConfig();
await sendConnectSnapshot("voicewake.routing.changed", { config: routing });
}, (err) => logGateway.warn(`voicewake routing snapshot failed for ${nodeSession.nodeId}: ${formatForLog(err)}`));
}
await sendGatewayHello(context, state, pluginSurfaceUrls, authenticatedUserProfile?.profileId);
if (nextClient.authenticatedGitHubIdentitySync) runDetachedConnectWork(async () => {
const result = await nextClient.authenticatedGitHubIdentitySync();
const profile = nextClient.authenticatedUserProfile;
const profilePic = authResult.tailscaleIdentity?.profilePic;
if (!profile?.hasAvatar && profilePic) try {
const updated = await adoptTailscaleProfileAvatar(result.profileId, profilePic);
if (updated.avatarMime) attachAuthenticatedProfile(updated.id, updated.updatedAt);
} catch (error) {
logGateway.warn(`Tailscale avatar adoption failed conn=${connId}: ${formatForLog(error)}`);
}
}, (error) => {
logGateway.warn(`GitHub identity sync failed conn=${connId}: ${formatForLog(error)}`);
});
const tailscaleProfilePic = authResult.tailscaleIdentity?.profilePic;
const tailscaleProfileId = nextClient.authenticatedUserProfile?.profileId;
if (!nextClient.authenticatedGitHubIdentitySync && tailscaleProfileId && !nextClient.authenticatedUserProfile?.hasAvatar && tailscaleProfilePic) runDetachedConnectWork(async () => {
const updated = await adoptTailscaleProfileAvatar(tailscaleProfileId, tailscaleProfilePic);
if (!updated.avatarMime) return;
attachAuthenticatedProfile(updated.id, updated.updatedAt);
}, (error) => logGateway.warn(`Tailscale avatar adoption failed conn=${connId}: ${formatForLog(error)}`));
}
//#endregion
//#region src/gateway/server/ws-connection/message-handler.ts
const GATEWAY_WORK_ADMISSION_RETRY_AFTER_MS = 1e3;
const GATEWAY_WORK_ADMISSION_CLOSE_CODE = 1013;
function claimsWorkerConnectionIdentity(value) {
if (!value || typeof value !== "object") return false;
const connect = value;
if (connect.role === "worker") return true;
if (!connect.client || typeof connect.client !== "object") return false;
const client = connect.client;
return client.id === GATEWAY_CLIENT_IDS.WORKER || client.mode === GATEWAY_CLIENT_MODES.WORKER;
}
function attachGatewayWsMessageHandler(params) {
const { socket, ingressAttribution, connId, remoteAddr, endpoint, forwardedFor, requestHost, requestOrigin, requestUserAgent, rateLimiter, browserRateLimiter, buildRequestContext, send, close, isClosed, getClient, setHandshakeState, setCloseCause, setLastFrameMeta, logGateway, logWsControl } = params;
const sendFrame = async (obj) => await new Promise((resolve, reject) => {
socket.send(JSON.stringify(obj), (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
const configSnapshot = getRuntimeConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const clientIp = ingressAttribution.clientIp;
const peerLabel = endpoint ?? remoteAddr ?? "n/a";
const hasProxyHeaders = ingressAttribution.kind === "trusted-proxy" || ingressAttribution.kind === "tailscale-serve" || ingressAttribution.kind === "tailscale-funnel";
const remoteIsTrustedProxy = ingressAttribution.kind === "trusted-proxy" || ingressAttribution.kind === "tailscale-serve" || ingressAttribution.kind === "tailscale-funnel";
const hostIsLocalish = isLocalishHost(requestHost);
const isLocalClient = ingressAttribution.kind === "direct-local";
const reportedClientIp = isLocalClient ? void 0 : clientIp && !isLoopbackAddress(clientIp) ? clientIp : void 0;
const reportedClientIpSource = resolveNodePairingClientIpSource({
reportedClientIp,
hasProxyHeaders,
remoteIsTrustedProxy,
remoteIsLoopback: isLoopbackAddress(remoteAddr)
});
if (!hostIsLocalish && isLoopbackAddress(remoteAddr) && !hasProxyHeaders) logWsControl.warn("Loopback connection with non-local Host header. Treating it as remote. If you're behind a reverse proxy, set gateway.trustedProxies and forward X-Forwarded-For/X-Real-IP.");
const isWebchatConnect = (p) => isWebchatClient(p?.client);
const authenticatedRequestDispatcher = createGatewayAuthenticatedRequestDispatcher({
handler: params,
isWebchatConnect
});
const { hasBrowserOriginHeader, enforceOriginCheckForAnyClient, rateLimitClientIp: browserRateLimitClientIp, authRateLimiter } = resolveHandshakeBrowserSecurityContext({
requestOrigin,
clientIp: ingressAttribution.rateLimit.subject.key,
rateLimiter,
browserRateLimiter
});
const runDetachedConnectWork = (run, onError) => {
params.connectionWork.track(() => runWithGatewayIndependentRootWorkAdmission(run, "ws:preauth", params.connectionWork.signal)).catch(onError);
};
const handleMessage = async (data, admission) => {
if (isClosed()) return;
const preauthPayloadBytes = !getClient() ? rawDataByteLength(data) : void 0;
if (preauthPayloadBytes !== void 0 && preauthPayloadBytes > 65536) {
logRejectedLargePayload({
surface: "gateway.ws.preauth",
bytes: preauthPayloadBytes,
limitBytes: MAX_PREAUTH_PAYLOAD_BYTES,
reason: "preauth_frame_limit"
});
setHandshakeState("failed");
setCloseCause("preauth-payload-too-large", {
payloadBytes: preauthPayloadBytes,
limitBytes: MAX_PREAUTH_PAYLOAD_BYTES
});
close(1009, "preauth payload too large");
return;
}
const text = rawDataToString(data);
const pendingNodePairingCleanup = {};
const broadcastNodePairingResult = (result) => {
const context = buildRequestContext();
const resolvedAt = Date.now();
for (const superseded of result.created ? result.superseded ?? [] : []) context.broadcast("node.pair.resolved", {
requestId: superseded.requestId,
nodeId: superseded.nodeId,
decision: "rejected",
ts: resolvedAt
}, { dropIfSlow: true });
if (result.created) context.broadcast("node.pair.requested", result.request, { dropIfSlow: true });
};
const releasePendingNodePairingCleanup = async () => {
const claim = pendingNodePairingCleanup.value;
pendingNodePairingCleanup.value = void 0;
if (!claim) return;
try {
await releaseNodePairingCleanupClaim(claim);
} catch (error) {
logGateway.warn(`failed to release pending pairing cleanup for ${claim.nodeId}: ${formatForLog(error)}`);
}
};
try {
const parsed = JSON.parse(text);
const client = getClient();
if (!client && parsed !== null && typeof parsed === "object" && "params" in parsed && claimsWorkerConnectionIdentity(parsed.params)) {
setHandshakeState("failed");
setCloseCause("invalid-handshake", { handshakeError: "invalid worker handshake" });
logWsControl.warn("worker admission rejected reason=invalid-handshake");
close(1008, "invalid-handshake");
return;
}
const frameType = parsed && typeof parsed === "object" && "type" in parsed ? typeof parsed.type === "string" ? String(parsed.type) : void 0 : void 0;
const frameMethod = parsed && typeof parsed === "object" && "method" in parsed ? typeof parsed.method === "string" ? String(parsed.method) : void 0 : void 0;
const frameId = parsed && typeof parsed === "object" && "id" in parsed ? typeof parsed.id === "string" ? String(parsed.id) : void 0 : void 0;
if (frameType || frameMethod || frameId) setLastFrameMeta({
type: frameType,
method: frameMethod,
id: frameId
});
if (!client) {
const isRequestFrame = validateRequestFrame(parsed);
if (!isRequestFrame || parsed.method !== "connect" || !validateConnectParams(parsed.params)) {
const handshakeError = isRequestFrame ? parsed.method === "connect" ? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}` : "invalid handshake: first request must be connect" : "invalid request frame";
setHandshakeState("failed");
setCloseCause("invalid-handshake", {
frameType,
frameMethod,
frameId,
handshakeError
});
if (isRequestFrame) send({
type: "res",
id: parsed.id,
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, handshakeError)
});
else logWsControl.warn(`invalid handshake conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} fwd=${formatForLog(forwardedFor ?? "n/a")} origin=${formatForLog(requestOrigin ?? "n/a")} host=${formatForLog(requestHost ?? "n/a")} ua=${formatForLog(requestUserAgent ?? "n/a")}`);
const closeReason = truncateCloseReason(handshakeError || "invalid handshake");
if (isRequestFrame) queueMicrotask(() => close(1008, closeReason));
else close(1008, closeReason);
return;
}
const frame = parsed;
const connectParams = frame.params;
const clientLabel = connectParams.client.displayName ?? connectParams.client.id;
const clientMeta = {
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
mode: connectParams.client.mode,
version: connectParams.client.version,
buildId: connectParams.client.buildId,
platform: connectParams.client.platform,
deviceFamily: connectParams.client.deviceFamily,
modelIdentifier: connectParams.client.modelIdentifier,
instanceId: connectParams.client.instanceId
};
const markHandshakeFailure = (cause, meta) => {
setHandshakeState("failed");
setCloseCause(cause, {
...meta,
...clientMeta
});
};
const sendHandshakeErrorResponse = (code, message, options) => {
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(code, message, options)
});
};
const phaseContext = {
handler: params,
frame,
connectParams,
configSnapshot,
trustedProxies,
allowRealIpFallback,
peerLabel,
hasProxyHeaders,
isLocalClient,
reportedClientIp,
reportedClientIpSource,
hasBrowserOriginHeader,
browserOrigin: resolveGatewayWsBrowserOrigin({
client: connectParams.client,
requestHost,
origin: requestOrigin,
isLocalClient,
enforceOriginCheckForAnyClient
}),
browserRateLimitClientIp,
authRateLimiter,
clientLabel,
clientMeta,
markHandshakeFailure,
sendHandshakeErrorResponse,
sendFrame,
isWebchatConnect,
runDetachedConnectWork,
pendingNodePairingCleanup,
broadcastNodePairingResult,
releasePendingNodePairingCleanup
};
const authenticated = await authenticateGatewayConnect(phaseContext);
if (!authenticated) return;
const deviceAuthorized = await authorizeGatewayConnectDevice(phaseContext, authenticated);
if (!deviceAuthorized) return;
await attachAuthenticatedGatewayConnect(phaseContext, deviceAuthorized);
return;
}
await authenticatedRequestDispatcher.dispatch(parsed, client, rawDataByteLength(data), admission);
} catch (err) {
await releasePendingNodePairingCleanup();
logGateway.error(`parse/handle error: ${String(err)}`);
logWs("out", "parse-error", {
connId,
error: formatForLog(err)
});
if (!getClient()) close();
}
};
const parsePreauthConnectFrame = (data) => {
if (isClosed() || rawDataByteLength(data) > 65536) return null;
let parsed;
try {
parsed = JSON.parse(rawDataToString(data));
} catch {
return null;
}
if (!validateRequestFrame(parsed) || parsed.method !== "connect" || !validateConnectParams(parsed.params)) return null;
return {
id: parsed.id,
params: parsed.params
};
};
const isPreparedControlConnect = (data) => {
const parsed = parsePreauthConnectFrame(data);
if (!parsed) return false;
return parsed.params.role !== "node" && !claimsWorkerConnectionIdentity(parsed.params);
};
const isStartupNodePreauth = (data) => {
const parsed = parsePreauthConnectFrame(data);
return parsed ? isStartupNodeConnect(parsed.params) : false;
};
const rejectConnectForClosedAdmission = async (data) => {
const parsed = parsePreauthConnectFrame(data);
if (!parsed) return false;
const restartDraining = isGatewayRestartDraining();
const reason = restartDraining ? GATEWAY_RESTART_UNAVAILABLE_REASON : GATEWAY_SUSPEND_UNAVAILABLE_REASON;
const operation = restartDraining ? "restart" : "suspension";
const phase = getGatewaySuspendAdmissionPhase();
setLastFrameMeta({
type: "req",
method: "connect",
id: parsed.id
});
setHandshakeState("failed");
setCloseCause(reason, {
method: "connect",
phase
});
await sendFrame({
type: "res",
id: parsed.id,
ok: false,
error: errorShape(ErrorCodes.UNAVAILABLE, `connect unavailable during gateway ${operation}`, {
retryable: true,
retryAfterMs: GATEWAY_WORK_ADMISSION_RETRY_AFTER_MS,
details: {
method: "connect",
reason,
phase
}
})
}).catch(() => {});
queueMicrotask(() => close(GATEWAY_WORK_ADMISSION_CLOSE_CODE, `gateway ${operation} in progress`));
return true;
};
const handleIncomingMessage = async (data, requestAdmission) => {
if (getClient()) {
await handleMessage(data, requestAdmission);
return;
}
const admission = tryBeginGatewayRootWorkAdmission("ws:connect");
if (!admission) {
if (isGatewayRestartDraining() && getGatewaySuspendAdmissionPhase() === "accepting" && params.isStartupPending?.() === true && isStartupNodePreauth(data)) {
const startupAdmission = tryBeginGatewayRestartStartupRootWorkAdmission();
if (startupAdmission) {
try {
await startupAdmission.run(() => handleMessage(data));
} finally {
startupAdmission.release();
}
return;
}
}
if (!isGatewayRestartDraining() && (getGatewaySuspendAdmissionPhase() === "draining" || getGatewaySuspendAdmissionPhase() === "prepared") && isPreparedControlConnect(data)) {
await handleMessage(data);
return;
}
if (await rejectConnectForClosedAdmission(data)) return;
await handleMessage(data);
return;
}
try {
await admission.run(() => handleMessage(data));
} finally {
admission.release();
}
};
socket.on("message", (data) => {
const admission = params.connectionWork.isClosing ? "continuation" : void 0;
if (admission && getClient()?.connect.role !== "node") return;
params.connectionWork.track(() => runWithDiagnosticTraceContext(createDiagnosticTraceContext(), () => handleIncomingMessage(data, admission))).catch((error) => {
logGateway.error(`request dispatch failed conn=${connId}: ${formatForLog(error)}`);
});
});
}
//#endregion
export { attachGatewayWsMessageHandler };