@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/README.md +35 -0
- package/dist/browser.d.mts +13 -4
- package/dist/browser.mjs +3 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +247 -126
- package/dist/{protocol-client-DbX7wQED.d.mts → protocol-client-DZ7F1DIs.d.mts} +14 -5
- package/dist/{readiness-uQKoJkl5.d.mts → readiness-B99dV5nY.d.mts} +44 -25
- package/dist/readiness.d.mts +1 -1
- package/dist/{session-subscriptions-DwU8T0CH.mjs → session-subscriptions-DrOaOXNR.mjs} +417 -115
- package/dist/{session-subscriptions-C3M4jnCd.d.mts → session-subscriptions-yRMtCvZZ.d.mts} +27 -15
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
this.
|
|
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
|
-
|
|
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
|
|
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(
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const
|
|
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.
|
|
1359
|
+
const result = this.deviceAuth.clear({
|
|
1240
1360
|
deviceId,
|
|
1241
|
-
role,
|
|
1242
|
-
env: this.opts.env
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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>) =>
|
|
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 {
|
|
181
|
+
export { GatewayProtocolSocketHandlers as a, GatewayProtocolSocket as i, GatewayProtocolCloseContext as n, GatewayProtocolTiming as o, GatewayProtocolConnectAuthority as r, GatewayProtocolRequestTiming as s, GatewayProtocolClient as t };
|