@openclaw/gateway-client 2026.7.2-beta.7 → 2026.8.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/browser-device-auth-Dz6H7eXy.d.mts +112 -0
- package/dist/browser.d.mts +6 -4
- package/dist/browser.mjs +3 -3
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +336 -147
- package/dist/{protocol-client-BfBHwA5H.d.mts → protocol-client-BbN-I582.d.mts} +21 -31
- package/dist/protocol-request-D4mVhLxE.d.mts +32 -0
- package/dist/{readiness-BJT_oUZE.d.mts → readiness--KX7i8Mq.d.mts} +29 -7
- package/dist/readiness.d.mts +1 -1
- package/dist/scope-upgrade.d.mts +38 -0
- package/dist/scope-upgrade.mjs +73 -0
- package/dist/{session-subscriptions-DrhzqL6v.d.mts → session-subscriptions-BTNpXybo.d.mts} +6 -110
- package/dist/{session-subscriptions-BV7MxYiL.mjs → session-subscriptions-uKmTyc3P.mjs} +280 -151
- package/dist/timeouts.mjs +3 -3
- package/dist/websocket-data.d.mts +6 -0
- package/dist/websocket-data.mjs +8 -0
- package/package.json +13 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearGatewayConnectTimeout, startGatewayConnectTimeout } from "./timeouts.mjs";
|
|
1
|
+
import { clearGatewayConnectTimeout, resolveSafeTimeoutDelayMs, startGatewayConnectTimeout } from "./timeouts.mjs";
|
|
2
2
|
import { ConnectErrorDetailCodes, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails } from "@openclaw/gateway-protocol/connect-error-details";
|
|
3
3
|
import { isGatewayEventFrame, isGatewayResponseFrame } from "@openclaw/gateway-protocol/frame-guards";
|
|
4
4
|
//#region packages/gateway-client/src/device-auth.ts
|
|
@@ -59,7 +59,7 @@ function selectGatewayConnectAuth(params) {
|
|
|
59
59
|
};
|
|
60
60
|
if (params.preferBootstrapToken && bootstrapToken) return {
|
|
61
61
|
authBootstrapToken: bootstrapToken,
|
|
62
|
-
|
|
62
|
+
signatureToken: bootstrapToken,
|
|
63
63
|
...stored
|
|
64
64
|
};
|
|
65
65
|
const useRetryToken = params.pendingDeviceTokenRetry === true && !explicitDeviceToken && Boolean(authToken && storedToken && params.trustedDeviceTokenRetry);
|
|
@@ -139,7 +139,8 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
139
139
|
scopes,
|
|
140
140
|
auth: buildGatewayConnectAuth(selectedAuth)
|
|
141
141
|
};
|
|
142
|
-
const signedAtMs = this.deps.nowMs?.() ?? Date.now();
|
|
142
|
+
const signedAtMs = params.challengeTs === void 0 ? this.deps.nowMs?.() ?? Date.now() : params.challengeTs;
|
|
143
|
+
if (typeof signedAtMs !== "number" || !Number.isSafeInteger(signedAtMs) || signedAtMs < 0) throw new Error("gateway connect challenge timestamp invalid");
|
|
143
144
|
const nonce = params.nonce ?? "";
|
|
144
145
|
const { authBootstrapToken: primary, signatureToken: signed } = selectedAuth;
|
|
145
146
|
let token = null;
|
|
@@ -176,12 +177,18 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
176
177
|
async acceptHello(hello, plan) {
|
|
177
178
|
const token = hello.auth?.deviceToken?.trim();
|
|
178
179
|
if (!token || !plan.identity) return;
|
|
180
|
+
const role = hello.auth?.role ?? plan.role;
|
|
181
|
+
const stored = await this.deps.tokenStore.load({
|
|
182
|
+
clientId: plan.clientId,
|
|
183
|
+
deviceId: plan.identity.deviceId,
|
|
184
|
+
role
|
|
185
|
+
});
|
|
179
186
|
await this.deps.tokenStore.store({
|
|
180
187
|
clientId: plan.clientId,
|
|
181
188
|
deviceId: plan.identity.deviceId,
|
|
182
|
-
role
|
|
189
|
+
role,
|
|
183
190
|
token,
|
|
184
|
-
scopes: hello.auth?.scopes ?? []
|
|
191
|
+
scopes: stored?.token === token ? stored.scopes : hello.auth?.scopes ?? []
|
|
185
192
|
});
|
|
186
193
|
}
|
|
187
194
|
async clearStoredToken(plan) {
|
|
@@ -194,6 +201,29 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
194
201
|
}
|
|
195
202
|
};
|
|
196
203
|
//#endregion
|
|
204
|
+
//#region packages/gateway-client/src/gateway-origin-scope.ts
|
|
205
|
+
function normalizeGatewayScope(gatewayUrl, includeSearch) {
|
|
206
|
+
const trimmed = gatewayUrl.trim();
|
|
207
|
+
if (!trimmed) return "default";
|
|
208
|
+
try {
|
|
209
|
+
const browserLocation = globalThis.location;
|
|
210
|
+
const base = browserLocation ? `${browserLocation.protocol}//${browserLocation.host}${browserLocation.pathname || "/"}` : void 0;
|
|
211
|
+
const parsed = base ? new URL(trimmed, base) : new URL(trimmed);
|
|
212
|
+
const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "") || parsed.pathname;
|
|
213
|
+
return `${parsed.protocol}//${parsed.host}${pathname}${includeSearch ? parsed.search : ""}`;
|
|
214
|
+
} catch {
|
|
215
|
+
return trimmed;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Normalizes the gateway URL scope used for origin-bound device tokens. */
|
|
219
|
+
function gatewayOriginScope(gatewayUrl) {
|
|
220
|
+
return normalizeGatewayScope(gatewayUrl, false);
|
|
221
|
+
}
|
|
222
|
+
/** Normalizes the gateway URL scope used for browser credential records. */
|
|
223
|
+
function gatewayCredentialScope(gatewayUrl) {
|
|
224
|
+
return normalizeGatewayScope(gatewayUrl, true);
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
197
227
|
//#region packages/retry/src/index.ts
|
|
198
228
|
const MAX_TIMER_TIMEOUT_MS = 2147e6;
|
|
199
229
|
function computeBackoff(policy, attempt) {
|
|
@@ -214,7 +244,9 @@ async function sleepWithAbort(ms, abortSignal, options = {}) {
|
|
|
214
244
|
if (timer) clearTimeout(timer);
|
|
215
245
|
timer = null;
|
|
216
246
|
cleanup();
|
|
217
|
-
|
|
247
|
+
const error = new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") });
|
|
248
|
+
error.name = "AbortError";
|
|
249
|
+
reject(error);
|
|
218
250
|
};
|
|
219
251
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
220
252
|
if (abortSignal?.aborted) {
|
|
@@ -276,24 +308,21 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
276
308
|
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
277
309
|
setTimeout(resolve, ms);
|
|
278
310
|
});
|
|
279
|
-
function asFiniteNumber(value) {
|
|
280
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
281
|
-
}
|
|
282
311
|
function clampNumber(value, fallback, min, max) {
|
|
283
|
-
const next =
|
|
312
|
+
const next = Number.isFinite(value) ? value : void 0;
|
|
284
313
|
if (next === void 0) return fallback;
|
|
285
314
|
return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
|
|
286
315
|
}
|
|
287
316
|
function resolveAttemptCount(value, fallback) {
|
|
288
|
-
return Math.max(1, Math.round(
|
|
317
|
+
return Math.max(1, Math.round(Number.isFinite(value) ? value : fallback));
|
|
289
318
|
}
|
|
290
319
|
function resolveRetryDelayMs(value) {
|
|
291
|
-
const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS :
|
|
320
|
+
const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : Number.isFinite(value) ? value : 0;
|
|
292
321
|
return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
|
|
293
322
|
}
|
|
294
323
|
function resolveJitterConfig(value, fallback) {
|
|
295
324
|
if (value === "full") return "full";
|
|
296
|
-
const fraction =
|
|
325
|
+
const fraction = Number.isFinite(value) ? value : void 0;
|
|
297
326
|
return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
|
|
298
327
|
}
|
|
299
328
|
function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
|
|
@@ -403,7 +432,7 @@ var GatewayEventListeners = class {
|
|
|
403
432
|
}
|
|
404
433
|
};
|
|
405
434
|
//#endregion
|
|
406
|
-
//#region packages/gateway-client/src/protocol-
|
|
435
|
+
//#region packages/gateway-client/src/protocol-request.ts
|
|
407
436
|
var GatewayProtocolRequestError = class extends Error {
|
|
408
437
|
constructor(error) {
|
|
409
438
|
super(error.message ?? "request failed");
|
|
@@ -415,6 +444,167 @@ var GatewayProtocolRequestError = class extends Error {
|
|
|
415
444
|
this.retryAfterMs = error.retryAfterMs;
|
|
416
445
|
}
|
|
417
446
|
};
|
|
447
|
+
/** A local transport deadline, distinct from a Gateway's authoritative rejection. */
|
|
448
|
+
var GatewayProtocolRequestTimeoutError = class extends Error {
|
|
449
|
+
constructor(params, message = `gateway request timed out after ${params.timeoutMs}ms: ${params.method}`) {
|
|
450
|
+
super(message);
|
|
451
|
+
this.code = "CLIENT_TIMEOUT";
|
|
452
|
+
this.name = "GatewayProtocolRequestTimeoutError";
|
|
453
|
+
this.method = params.method;
|
|
454
|
+
this.timeoutMs = params.timeoutMs;
|
|
455
|
+
this.requestSent = params.requestSent;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region packages/gateway-client/src/pending-request.ts
|
|
460
|
+
/** Owns request deadlines, correlation, settlement, and generation-scoped IDs. */
|
|
461
|
+
var GatewayPendingRequests = class {
|
|
462
|
+
constructor(opts) {
|
|
463
|
+
this.opts = opts;
|
|
464
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
465
|
+
this.retiredIds = /* @__PURE__ */ new Set();
|
|
466
|
+
this.collisionSuffix = 0;
|
|
467
|
+
}
|
|
468
|
+
get hasPending() {
|
|
469
|
+
return this.pending.size > 0;
|
|
470
|
+
}
|
|
471
|
+
get hasUnboundedPending() {
|
|
472
|
+
return [...this.pending.values()].some((pending) => pending.unbounded);
|
|
473
|
+
}
|
|
474
|
+
request(sender, method, params, options) {
|
|
475
|
+
let id;
|
|
476
|
+
try {
|
|
477
|
+
id = this.allocateRequestId();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
480
|
+
}
|
|
481
|
+
const requestedTimeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
|
|
482
|
+
const timeoutMs = typeof requestedTimeoutMs === "number" && Number.isFinite(requestedTimeoutMs) ? resolveSafeTimeoutDelayMs(requestedTimeoutMs, { minMs: 0 }) : void 0;
|
|
483
|
+
return new Promise((resolve, reject) => {
|
|
484
|
+
let timeout;
|
|
485
|
+
let requestSent = false;
|
|
486
|
+
const pending = {
|
|
487
|
+
resolve: (value) => resolve(value),
|
|
488
|
+
reject,
|
|
489
|
+
expectFinal: options?.expectFinal === true,
|
|
490
|
+
acceptedNotified: false,
|
|
491
|
+
onAccepted: options?.onAccepted,
|
|
492
|
+
unbounded: timeoutMs === void 0,
|
|
493
|
+
method,
|
|
494
|
+
startedAtMs: this.opts.nowMs()
|
|
495
|
+
};
|
|
496
|
+
const cleanup = () => {
|
|
497
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
498
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
499
|
+
};
|
|
500
|
+
const retire = (errorCode) => {
|
|
501
|
+
if (this.pending.get(id) !== pending) return false;
|
|
502
|
+
this.pending.delete(id);
|
|
503
|
+
this.retiredIds.add(id);
|
|
504
|
+
cleanup();
|
|
505
|
+
this.finishTiming(id, pending, false, errorCode);
|
|
506
|
+
return true;
|
|
507
|
+
};
|
|
508
|
+
const onAbort = () => {
|
|
509
|
+
if (!retire("CLIENT_ABORTED")) return;
|
|
510
|
+
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
511
|
+
};
|
|
512
|
+
if (options?.signal?.aborted) {
|
|
513
|
+
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
pending.cleanup = cleanup;
|
|
517
|
+
if (timeoutMs !== void 0) {
|
|
518
|
+
timeout = setTimeout(() => {
|
|
519
|
+
if (!retire("CLIENT_TIMEOUT")) return;
|
|
520
|
+
reject(this.opts.createRequestTimeoutError?.(method, timeoutMs, requestSent) ?? new GatewayProtocolRequestTimeoutError({
|
|
521
|
+
method,
|
|
522
|
+
timeoutMs,
|
|
523
|
+
requestSent
|
|
524
|
+
}));
|
|
525
|
+
}, timeoutMs);
|
|
526
|
+
timeout.unref?.();
|
|
527
|
+
}
|
|
528
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
529
|
+
this.pending.set(id, pending);
|
|
530
|
+
try {
|
|
531
|
+
sender.send(JSON.stringify({
|
|
532
|
+
type: "req",
|
|
533
|
+
id,
|
|
534
|
+
method,
|
|
535
|
+
params
|
|
536
|
+
}));
|
|
537
|
+
if (this.pending.get(id) !== pending) return;
|
|
538
|
+
requestSent = true;
|
|
539
|
+
this.invoke("sent", () => options?.onSent?.());
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (retire("CLIENT_SEND_ERROR")) reject(error instanceof Error ? error : new Error(String(error)));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
handleResponse(frame) {
|
|
546
|
+
const pending = this.pending.get(frame.id);
|
|
547
|
+
if (!pending) return;
|
|
548
|
+
const status = frame.payload?.status;
|
|
549
|
+
if (pending.expectFinal && status === "accepted") {
|
|
550
|
+
if (!pending.acceptedNotified) {
|
|
551
|
+
pending.acceptedNotified = true;
|
|
552
|
+
this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
this.pending.delete(frame.id);
|
|
557
|
+
pending.cleanup?.();
|
|
558
|
+
if (frame.ok) {
|
|
559
|
+
this.finishTiming(frame.id, pending, true);
|
|
560
|
+
pending.resolve(frame.payload);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.finishTiming(frame.id, pending, false, frame.error?.code);
|
|
564
|
+
pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
|
|
565
|
+
}
|
|
566
|
+
flush(error) {
|
|
567
|
+
for (const [id, pending] of this.pending) {
|
|
568
|
+
this.finishTiming(id, pending, false, "CLIENT_CLOSED");
|
|
569
|
+
pending.cleanup?.();
|
|
570
|
+
pending.reject(error);
|
|
571
|
+
}
|
|
572
|
+
this.pending.clear();
|
|
573
|
+
this.retiredIds.clear();
|
|
574
|
+
this.collisionSuffix = 0;
|
|
575
|
+
}
|
|
576
|
+
allocateRequestId() {
|
|
577
|
+
const id = this.opts.createRequestId();
|
|
578
|
+
if (!this.pending.has(id) && !this.retiredIds.has(id)) return id;
|
|
579
|
+
let uniqueId;
|
|
580
|
+
do {
|
|
581
|
+
this.collisionSuffix += 1;
|
|
582
|
+
uniqueId = `${id}:${this.collisionSuffix}`;
|
|
583
|
+
} while (this.pending.has(uniqueId) || this.retiredIds.has(uniqueId));
|
|
584
|
+
return uniqueId;
|
|
585
|
+
}
|
|
586
|
+
finishTiming(id, pending, ok, errorCode) {
|
|
587
|
+
const endedAtMs = this.opts.nowMs();
|
|
588
|
+
this.invoke("request timing", () => this.opts.onTiming?.({
|
|
589
|
+
id,
|
|
590
|
+
method: pending.method,
|
|
591
|
+
ok,
|
|
592
|
+
durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
|
|
593
|
+
startedAtMs: pending.startedAtMs,
|
|
594
|
+
endedAtMs,
|
|
595
|
+
errorCode
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
invoke(label, callback) {
|
|
599
|
+
try {
|
|
600
|
+
callback();
|
|
601
|
+
} catch (error) {
|
|
602
|
+
this.opts.onCallbackError?.(label, error);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region packages/gateway-client/src/protocol-client.ts
|
|
418
608
|
/**
|
|
419
609
|
* Browser-safe gateway wire client. Environment adapters own transport and auth
|
|
420
610
|
* policy; this class owns the single socket/handshake/reconnect/frame state machine.
|
|
@@ -423,7 +613,6 @@ var GatewayProtocolClient = class {
|
|
|
423
613
|
constructor(opts) {
|
|
424
614
|
this.opts = opts;
|
|
425
615
|
this.socket = null;
|
|
426
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
427
616
|
this.listeners = new GatewayEventListeners();
|
|
428
617
|
this.stopped = true;
|
|
429
618
|
this.generation = 0;
|
|
@@ -442,18 +631,28 @@ var GatewayProtocolClient = class {
|
|
|
442
631
|
factor: opts.reconnect.multiplier,
|
|
443
632
|
jitter: 0
|
|
444
633
|
});
|
|
634
|
+
this.requests = new GatewayPendingRequests({
|
|
635
|
+
createRequestId: opts.createRequestId,
|
|
636
|
+
createRequestError: opts.createRequestError,
|
|
637
|
+
createRequestTimeoutError: opts.createRequestTimeoutError,
|
|
638
|
+
createRequestAbortError: opts.createRequestAbortError,
|
|
639
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
640
|
+
nowMs: () => this.nowMs(),
|
|
641
|
+
onTiming: opts.onRequestTiming,
|
|
642
|
+
onCallbackError: opts.onCallbackError
|
|
643
|
+
});
|
|
445
644
|
}
|
|
446
645
|
get connected() {
|
|
447
646
|
return this.socket?.isOpen() ?? false;
|
|
448
647
|
}
|
|
449
648
|
get hasPendingRequests() {
|
|
450
|
-
return this.
|
|
649
|
+
return this.requests.hasPending;
|
|
451
650
|
}
|
|
452
651
|
get connecting() {
|
|
453
652
|
return this.connectSent && !this.helloReceived;
|
|
454
653
|
}
|
|
455
654
|
get hasUnboundedPendingRequests() {
|
|
456
|
-
return
|
|
655
|
+
return this.requests.hasUnboundedPending;
|
|
457
656
|
}
|
|
458
657
|
start() {
|
|
459
658
|
if (this.socket || this.reconnectSignal) return;
|
|
@@ -474,68 +673,14 @@ var GatewayProtocolClient = class {
|
|
|
474
673
|
this.socket = null;
|
|
475
674
|
this.connectFailure = void 0;
|
|
476
675
|
this.connectTiming = null;
|
|
477
|
-
this.
|
|
676
|
+
this.requests.flush(/* @__PURE__ */ new Error("gateway client stopped"));
|
|
478
677
|
socket?.close();
|
|
479
678
|
}
|
|
480
679
|
request(method, params, options) {
|
|
481
680
|
const socket = this.socket;
|
|
482
681
|
if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
|
|
483
682
|
if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
|
|
484
|
-
|
|
485
|
-
const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
|
|
486
|
-
return new Promise((resolve, reject) => {
|
|
487
|
-
let timeout;
|
|
488
|
-
const pending = {
|
|
489
|
-
resolve: (value) => resolve(value),
|
|
490
|
-
reject,
|
|
491
|
-
expectFinal: options?.expectFinal === true,
|
|
492
|
-
acceptedNotified: false,
|
|
493
|
-
onAccepted: options?.onAccepted,
|
|
494
|
-
unbounded: timeoutMs === void 0,
|
|
495
|
-
method,
|
|
496
|
-
startedAtMs: this.nowMs()
|
|
497
|
-
};
|
|
498
|
-
const onAbort = () => {
|
|
499
|
-
this.pending.delete(id);
|
|
500
|
-
if (timeout) clearTimeout(timeout);
|
|
501
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
|
|
502
|
-
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
503
|
-
};
|
|
504
|
-
const cleanup = () => {
|
|
505
|
-
if (timeout) clearTimeout(timeout);
|
|
506
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
507
|
-
};
|
|
508
|
-
if (options?.signal?.aborted) {
|
|
509
|
-
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
pending.cleanup = cleanup;
|
|
513
|
-
if (timeoutMs !== void 0 && timeoutMs >= 0) {
|
|
514
|
-
timeout = setTimeout(() => {
|
|
515
|
-
this.pending.delete(id);
|
|
516
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
517
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
|
|
518
|
-
reject(this.opts.createRequestTimeoutError?.(method, timeoutMs) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
|
|
519
|
-
}, timeoutMs);
|
|
520
|
-
timeout.unref?.();
|
|
521
|
-
}
|
|
522
|
-
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
523
|
-
this.pending.set(id, pending);
|
|
524
|
-
try {
|
|
525
|
-
socket.send(JSON.stringify({
|
|
526
|
-
type: "req",
|
|
527
|
-
id,
|
|
528
|
-
method,
|
|
529
|
-
params
|
|
530
|
-
}));
|
|
531
|
-
this.invoke("sent", () => options?.onSent?.());
|
|
532
|
-
} catch (error) {
|
|
533
|
-
this.pending.delete(id);
|
|
534
|
-
cleanup();
|
|
535
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
|
|
536
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
537
|
-
}
|
|
538
|
-
});
|
|
683
|
+
return this.requests.request(socket, method, params, options);
|
|
539
684
|
}
|
|
540
685
|
addEventListener(listener) {
|
|
541
686
|
return this.listeners.add(listener);
|
|
@@ -571,6 +716,7 @@ var GatewayProtocolClient = class {
|
|
|
571
716
|
const generation = this.generation + 1;
|
|
572
717
|
this.lastSeq = null;
|
|
573
718
|
this.connectNonce = null;
|
|
719
|
+
this.connectChallengeTs = void 0;
|
|
574
720
|
this.connectSent = this.connectRequestSent = false;
|
|
575
721
|
this.socketOpened = false;
|
|
576
722
|
this.helloReceived = false;
|
|
@@ -641,6 +787,7 @@ var GatewayProtocolClient = class {
|
|
|
641
787
|
try {
|
|
642
788
|
planOrPromise = this.opts.buildConnectPlan({
|
|
643
789
|
nonce: this.connectNonce,
|
|
790
|
+
challengeTs: this.connectChallengeTs,
|
|
644
791
|
generation
|
|
645
792
|
});
|
|
646
793
|
} catch (error) {
|
|
@@ -669,6 +816,7 @@ var GatewayProtocolClient = class {
|
|
|
669
816
|
const context = {
|
|
670
817
|
generation,
|
|
671
818
|
nonce: this.connectNonce,
|
|
819
|
+
challengeTs: this.connectChallengeTs,
|
|
672
820
|
plan
|
|
673
821
|
};
|
|
674
822
|
this.recordTiming("connect-plan-ready", generation, plan);
|
|
@@ -721,6 +869,8 @@ var GatewayProtocolClient = class {
|
|
|
721
869
|
return;
|
|
722
870
|
}
|
|
723
871
|
this.connectNonce = nonce;
|
|
872
|
+
const challengeTs = payload?.ts;
|
|
873
|
+
this.connectChallengeTs = typeof challengeTs === "number" && Number.isSafeInteger(challengeTs) && challengeTs >= 0 ? challengeTs : null;
|
|
724
874
|
this.recordTiming("challenge", generation);
|
|
725
875
|
this.sendConnect(socket, generation);
|
|
726
876
|
return;
|
|
@@ -747,28 +897,7 @@ var GatewayProtocolClient = class {
|
|
|
747
897
|
}
|
|
748
898
|
if (!isGatewayResponseFrame(parsed)) return;
|
|
749
899
|
this.opts.onActivity?.();
|
|
750
|
-
this.handleResponse(parsed);
|
|
751
|
-
}
|
|
752
|
-
handleResponse(frame) {
|
|
753
|
-
const pending = this.pending.get(frame.id);
|
|
754
|
-
if (!pending) return;
|
|
755
|
-
const status = frame.payload?.status;
|
|
756
|
-
if (pending.expectFinal && status === "accepted") {
|
|
757
|
-
if (!pending.acceptedNotified) {
|
|
758
|
-
pending.acceptedNotified = true;
|
|
759
|
-
this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
|
|
760
|
-
}
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
this.pending.delete(frame.id);
|
|
764
|
-
pending.cleanup?.();
|
|
765
|
-
if (frame.ok) {
|
|
766
|
-
this.finishRequestTiming(frame.id, pending, true);
|
|
767
|
-
pending.resolve(frame.payload);
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
|
|
771
|
-
pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
|
|
900
|
+
this.requests.handleResponse(parsed);
|
|
772
901
|
}
|
|
773
902
|
handleClose(socket, generation, code, reason) {
|
|
774
903
|
if (this.socket !== socket) {
|
|
@@ -796,34 +925,15 @@ var GatewayProtocolClient = class {
|
|
|
796
925
|
};
|
|
797
926
|
this.connectFailure = void 0;
|
|
798
927
|
const decision = this.opts.resolveClose(context);
|
|
799
|
-
this.
|
|
928
|
+
this.requests.flush(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
|
|
800
929
|
this.invoke("close", () => this.opts.onClose?.(context, decision));
|
|
801
930
|
if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
|
|
802
931
|
}
|
|
803
932
|
handleSocketError(socket, generation, error) {
|
|
804
933
|
if (!this.isActive(socket, generation) || this.connectSent) return;
|
|
934
|
+
this.connectFailure = { error };
|
|
805
935
|
this.opts.onConnectError?.(error);
|
|
806
936
|
}
|
|
807
|
-
flushRequests(error) {
|
|
808
|
-
for (const [id, pending] of this.pending) {
|
|
809
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
|
|
810
|
-
pending.cleanup?.();
|
|
811
|
-
pending.reject(error);
|
|
812
|
-
}
|
|
813
|
-
this.pending.clear();
|
|
814
|
-
}
|
|
815
|
-
finishRequestTiming(id, pending, ok, errorCode) {
|
|
816
|
-
const endedAtMs = this.nowMs();
|
|
817
|
-
this.invoke("request timing", () => this.opts.onRequestTiming?.({
|
|
818
|
-
id,
|
|
819
|
-
method: pending.method,
|
|
820
|
-
ok,
|
|
821
|
-
durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
|
|
822
|
-
startedAtMs: pending.startedAtMs,
|
|
823
|
-
endedAtMs,
|
|
824
|
-
errorCode
|
|
825
|
-
}));
|
|
826
|
-
}
|
|
827
937
|
scheduleReconnect(overrideMs) {
|
|
828
938
|
if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
|
|
829
939
|
const retry = this.reconnectSupervisor.next();
|
|
@@ -873,6 +983,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
|
873
983
|
ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
|
|
874
984
|
ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
|
|
875
985
|
ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
|
|
986
|
+
ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
|
|
876
987
|
ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
|
877
988
|
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
|
878
989
|
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
|
|
@@ -886,7 +997,47 @@ function shouldPauseGatewayReconnect(params) {
|
|
|
886
997
|
return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
|
|
887
998
|
}
|
|
888
999
|
//#endregion
|
|
1000
|
+
//#region packages/gateway-client/src/session-projection-run-event.ts
|
|
1001
|
+
function readNonemptyString$1(value) {
|
|
1002
|
+
return typeof value === "string" ? value.trim() || null : null;
|
|
1003
|
+
}
|
|
1004
|
+
function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
|
|
1005
|
+
const runId = readNonemptyString$1(event.runId);
|
|
1006
|
+
if (!runId || typeof event.state !== "string" || ![
|
|
1007
|
+
"delta",
|
|
1008
|
+
"final",
|
|
1009
|
+
"error",
|
|
1010
|
+
"aborted"
|
|
1011
|
+
].includes(event.state)) return null;
|
|
1012
|
+
const message = event.message;
|
|
1013
|
+
const messageStopReason = message !== null && typeof message === "object" && !Array.isArray(message) ? readNonemptyString$1(message.stopReason) : null;
|
|
1014
|
+
const stopReason = readNonemptyString$1(event.stopReason) ?? messageStopReason;
|
|
1015
|
+
const errorKind = readNonemptyString$1(event.errorKind);
|
|
1016
|
+
const base = {
|
|
1017
|
+
runId,
|
|
1018
|
+
...message === void 0 ? {} : { message },
|
|
1019
|
+
scope
|
|
1020
|
+
};
|
|
1021
|
+
const next = reduceSessionProjection(projection, event.state === "delta" ? {
|
|
1022
|
+
type: "runDelta",
|
|
1023
|
+
...base
|
|
1024
|
+
} : {
|
|
1025
|
+
type: "runTerminal",
|
|
1026
|
+
...base,
|
|
1027
|
+
status: event.state === "aborted" ? "aborted" : event.state === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
|
|
1028
|
+
...stopReason === null ? {} : { stopReason },
|
|
1029
|
+
...errorKind === null ? {} : { errorKind },
|
|
1030
|
+
...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
|
|
1031
|
+
});
|
|
1032
|
+
return {
|
|
1033
|
+
projection: next,
|
|
1034
|
+
previousRun: projection.runs[runId],
|
|
1035
|
+
currentRun: next.runs[runId]
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
889
1039
|
//#region packages/gateway-client/src/session-projection.ts
|
|
1040
|
+
/** Browser-safe identity and replay rules shared by Gateway conversation clients. */
|
|
890
1041
|
const MAX_TRACKED_SESSION_RUNS = 200;
|
|
891
1042
|
const RETAINED_SESSION_RUNS = 150;
|
|
892
1043
|
const SESSION_PROJECTION_SCOPE_KEYS = [
|
|
@@ -1000,13 +1151,16 @@ function sameTranscriptIdentity(left, right) {
|
|
|
1000
1151
|
}
|
|
1001
1152
|
function entryMatches(left, right, allowSnapshotPromotion = false) {
|
|
1002
1153
|
if (sameTranscriptIdentity(left.identity, right.identity)) return true;
|
|
1154
|
+
const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
|
|
1155
|
+
const provisionalEntry = durableEntry === left ? right : durableEntry === right ? left : null;
|
|
1156
|
+
if (durableEntry?.live && provisionalEntry?.live && durableEntry.identity?.role === "assistant" && provisionalEntry.identity?.role === "assistant" && !durableEntry.identity.isImported && !provisionalEntry.identity.isImported && !provisionalEntry.identity.id && durableEntry.identity.runId && durableEntry.identity.runId === provisionalEntry.identity.runId) return true;
|
|
1003
1157
|
const persisted = left.identity;
|
|
1004
1158
|
const observed = right.identity;
|
|
1005
1159
|
if (allowSnapshotPromotion && right.live && persisted && observed && persisted.role === observed.role && !persisted.isImported && !observed.isImported && persisted.id && !observed.id && (persisted.sequence !== null && persisted.sequence === observed.sequence || persisted.role === "assistant" && observed.sequence === null && persisted.runId !== null && persisted.runId === observed.runId)) return true;
|
|
1006
1160
|
if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
|
|
1007
1161
|
const pending = left.pending ? left : right.pending ? right : null;
|
|
1008
1162
|
const authoritative = pending === left ? right : pending === right ? left : null;
|
|
1009
|
-
return Boolean(pending && authoritative && pending.identity
|
|
1163
|
+
return Boolean(pending && authoritative && pending.identity && authoritative.identity && pending.identity.role === authoritative.identity.role && !pending.identity.isImported && !authoritative.identity.isImported && pending.pendingRunId && pending.pendingRunId === authoritative.identity.runId && (pending.identity.sequence === null || authoritative.identity.sequence === null || pending.identity.sequence === authoritative.identity.sequence));
|
|
1010
1164
|
}
|
|
1011
1165
|
function withEntries(state, entries) {
|
|
1012
1166
|
return {
|
|
@@ -1188,7 +1342,13 @@ function reduceSessionProjection(state, event) {
|
|
|
1188
1342
|
const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
|
|
1189
1343
|
const incoming = createEntry(event.message, { pendingRunId });
|
|
1190
1344
|
if (!pendingRunId || !incoming.identity) return state;
|
|
1191
|
-
|
|
1345
|
+
const seed = state.entries.find((entry) => entry.message === event.message);
|
|
1346
|
+
if (seed && !seed.pending && incoming.identity.id === null && !incoming.identity.isImported && incoming.identity.runId === pendingRunId) return withEntries(state, state.entries.map((entry) => entry === seed ? {
|
|
1347
|
+
...seed,
|
|
1348
|
+
pending: true,
|
|
1349
|
+
pendingRunId
|
|
1350
|
+
} : entry));
|
|
1351
|
+
return seed || state.entries.some((entry) => entryMatches(entry, incoming)) ? state : withEntries(state, insertEntry(state.entries, incoming, state.runs));
|
|
1192
1352
|
}
|
|
1193
1353
|
case "sendAcknowledged": {
|
|
1194
1354
|
const runId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
|
|
@@ -1234,38 +1394,7 @@ function reduceSessionProjection(state, event) {
|
|
|
1234
1394
|
}
|
|
1235
1395
|
/** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
|
|
1236
1396
|
function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
|
|
1237
|
-
|
|
1238
|
-
const eventState = event.state;
|
|
1239
|
-
if (!runId || typeof eventState !== "string" || ![
|
|
1240
|
-
"delta",
|
|
1241
|
-
"final",
|
|
1242
|
-
"error",
|
|
1243
|
-
"aborted"
|
|
1244
|
-
].includes(eventState)) return null;
|
|
1245
|
-
const message = event.message;
|
|
1246
|
-
const stopReason = readNonemptyString(event.stopReason) ?? readNonemptyString(readRecord(message)?.stopReason);
|
|
1247
|
-
const errorKind = readNonemptyString(event.errorKind);
|
|
1248
|
-
const base = {
|
|
1249
|
-
runId,
|
|
1250
|
-
...message === void 0 ? {} : { message },
|
|
1251
|
-
scope
|
|
1252
|
-
};
|
|
1253
|
-
const next = reduceSessionProjection(projection, eventState === "delta" ? {
|
|
1254
|
-
type: "runDelta",
|
|
1255
|
-
...base
|
|
1256
|
-
} : {
|
|
1257
|
-
type: "runTerminal",
|
|
1258
|
-
...base,
|
|
1259
|
-
status: eventState === "aborted" ? "aborted" : eventState === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
|
|
1260
|
-
...stopReason === null ? {} : { stopReason },
|
|
1261
|
-
...errorKind === null ? {} : { errorKind },
|
|
1262
|
-
...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
|
|
1263
|
-
});
|
|
1264
|
-
return {
|
|
1265
|
-
projection: next,
|
|
1266
|
-
previousRun: projection.runs[runId],
|
|
1267
|
-
currentRun: next.runs[runId]
|
|
1268
|
-
};
|
|
1397
|
+
return reduceSessionProjectionRunEventImpl(projection, event, scope);
|
|
1269
1398
|
}
|
|
1270
1399
|
//#endregion
|
|
1271
1400
|
//#region packages/gateway-client/src/session-subscriptions.ts
|
|
@@ -1473,4 +1602,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
|
|
|
1473
1602
|
return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
|
|
1474
1603
|
}
|
|
1475
1604
|
//#endregion
|
|
1476
|
-
export {
|
|
1605
|
+
export { resolveGatewayConnectScopes as C, buildDeviceAuthPayloadV3 as D, buildDeviceAuthPayload as E, normalizeDeviceMetadataForAuth as O, buildGatewayConnectAuth as S, shouldRetryGatewayWithDeviceToken as T, GatewayProtocolRequestError as _, createSessionProjection as a, gatewayOriginScope as b, normalizeSessionProjectionRunId as c, readSessionMessageSequence as d, reconcileSessionProjectionSnapshot as f, GatewayProtocolClient as g, shouldPauseGatewayReconnect as h, resetGatewaySessionMessageSubscriptionCoordinator as i, projectLiveSessionMessage as l, reduceSessionProjectionRunEvent as m, getGatewaySessionMessageSubscriptionCoordinator as n, hasSessionProjectionAcceptedFinal as o, reduceSessionProjection as p, releaseGatewaySessionMessageSubscription as r, isLocallyOptimisticSessionMessage as s, GatewaySessionMessageSubscriptionCoordinator as t, readSessionMessageIdentity as u, GatewayProtocolRequestTimeoutError as v, selectGatewayConnectAuth as w, GatewayBrowserDeviceAuthLifecycle as x, gatewayCredentialScope as y };
|
package/dist/timeouts.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region packages/gateway-client/src/timeouts.ts
|
|
2
|
-
function
|
|
2
|
+
function parsePositiveTimeoutSetting(value) {
|
|
3
3
|
const trimmed = value.trim();
|
|
4
4
|
if (!/^\+?\d+$/u.test(trimmed)) return;
|
|
5
5
|
const parsed = Number(trimmed);
|
|
@@ -53,7 +53,7 @@ function clampConnectChallengeTimeoutMs(timeoutMs, maxTimeoutMs = MAX_CONNECT_CH
|
|
|
53
53
|
function getConnectChallengeTimeoutMsFromEnv(env = process.env) {
|
|
54
54
|
const raw = env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS;
|
|
55
55
|
if (raw) {
|
|
56
|
-
const parsed =
|
|
56
|
+
const parsed = parsePositiveTimeoutSetting(raw);
|
|
57
57
|
if (parsed !== void 0) return resolveSafeTimeoutDelayMs(parsed);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -77,7 +77,7 @@ function resolvePreauthHandshakeTimeoutMs(params) {
|
|
|
77
77
|
const env = params?.env ?? process.env;
|
|
78
78
|
const configuredTimeout = env.OPENCLAW_HANDSHAKE_TIMEOUT_MS || (isTestRuntimeEnv(env) ? env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS : void 0);
|
|
79
79
|
if (configuredTimeout) {
|
|
80
|
-
const parsed =
|
|
80
|
+
const parsed = parsePositiveTimeoutSetting(configuredTimeout);
|
|
81
81
|
if (parsed !== void 0) return resolveSafeTimeoutDelayMs(parsed);
|
|
82
82
|
}
|
|
83
83
|
const configured = normalizePositiveTimeoutMs(params?.configuredTimeoutMs);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
//#region packages/gateway-client/src/websocket-data.ts
|
|
3
|
+
function rawDataToString(data, encoding = "utf8") {
|
|
4
|
+
if (Array.isArray(data)) return Buffer.concat(data).toString(encoding);
|
|
5
|
+
return data instanceof ArrayBuffer ? Buffer.from(data).toString(encoding) : data.toString(encoding);
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { rawDataToString };
|