@openclaw/gateway-client 2026.9.3 → 2026.9.5

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 projectLiveSessionMessage, d as reduceSessionProjection, f as reduceSessionProjectionRunEvent, g as readSessionMessageIdentity, h as readAssistantStreamSegmentIdentity, i as resetGatewaySessionMessageSubscriptionCoordinator, j as buildDeviceAuthPayload, k as selectGatewayConnectAuth, l as readSessionProjectionFinalMessageIdentity, m as normalizeSessionProjectionRunId, n as getGatewaySessionMessageSubscriptionCoordinator, o as hasSessionProjectionAcceptedFinal, p as isSessionProjectionErrorMessage, r as releaseGatewaySessionMessageSubscription, s as isLocallyOptimisticSessionMessage, t as GatewaySessionMessageSubscriptionCoordinator, u as reconcileSessionProjectionSnapshot, v as isRecord, w as gatewayCredentialScope, x as GatewayProtocolRequestError, y as shouldPauseGatewayReconnect } from "./session-subscriptions-DwU8T0CH.mjs";
1
+ import { A as selectGatewayConnectAuth, C as isGatewayProtocolResponseError, D as GatewayBrowserDeviceAuthLifecycle, E as gatewayOriginScope, M as buildDeviceAuthPayload, N as buildDeviceAuthPayloadV3, O as buildGatewayConnectAuth, P as normalizeDeviceMetadataForAuth, S as GatewayProtocolRequestTimeoutError, T as gatewayCredentialScope, _ as readSessionMessageSequence, a as createSessionProjection, b as GatewayProtocolClient, c as reduceSessionProjection, d as readSessionProjectionFinalMessageIdentity, f as isSessionProjectionErrorMessage, g as readSessionMessageIdentity, h as readAssistantStreamSegmentIdentity, i as resetGatewaySessionMessageSubscriptionCoordinator, j as shouldRetryGatewayWithDeviceToken, k as resolveGatewayConnectScopes, l as reduceSessionProjectionRunEvent, m as normalizeSessionProjectionRunId, n as getGatewaySessionMessageSubscriptionCoordinator, o as projectLiveSessionMessage, p as isLocallyOptimisticSessionMessage, r as releaseGatewaySessionMessageSubscription, s as reconcileSessionProjectionSnapshot, t as GatewaySessionMessageSubscriptionCoordinator, u as hasSessionProjectionAcceptedFinal, v as isRecord, w as resolveModelCatalogConnect, x as GatewayProtocolRequestError, y as shouldPauseGatewayReconnect } from "./session-subscriptions-DrOaOXNR.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),
@@ -744,6 +831,11 @@ var GatewayClient = class {
744
831
  rethrowSocketFactoryError: (error) => error instanceof GatewayClientTransportPolicyError
745
832
  });
746
833
  }
834
+ /** Current transport state, including CLOSING before the close callback fires.
835
+ * This is not authentication or readiness evidence on its own. */
836
+ get connected() {
837
+ return !this.stopped && this.protocol.connected;
838
+ }
747
839
  getConnectionMetadata() {
748
840
  return {
749
841
  clientName: this.opts.clientName,
@@ -852,6 +944,7 @@ var GatewayClient = class {
852
944
  });
853
945
  ws.on("error", (err) => {
854
946
  if (upgradeError) return;
947
+ if (err.message === "Opening handshake has timed out") Object.assign(err, { code: "ETIMEDOUT" });
855
948
  this.logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`);
856
949
  handlers.error(err instanceof Error ? err : new Error(String(err)));
857
950
  });
@@ -866,7 +959,6 @@ var GatewayClient = class {
866
959
  }
867
960
  async stopAndWait(opts) {
868
961
  const stopPromise = this.beginStop();
869
- if (!stopPromise) return;
870
962
  const timeoutMs = opts?.timeoutMs === void 0 ? STOP_AND_WAIT_TIMEOUT_MS : resolveSafeTimeoutDelayMs(opts.timeoutMs);
871
963
  let timeout = null;
872
964
  try {
@@ -878,6 +970,7 @@ var GatewayClient = class {
878
970
  })]);
879
971
  } finally {
880
972
  if (timeout) clearTimeout(timeout);
973
+ await this.deviceAuth.drain();
881
974
  }
882
975
  }
883
976
  beginStop() {
@@ -941,7 +1034,7 @@ var GatewayClient = class {
941
1034
  }
942
1035
  assembleConnectParams(params) {
943
1036
  const { role, nonce, signedAtMs } = params;
944
- const selectedAuth = this.selectConnectAuth(role);
1037
+ const selectedAuth = this.selectConnectAuth(params.storedAuth);
945
1038
  const { authDeviceToken, authApprovalRuntimeToken, authAgentRuntimeIdentityToken, signatureToken, resolvedDeviceToken, storedToken, storedScopes, usingStoredDeviceToken } = selectedAuth;
946
1039
  if (this.pendingDeviceTokenRetry && authDeviceToken) this.pendingDeviceTokenRetry = false;
947
1040
  const auth = buildGatewayConnectAuth(selectedAuth);
@@ -976,7 +1069,11 @@ var GatewayClient = class {
976
1069
  mode: clientMode,
977
1070
  instanceId: this.opts.instanceId
978
1071
  },
979
- 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
+ }),
980
1077
  commands: Array.isArray(this.opts.commands) ? this.opts.commands : void 0,
981
1078
  computerUse: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.computerUse,
982
1079
  workerRuns: useLegacyNodeProtocolEnvelope ? void 0 : this.opts.workerRuns,
@@ -1042,35 +1139,56 @@ var GatewayClient = class {
1042
1139
  nonce
1043
1140
  };
1044
1141
  }
1045
- handleConnectHello(helloOk, assembled) {
1046
- const reconnectWithCurrentNodeProtocol = this.useLegacyNodeProtocolEnvelope && this.shouldNegotiateLegacyNodeProtocol() && helloOk.protocol > MIN_NODE_PROTOCOL_VERSION;
1047
- if (reconnectWithCurrentNodeProtocol) this.useLegacyNodeProtocolEnvelope = false;
1048
- this.nodeProtocolTransitionPending = false;
1049
- this.pendingDeviceTokenRetry = false;
1050
- this.deviceTokenRetryBudgetUsed = false;
1051
- this.suppressedTransientPreHelloCleanCloses = 0;
1142
+ handleConnectHello(helloOk, assembled, authority) {
1143
+ authority.assertCurrent();
1052
1144
  const role = this.opts.role ?? "operator";
1053
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;
1054
1157
  if (authInfo?.deviceToken && this.opts.deviceIdentity) {
1055
1158
  const tokenRole = authInfo.role ?? role;
1056
1159
  const scopes = tokenRole === role && authInfo.deviceToken === assembled.storedToken ? assembled.storedScopes ?? authInfo.scopes ?? [] : authInfo.scopes ?? [];
1057
- this.deps.storeDeviceAuthToken({
1160
+ if (tokenRole === role && observation) observation.receiptToken = authInfo.deviceToken;
1161
+ stored = this.deviceAuth.store({
1058
1162
  deviceId: this.opts.deviceIdentity.deviceId,
1059
1163
  role: tokenRole,
1060
1164
  token: authInfo.deviceToken,
1061
1165
  scopes,
1062
- 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;
1063
1173
  });
1064
- }
1065
- if (this.opts.preferBootstrapToken) {
1066
- this.opts.token = void 0;
1067
- this.opts.bootstrapToken = void 0;
1068
- this.opts.password = void 0;
1069
- this.opts.preferBootstrapToken = false;
1070
- }
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;
1071
1190
  this.tickIntervalMs = typeof helloOk.policy?.tickIntervalMs === "number" ? helloOk.policy.tickIntervalMs : 3e4;
1072
1191
  if (reconnectWithCurrentNodeProtocol) {
1073
- this.suppressNextHelloCallback = true;
1074
1192
  this.protocol.resetReconnectBackoff(250);
1075
1193
  this.protocol.closeSocket(1012, "gateway protocol upgraded");
1076
1194
  return;
@@ -1079,6 +1197,16 @@ var GatewayClient = class {
1079
1197
  this.startTickWatch();
1080
1198
  }
1081
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) {
1082
1210
  if (this.shouldRetryWithCurrentNodeProtocol(error)) {
1083
1211
  const resetBackoff = !this.nodeProtocolTransitionPending;
1084
1212
  this.useLegacyNodeProtocolEnvelope = false;
@@ -1102,30 +1230,15 @@ var GatewayClient = class {
1102
1230
  };
1103
1231
  }
1104
1232
  this.nodeProtocolTransitionPending = false;
1105
- const role = this.opts.role ?? "operator";
1106
1233
  const detailCode = error instanceof GatewayClientRequestError ? readConnectErrorDetailCode(error.details) : null;
1107
- const shouldRetryWithDeviceToken = shouldRetryGatewayWithDeviceToken({
1234
+ if (shouldRetryGatewayWithDeviceToken({
1108
1235
  retryBudgetUsed: this.deviceTokenRetryBudgetUsed,
1109
1236
  currentDeviceToken: assembled.resolvedDeviceToken,
1110
1237
  explicitToken: this.opts.token?.trim() || void 0,
1111
1238
  storedToken: assembled.storedToken,
1112
1239
  trustedEndpoint: this.isTrustedDeviceRetryEndpoint(),
1113
1240
  errorDetails: error instanceof GatewayClientRequestError ? error.details : void 0
1114
- });
1115
- if (this.opts.deviceIdentity && assembled.usingStoredDeviceToken && detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH) {
1116
- const deviceId = this.opts.deviceIdentity.deviceId;
1117
- try {
1118
- this.deps.clearDeviceAuthToken({
1119
- deviceId,
1120
- role,
1121
- env: this.opts.env
1122
- });
1123
- this.logDebug(`cleared stale device-auth token for device ${deviceId}`);
1124
- } catch (clearError) {
1125
- this.logDebug(`failed clearing stale device-auth token for device ${deviceId}: ${String(clearError)}`);
1126
- }
1127
- }
1128
- if (shouldRetryWithDeviceToken) {
1241
+ })) {
1129
1242
  this.pendingDeviceTokenRetry = true;
1130
1243
  this.deviceTokenRetryBudgetUsed = true;
1131
1244
  this.protocol.resetReconnectBackoff(250);
@@ -1197,7 +1310,7 @@ var GatewayClient = class {
1197
1310
  this.notifyConnectError(error);
1198
1311
  this.logError(`gateway connect failed: ${formatGatewayClientErrorForLog(error)}`);
1199
1312
  }
1200
- this.clearStaleDeviceTokenForClose(context.code, context.reason);
1313
+ this.clearStaleDeviceTokenForClose(context);
1201
1314
  if (shouldPauseGatewayReconnect({
1202
1315
  details,
1203
1316
  deviceTokenRetryPending: this.pendingDeviceTokenRetry,
@@ -1231,26 +1344,39 @@ var GatewayClient = class {
1231
1344
  ...context.connectFailure?.error ? { connectError: context.connectFailure.error } : {}
1232
1345
  };
1233
1346
  }
1234
- clearStaleDeviceTokenForClose(code, reason) {
1235
- if (code !== 1008 || !normalizeGatewayErrorText(reason).includes("device token mismatch") || this.opts.token || this.opts.password || !this.opts.deviceIdentity) return;
1236
- const deviceId = this.opts.deviceIdentity.deviceId;
1237
- 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}`);
1238
1358
  try {
1239
- this.deps.clearDeviceAuthToken({
1359
+ const result = this.deviceAuth.clear({
1240
1360
  deviceId,
1241
- role,
1242
- env: this.opts.env
1243
- });
1244
- 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();
1245
1368
  } catch (error) {
1246
- this.logDebug(`failed clearing stale device-auth token for device ${deviceId}: ${String(error)}`);
1369
+ failed(error);
1247
1370
  }
1248
1371
  }
1249
1372
  notifyConnectError(error) {
1373
+ if (!this.opts.onConnectError) return false;
1250
1374
  try {
1251
- this.opts.onConnectError?.(error);
1375
+ this.opts.onConnectError(error);
1376
+ return true;
1252
1377
  } catch (err) {
1253
1378
  this.logDebug(`gateway client connect error handler error: ${formatGatewayClientErrorForLog(err)}`);
1379
+ return false;
1254
1380
  }
1255
1381
  }
1256
1382
  notifyReconnectPaused(info) {
@@ -1286,12 +1412,7 @@ var GatewayClient = class {
1286
1412
  return false;
1287
1413
  }
1288
1414
  }
1289
- selectConnectAuth(role) {
1290
- const storedAuth = this.opts.deviceIdentity ? this.deps.loadDeviceAuthToken({
1291
- deviceId: this.opts.deviceIdentity.deviceId,
1292
- role,
1293
- env: this.opts.env
1294
- }) : null;
1415
+ selectConnectAuth(storedAuth) {
1295
1416
  return selectGatewayConnectAuth({
1296
1417
  token: this.opts.token,
1297
1418
  bootstrapToken: this.opts.bootstrapToken,
@@ -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 };