@openclaw/gateway-client 2026.9.4 → 2026.9.6

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,4 +1,4 @@
1
- import { A as shouldRetryGatewayWithDeviceToken, C as isGatewayProtocolResponseError, D as buildGatewayConnectAuth, E as GatewayBrowserDeviceAuthLifecycle, M as buildDeviceAuthPayloadV3, N as normalizeDeviceMetadataForAuth, O as resolveGatewayConnectScopes, S as GatewayProtocolRequestTimeoutError, T as gatewayOriginScope, _ as readSessionMessageSequence, a as createSessionProjection, b as GatewayProtocolClient, c as reconcileSessionProjectionSnapshot, d as hasSessionProjectionAcceptedFinal, f as readSessionProjectionFinalMessageIdentity, g as readSessionMessageIdentity, h as readAssistantStreamSegmentIdentity, i as resetGatewaySessionMessageSubscriptionCoordinator, j as buildDeviceAuthPayload, k as selectGatewayConnectAuth, l as reduceSessionProjection, m as normalizeSessionProjectionRunId, n as getGatewaySessionMessageSubscriptionCoordinator, o as isLocallyOptimisticSessionMessage, p as isSessionProjectionErrorMessage, r as releaseGatewaySessionMessageSubscription, s as projectLiveSessionMessage, t as GatewaySessionMessageSubscriptionCoordinator, u as reduceSessionProjectionRunEvent, v as isRecord, w as gatewayCredentialScope, x as GatewayProtocolRequestError, y as shouldPauseGatewayReconnect } from "./session-subscriptions-DIcpqS8A.mjs";
1
+ import { A as resolveGatewayConnectScopes, C as GatewayProtocolRequestTimeoutError, D as gatewayOriginScope, E as gatewayCredentialScope, F as normalizeDeviceMetadataForAuth, M as shouldRetryGatewayWithDeviceToken, N as buildDeviceAuthPayload, O as GatewayBrowserDeviceAuthLifecycle, P as buildDeviceAuthPayloadV3, S as GatewayProtocolRequestError, T as resolveModelCatalogConnect, _ as readSessionMessageIdentity, a as createSessionProjection, b as shouldPauseGatewayReconnect, c as reduceSessionProjection, d as readSessionProjectionFinalMessageIdentity, f as isSessionProjectionErrorMessage, g as readAssistantStreamSegmentIdentity, h as normalizeSessionProjectionRunId, i as resetGatewaySessionMessageSubscriptionCoordinator, j as selectGatewayConnectAuth, k as buildGatewayConnectAuth, l as reduceSessionProjectionRunEvent, m as isLocallyOptimisticSessionMessage, n as getGatewaySessionMessageSubscriptionCoordinator, o as projectLiveSessionMessage, p as projectSessionTerminalReplyMessage, r as releaseGatewaySessionMessageSubscription, s as reconcileSessionProjectionSnapshot, t as GatewaySessionMessageSubscriptionCoordinator, u as hasSessionProjectionAcceptedFinal, v as readSessionMessageSequence, w as isGatewayProtocolResponseError, x as GatewayProtocolClient, y as isRecord } from "./session-subscriptions-DUpRfWJb.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
3
  import { rawDataToString } from "./websocket-data.mjs";
4
4
  import { n as startGatewayClientWithReadinessWait, r as waitForEventLoopReady, t as startGatewayClientWhenEventLoopReady } from "./readiness-CrWxd4TI.mjs";
@@ -385,6 +385,13 @@ function normalizeGatewayErrorText(value) {
385
385
  function isSensitiveUrlQueryParamName(key) {
386
386
  return /(?:token|password|secret|key|auth|credential)/iu.test(key);
387
387
  }
388
+ function isGatewayClientStoppedError(err) {
389
+ const message = err instanceof Error ? err.message : String(err);
390
+ return message === "gateway client stopped" || message === "Error: gateway client stopped";
391
+ }
392
+ function formatGatewayClientErrorForLog(err) {
393
+ return String(err).replace(/\/\/([^@/?#\s]+)@/g, "//***:***@").replace(/(Authorization:\s*Bearer\s+)[^\s]+/giu, "$1***").replace(/([?&])([^=&\s]+)=([^&#\s"'<>)]*)/g, (match, prefix, key) => isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match);
394
+ }
388
395
  const SHA256_HEX_FINGERPRINT = /^[a-fA-F0-9]{64}$/u;
389
396
  const SHA256_COLON_FINGERPRINT = /^(?:[a-fA-F0-9]{2}:){31}[a-fA-F0-9]{2}$/u;
390
397
  function normalizeTlsFingerprint(fingerprint) {
@@ -410,6 +417,115 @@ function parseGatewayIpAddress(host) {
410
417
  return normalized ? parseCanonicalIpAddress(normalized) : void 0;
411
418
  }
412
419
  //#endregion
420
+ //#region packages/gateway-client/src/client-device-auth.ts
421
+ /** Owns pending host storage work for one client across connection generations. */
422
+ var GatewayClientDeviceAuth = class {
423
+ constructor(storage) {
424
+ this.storage = storage;
425
+ this.operations = /* @__PURE__ */ new Set();
426
+ }
427
+ load(params) {
428
+ const load = () => {
429
+ params.signal?.throwIfAborted();
430
+ params.assertCurrent?.();
431
+ return this.track(this.storage.loadDeviceAuthToken(params));
432
+ };
433
+ return this.operations.size > 0 ? this.settle().then(load) : load();
434
+ }
435
+ store(params, onStored, onFailed) {
436
+ const failed = (error) => {
437
+ const failure = error instanceof Error ? error : new Error(String(error));
438
+ let reported = false;
439
+ try {
440
+ reported = onFailed(failure);
441
+ } catch {}
442
+ if (!reported) this.unreportedPersistenceFailure ??= failure;
443
+ throw failure;
444
+ };
445
+ params.signal?.throwIfAborted();
446
+ params.assertCurrent?.();
447
+ const stored = this.storage.storeDeviceAuthToken(params);
448
+ return stored instanceof Promise ? this.track(stored.then(onStored).catch(failed)) : onStored();
449
+ }
450
+ clear(params, observation, canClear) {
451
+ const clear = () => {
452
+ if (!canClear()) return;
453
+ const knownTokens = [observation.token, observation.receiptToken].filter((token) => typeof token === "string");
454
+ const expectedTokens = [...new Set(knownTokens)];
455
+ if (observation.token === void 0 && expectedTokens.length === 0) expectedTokens.push(void 0);
456
+ const clearToken = (expectedToken) => {
457
+ params.signal?.throwIfAborted();
458
+ params.assertCurrent?.();
459
+ const cleared = this.storage.clearDeviceAuthToken({
460
+ ...params,
461
+ ...expectedToken === void 0 ? {} : { expectedToken }
462
+ });
463
+ return cleared instanceof Promise ? cleared.then(() => {}) : void 0;
464
+ };
465
+ let pending;
466
+ for (const expectedToken of expectedTokens) pending = pending ? pending.then(() => clearToken(expectedToken)) : clearToken(expectedToken);
467
+ return pending;
468
+ };
469
+ return this.track(observation.persistence ? observation.persistence.then(clear, clear) : clear());
470
+ }
471
+ async settle() {
472
+ await Promise.allSettled(this.operations);
473
+ }
474
+ async drain() {
475
+ await this.settle();
476
+ const failure = this.unreportedPersistenceFailure;
477
+ this.unreportedPersistenceFailure = void 0;
478
+ if (failure) throw failure;
479
+ }
480
+ track(operation) {
481
+ if (!(operation instanceof Promise)) return operation;
482
+ this.operations.add(operation);
483
+ operation.then(() => this.operations.delete(operation), () => this.operations.delete(operation));
484
+ return operation;
485
+ }
486
+ };
487
+ //#endregion
488
+ //#region packages/gateway-client/src/client-upgrade-error.ts
489
+ const MAX_UPGRADE_ERROR_BODY_BYTES = 2048;
490
+ const UPGRADE_ERROR_BODY_TIMEOUT_MS = 1e3;
491
+ async function readUpgradeErrorBody(response) {
492
+ return await new Promise((resolve) => {
493
+ const chunks = [];
494
+ let totalBytes = 0;
495
+ let settled = false;
496
+ const finish = () => {
497
+ if (settled) return;
498
+ settled = true;
499
+ clearTimeout(timer);
500
+ response.off("data", onData);
501
+ response.off("end", finish);
502
+ response.off("error", finish);
503
+ response.off("aborted", finish);
504
+ resolve(Buffer.concat(chunks, totalBytes).toString("utf8").replace(/\s+/gu, " ").trim());
505
+ };
506
+ const stop = () => {
507
+ finish();
508
+ response.destroy();
509
+ };
510
+ const onData = (chunk) => {
511
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
512
+ const remaining = MAX_UPGRADE_ERROR_BODY_BYTES - totalBytes;
513
+ if (remaining > 0) {
514
+ const prefix = buffer.subarray(0, remaining);
515
+ chunks.push(prefix);
516
+ totalBytes += prefix.byteLength;
517
+ }
518
+ if (buffer.byteLength >= remaining) stop();
519
+ };
520
+ const timer = setTimeout(stop, UPGRADE_ERROR_BODY_TIMEOUT_MS);
521
+ timer.unref?.();
522
+ response.on("data", onData);
523
+ response.once("end", finish);
524
+ response.once("error", finish);
525
+ response.once("aborted", finish);
526
+ });
527
+ }
528
+ //#endregion
413
529
  //#region packages/gateway-client/src/request-error.ts
414
530
  var GatewayClientRequestError = class extends GatewayProtocolRequestError {
415
531
  constructor(error) {
@@ -423,6 +539,17 @@ var GatewayClientRequestError = class extends GatewayProtocolRequestError {
423
539
  this.name = "GatewayClientRequestError";
424
540
  }
425
541
  };
542
+ const GATEWAY_CONNECT_ASSEMBLY_ERROR = Symbol("gateway.connectAssemblyError");
543
+ function markGatewayConnectAssemblyError(error) {
544
+ Object.defineProperty(error, GATEWAY_CONNECT_ASSEMBLY_ERROR, {
545
+ configurable: true,
546
+ value: true
547
+ });
548
+ return error;
549
+ }
550
+ function isGatewayConnectAssemblyError(value) {
551
+ return value instanceof Error && value[GATEWAY_CONNECT_ASSEMBLY_ERROR] === true;
552
+ }
426
553
  //#endregion
427
554
  //#region packages/net-policy/src/url-protocol.ts
428
555
  function parseUrl(value) {
@@ -538,7 +665,10 @@ function applyGatewayWebSocketTlsPin(options, expectedFingerprint, normalize = n
538
665
  //#endregion
539
666
  //#region packages/gateway-client/src/websocket.ts
540
667
  const require = createRequire(import.meta.url);
541
- const { WebSocket } = require(path.join(path.dirname(require.resolve("ws/package.json")), "index.js"));
668
+ const wsPackageRoot = path.dirname(require.resolve("ws/package.json"));
669
+ const WebSocket = require(path.join(wsPackageRoot, "lib/websocket.js"));
670
+ require(path.join(wsPackageRoot, "lib/websocket-server.js"));
671
+ require(path.join(wsPackageRoot, "lib/stream.js"));
542
672
  //#endregion
543
673
  //#region packages/gateway-client/src/client.ts
544
674
  const DEFAULT_HOST_DEPS = {
@@ -564,45 +694,6 @@ function resolveHostDeps(overrides) {
564
694
  }
565
695
  const DEFAULT_GATEWAY_CLIENT_URL = "ws://127.0.0.1:18789";
566
696
  const DEFAULT_CLIENT_VERSION = "0.0.0";
567
- const MAX_UPGRADE_ERROR_BODY_BYTES = 2048;
568
- const UPGRADE_ERROR_BODY_TIMEOUT_MS = 1e3;
569
- async function readUpgradeErrorBody(response) {
570
- return await new Promise((resolve) => {
571
- const chunks = [];
572
- let totalBytes = 0;
573
- let settled = false;
574
- const finish = () => {
575
- if (settled) return;
576
- settled = true;
577
- clearTimeout(timer);
578
- response.off("data", onData);
579
- response.off("end", finish);
580
- response.off("error", finish);
581
- response.off("aborted", finish);
582
- resolve(Buffer.concat(chunks, totalBytes).toString("utf8").replace(/\s+/gu, " ").trim());
583
- };
584
- const stop = () => {
585
- finish();
586
- response.destroy();
587
- };
588
- const onData = (chunk) => {
589
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
590
- const remaining = MAX_UPGRADE_ERROR_BODY_BYTES - totalBytes;
591
- if (remaining > 0) {
592
- const prefix = buffer.subarray(0, remaining);
593
- chunks.push(prefix);
594
- totalBytes += prefix.byteLength;
595
- }
596
- if (buffer.byteLength >= remaining) stop();
597
- };
598
- const timer = setTimeout(stop, UPGRADE_ERROR_BODY_TIMEOUT_MS);
599
- timer.unref?.();
600
- response.on("data", onData);
601
- response.once("end", finish);
602
- response.once("error", finish);
603
- response.once("aborted", finish);
604
- });
605
- }
606
697
  var GatewayClientRequestTimeoutError = class extends GatewayProtocolRequestTimeoutError {
607
698
  constructor(params) {
608
699
  super(params, `gateway request timeout for ${params.method}`);
@@ -610,24 +701,6 @@ var GatewayClientRequestTimeoutError = class extends GatewayProtocolRequestTimeo
610
701
  }
611
702
  };
612
703
  var GatewayClientTransportPolicyError = class extends GatewayWebSocketTransportConfigurationError {};
613
- const GATEWAY_CONNECT_ASSEMBLY_ERROR = Symbol("gateway.connectAssemblyError");
614
- function markGatewayConnectAssemblyError(error) {
615
- Object.defineProperty(error, GATEWAY_CONNECT_ASSEMBLY_ERROR, {
616
- configurable: true,
617
- value: true
618
- });
619
- return error;
620
- }
621
- function isGatewayConnectAssemblyError(value) {
622
- return value instanceof Error && value[GATEWAY_CONNECT_ASSEMBLY_ERROR] === true;
623
- }
624
- function isGatewayClientStoppedError(err) {
625
- const message = err instanceof Error ? err.message : String(err);
626
- return message === "gateway client stopped" || message === "Error: gateway client stopped";
627
- }
628
- function formatGatewayClientErrorForLog(err) {
629
- return String(err).replace(/\/\/([^@/?#\s]+)@/g, "//***:***@").replace(/(Authorization:\s*Bearer\s+)[^\s]+/giu, "$1***").replace(/([?&])([^=&\s]+)=([^&#\s"'<>)]*)/g, (match, prefix, key) => isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match);
630
- }
631
704
  const FORCE_STOP_TERMINATE_GRACE_MS = 250;
632
705
  const STOP_AND_WAIT_TIMEOUT_MS = 1e3;
633
706
  const MAX_SUPPRESSED_TRANSIENT_PRE_HELLO_CLEAN_CLOSES = 1;
@@ -644,7 +717,6 @@ var GatewayClient = class {
644
717
  this.stopped = false;
645
718
  this.useLegacyNodeProtocolEnvelope = false;
646
719
  this.nodeProtocolTransitionPending = false;
647
- this.suppressNextHelloCallback = false;
648
720
  this.pendingDeviceTokenRetry = false;
649
721
  this.deviceTokenRetryBudgetUsed = false;
650
722
  this.approvalRuntimeTokenCompatibilityDisabled = false;
@@ -656,6 +728,7 @@ var GatewayClient = class {
656
728
  this.transportValidated = false;
657
729
  this.suppressedTransientPreHelloCleanCloses = 0;
658
730
  this.deps = resolveHostDeps(opts.hostDeps);
731
+ this.deviceAuth = new GatewayClientDeviceAuth(this.deps);
659
732
  this.opts = {
660
733
  ...opts,
661
734
  deviceIdentity: opts.deviceIdentity === null ? void 0 : opts.deviceIdentity ?? this.deps.loadOrCreateDeviceIdentity()
@@ -675,14 +748,32 @@ var GatewayClient = class {
675
748
  requestSent
676
749
  }),
677
750
  createRequestAbortError: createGatewayRequestAbortError,
678
- buildConnectPlan: ({ nonce, challengeTs }) => {
751
+ buildConnectPlan: ({ nonce, challengeTs, serverCapabilities, generation, ...authority }) => {
679
752
  if (!nonce) throw new Error("gateway connect challenge missing nonce");
680
753
  if (this.opts.deviceIdentity && challengeTs == null) throw new Error("gateway connect challenge timestamp invalid");
681
- return this.assembleConnectParams({
682
- role: this.opts.role ?? "operator",
683
- nonce,
684
- signedAtMs: challengeTs ?? Date.now()
685
- });
754
+ const role = this.opts.role ?? "operator";
755
+ const assemble = (storedAuth) => {
756
+ authority.assertCurrent();
757
+ const assembled = this.assembleConnectParams({
758
+ role,
759
+ nonce,
760
+ signedAtMs: challengeTs ?? Date.now(),
761
+ serverCapabilities,
762
+ storedAuth
763
+ });
764
+ this.connectionStoredToken = {
765
+ generation,
766
+ token: assembled.storedToken ?? null
767
+ };
768
+ return assembled;
769
+ };
770
+ const storedAuth = this.opts.deviceIdentity ? this.deviceAuth.load({
771
+ deviceId: this.opts.deviceIdentity.deviceId,
772
+ role,
773
+ env: this.opts.env,
774
+ ...authority
775
+ }) : null;
776
+ return storedAuth instanceof Promise ? storedAuth.then(assemble) : assemble(storedAuth);
686
777
  },
687
778
  buildConnectParams: (assembled) => assembled.params,
688
779
  onConnectPlanError: (error) => {
@@ -698,12 +789,8 @@ var GatewayClient = class {
698
789
  error: marked
699
790
  };
700
791
  },
701
- onConnectHello: (hello, context) => this.handleConnectHello(hello, context.plan),
792
+ onConnectHello: (hello, context) => this.handleConnectHello(hello, context.plan, context),
702
793
  onHello: (hello) => {
703
- if (this.suppressNextHelloCallback) {
704
- this.suppressNextHelloCallback = false;
705
- return;
706
- }
707
794
  this.opts.onHelloOk?.(hello);
708
795
  },
709
796
  onConnectFailure: (error, context) => this.handleConnectRequestFailure(error, context.plan),
@@ -857,6 +944,7 @@ var GatewayClient = class {
857
944
  });
858
945
  ws.on("error", (err) => {
859
946
  if (upgradeError) return;
947
+ if (err.message === "Opening handshake has timed out") Object.assign(err, { code: "ETIMEDOUT" });
860
948
  this.logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`);
861
949
  handlers.error(err instanceof Error ? err : new Error(String(err)));
862
950
  });
@@ -871,7 +959,6 @@ var GatewayClient = class {
871
959
  }
872
960
  async stopAndWait(opts) {
873
961
  const stopPromise = this.beginStop();
874
- if (!stopPromise) return;
875
962
  const timeoutMs = opts?.timeoutMs === void 0 ? STOP_AND_WAIT_TIMEOUT_MS : resolveSafeTimeoutDelayMs(opts.timeoutMs);
876
963
  let timeout = null;
877
964
  try {
@@ -883,6 +970,7 @@ var GatewayClient = class {
883
970
  })]);
884
971
  } finally {
885
972
  if (timeout) clearTimeout(timeout);
973
+ await this.deviceAuth.drain();
886
974
  }
887
975
  }
888
976
  beginStop() {
@@ -946,7 +1034,7 @@ var GatewayClient = class {
946
1034
  }
947
1035
  assembleConnectParams(params) {
948
1036
  const { role, nonce, signedAtMs } = params;
949
- const selectedAuth = this.selectConnectAuth(role);
1037
+ const selectedAuth = this.selectConnectAuth(params.storedAuth);
950
1038
  const { authDeviceToken, authApprovalRuntimeToken, authAgentRuntimeIdentityToken, signatureToken, resolvedDeviceToken, storedToken, storedScopes, usingStoredDeviceToken } = selectedAuth;
951
1039
  if (this.pendingDeviceTokenRetry && authDeviceToken) this.pendingDeviceTokenRetry = false;
952
1040
  const auth = buildGatewayConnectAuth(selectedAuth);
@@ -981,7 +1069,11 @@ var GatewayClient = class {
981
1069
  mode: clientMode,
982
1070
  instanceId: this.opts.instanceId
983
1071
  },
984
- caps: Array.isArray(this.opts.caps) ? this.opts.caps : [],
1072
+ ...resolveModelCatalogConnect({
1073
+ caps: Array.isArray(this.opts.caps) ? this.opts.caps : [],
1074
+ modelCatalog: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.modelCatalog,
1075
+ serverCapabilities: params.serverCapabilities
1076
+ }),
985
1077
  commands: Array.isArray(this.opts.commands) ? this.opts.commands : void 0,
986
1078
  computerUse: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.computerUse,
987
1079
  workerRuns: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.workerRuns,
@@ -1047,35 +1139,56 @@ var GatewayClient = class {
1047
1139
  nonce
1048
1140
  };
1049
1141
  }
1050
- handleConnectHello(helloOk, assembled) {
1051
- const reconnectWithCurrentNodeProtocol = this.useLegacyNodeProtocolEnvelope && this.shouldNegotiateLegacyNodeProtocol() && helloOk.protocol > MIN_NODE_PROTOCOL_VERSION;
1052
- if (reconnectWithCurrentNodeProtocol) this.useLegacyNodeProtocolEnvelope = false;
1053
- this.nodeProtocolTransitionPending = false;
1054
- this.pendingDeviceTokenRetry = false;
1055
- this.deviceTokenRetryBudgetUsed = false;
1056
- this.suppressedTransientPreHelloCleanCloses = 0;
1142
+ handleConnectHello(helloOk, assembled, authority) {
1143
+ authority.assertCurrent();
1057
1144
  const role = this.opts.role ?? "operator";
1058
1145
  const authInfo = helloOk.auth;
1146
+ const observation = this.connectionStoredToken;
1147
+ const persisted = () => {
1148
+ if (authInfo?.deviceToken && (authInfo.role ?? role) === role && observation) observation.token = authInfo.deviceToken;
1149
+ if (this.opts.preferBootstrapToken) {
1150
+ this.opts.token = void 0;
1151
+ this.opts.bootstrapToken = void 0;
1152
+ this.opts.password = void 0;
1153
+ this.opts.preferBootstrapToken = false;
1154
+ }
1155
+ };
1156
+ let stored = void 0;
1059
1157
  if (authInfo?.deviceToken && this.opts.deviceIdentity) {
1060
1158
  const tokenRole = authInfo.role ?? role;
1061
1159
  const scopes = tokenRole === role && authInfo.deviceToken === assembled.storedToken ? assembled.storedScopes ?? authInfo.scopes ?? [] : authInfo.scopes ?? [];
1062
- this.deps.storeDeviceAuthToken({
1160
+ if (tokenRole === role && observation) observation.receiptToken = authInfo.deviceToken;
1161
+ stored = this.deviceAuth.store({
1063
1162
  deviceId: this.opts.deviceIdentity.deviceId,
1064
1163
  role: tokenRole,
1065
1164
  token: authInfo.deviceToken,
1066
1165
  scopes,
1067
- env: this.opts.env
1166
+ env: this.opts.env,
1167
+ ...tokenRole === role ? { expectedToken: assembled.storedToken ?? null } : {}
1168
+ }, persisted, (error) => {
1169
+ assembled.persistenceFailed = true;
1170
+ const reported = this.notifyConnectError(error);
1171
+ this.logError(`gateway device token persistence failed: ${formatGatewayClientErrorForLog(error)}`);
1172
+ return reported;
1068
1173
  });
1069
- }
1070
- if (this.opts.preferBootstrapToken) {
1071
- this.opts.token = void 0;
1072
- this.opts.bootstrapToken = void 0;
1073
- this.opts.password = void 0;
1074
- this.opts.preferBootstrapToken = false;
1075
- }
1174
+ if (observation && stored instanceof Promise) observation.persistence = stored;
1175
+ } else persisted();
1176
+ const complete = () => {
1177
+ if (authority.signal.aborted || !this.protocol.connected) return;
1178
+ authority.assertCurrent();
1179
+ this.completeConnectHello(helloOk);
1180
+ };
1181
+ return stored instanceof Promise ? stored.then(complete) : complete();
1182
+ }
1183
+ completeConnectHello(helloOk) {
1184
+ const reconnectWithCurrentNodeProtocol = this.useLegacyNodeProtocolEnvelope && this.shouldNegotiateLegacyNodeProtocol() && helloOk.protocol > MIN_NODE_PROTOCOL_VERSION;
1185
+ if (reconnectWithCurrentNodeProtocol) this.useLegacyNodeProtocolEnvelope = false;
1186
+ this.nodeProtocolTransitionPending = false;
1187
+ this.pendingDeviceTokenRetry = false;
1188
+ this.deviceTokenRetryBudgetUsed = false;
1189
+ this.suppressedTransientPreHelloCleanCloses = 0;
1076
1190
  this.tickIntervalMs = typeof helloOk.policy?.tickIntervalMs === "number" ? helloOk.policy.tickIntervalMs : 3e4;
1077
1191
  if (reconnectWithCurrentNodeProtocol) {
1078
- this.suppressNextHelloCallback = true;
1079
1192
  this.protocol.resetReconnectBackoff(250);
1080
1193
  this.protocol.closeSocket(1012, "gateway protocol upgraded");
1081
1194
  return;
@@ -1084,6 +1197,16 @@ var GatewayClient = class {
1084
1197
  this.startTickWatch();
1085
1198
  }
1086
1199
  handleConnectRequestFailure(error, assembled) {
1200
+ if (assembled.persistenceFailed) return {
1201
+ closeCode: 1008,
1202
+ closeReason: "connect failed"
1203
+ };
1204
+ const detailCode = error instanceof GatewayClientRequestError ? readConnectErrorDetailCode(error.details) : null;
1205
+ const cleared = this.opts.deviceIdentity && assembled.usingStoredDeviceToken && detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH ? this.clearDeviceToken(this.opts.deviceIdentity.deviceId, { token: assembled.storedToken }) : void 0;
1206
+ const decision = this.resolveConnectRequestFailure(error, assembled);
1207
+ return cleared instanceof Promise ? cleared.then(() => decision) : decision;
1208
+ }
1209
+ resolveConnectRequestFailure(error, assembled) {
1087
1210
  if (this.shouldRetryWithCurrentNodeProtocol(error)) {
1088
1211
  const resetBackoff = !this.nodeProtocolTransitionPending;
1089
1212
  this.useLegacyNodeProtocolEnvelope = false;
@@ -1107,30 +1230,15 @@ var GatewayClient = class {
1107
1230
  };
1108
1231
  }
1109
1232
  this.nodeProtocolTransitionPending = false;
1110
- const role = this.opts.role ?? "operator";
1111
1233
  const detailCode = error instanceof GatewayClientRequestError ? readConnectErrorDetailCode(error.details) : null;
1112
- const shouldRetryWithDeviceToken = shouldRetryGatewayWithDeviceToken({
1234
+ if (shouldRetryGatewayWithDeviceToken({
1113
1235
  retryBudgetUsed: this.deviceTokenRetryBudgetUsed,
1114
1236
  currentDeviceToken: assembled.resolvedDeviceToken,
1115
1237
  explicitToken: this.opts.token?.trim() || void 0,
1116
1238
  storedToken: assembled.storedToken,
1117
1239
  trustedEndpoint: this.isTrustedDeviceRetryEndpoint(),
1118
1240
  errorDetails: error instanceof GatewayClientRequestError ? error.details : void 0
1119
- });
1120
- if (this.opts.deviceIdentity && assembled.usingStoredDeviceToken && detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH) {
1121
- const deviceId = this.opts.deviceIdentity.deviceId;
1122
- try {
1123
- this.deps.clearDeviceAuthToken({
1124
- deviceId,
1125
- role,
1126
- env: this.opts.env
1127
- });
1128
- this.logDebug(`cleared stale device-auth token for device ${deviceId}`);
1129
- } catch (clearError) {
1130
- this.logDebug(`failed clearing stale device-auth token for device ${deviceId}: ${String(clearError)}`);
1131
- }
1132
- }
1133
- if (shouldRetryWithDeviceToken) {
1241
+ })) {
1134
1242
  this.pendingDeviceTokenRetry = true;
1135
1243
  this.deviceTokenRetryBudgetUsed = true;
1136
1244
  this.protocol.resetReconnectBackoff(250);
@@ -1202,7 +1310,7 @@ var GatewayClient = class {
1202
1310
  this.notifyConnectError(error);
1203
1311
  this.logError(`gateway connect failed: ${formatGatewayClientErrorForLog(error)}`);
1204
1312
  }
1205
- this.clearStaleDeviceTokenForClose(context.code, context.reason);
1313
+ this.clearStaleDeviceTokenForClose(context);
1206
1314
  if (shouldPauseGatewayReconnect({
1207
1315
  details,
1208
1316
  deviceTokenRetryPending: this.pendingDeviceTokenRetry,
@@ -1236,26 +1344,39 @@ var GatewayClient = class {
1236
1344
  ...context.connectFailure?.error ? { connectError: context.connectFailure.error } : {}
1237
1345
  };
1238
1346
  }
1239
- clearStaleDeviceTokenForClose(code, reason) {
1240
- if (code !== 1008 || !normalizeGatewayErrorText(reason).includes("device token mismatch") || this.opts.token || this.opts.password || !this.opts.deviceIdentity) return;
1241
- const deviceId = this.opts.deviceIdentity.deviceId;
1242
- const role = this.opts.role ?? "operator";
1347
+ clearStaleDeviceTokenForClose(context) {
1348
+ const { code, reason, generation } = context;
1349
+ if (code !== 1008 || !normalizeGatewayErrorText(reason).includes("device token mismatch") || !this.opts.deviceIdentity) return;
1350
+ const observation = this.connectionStoredToken?.generation === generation ? this.connectionStoredToken : { token: void 0 };
1351
+ this.clearDeviceToken(this.opts.deviceIdentity.deviceId, observation, () => !this.opts.token && !this.opts.password);
1352
+ }
1353
+ clearDeviceToken(deviceId, observation, canClear = () => true) {
1354
+ const failed = (error) => {
1355
+ this.logDebug(`failed clearing stale device-auth token for device ${deviceId}: ${String(error)}`);
1356
+ };
1357
+ const cleared = () => this.logDebug(`cleared stale device-auth token for device ${deviceId}`);
1243
1358
  try {
1244
- this.deps.clearDeviceAuthToken({
1359
+ const result = this.deviceAuth.clear({
1245
1360
  deviceId,
1246
- role,
1247
- env: this.opts.env
1248
- });
1249
- this.logDebug(`cleared stale device-auth token for device ${deviceId}`);
1361
+ role: this.opts.role ?? "operator",
1362
+ env: this.opts.env,
1363
+ assertCurrent: () => {
1364
+ if (this.stopped) throw new Error("gateway client stopped");
1365
+ }
1366
+ }, observation, canClear);
1367
+ return result instanceof Promise ? result.then(cleared, failed) : cleared();
1250
1368
  } catch (error) {
1251
- this.logDebug(`failed clearing stale device-auth token for device ${deviceId}: ${String(error)}`);
1369
+ failed(error);
1252
1370
  }
1253
1371
  }
1254
1372
  notifyConnectError(error) {
1373
+ if (!this.opts.onConnectError) return false;
1255
1374
  try {
1256
- this.opts.onConnectError?.(error);
1375
+ this.opts.onConnectError(error);
1376
+ return true;
1257
1377
  } catch (err) {
1258
1378
  this.logDebug(`gateway client connect error handler error: ${formatGatewayClientErrorForLog(err)}`);
1379
+ return false;
1259
1380
  }
1260
1381
  }
1261
1382
  notifyReconnectPaused(info) {
@@ -1291,12 +1412,7 @@ var GatewayClient = class {
1291
1412
  return false;
1292
1413
  }
1293
1414
  }
1294
- selectConnectAuth(role) {
1295
- const storedAuth = this.opts.deviceIdentity ? this.deps.loadDeviceAuthToken({
1296
- deviceId: this.opts.deviceIdentity.deviceId,
1297
- role,
1298
- env: this.opts.env
1299
- }) : null;
1415
+ selectConnectAuth(storedAuth) {
1300
1416
  return selectGatewayConnectAuth({
1301
1417
  token: this.opts.token,
1302
1418
  bootstrapToken: this.opts.bootstrapToken,
@@ -1354,4 +1470,4 @@ function buildCloudflareAccessHeaders(credentials) {
1354
1470
  };
1355
1471
  }
1356
1472
  //#endregion
1357
- export { CF_ACCESS_CLIENT_ID_HEADER, CF_ACCESS_CLIENT_SECRET_HEADER, 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, buildCloudflareAccessHeaders, buildDeviceAuthPayload, buildDeviceAuthPayloadV3, buildGatewayConnectAuth, clampConnectChallengeTimeoutMs, clearGatewayConnectTimeout, createSessionProjection, gatewayCredentialScope, gatewayOriginScope, getConnectChallengeTimeoutMsFromEnv, getGatewaySessionMessageSubscriptionCoordinator, hasSessionProjectionAcceptedFinal, isGatewayConnectAssemblyError, isGatewayProtocolResponseError, isLocallyOptimisticSessionMessage, isSessionProjectionErrorMessage, normalizeDeviceMetadataForAuth, normalizeSessionProjectionRunId, projectLiveSessionMessage, readAssistantStreamSegmentIdentity, readSessionMessageIdentity, readSessionMessageSequence, readSessionProjectionFinalMessageIdentity, reconcileSessionProjectionSnapshot, reduceSessionProjection, reduceSessionProjectionRunEvent, releaseGatewaySessionMessageSubscription, resetGatewaySessionMessageSubscriptionCoordinator, resolveConnectChallengeTimeoutMs, resolveFiniteTimeoutDelayMs, resolveGatewayConnectScopes, resolvePreauthHandshakeTimeoutMs, resolveSafeTimeoutDelayMs, selectGatewayConnectAuth, shouldRetryGatewayWithDeviceToken, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait, startGatewayConnectTimeout, waitForEventLoopReady };
1473
+ export { CF_ACCESS_CLIENT_ID_HEADER, CF_ACCESS_CLIENT_SECRET_HEADER, 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, buildCloudflareAccessHeaders, buildDeviceAuthPayload, buildDeviceAuthPayloadV3, buildGatewayConnectAuth, clampConnectChallengeTimeoutMs, clearGatewayConnectTimeout, createSessionProjection, gatewayCredentialScope, gatewayOriginScope, getConnectChallengeTimeoutMsFromEnv, getGatewaySessionMessageSubscriptionCoordinator, hasSessionProjectionAcceptedFinal, isGatewayConnectAssemblyError, isGatewayProtocolResponseError, isLocallyOptimisticSessionMessage, isSessionProjectionErrorMessage, normalizeDeviceMetadataForAuth, normalizeSessionProjectionRunId, projectLiveSessionMessage, projectSessionTerminalReplyMessage, readAssistantStreamSegmentIdentity, readSessionMessageIdentity, readSessionMessageSequence, readSessionProjectionFinalMessageIdentity, reconcileSessionProjectionSnapshot, reduceSessionProjection, reduceSessionProjectionRunEvent, releaseGatewaySessionMessageSubscription, resetGatewaySessionMessageSubscriptionCoordinator, resolveConnectChallengeTimeoutMs, resolveFiniteTimeoutDelayMs, resolveGatewayConnectScopes, resolvePreauthHandshakeTimeoutMs, resolveSafeTimeoutDelayMs, selectGatewayConnectAuth, shouldRetryGatewayWithDeviceToken, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait, startGatewayConnectTimeout, waitForEventLoopReady };
@@ -1,4 +1,4 @@
1
- import { n as GatewayProtocolRequestOptions, t as GatewayProtocolRequestError } from "./protocol-request-B5gBmLgq.mjs";
1
+ import { n as GatewayProtocolRequestOptions, t as GatewayProtocolRequestError } from "./protocol-request-CtVhe3Bj.mjs";
2
2
  import { ErrorShape, EventFrame, HelloOk } from "@openclaw/gateway-protocol";
3
3
  //#region packages/gateway-client/src/pending-request.d.ts
4
4
  type GatewayProtocolRequestTiming = {
@@ -23,7 +23,11 @@ type GatewayProtocolSocketHandlers = {
23
23
  close: (code: number, reason: string) => void;
24
24
  error: (error: Error) => void;
25
25
  };
26
- type GatewayProtocolConnectContext<TPlan> = {
26
+ type GatewayProtocolConnectAuthority = {
27
+ signal: AbortSignal;
28
+ assertCurrent: () => void;
29
+ };
30
+ type GatewayProtocolConnectContext<TPlan> = GatewayProtocolConnectAuthority & {
27
31
  generation: number;
28
32
  nonce: string | null;
29
33
  challengeTs: number | null | undefined;
@@ -70,16 +74,17 @@ type GatewayProtocolClientOptions<TPlan> = {
70
74
  createRequestError?: (error: Partial<ErrorShape>) => GatewayProtocolRequestError;
71
75
  createRequestTimeoutError?: (method: string, timeoutMs: number, requestSent: boolean) => Error;
72
76
  createRequestAbortError?: (method: string) => Error;
73
- buildConnectPlan: (params: {
77
+ buildConnectPlan: (params: GatewayProtocolConnectAuthority & {
74
78
  nonce: string | null;
75
79
  challengeTs: number | null | undefined;
80
+ serverCapabilities: readonly string[];
76
81
  generation: number;
77
82
  }) => TPlan | Promise<TPlan>;
78
83
  buildConnectParams: (plan: TPlan) => unknown;
79
84
  onConnectPlanError?: (error: Error) => GatewayProtocolConnectDecision;
80
- onConnectHello?: (hello: HelloOk, context: GatewayProtocolConnectContext<TPlan>) => void;
85
+ onConnectHello?: (hello: HelloOk, context: GatewayProtocolConnectContext<TPlan>) => unknown;
81
86
  onHello?: (hello: HelloOk) => void;
82
- onConnectFailure?: (error: GatewayProtocolRequestError, context: GatewayProtocolConnectContext<TPlan>) => GatewayProtocolConnectDecision;
87
+ onConnectFailure?: (error: GatewayProtocolRequestError, context: GatewayProtocolConnectContext<TPlan>) => GatewayProtocolConnectDecision | Promise<GatewayProtocolConnectDecision>;
83
88
  resolveClose: (context: GatewayProtocolCloseContext) => GatewayProtocolCloseDecision;
84
89
  onClose?: (context: GatewayProtocolCloseContext, decision: GatewayProtocolCloseDecision) => void;
85
90
  notifyStoppedClose?: boolean;
@@ -127,9 +132,11 @@ declare class GatewayProtocolClient<TPlan> {
127
132
  private readonly listeners;
128
133
  private stopped;
129
134
  private generation;
135
+ private connectionAbort;
130
136
  private lastSeq;
131
137
  private connectNonce;
132
138
  private connectChallengeTs;
139
+ private serverCapabilities;
133
140
  private connectSent;
134
141
  private connectRequestSent;
135
142
  private handshakeTimer;
@@ -163,10 +170,12 @@ declare class GatewayProtocolClient<TPlan> {
163
170
  private handleSocketError;
164
171
  private scheduleReconnect;
165
172
  private closeContext;
173
+ private isConnectCurrent;
174
+ private connectAuthority;
166
175
  private isActive;
167
176
  private nowMs;
168
177
  private clearHandshakeTimer;
169
178
  private invoke;
170
179
  }
171
180
  //#endregion
172
- export { GatewayProtocolTiming as a, GatewayProtocolSocketHandlers as i, GatewayProtocolCloseContext as n, GatewayProtocolRequestTiming as o, GatewayProtocolSocket as r, GatewayProtocolClient as t };
181
+ export { GatewayProtocolSocketHandlers as a, GatewayProtocolSocket as i, GatewayProtocolCloseContext as n, GatewayProtocolTiming as o, GatewayProtocolConnectAuthority as r, GatewayProtocolRequestTiming as s, GatewayProtocolClient as t };
@@ -5,7 +5,7 @@ declare function isGatewayProtocolResponseError(error: unknown): error is Gatewa
5
5
  type GatewayProtocolRequestOptions = {
6
6
  timeoutMs?: number | null;
7
7
  expectFinal?: boolean;
8
- onSent?: () => void;
8
+ onSent?: (requestId: string) => void;
9
9
  onAccepted?: (payload: unknown) => void;
10
10
  signal?: AbortSignal;
11
11
  };