@openclaw/gateway-client 2026.7.2-beta.7 → 2026.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
- import { C as buildDeviceAuthPayload, S as shouldRetryGatewayWithDeviceToken, T as normalizeDeviceMetadataForAuth, _ as GatewayProtocolRequestError, a as createSessionProjection, b as resolveGatewayConnectScopes, c as normalizeSessionProjectionRunId, d as readSessionMessageSequence, f as reconcileSessionProjectionSnapshot, g as GatewayProtocolClient, h as shouldPauseGatewayReconnect, i as resetGatewaySessionMessageSubscriptionCoordinator, l as projectLiveSessionMessage, m as reduceSessionProjectionRunEvent, n as getGatewaySessionMessageSubscriptionCoordinator, o as hasSessionProjectionAcceptedFinal, p as reduceSessionProjection, r as releaseGatewaySessionMessageSubscription, s as isLocallyOptimisticSessionMessage, t as GatewaySessionMessageSubscriptionCoordinator, u as readSessionMessageIdentity, v as GatewayBrowserDeviceAuthLifecycle, w as buildDeviceAuthPayloadV3, x as selectGatewayConnectAuth, y as buildGatewayConnectAuth } from "./session-subscriptions-BV7MxYiL.mjs";
1
+ import { C as resolveGatewayConnectScopes, D as buildDeviceAuthPayloadV3, E as buildDeviceAuthPayload, O as normalizeDeviceMetadataForAuth, S as buildGatewayConnectAuth, T as shouldRetryGatewayWithDeviceToken, _ as GatewayProtocolRequestError, a as createSessionProjection, b as gatewayOriginScope, c as normalizeSessionProjectionRunId, d as readSessionMessageSequence, f as reconcileSessionProjectionSnapshot, g as GatewayProtocolClient, h as shouldPauseGatewayReconnect, i as resetGatewaySessionMessageSubscriptionCoordinator, l as projectLiveSessionMessage, m as reduceSessionProjectionRunEvent, n as getGatewaySessionMessageSubscriptionCoordinator, o as hasSessionProjectionAcceptedFinal, p as reduceSessionProjection, r as releaseGatewaySessionMessageSubscription, s as isLocallyOptimisticSessionMessage, t as GatewaySessionMessageSubscriptionCoordinator, u as readSessionMessageIdentity, v as GatewayProtocolRequestTimeoutError, w as selectGatewayConnectAuth, x as GatewayBrowserDeviceAuthLifecycle, y as gatewayCredentialScope } from "./session-subscriptions-uKmTyc3P.mjs";
2
2
  import { DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS, DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, MAX_CONNECT_CHALLENGE_TIMEOUT_MS, MAX_SAFE_TIMEOUT_DELAY_MS, MIN_CONNECT_CHALLENGE_TIMEOUT_MS, addSafeTimeoutDelayGraceMs, clampConnectChallengeTimeoutMs, clearGatewayConnectTimeout, getConnectChallengeTimeoutMsFromEnv, resolveConnectChallengeTimeoutMs, resolveFiniteTimeoutDelayMs, resolvePreauthHandshakeTimeoutMs, resolveSafeTimeoutDelayMs, startGatewayConnectTimeout } from "./timeouts.mjs";
3
+ import { rawDataToString } from "./websocket-data.mjs";
3
4
  import { n as startGatewayClientWithReadinessWait, r as waitForEventLoopReady, t as startGatewayClientWhenEventLoopReady } from "./readiness-CrWxd4TI.mjs";
5
+ import { GatewayScopeUpgrade } from "./scope-upgrade.mjs";
4
6
  import { ConnectErrorDetailCodes, formatConnectErrorMessage, readConnectErrorDetailCode } from "@openclaw/gateway-protocol/connect-error-details";
5
7
  import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "@openclaw/gateway-protocol/client-info";
6
8
  import { resolveGatewayStartupRetryAfterMs } from "@openclaw/gateway-protocol/startup-unavailable";
7
- import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";
9
+ import { MIN_CLIENT_PROTOCOL_VERSION, MIN_NODE_PROTOCOL_VERSION, MIN_PROBE_PROTOCOL_VERSION, PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";
8
10
  import { randomUUID } from "node:crypto";
9
11
  import { WebSocket } from "ws";
10
- import { Buffer as Buffer$1 } from "node:buffer";
11
12
  //#region \0rolldown/runtime.js
12
13
  var __create = Object.create;
13
14
  var __defProp = Object.defineProperty;
@@ -31,6 +32,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
32
  enumerable: true
32
33
  }) : target, mod));
33
34
  //#endregion
35
+ //#region packages/normalization-core/src/string-coerce.ts
36
+ /** Trims string input and returns null for non-strings or empty strings. */
37
+ function normalizeNullableString(value) {
38
+ if (typeof value !== "string") return null;
39
+ const trimmed = value.trim();
40
+ return trimmed ? trimmed : null;
41
+ }
42
+ /** Trims string input and returns undefined for non-strings or empty strings. */
43
+ function normalizeOptionalString(value) {
44
+ return normalizeNullableString(value) ?? void 0;
45
+ }
46
+ /** Lowercases a normalized optional string. */
47
+ function normalizeOptionalLowercaseString(value) {
48
+ return normalizeOptionalString(value)?.toLowerCase();
49
+ }
50
+ /** Lowercases a normalized string or returns an empty string when absent. */
51
+ function normalizeLowercaseStringOrEmpty(value) {
52
+ return normalizeOptionalLowercaseString(value) ?? "";
53
+ }
54
+ //#endregion
34
55
  //#region packages/net-policy/src/ip.ts
35
56
  var import_ipaddr = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36
57
  (function(root) {
@@ -990,13 +1011,6 @@ var import_ipaddr = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
990
1011
  else root.ipaddr = ipaddr;
991
1012
  })(exports);
992
1013
  })))(), 1);
993
- function normalizeOptionalString(value) {
994
- if (typeof value !== "string") return;
995
- return value.trim() || void 0;
996
- }
997
- function normalizeLowercaseStringOrEmpty$1(value) {
998
- return typeof value === "string" ? value.trim().toLowerCase() : "";
999
- }
1000
1014
  import_ipaddr.default.IPv4.parse("198.18.0.0");
1001
1015
  function stripIpv6Brackets(value) {
1002
1016
  if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
@@ -1026,7 +1040,7 @@ function parseCanonicalIpAddress(raw) {
1026
1040
  function normalizeIpAddress(raw) {
1027
1041
  const parsed = parseCanonicalIpAddress(raw);
1028
1042
  if (!parsed) return;
1029
- return normalizeLowercaseStringOrEmpty$1(normalizeIpv4MappedAddress(parsed).toString());
1043
+ return normalizeLowercaseStringOrEmpty(normalizeIpv4MappedAddress(parsed).toString());
1030
1044
  }
1031
1045
  /** True when a canonical IP literal is loopback, including IPv4-mapped IPv6. */
1032
1046
  function isLoopbackIpAddress(raw) {
@@ -1036,7 +1050,7 @@ function isLoopbackIpAddress(raw) {
1036
1050
  }
1037
1051
  //#endregion
1038
1052
  //#region packages/gateway-client/src/client-address-utils.ts
1039
- function normalizeLowercaseStringOrEmpty(value) {
1053
+ function normalizeGatewayErrorText(value) {
1040
1054
  return typeof value === "string" ? value.trim().toLowerCase() : "";
1041
1055
  }
1042
1056
  function isSensitiveUrlQueryParamName(key) {
@@ -1063,34 +1077,34 @@ function parseGatewayIpAddress(host) {
1063
1077
  return normalized ? parseCanonicalIpAddress(normalized) : void 0;
1064
1078
  }
1065
1079
  //#endregion
1066
- //#region packages/gateway-client/src/websocket-data.ts
1067
- function rawDataToString(data) {
1068
- if (Array.isArray(data)) return Buffer$1.concat(data).toString("utf8");
1069
- return data instanceof ArrayBuffer ? Buffer$1.from(data).toString("utf8") : data.toString("utf8");
1070
- }
1071
- //#endregion
1072
- //#region packages/gateway-client/src/client.ts
1073
- const DEFAULT_HOST_DEPS = {
1074
- loadOrCreateDeviceIdentity: () => void 0,
1075
- signDevicePayload: () => {
1076
- throw new Error("GatewayClient device signature dependency is not configured");
1077
- },
1078
- publicKeyRawBase64UrlFromPem: () => {
1079
- throw new Error("GatewayClient public key dependency is not configured");
1080
- },
1081
- loadDeviceAuthToken: () => null,
1082
- storeDeviceAuthToken: () => {},
1083
- clearDeviceAuthToken: () => {},
1084
- beforeConnect: () => {},
1085
- registerGatewayLoopbackBypass: () => void 0,
1086
- logDebug: () => {},
1087
- logError: () => {},
1088
- redactForLog: (message) => message,
1089
- normalizeTlsFingerprint: normalizeFingerprint
1080
+ //#region packages/gateway-client/src/request-error.ts
1081
+ var GatewayClientRequestError = class extends GatewayProtocolRequestError {
1082
+ constructor(error) {
1083
+ super({
1084
+ ...error,
1085
+ message: formatConnectErrorMessage({
1086
+ message: error.message,
1087
+ details: error.details
1088
+ })
1089
+ });
1090
+ this.name = "GatewayClientRequestError";
1091
+ }
1090
1092
  };
1091
- function resolveHostDeps(overrides) {
1092
- return Object.fromEntries(Object.entries(DEFAULT_HOST_DEPS).map(([key, fallback]) => [key, overrides?.[key] ?? fallback]));
1093
+ //#endregion
1094
+ //#region packages/net-policy/src/url-protocol.ts
1095
+ function parseUrl(value) {
1096
+ if (value instanceof URL) return value;
1097
+ try {
1098
+ return new URL(value);
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ function isWssUrl(value) {
1104
+ return parseUrl(value)?.protocol === "wss:";
1093
1105
  }
1106
+ //#endregion
1107
+ //#region packages/gateway-client/src/websocket-transport.ts
1094
1108
  const PRIVATE_OR_LOOPBACK_IPV4_RANGES = /* @__PURE__ */ new Set([
1095
1109
  "loopback",
1096
1110
  "private",
@@ -1106,19 +1120,16 @@ const PRIVATE_OR_LOOPBACK_IPV6_RANGES = /* @__PURE__ */ new Set([
1106
1120
  function isPrivateOrLoopbackIpAddress(address) {
1107
1121
  return (address.kind() === "ipv4" ? PRIVATE_OR_LOOPBACK_IPV4_RANGES : PRIVATE_OR_LOOPBACK_IPV6_RANGES).has(address.range());
1108
1122
  }
1109
- function isLoopbackHost(host) {
1123
+ function isGatewayLoopbackHost(host) {
1110
1124
  const parsed = parseHostForAddressChecks(host);
1111
- if (!parsed) return false;
1112
- if (parsed.isLocalhost) return true;
1113
- return isLoopbackIpAddress(parsed.unbracketedHost);
1125
+ return Boolean(parsed && (parsed.isLocalhost || isLoopbackIpAddress(parsed.unbracketedHost)));
1114
1126
  }
1115
1127
  function isPrivateOrLoopbackHost(host) {
1116
1128
  const parsed = parseHostForAddressChecks(host);
1117
1129
  if (!parsed) return false;
1118
1130
  if (parsed.isLocalhost) return true;
1119
1131
  const address = parseGatewayIpAddress(parsed.unbracketedHost);
1120
- if (!address) return false;
1121
- return isPrivateOrLoopbackIpAddress(address);
1132
+ return Boolean(address && isPrivateOrLoopbackIpAddress(address));
1122
1133
  }
1123
1134
  function isTrustedPlaintextWebSocketHost(hostname) {
1124
1135
  if (isPrivateOrLoopbackHost(hostname)) return true;
@@ -1131,7 +1142,7 @@ function isSecureWebSocketUrl(rawUrl, options) {
1131
1142
  const protocol = url.protocol === "https:" ? "wss:" : url.protocol === "http:" ? "ws:" : url.protocol;
1132
1143
  if (protocol === "wss:") return true;
1133
1144
  if (protocol !== "ws:") return false;
1134
- if (isLoopbackHost(url.hostname) || isTrustedPlaintextWebSocketHost(url.hostname)) return true;
1145
+ if (isGatewayLoopbackHost(url.hostname) || isTrustedPlaintextWebSocketHost(url.hostname)) return true;
1135
1146
  if (options?.allowPrivateWs === true) {
1136
1147
  const hostForIpCheck = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
1137
1148
  return isPrivateOrLoopbackHost(url.hostname) || parseGatewayIpAddress(hostForIpCheck) === void 0;
@@ -1141,28 +1152,117 @@ function isSecureWebSocketUrl(rawUrl, options) {
1141
1152
  return false;
1142
1153
  }
1143
1154
  }
1144
- const DEFAULT_GATEWAY_CLIENT_URL = "ws://127.0.0.1:18789";
1145
- const DEFAULT_CLIENT_VERSION = "0.0.0";
1146
- var GatewayClientRequestError = class extends GatewayProtocolRequestError {
1147
- constructor(error) {
1148
- super({
1149
- ...error,
1150
- message: formatConnectErrorMessage({
1151
- message: error.message,
1152
- details: error.details
1153
- })
1154
- });
1155
- this.name = "GatewayClientRequestError";
1155
+ var GatewayWebSocketTransportConfigurationError = class extends Error {};
1156
+ function resolveGatewayWebSocketTransport(params) {
1157
+ const usesTls = isWssUrl(params.url);
1158
+ if (params.tlsFingerprint && !usesTls) throw new GatewayWebSocketTransportConfigurationError("gateway tls fingerprint requires wss:// gateway url");
1159
+ const allowPrivateWs = (params.env ?? process.env).OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
1160
+ if (!isSecureWebSocketUrl(params.url, { allowPrivateWs })) {
1161
+ let displayHost = params.url;
1162
+ try {
1163
+ displayHost = new URL(params.url).hostname || params.url;
1164
+ } catch {}
1165
+ throw new GatewayWebSocketTransportConfigurationError(`SECURITY ERROR: Cannot connect to "${displayHost}" over plaintext ws://. Both credentials and chat data would be exposed to network interception. Use wss:// for remote URLs. Safe defaults: keep gateway.bind=loopback and connect via SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host), or use Tailscale Serve/Funnel. ` + (allowPrivateWs ? "" : "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1. ") + "Run `openclaw doctor --fix` for guidance.");
1166
+ }
1167
+ const normalize = params.normalizeTlsFingerprint ?? normalizeFingerprint;
1168
+ const options = { ...params.options };
1169
+ if (usesTls && params.tlsFingerprint) {
1170
+ options.rejectUnauthorized = false;
1171
+ options.checkServerIdentity = (_hostValue, cert) => {
1172
+ const fingerprintValue = typeof cert === "object" && cert && "fingerprint256" in cert ? cert.fingerprint256 ?? "" : "";
1173
+ const fingerprint = normalize(typeof fingerprintValue === "string" ? fingerprintValue : "");
1174
+ const expected = normalize(params.tlsFingerprint);
1175
+ if (!expected) return;
1176
+ if (!fingerprint) return /* @__PURE__ */ new Error("Missing server TLS fingerprint");
1177
+ if (fingerprint !== expected) return /* @__PURE__ */ new Error("Server TLS fingerprint mismatch");
1178
+ };
1156
1179
  }
1180
+ return {
1181
+ options,
1182
+ validateSocket: (socket) => {
1183
+ if (!params.tlsFingerprint) return null;
1184
+ const expected = normalize(params.tlsFingerprint);
1185
+ if (!expected) return /* @__PURE__ */ new Error("gateway tls fingerprint missing");
1186
+ const rawSocket = socket["_socket"];
1187
+ if (!rawSocket || typeof rawSocket.getPeerCertificate !== "function") return /* @__PURE__ */ new Error("gateway tls fingerprint unavailable");
1188
+ const cert = rawSocket.getPeerCertificate();
1189
+ const fingerprint = normalize(cert?.fingerprint256 ?? "");
1190
+ if (!fingerprint) return /* @__PURE__ */ new Error("gateway tls fingerprint unavailable");
1191
+ return fingerprint === expected ? null : /* @__PURE__ */ new Error("gateway tls fingerprint mismatch");
1192
+ }
1193
+ };
1194
+ }
1195
+ //#endregion
1196
+ //#region packages/gateway-client/src/client.ts
1197
+ const DEFAULT_HOST_DEPS = {
1198
+ loadOrCreateDeviceIdentity: () => void 0,
1199
+ signDevicePayload: () => {
1200
+ throw new Error("GatewayClient device signature dependency is not configured");
1201
+ },
1202
+ publicKeyRawBase64UrlFromPem: () => {
1203
+ throw new Error("GatewayClient public key dependency is not configured");
1204
+ },
1205
+ loadDeviceAuthToken: () => null,
1206
+ storeDeviceAuthToken: () => {},
1207
+ clearDeviceAuthToken: () => {},
1208
+ beforeConnect: () => {},
1209
+ registerGatewayLoopbackBypass: () => void 0,
1210
+ logDebug: () => {},
1211
+ logError: () => {},
1212
+ redactForLog: (message) => message,
1213
+ normalizeTlsFingerprint: normalizeFingerprint
1157
1214
  };
1158
- var GatewayClientTransientPreHelloCloseError = class extends Error {
1159
- constructor() {
1160
- super("gateway transient pre-hello clean close");
1161
- this.name = "GatewayClientTransientPreHelloCloseError";
1215
+ function resolveHostDeps(overrides) {
1216
+ return Object.fromEntries(Object.entries(DEFAULT_HOST_DEPS).map(([key, fallback]) => [key, overrides?.[key] ?? fallback]));
1217
+ }
1218
+ const DEFAULT_GATEWAY_CLIENT_URL = "ws://127.0.0.1:18789";
1219
+ const DEFAULT_CLIENT_VERSION = "0.0.0";
1220
+ const MAX_UPGRADE_ERROR_BODY_BYTES = 2 * 1024;
1221
+ const UPGRADE_ERROR_BODY_TIMEOUT_MS = 1e3;
1222
+ async function readUpgradeErrorBody(response) {
1223
+ return await new Promise((resolve) => {
1224
+ const chunks = [];
1225
+ let totalBytes = 0;
1226
+ let settled = false;
1227
+ const finish = () => {
1228
+ if (settled) return;
1229
+ settled = true;
1230
+ clearTimeout(timer);
1231
+ response.off("data", onData);
1232
+ response.off("end", finish);
1233
+ response.off("error", finish);
1234
+ response.off("aborted", finish);
1235
+ resolve(Buffer.concat(chunks, totalBytes).toString("utf8").replace(/\s+/gu, " ").trim());
1236
+ };
1237
+ const stop = () => {
1238
+ finish();
1239
+ response.destroy();
1240
+ };
1241
+ const onData = (chunk) => {
1242
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1243
+ const remaining = MAX_UPGRADE_ERROR_BODY_BYTES - totalBytes;
1244
+ if (remaining > 0) {
1245
+ const prefix = buffer.subarray(0, remaining);
1246
+ chunks.push(prefix);
1247
+ totalBytes += prefix.byteLength;
1248
+ }
1249
+ if (buffer.byteLength >= remaining) stop();
1250
+ };
1251
+ const timer = setTimeout(stop, UPGRADE_ERROR_BODY_TIMEOUT_MS);
1252
+ timer.unref?.();
1253
+ response.on("data", onData);
1254
+ response.once("end", finish);
1255
+ response.once("error", finish);
1256
+ response.once("aborted", finish);
1257
+ });
1258
+ }
1259
+ var GatewayClientRequestTimeoutError = class extends GatewayProtocolRequestTimeoutError {
1260
+ constructor(params) {
1261
+ super(params, `gateway request timeout for ${params.method}`);
1262
+ this.name = "GatewayClientRequestTimeoutError";
1162
1263
  }
1163
1264
  };
1164
- var GatewayClientSocketFactoryConfigurationError = class extends Error {};
1165
- var GatewayClientTransportPolicyError = class extends GatewayClientSocketFactoryConfigurationError {};
1265
+ var GatewayClientTransportPolicyError = class extends GatewayWebSocketTransportConfigurationError {};
1166
1266
  const GATEWAY_CONNECT_ASSEMBLY_ERROR = Symbol("gateway.connectAssemblyError");
1167
1267
  function markGatewayConnectAssemblyError(error) {
1168
1268
  Object.defineProperty(error, GATEWAY_CONNECT_ASSEMBLY_ERROR, {
@@ -1184,10 +1284,20 @@ function formatGatewayClientErrorForLog(err) {
1184
1284
  const FORCE_STOP_TERMINATE_GRACE_MS = 250;
1185
1285
  const STOP_AND_WAIT_TIMEOUT_MS = 1e3;
1186
1286
  const MAX_SUPPRESSED_TRANSIENT_PRE_HELLO_CLEAN_CLOSES = 1;
1287
+ function resolveLegacyNodePlatform(platform) {
1288
+ switch (platform) {
1289
+ case "macos": return "darwin";
1290
+ case "windows": return "win32";
1291
+ default: return;
1292
+ }
1293
+ }
1187
1294
  var GatewayClient = class {
1188
1295
  constructor(opts) {
1189
1296
  this.ws = null;
1190
1297
  this.stopped = false;
1298
+ this.useLegacyNodeProtocolEnvelope = false;
1299
+ this.nodeProtocolTransitionPending = false;
1300
+ this.suppressNextHelloCallback = false;
1191
1301
  this.pendingDeviceTokenRetry = false;
1192
1302
  this.deviceTokenRetryBudgetUsed = false;
1193
1303
  this.approvalRuntimeTokenCompatibilityDisabled = false;
@@ -1203,7 +1313,7 @@ var GatewayClient = class {
1203
1313
  ...opts,
1204
1314
  deviceIdentity: opts.deviceIdentity === null ? void 0 : opts.deviceIdentity ?? this.deps.loadOrCreateDeviceIdentity()
1205
1315
  };
1206
- this.requestTimeoutMs = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) ? resolveSafeTimeoutDelayMs(opts.requestTimeoutMs, { minMs: 0 }) : DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS;
1316
+ this.requestTimeoutMs = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) ? opts.requestTimeoutMs : DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS;
1207
1317
  const connectChallengeTimeoutMs = resolveConnectChallengeTimeoutMs(this.opts.connectChallengeTimeoutMs, {
1208
1318
  env: this.opts.env,
1209
1319
  configuredTimeoutMs: this.opts.preauthHandshakeTimeoutMs
@@ -1212,13 +1322,19 @@ var GatewayClient = class {
1212
1322
  createSocket: (handlers) => this.createSocket(handlers),
1213
1323
  createRequestId: randomUUID,
1214
1324
  createRequestError: (error) => new GatewayClientRequestError(error),
1215
- createRequestTimeoutError: (method) => /* @__PURE__ */ new Error(`gateway request timeout for ${method}`),
1325
+ createRequestTimeoutError: (method, timeoutMs, requestSent) => new GatewayClientRequestTimeoutError({
1326
+ method,
1327
+ timeoutMs,
1328
+ requestSent
1329
+ }),
1216
1330
  createRequestAbortError: createGatewayRequestAbortError,
1217
- buildConnectPlan: ({ nonce }) => {
1331
+ buildConnectPlan: ({ nonce, challengeTs }) => {
1218
1332
  if (!nonce) throw new Error("gateway connect challenge missing nonce");
1333
+ if (this.opts.deviceIdentity && challengeTs == null) throw new Error("gateway connect challenge timestamp invalid");
1219
1334
  return this.assembleConnectParams({
1220
1335
  role: this.opts.role ?? "operator",
1221
- nonce
1336
+ nonce,
1337
+ signedAtMs: challengeTs ?? Date.now()
1222
1338
  });
1223
1339
  },
1224
1340
  buildConnectParams: (assembled) => assembled.params,
@@ -1236,7 +1352,13 @@ var GatewayClient = class {
1236
1352
  };
1237
1353
  },
1238
1354
  onConnectHello: (hello, context) => this.handleConnectHello(hello, context.plan),
1239
- onHello: (hello) => this.opts.onHelloOk?.(hello),
1355
+ onHello: (hello) => {
1356
+ if (this.suppressNextHelloCallback) {
1357
+ this.suppressNextHelloCallback = false;
1358
+ return;
1359
+ }
1360
+ this.opts.onHelloOk?.(hello);
1361
+ },
1240
1362
  onConnectFailure: (error, context) => this.handleConnectRequestFailure(error, context.plan),
1241
1363
  resolveClose: (context) => this.resolveClose(context),
1242
1364
  onClose: (context, decision) => {
@@ -1266,7 +1388,7 @@ var GatewayClient = class {
1266
1388
  maxMs: 3e4
1267
1389
  },
1268
1390
  requestTimeoutMs: this.requestTimeoutMs,
1269
- shouldRetrySocketFactoryError: (error) => !(error instanceof GatewayClientSocketFactoryConfigurationError) && !(error instanceof SyntaxError) && !(error instanceof TypeError) && !(error instanceof RangeError),
1391
+ shouldRetrySocketFactoryError: (error) => !(error instanceof GatewayWebSocketTransportConfigurationError) && !(error instanceof SyntaxError) && !(error instanceof TypeError) && !(error instanceof RangeError),
1270
1392
  rethrowSocketFactoryError: (error) => error instanceof GatewayClientTransportPolicyError
1271
1393
  });
1272
1394
  }
@@ -1282,7 +1404,9 @@ var GatewayClient = class {
1282
1404
  this.opts = {
1283
1405
  ...this.opts,
1284
1406
  caps: [...manifest.caps],
1285
- commands: [...manifest.commands]
1407
+ commands: [...manifest.commands],
1408
+ computerUse: manifest.computerUse === void 0 ? void 0 : structuredClone(manifest.computerUse),
1409
+ workerRuns: manifest.workerRuns ? structuredClone(manifest.workerRuns) : void 0
1286
1410
  };
1287
1411
  if (!this.stopped) this.protocol.closeSocket(1012, "node manifest changed");
1288
1412
  }
@@ -1292,35 +1416,22 @@ var GatewayClient = class {
1292
1416
  }
1293
1417
  createSocket(handlers) {
1294
1418
  const url = this.opts.url ?? DEFAULT_GATEWAY_CLIENT_URL;
1295
- if (this.opts.tlsFingerprint && !url.startsWith("wss://")) throw new GatewayClientSocketFactoryConfigurationError("gateway tls fingerprint requires wss:// gateway url");
1296
- const allowPrivateWs = (this.opts.env ?? process.env).OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
1297
- if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
1298
- let displayHost = url;
1299
- try {
1300
- displayHost = new URL(url).hostname || url;
1301
- } catch {}
1302
- throw new GatewayClientSocketFactoryConfigurationError(`SECURITY ERROR: Cannot connect to "${displayHost}" over plaintext ws://. Both credentials and chat data would be exposed to network interception. Use wss:// for remote URLs. Safe defaults: keep gateway.bind=loopback and connect via SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host), or use Tailscale Serve/Funnel. ` + (allowPrivateWs ? "" : "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1. ") + "Run `openclaw doctor --fix` for guidance.");
1303
- }
1419
+ const handshakeTimeoutMs = resolvePreauthHandshakeTimeoutMs({
1420
+ env: this.opts.env,
1421
+ configuredTimeoutMs: this.opts.preauthHandshakeTimeoutMs
1422
+ });
1423
+ const transport = resolveGatewayWebSocketTransport({
1424
+ url,
1425
+ tlsFingerprint: this.opts.tlsFingerprint,
1426
+ env: this.opts.env,
1427
+ normalizeTlsFingerprint: this.deps.normalizeTlsFingerprint,
1428
+ options: {
1429
+ maxPayload: 25 * 1024 * 1024,
1430
+ handshakeTimeout: handshakeTimeoutMs,
1431
+ ...this.opts.origin ? { origin: this.opts.origin } : {}
1432
+ }
1433
+ });
1304
1434
  this.deps.beforeConnect();
1305
- const wsOptions = {
1306
- maxPayload: 25 * 1024 * 1024,
1307
- handshakeTimeout: resolvePreauthHandshakeTimeoutMs({
1308
- env: this.opts.env,
1309
- configuredTimeoutMs: this.opts.preauthHandshakeTimeoutMs
1310
- }),
1311
- ...this.opts.origin ? { origin: this.opts.origin } : {}
1312
- };
1313
- if (url.startsWith("wss://") && this.opts.tlsFingerprint) {
1314
- wsOptions.rejectUnauthorized = false;
1315
- wsOptions.checkServerIdentity = (_hostValue, cert) => {
1316
- const fingerprintValue = typeof cert === "object" && cert && "fingerprint256" in cert ? cert.fingerprint256 ?? "" : "";
1317
- const fingerprint = this.deps.normalizeTlsFingerprint(typeof fingerprintValue === "string" ? fingerprintValue : "");
1318
- const expected = this.deps.normalizeTlsFingerprint(this.opts.tlsFingerprint ?? "");
1319
- if (!expected) return;
1320
- if (!fingerprint) return /* @__PURE__ */ new Error("Missing server TLS fingerprint");
1321
- if (fingerprint !== expected) return /* @__PURE__ */ new Error("Server TLS fingerprint mismatch");
1322
- };
1323
- }
1324
1435
  let ws;
1325
1436
  let unregisterGatewayLoopbackBypass;
1326
1437
  try {
@@ -1329,7 +1440,7 @@ var GatewayClient = class {
1329
1440
  throw new GatewayClientTransportPolicyError(error instanceof Error ? error.message : String(error));
1330
1441
  }
1331
1442
  try {
1332
- ws = new WebSocket(url, wsOptions);
1443
+ ws = new WebSocket(url, transport.options);
1333
1444
  ws.binaryType = "nodebuffer";
1334
1445
  } catch (error) {
1335
1446
  throw error instanceof Error ? error : new Error(String(error));
@@ -1338,15 +1449,14 @@ var GatewayClient = class {
1338
1449
  }
1339
1450
  this.ws = ws;
1340
1451
  this.transportValidated = false;
1452
+ let upgradeError;
1341
1453
  ws.on("open", () => {
1342
1454
  handlers.open();
1343
- if (url.startsWith("wss://") && this.opts.tlsFingerprint) {
1344
- const tlsError = this.validateTlsFingerprint();
1345
- if (tlsError) {
1346
- handlers.error(tlsError);
1347
- ws.close(1008, tlsError.message);
1348
- return;
1349
- }
1455
+ const tlsError = transport.validateSocket(ws);
1456
+ if (tlsError) {
1457
+ handlers.error(tlsError);
1458
+ ws.close(1008, tlsError.message);
1459
+ return;
1350
1460
  }
1351
1461
  this.transportValidated = true;
1352
1462
  });
@@ -1357,7 +1467,25 @@ var GatewayClient = class {
1357
1467
  this.resolvePendingStop(ws);
1358
1468
  handlers.close(code, reasonText);
1359
1469
  });
1470
+ ws.on("unexpected-response", (request, response) => {
1471
+ readUpgradeErrorBody(response).then((body) => {
1472
+ const statusCode = response.statusCode;
1473
+ upgradeError = new GatewayClientRequestError({
1474
+ code: "UNAVAILABLE",
1475
+ message: `gateway rejected websocket upgrade (HTTP ${statusCode ?? "unknown"})${body ? `: ${body}` : ""}`,
1476
+ retryable: true,
1477
+ details: {
1478
+ reason: "websocket-upgrade-rejected",
1479
+ ...statusCode === void 0 ? {} : { httpStatus: statusCode }
1480
+ }
1481
+ });
1482
+ handlers.error(upgradeError);
1483
+ request.destroy();
1484
+ ws.close();
1485
+ });
1486
+ });
1360
1487
  ws.on("error", (err) => {
1488
+ if (upgradeError) return;
1361
1489
  this.logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`);
1362
1490
  handlers.error(err instanceof Error ? err : new Error(String(err)));
1363
1491
  });
@@ -1446,34 +1574,45 @@ var GatewayClient = class {
1446
1574
  this.deps.logError(this.deps.redactForLog(message));
1447
1575
  }
1448
1576
  assembleConnectParams(params) {
1449
- const { role, nonce } = params;
1577
+ const { role, nonce, signedAtMs } = params;
1450
1578
  const selectedAuth = this.selectConnectAuth(role);
1451
1579
  const { authDeviceToken, authApprovalRuntimeToken, authAgentRuntimeIdentityToken, signatureToken, resolvedDeviceToken, storedToken, storedScopes, usingStoredDeviceToken } = selectedAuth;
1452
1580
  if (this.pendingDeviceTokenRetry && authDeviceToken) this.pendingDeviceTokenRetry = false;
1453
1581
  const auth = buildGatewayConnectAuth(selectedAuth);
1454
- const signedAtMs = Date.now();
1455
1582
  const scopes = resolveGatewayConnectScopes({
1456
1583
  requestedScopes: this.opts.scopes,
1457
1584
  usingStoredDeviceToken,
1458
1585
  storedScopes,
1459
1586
  defaultScopes: ["operator.admin"]
1460
1587
  });
1461
- const platform = this.opts.platform ?? process.platform;
1588
+ const clientMode = this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND;
1589
+ const clientId = this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT;
1590
+ const isBuiltInNodeHost = role === "node" && clientMode === GATEWAY_CLIENT_MODES.NODE && clientId === GATEWAY_CLIENT_NAMES.NODE_HOST;
1591
+ const negotiatesNodeProtocol = this.shouldNegotiateLegacyNodeProtocol();
1592
+ const useLegacyNodeProtocolEnvelope = isBuiltInNodeHost && (this.useLegacyNodeProtocolEnvelope || this.opts.maxProtocol === MIN_NODE_PROTOCOL_VERSION && (this.opts.minProtocol ?? MIN_NODE_PROTOCOL_VERSION) <= MIN_NODE_PROTOCOL_VERSION);
1593
+ const minProtocol = useLegacyNodeProtocolEnvelope ? MIN_NODE_PROTOCOL_VERSION : negotiatesNodeProtocol ? PROTOCOL_VERSION : this.opts.minProtocol ?? (clientMode === GATEWAY_CLIENT_MODES.PROBE ? MIN_PROBE_PROTOCOL_VERSION : role === "node" && clientMode === GATEWAY_CLIENT_MODES.NODE ? MIN_NODE_PROTOCOL_VERSION : MIN_CLIENT_PROTOCOL_VERSION);
1594
+ const maxProtocol = useLegacyNodeProtocolEnvelope ? MIN_NODE_PROTOCOL_VERSION : negotiatesNodeProtocol ? PROTOCOL_VERSION : this.opts.maxProtocol ?? PROTOCOL_VERSION;
1595
+ const configuredPlatform = this.opts.platform ?? process.platform;
1596
+ const platform = useLegacyNodeProtocolEnvelope ? resolveLegacyNodePlatform(configuredPlatform) ?? configuredPlatform : configuredPlatform;
1597
+ const deviceFamily = useLegacyNodeProtocolEnvelope ? void 0 : this.opts.deviceFamily;
1462
1598
  return {
1463
1599
  params: {
1464
- minProtocol: this.opts.minProtocol ?? MIN_CLIENT_PROTOCOL_VERSION,
1465
- maxProtocol: this.opts.maxProtocol ?? PROTOCOL_VERSION,
1600
+ minProtocol,
1601
+ maxProtocol,
1466
1602
  client: {
1467
- id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
1603
+ id: clientId,
1468
1604
  displayName: this.opts.clientDisplayName,
1469
1605
  version: this.opts.clientVersion ?? DEFAULT_CLIENT_VERSION,
1606
+ buildId: this.opts.clientBuildId,
1470
1607
  platform,
1471
- deviceFamily: this.opts.deviceFamily,
1472
- mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND,
1608
+ deviceFamily,
1609
+ mode: clientMode,
1473
1610
  instanceId: this.opts.instanceId
1474
1611
  },
1475
1612
  caps: Array.isArray(this.opts.caps) ? this.opts.caps : [],
1476
1613
  commands: Array.isArray(this.opts.commands) ? this.opts.commands : void 0,
1614
+ computerUse: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.computerUse,
1615
+ workerRuns: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.workerRuns,
1477
1616
  permissions: this.opts.permissions && typeof this.opts.permissions === "object" ? this.opts.permissions : void 0,
1478
1617
  pathEnv: this.opts.pathEnv,
1479
1618
  auth,
@@ -1485,30 +1624,47 @@ var GatewayClient = class {
1485
1624
  scopes,
1486
1625
  signatureToken,
1487
1626
  signedAtMs,
1488
- platform
1627
+ platform,
1628
+ deviceFamily,
1629
+ clientMode
1489
1630
  })
1490
1631
  },
1491
1632
  authApprovalRuntimeToken,
1492
1633
  authAgentRuntimeIdentityToken,
1493
1634
  resolvedDeviceToken,
1635
+ storedScopes,
1494
1636
  storedToken,
1495
1637
  usingStoredDeviceToken
1496
1638
  };
1497
1639
  }
1640
+ shouldNegotiateLegacyNodeProtocol() {
1641
+ if (this.opts.role !== "node" || this.opts.mode !== GATEWAY_CLIENT_MODES.NODE || this.opts.clientName !== GATEWAY_CLIENT_NAMES.NODE_HOST) return false;
1642
+ return (this.opts.minProtocol ?? MIN_NODE_PROTOCOL_VERSION) === MIN_NODE_PROTOCOL_VERSION && (this.opts.maxProtocol ?? PROTOCOL_VERSION) === PROTOCOL_VERSION;
1643
+ }
1644
+ shouldRetryWithLegacyNodeProtocol(error) {
1645
+ if (this.useLegacyNodeProtocolEnvelope || !this.shouldNegotiateLegacyNodeProtocol() || !(error instanceof GatewayClientRequestError)) return false;
1646
+ const detailCode = readConnectErrorDetailCode(error.details);
1647
+ return error.details?.expectedProtocol === MIN_NODE_PROTOCOL_VERSION && (detailCode === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || normalizeGatewayErrorText(error.message).includes("protocol mismatch"));
1648
+ }
1649
+ shouldRetryWithCurrentNodeProtocol(error) {
1650
+ if (!this.useLegacyNodeProtocolEnvelope || !this.shouldNegotiateLegacyNodeProtocol() || !(error instanceof GatewayClientRequestError)) return false;
1651
+ const detailCode = readConnectErrorDetailCode(error.details);
1652
+ return error.details?.expectedProtocol === PROTOCOL_VERSION && (detailCode === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || normalizeGatewayErrorText(error.message).includes("protocol mismatch"));
1653
+ }
1498
1654
  buildDeviceConnectParams(params) {
1499
1655
  if (!this.opts.deviceIdentity) return;
1500
- const { nonce, role, scopes, signatureToken, signedAtMs, platform } = params;
1656
+ const { nonce, role, scopes, signatureToken, signedAtMs, platform, deviceFamily, clientMode } = params;
1501
1657
  const payload = buildDeviceAuthPayloadV3({
1502
1658
  deviceId: this.opts.deviceIdentity.deviceId,
1503
1659
  clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
1504
- clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND,
1660
+ clientMode,
1505
1661
  role,
1506
1662
  scopes,
1507
1663
  signedAtMs,
1508
1664
  token: signatureToken ?? null,
1509
1665
  nonce,
1510
1666
  platform,
1511
- deviceFamily: this.opts.deviceFamily
1667
+ deviceFamily
1512
1668
  });
1513
1669
  const signature = this.deps.signDevicePayload(this.opts.deviceIdentity.privateKeyPem, payload);
1514
1670
  return {
@@ -1520,24 +1676,66 @@ var GatewayClient = class {
1520
1676
  };
1521
1677
  }
1522
1678
  handleConnectHello(helloOk, assembled) {
1679
+ const reconnectWithCurrentNodeProtocol = this.useLegacyNodeProtocolEnvelope && this.shouldNegotiateLegacyNodeProtocol() && helloOk.protocol > MIN_NODE_PROTOCOL_VERSION;
1680
+ if (reconnectWithCurrentNodeProtocol) this.useLegacyNodeProtocolEnvelope = false;
1681
+ this.nodeProtocolTransitionPending = false;
1523
1682
  this.pendingDeviceTokenRetry = false;
1524
1683
  this.deviceTokenRetryBudgetUsed = false;
1525
1684
  this.suppressedTransientPreHelloCleanCloses = 0;
1526
1685
  const role = this.opts.role ?? "operator";
1527
1686
  const authInfo = helloOk.auth;
1528
- if (authInfo?.deviceToken && this.opts.deviceIdentity) this.deps.storeDeviceAuthToken({
1529
- deviceId: this.opts.deviceIdentity.deviceId,
1530
- role: authInfo.role ?? role,
1531
- token: authInfo.deviceToken,
1532
- scopes: authInfo.scopes ?? [],
1533
- env: this.opts.env
1534
- });
1687
+ if (authInfo?.deviceToken && this.opts.deviceIdentity) {
1688
+ const tokenRole = authInfo.role ?? role;
1689
+ const scopes = tokenRole === role && authInfo.deviceToken === assembled.storedToken ? assembled.storedScopes ?? authInfo.scopes ?? [] : authInfo.scopes ?? [];
1690
+ this.deps.storeDeviceAuthToken({
1691
+ deviceId: this.opts.deviceIdentity.deviceId,
1692
+ role: tokenRole,
1693
+ token: authInfo.deviceToken,
1694
+ scopes,
1695
+ env: this.opts.env
1696
+ });
1697
+ }
1698
+ if (this.opts.preferBootstrapToken) {
1699
+ this.opts.token = void 0;
1700
+ this.opts.bootstrapToken = void 0;
1701
+ this.opts.password = void 0;
1702
+ this.opts.preferBootstrapToken = false;
1703
+ }
1535
1704
  this.tickIntervalMs = typeof helloOk.policy?.tickIntervalMs === "number" ? helloOk.policy.tickIntervalMs : 3e4;
1705
+ if (reconnectWithCurrentNodeProtocol) {
1706
+ this.suppressNextHelloCallback = true;
1707
+ this.protocol.resetReconnectBackoff(250);
1708
+ this.protocol.closeSocket(1012, "gateway protocol upgraded");
1709
+ return;
1710
+ }
1536
1711
  this.lastTick = Date.now();
1537
1712
  this.startTickWatch();
1538
1713
  }
1539
1714
  handleConnectRequestFailure(error, assembled) {
1715
+ if (this.shouldRetryWithCurrentNodeProtocol(error)) {
1716
+ const resetBackoff = !this.nodeProtocolTransitionPending;
1717
+ this.useLegacyNodeProtocolEnvelope = false;
1718
+ this.nodeProtocolTransitionPending = true;
1719
+ if (resetBackoff) this.protocol.resetReconnectBackoff(250);
1720
+ this.logDebug("gateway rejected protocol v3; retrying node host with protocol v4");
1721
+ return {
1722
+ closeCode: 1008,
1723
+ closeReason: "connect retry"
1724
+ };
1725
+ }
1726
+ if (this.shouldRetryWithLegacyNodeProtocol(error)) {
1727
+ const resetBackoff = !this.nodeProtocolTransitionPending;
1728
+ this.useLegacyNodeProtocolEnvelope = true;
1729
+ this.nodeProtocolTransitionPending = true;
1730
+ if (resetBackoff) this.protocol.resetReconnectBackoff(250);
1731
+ this.logDebug("gateway rejected protocol v4; retrying node host with protocol v3");
1732
+ return {
1733
+ closeCode: 1008,
1734
+ closeReason: "connect retry"
1735
+ };
1736
+ }
1540
1737
  const role = this.opts.role ?? "operator";
1738
+ const detailCode = error instanceof GatewayClientRequestError ? readConnectErrorDetailCode(error.details) : null;
1541
1739
  const shouldRetryWithDeviceToken = shouldRetryGatewayWithDeviceToken({
1542
1740
  retryBudgetUsed: this.deviceTokenRetryBudgetUsed,
1543
1741
  currentDeviceToken: assembled.resolvedDeviceToken,
@@ -1546,7 +1744,7 @@ var GatewayClient = class {
1546
1744
  trustedEndpoint: this.isTrustedDeviceRetryEndpoint(),
1547
1745
  errorDetails: error instanceof GatewayClientRequestError ? error.details : void 0
1548
1746
  });
1549
- if (this.opts.deviceIdentity && assembled.usingStoredDeviceToken && error instanceof GatewayClientRequestError && readConnectErrorDetailCode(error.details) === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH) {
1747
+ if (this.opts.deviceIdentity && assembled.usingStoredDeviceToken && detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH) {
1550
1748
  const deviceId = this.opts.deviceIdentity.deviceId;
1551
1749
  try {
1552
1750
  this.deps.clearDeviceAuthToken({
@@ -1602,7 +1800,7 @@ var GatewayClient = class {
1602
1800
  }
1603
1801
  this.notifyConnectError(error);
1604
1802
  const message = `gateway connect failed: ${formatGatewayClientErrorForLog(error)}`;
1605
- if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(error)) this.logDebug(message);
1803
+ if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(error) || detailCode === ConnectErrorDetailCodes.AUTH_RATE_LIMITED) this.logDebug(message);
1606
1804
  else this.logError(message);
1607
1805
  return {
1608
1806
  closeCode: 1008,
@@ -1623,7 +1821,7 @@ var GatewayClient = class {
1623
1821
  return {
1624
1822
  retry: true,
1625
1823
  notify: true,
1626
- pendingError: new GatewayClientTransientPreHelloCloseError()
1824
+ pendingError: /* @__PURE__ */ new Error("gateway transient pre-hello clean close")
1627
1825
  };
1628
1826
  }
1629
1827
  if (info.transientPreHelloCleanClose || context.connectRequestSent && !context.helloReceived && !context.connectFailure) {
@@ -1659,11 +1857,13 @@ var GatewayClient = class {
1659
1857
  phase: context.helloReceived ? "post-hello" : "pre-hello",
1660
1858
  socketOpened: context.socketOpened,
1661
1859
  transportValidated: this.transportValidated,
1662
- transientPreHelloCleanClose: !context.helloReceived && context.code === 1e3 && context.reason === ""
1860
+ connectRequestSent: context.connectRequestSent,
1861
+ transientPreHelloCleanClose: !context.helloReceived && context.code === 1e3 && context.reason === "",
1862
+ ...context.connectFailure?.error ? { connectError: context.connectFailure.error } : {}
1663
1863
  };
1664
1864
  }
1665
1865
  clearStaleDeviceTokenForClose(code, reason) {
1666
- if (code !== 1008 || !normalizeLowercaseStringOrEmpty(reason).includes("device token mismatch") || this.opts.token || this.opts.password || !this.opts.deviceIdentity) return;
1866
+ if (code !== 1008 || !normalizeGatewayErrorText(reason).includes("device token mismatch") || this.opts.token || this.opts.password || !this.opts.deviceIdentity) return;
1667
1867
  const deviceId = this.opts.deviceIdentity.deviceId;
1668
1868
  const role = this.opts.role ?? "operator";
1669
1869
  try {
@@ -1696,14 +1896,14 @@ var GatewayClient = class {
1696
1896
  if (!params.authApprovalRuntimeToken) return false;
1697
1897
  if (!(params.error instanceof GatewayClientRequestError)) return false;
1698
1898
  if (params.error.gatewayCode !== "INVALID_REQUEST") return false;
1699
- const message = normalizeLowercaseStringOrEmpty(params.error.message);
1899
+ const message = normalizeGatewayErrorText(params.error.message);
1700
1900
  return message.includes("invalid connect params") && message.includes("approvalruntimetoken");
1701
1901
  }
1702
1902
  shouldFailClosedForUnsupportedAgentRuntimeIdentity(params) {
1703
1903
  if (!params.authAgentRuntimeIdentityToken) return false;
1704
1904
  if (!(params.error instanceof GatewayClientRequestError)) return false;
1705
1905
  if (params.error.gatewayCode !== "INVALID_REQUEST") return false;
1706
- const message = normalizeLowercaseStringOrEmpty(params.error.message);
1906
+ const message = normalizeGatewayErrorText(params.error.message);
1707
1907
  return message.includes("invalid connect params") && message.includes("agentruntimeidentitytoken");
1708
1908
  }
1709
1909
  isTrustedDeviceRetryEndpoint() {
@@ -1711,7 +1911,7 @@ var GatewayClient = class {
1711
1911
  try {
1712
1912
  const parsed = new URL(rawUrl);
1713
1913
  const protocol = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol;
1714
- if (isLoopbackHost(parsed.hostname)) return true;
1914
+ if (isGatewayLoopbackHost(parsed.hostname)) return true;
1715
1915
  return protocol === "wss:" && Boolean(this.opts.tlsFingerprint?.trim());
1716
1916
  } catch {
1717
1917
  return false;
@@ -1726,6 +1926,7 @@ var GatewayClient = class {
1726
1926
  return selectGatewayConnectAuth({
1727
1927
  token: this.opts.token,
1728
1928
  bootstrapToken: this.opts.bootstrapToken,
1929
+ preferBootstrapToken: this.opts.preferBootstrapToken,
1729
1930
  deviceToken: this.opts.deviceToken,
1730
1931
  password: this.opts.password,
1731
1932
  approvalRuntimeToken: this.approvalRuntimeTokenCompatibilityDisabled ? void 0 : this.opts.approvalRuntimeToken,
@@ -1750,21 +1951,9 @@ var GatewayClient = class {
1750
1951
  if (gap > (typeof rawTimeoutMs === "number" && Number.isFinite(rawTimeoutMs) ? Math.max(1, rawTimeoutMs) : this.tickIntervalMs * 2)) this.protocol.closeSocket(4e3, "tick timeout");
1751
1952
  }, interval);
1752
1953
  }
1753
- validateTlsFingerprint() {
1754
- if (!this.opts.tlsFingerprint || !this.ws) return null;
1755
- const expected = this.deps.normalizeTlsFingerprint(this.opts.tlsFingerprint);
1756
- if (!expected) return /* @__PURE__ */ new Error("gateway tls fingerprint missing");
1757
- const socket = this.ws["_socket"];
1758
- if (!socket || typeof socket.getPeerCertificate !== "function") return /* @__PURE__ */ new Error("gateway tls fingerprint unavailable");
1759
- const cert = socket.getPeerCertificate();
1760
- const fingerprint = this.deps.normalizeTlsFingerprint(cert?.fingerprint256 ?? "");
1761
- if (!fingerprint) return /* @__PURE__ */ new Error("gateway tls fingerprint unavailable");
1762
- if (fingerprint !== expected) return /* @__PURE__ */ new Error("gateway tls fingerprint mismatch");
1763
- return null;
1764
- }
1765
1954
  async request(method, params, opts) {
1766
1955
  const expectFinal = opts?.expectFinal === true;
1767
- const timeoutMs = opts?.timeoutMs === null ? null : typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? resolveSafeTimeoutDelayMs(opts.timeoutMs, { minMs: 0 }) : expectFinal ? null : this.requestTimeoutMs;
1956
+ const timeoutMs = opts?.timeoutMs === null ? null : typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : expectFinal ? null : this.requestTimeoutMs;
1768
1957
  return this.protocol.request(method, params, {
1769
1958
  expectFinal,
1770
1959
  timeoutMs,
@@ -1780,4 +1969,4 @@ function createGatewayRequestAbortError(method) {
1780
1969
  return err;
1781
1970
  }
1782
1971
  //#endregion
1783
- export { DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS, DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, GatewayBrowserDeviceAuthLifecycle, GatewayClient, GatewayClientRequestError, GatewaySessionMessageSubscriptionCoordinator, MAX_CONNECT_CHALLENGE_TIMEOUT_MS, MAX_SAFE_TIMEOUT_DELAY_MS, MIN_CONNECT_CHALLENGE_TIMEOUT_MS, addSafeTimeoutDelayGraceMs, buildDeviceAuthPayload, buildDeviceAuthPayloadV3, buildGatewayConnectAuth, clampConnectChallengeTimeoutMs, clearGatewayConnectTimeout, createSessionProjection, getConnectChallengeTimeoutMsFromEnv, getGatewaySessionMessageSubscriptionCoordinator, hasSessionProjectionAcceptedFinal, isGatewayConnectAssemblyError, isLocallyOptimisticSessionMessage, normalizeDeviceMetadataForAuth, normalizeSessionProjectionRunId, projectLiveSessionMessage, readSessionMessageIdentity, readSessionMessageSequence, reconcileSessionProjectionSnapshot, reduceSessionProjection, reduceSessionProjectionRunEvent, releaseGatewaySessionMessageSubscription, resetGatewaySessionMessageSubscriptionCoordinator, resolveConnectChallengeTimeoutMs, resolveFiniteTimeoutDelayMs, resolveGatewayConnectScopes, resolvePreauthHandshakeTimeoutMs, resolveSafeTimeoutDelayMs, selectGatewayConnectAuth, shouldRetryGatewayWithDeviceToken, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait, startGatewayConnectTimeout, waitForEventLoopReady };
1972
+ export { DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS, DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, GatewayBrowserDeviceAuthLifecycle, GatewayClient, GatewayClientRequestError, GatewayClientRequestTimeoutError, GatewayScopeUpgrade, GatewaySessionMessageSubscriptionCoordinator, MAX_CONNECT_CHALLENGE_TIMEOUT_MS, MAX_SAFE_TIMEOUT_DELAY_MS, MIN_CONNECT_CHALLENGE_TIMEOUT_MS, addSafeTimeoutDelayGraceMs, buildDeviceAuthPayload, buildDeviceAuthPayloadV3, buildGatewayConnectAuth, clampConnectChallengeTimeoutMs, clearGatewayConnectTimeout, createSessionProjection, gatewayCredentialScope, gatewayOriginScope, getConnectChallengeTimeoutMsFromEnv, getGatewaySessionMessageSubscriptionCoordinator, hasSessionProjectionAcceptedFinal, isGatewayConnectAssemblyError, isLocallyOptimisticSessionMessage, normalizeDeviceMetadataForAuth, normalizeSessionProjectionRunId, projectLiveSessionMessage, readSessionMessageIdentity, readSessionMessageSequence, reconcileSessionProjectionSnapshot, reduceSessionProjection, reduceSessionProjectionRunEvent, releaseGatewaySessionMessageSubscription, resetGatewaySessionMessageSubscriptionCoordinator, resolveConnectChallengeTimeoutMs, resolveFiniteTimeoutDelayMs, resolveGatewayConnectScopes, resolvePreauthHandshakeTimeoutMs, resolveSafeTimeoutDelayMs, selectGatewayConnectAuth, shouldRetryGatewayWithDeviceToken, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait, startGatewayConnectTimeout, waitForEventLoopReady };