@openclaw/gateway-client 2026.8.1-beta.1 → 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 +15 -3
- package/dist/browser-device-auth-Dz6H7eXy.d.mts +112 -0
- package/dist/browser.d.mts +5 -3
- package/dist/browser.mjs +2 -2
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +324 -163
- package/dist/{protocol-client-U142L0j3.d.mts → protocol-client-BbN-I582.d.mts} +15 -30
- package/dist/protocol-request-D4mVhLxE.d.mts +32 -0
- package/dist/{readiness-uSDrqxDM.d.mts → readiness--KX7i8Mq.d.mts} +24 -12
- 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-Das1PATe.d.mts → session-subscriptions-BTNpXybo.d.mts} +6 -111
- package/dist/{session-subscriptions-D4Xv60cN.mjs → session-subscriptions-uKmTyc3P.mjs} +270 -152
- 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);
|
|
@@ -177,12 +177,18 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
177
177
|
async acceptHello(hello, plan) {
|
|
178
178
|
const token = hello.auth?.deviceToken?.trim();
|
|
179
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
|
+
});
|
|
180
186
|
await this.deps.tokenStore.store({
|
|
181
187
|
clientId: plan.clientId,
|
|
182
188
|
deviceId: plan.identity.deviceId,
|
|
183
|
-
role
|
|
189
|
+
role,
|
|
184
190
|
token,
|
|
185
|
-
scopes: hello.auth?.scopes ?? []
|
|
191
|
+
scopes: stored?.token === token ? stored.scopes : hello.auth?.scopes ?? []
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
194
|
async clearStoredToken(plan) {
|
|
@@ -195,6 +201,29 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
195
201
|
}
|
|
196
202
|
};
|
|
197
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
|
|
198
227
|
//#region packages/retry/src/index.ts
|
|
199
228
|
const MAX_TIMER_TIMEOUT_MS = 2147e6;
|
|
200
229
|
function computeBackoff(policy, attempt) {
|
|
@@ -215,7 +244,9 @@ async function sleepWithAbort(ms, abortSignal, options = {}) {
|
|
|
215
244
|
if (timer) clearTimeout(timer);
|
|
216
245
|
timer = null;
|
|
217
246
|
cleanup();
|
|
218
|
-
|
|
247
|
+
const error = new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") });
|
|
248
|
+
error.name = "AbortError";
|
|
249
|
+
reject(error);
|
|
219
250
|
};
|
|
220
251
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
221
252
|
if (abortSignal?.aborted) {
|
|
@@ -277,24 +308,21 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
277
308
|
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
278
309
|
setTimeout(resolve, ms);
|
|
279
310
|
});
|
|
280
|
-
function asFiniteNumber(value) {
|
|
281
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
282
|
-
}
|
|
283
311
|
function clampNumber(value, fallback, min, max) {
|
|
284
|
-
const next =
|
|
312
|
+
const next = Number.isFinite(value) ? value : void 0;
|
|
285
313
|
if (next === void 0) return fallback;
|
|
286
314
|
return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
|
|
287
315
|
}
|
|
288
316
|
function resolveAttemptCount(value, fallback) {
|
|
289
|
-
return Math.max(1, Math.round(
|
|
317
|
+
return Math.max(1, Math.round(Number.isFinite(value) ? value : fallback));
|
|
290
318
|
}
|
|
291
319
|
function resolveRetryDelayMs(value) {
|
|
292
|
-
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;
|
|
293
321
|
return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
|
|
294
322
|
}
|
|
295
323
|
function resolveJitterConfig(value, fallback) {
|
|
296
324
|
if (value === "full") return "full";
|
|
297
|
-
const fraction =
|
|
325
|
+
const fraction = Number.isFinite(value) ? value : void 0;
|
|
298
326
|
return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
|
|
299
327
|
}
|
|
300
328
|
function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
|
|
@@ -416,6 +444,165 @@ var GatewayProtocolRequestError = class extends Error {
|
|
|
416
444
|
this.retryAfterMs = error.retryAfterMs;
|
|
417
445
|
}
|
|
418
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
|
+
};
|
|
419
606
|
//#endregion
|
|
420
607
|
//#region packages/gateway-client/src/protocol-client.ts
|
|
421
608
|
/**
|
|
@@ -426,7 +613,6 @@ var GatewayProtocolClient = class {
|
|
|
426
613
|
constructor(opts) {
|
|
427
614
|
this.opts = opts;
|
|
428
615
|
this.socket = null;
|
|
429
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
430
616
|
this.listeners = new GatewayEventListeners();
|
|
431
617
|
this.stopped = true;
|
|
432
618
|
this.generation = 0;
|
|
@@ -445,18 +631,28 @@ var GatewayProtocolClient = class {
|
|
|
445
631
|
factor: opts.reconnect.multiplier,
|
|
446
632
|
jitter: 0
|
|
447
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
|
+
});
|
|
448
644
|
}
|
|
449
645
|
get connected() {
|
|
450
646
|
return this.socket?.isOpen() ?? false;
|
|
451
647
|
}
|
|
452
648
|
get hasPendingRequests() {
|
|
453
|
-
return this.
|
|
649
|
+
return this.requests.hasPending;
|
|
454
650
|
}
|
|
455
651
|
get connecting() {
|
|
456
652
|
return this.connectSent && !this.helloReceived;
|
|
457
653
|
}
|
|
458
654
|
get hasUnboundedPendingRequests() {
|
|
459
|
-
return
|
|
655
|
+
return this.requests.hasUnboundedPending;
|
|
460
656
|
}
|
|
461
657
|
start() {
|
|
462
658
|
if (this.socket || this.reconnectSignal) return;
|
|
@@ -477,71 +673,14 @@ var GatewayProtocolClient = class {
|
|
|
477
673
|
this.socket = null;
|
|
478
674
|
this.connectFailure = void 0;
|
|
479
675
|
this.connectTiming = null;
|
|
480
|
-
this.
|
|
676
|
+
this.requests.flush(/* @__PURE__ */ new Error("gateway client stopped"));
|
|
481
677
|
socket?.close();
|
|
482
678
|
}
|
|
483
679
|
request(method, params, options) {
|
|
484
680
|
const socket = this.socket;
|
|
485
681
|
if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
|
|
486
682
|
if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
|
|
487
|
-
|
|
488
|
-
const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
|
|
489
|
-
return new Promise((resolve, reject) => {
|
|
490
|
-
let timeout;
|
|
491
|
-
let requestSent = false;
|
|
492
|
-
const pending = {
|
|
493
|
-
resolve: (value) => resolve(value),
|
|
494
|
-
reject,
|
|
495
|
-
expectFinal: options?.expectFinal === true,
|
|
496
|
-
acceptedNotified: false,
|
|
497
|
-
onAccepted: options?.onAccepted,
|
|
498
|
-
unbounded: timeoutMs === void 0,
|
|
499
|
-
method,
|
|
500
|
-
startedAtMs: this.nowMs()
|
|
501
|
-
};
|
|
502
|
-
const onAbort = () => {
|
|
503
|
-
this.pending.delete(id);
|
|
504
|
-
pending.cleanup?.();
|
|
505
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
|
|
506
|
-
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
507
|
-
};
|
|
508
|
-
const cleanup = () => {
|
|
509
|
-
if (timeout) clearTimeout(timeout);
|
|
510
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
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 && timeoutMs >= 0) {
|
|
518
|
-
timeout = setTimeout(() => {
|
|
519
|
-
if (this.pending.get(id) !== pending) return;
|
|
520
|
-
this.pending.delete(id);
|
|
521
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
522
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
|
|
523
|
-
reject(this.opts.createRequestTimeoutError?.(method, timeoutMs, requestSent) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
|
|
524
|
-
}, timeoutMs);
|
|
525
|
-
timeout.unref?.();
|
|
526
|
-
}
|
|
527
|
-
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
528
|
-
this.pending.set(id, pending);
|
|
529
|
-
try {
|
|
530
|
-
socket.send(JSON.stringify({
|
|
531
|
-
type: "req",
|
|
532
|
-
id,
|
|
533
|
-
method,
|
|
534
|
-
params
|
|
535
|
-
}));
|
|
536
|
-
requestSent = true;
|
|
537
|
-
this.invoke("sent", () => options?.onSent?.());
|
|
538
|
-
} catch (error) {
|
|
539
|
-
this.pending.delete(id);
|
|
540
|
-
cleanup();
|
|
541
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
|
|
542
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
543
|
-
}
|
|
544
|
-
});
|
|
683
|
+
return this.requests.request(socket, method, params, options);
|
|
545
684
|
}
|
|
546
685
|
addEventListener(listener) {
|
|
547
686
|
return this.listeners.add(listener);
|
|
@@ -758,28 +897,7 @@ var GatewayProtocolClient = class {
|
|
|
758
897
|
}
|
|
759
898
|
if (!isGatewayResponseFrame(parsed)) return;
|
|
760
899
|
this.opts.onActivity?.();
|
|
761
|
-
this.handleResponse(parsed);
|
|
762
|
-
}
|
|
763
|
-
handleResponse(frame) {
|
|
764
|
-
const pending = this.pending.get(frame.id);
|
|
765
|
-
if (!pending) return;
|
|
766
|
-
const status = frame.payload?.status;
|
|
767
|
-
if (pending.expectFinal && status === "accepted") {
|
|
768
|
-
if (!pending.acceptedNotified) {
|
|
769
|
-
pending.acceptedNotified = true;
|
|
770
|
-
this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
|
|
771
|
-
}
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
this.pending.delete(frame.id);
|
|
775
|
-
pending.cleanup?.();
|
|
776
|
-
if (frame.ok) {
|
|
777
|
-
this.finishRequestTiming(frame.id, pending, true);
|
|
778
|
-
pending.resolve(frame.payload);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
|
|
782
|
-
pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
|
|
900
|
+
this.requests.handleResponse(parsed);
|
|
783
901
|
}
|
|
784
902
|
handleClose(socket, generation, code, reason) {
|
|
785
903
|
if (this.socket !== socket) {
|
|
@@ -807,34 +925,15 @@ var GatewayProtocolClient = class {
|
|
|
807
925
|
};
|
|
808
926
|
this.connectFailure = void 0;
|
|
809
927
|
const decision = this.opts.resolveClose(context);
|
|
810
|
-
this.
|
|
928
|
+
this.requests.flush(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
|
|
811
929
|
this.invoke("close", () => this.opts.onClose?.(context, decision));
|
|
812
930
|
if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
|
|
813
931
|
}
|
|
814
932
|
handleSocketError(socket, generation, error) {
|
|
815
933
|
if (!this.isActive(socket, generation) || this.connectSent) return;
|
|
934
|
+
this.connectFailure = { error };
|
|
816
935
|
this.opts.onConnectError?.(error);
|
|
817
936
|
}
|
|
818
|
-
flushRequests(error) {
|
|
819
|
-
for (const [id, pending] of this.pending) {
|
|
820
|
-
this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
|
|
821
|
-
pending.cleanup?.();
|
|
822
|
-
pending.reject(error);
|
|
823
|
-
}
|
|
824
|
-
this.pending.clear();
|
|
825
|
-
}
|
|
826
|
-
finishRequestTiming(id, pending, ok, errorCode) {
|
|
827
|
-
const endedAtMs = this.nowMs();
|
|
828
|
-
this.invoke("request timing", () => this.opts.onRequestTiming?.({
|
|
829
|
-
id,
|
|
830
|
-
method: pending.method,
|
|
831
|
-
ok,
|
|
832
|
-
durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
|
|
833
|
-
startedAtMs: pending.startedAtMs,
|
|
834
|
-
endedAtMs,
|
|
835
|
-
errorCode
|
|
836
|
-
}));
|
|
837
|
-
}
|
|
838
937
|
scheduleReconnect(overrideMs) {
|
|
839
938
|
if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
|
|
840
939
|
const retry = this.reconnectSupervisor.next();
|
|
@@ -884,6 +983,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
|
884
983
|
ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
|
|
885
984
|
ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
|
|
886
985
|
ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
|
|
986
|
+
ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
|
|
887
987
|
ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
|
888
988
|
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
|
889
989
|
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
|
|
@@ -897,7 +997,47 @@ function shouldPauseGatewayReconnect(params) {
|
|
|
897
997
|
return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
|
|
898
998
|
}
|
|
899
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
|
|
900
1039
|
//#region packages/gateway-client/src/session-projection.ts
|
|
1040
|
+
/** Browser-safe identity and replay rules shared by Gateway conversation clients. */
|
|
901
1041
|
const MAX_TRACKED_SESSION_RUNS = 200;
|
|
902
1042
|
const RETAINED_SESSION_RUNS = 150;
|
|
903
1043
|
const SESSION_PROJECTION_SCOPE_KEYS = [
|
|
@@ -1011,13 +1151,16 @@ function sameTranscriptIdentity(left, right) {
|
|
|
1011
1151
|
}
|
|
1012
1152
|
function entryMatches(left, right, allowSnapshotPromotion = false) {
|
|
1013
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;
|
|
1014
1157
|
const persisted = left.identity;
|
|
1015
1158
|
const observed = right.identity;
|
|
1016
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;
|
|
1017
1160
|
if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
|
|
1018
1161
|
const pending = left.pending ? left : right.pending ? right : null;
|
|
1019
1162
|
const authoritative = pending === left ? right : pending === right ? left : null;
|
|
1020
|
-
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));
|
|
1021
1164
|
}
|
|
1022
1165
|
function withEntries(state, entries) {
|
|
1023
1166
|
return {
|
|
@@ -1199,7 +1342,13 @@ function reduceSessionProjection(state, event) {
|
|
|
1199
1342
|
const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
|
|
1200
1343
|
const incoming = createEntry(event.message, { pendingRunId });
|
|
1201
1344
|
if (!pendingRunId || !incoming.identity) return state;
|
|
1202
|
-
|
|
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));
|
|
1203
1352
|
}
|
|
1204
1353
|
case "sendAcknowledged": {
|
|
1205
1354
|
const runId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
|
|
@@ -1245,38 +1394,7 @@ function reduceSessionProjection(state, event) {
|
|
|
1245
1394
|
}
|
|
1246
1395
|
/** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
|
|
1247
1396
|
function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
|
|
1248
|
-
|
|
1249
|
-
const eventState = event.state;
|
|
1250
|
-
if (!runId || typeof eventState !== "string" || ![
|
|
1251
|
-
"delta",
|
|
1252
|
-
"final",
|
|
1253
|
-
"error",
|
|
1254
|
-
"aborted"
|
|
1255
|
-
].includes(eventState)) return null;
|
|
1256
|
-
const message = event.message;
|
|
1257
|
-
const stopReason = readNonemptyString(event.stopReason) ?? readNonemptyString(readRecord(message)?.stopReason);
|
|
1258
|
-
const errorKind = readNonemptyString(event.errorKind);
|
|
1259
|
-
const base = {
|
|
1260
|
-
runId,
|
|
1261
|
-
...message === void 0 ? {} : { message },
|
|
1262
|
-
scope
|
|
1263
|
-
};
|
|
1264
|
-
const next = reduceSessionProjection(projection, eventState === "delta" ? {
|
|
1265
|
-
type: "runDelta",
|
|
1266
|
-
...base
|
|
1267
|
-
} : {
|
|
1268
|
-
type: "runTerminal",
|
|
1269
|
-
...base,
|
|
1270
|
-
status: eventState === "aborted" ? "aborted" : eventState === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
|
|
1271
|
-
...stopReason === null ? {} : { stopReason },
|
|
1272
|
-
...errorKind === null ? {} : { errorKind },
|
|
1273
|
-
...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
|
|
1274
|
-
});
|
|
1275
|
-
return {
|
|
1276
|
-
projection: next,
|
|
1277
|
-
previousRun: projection.runs[runId],
|
|
1278
|
-
currentRun: next.runs[runId]
|
|
1279
|
-
};
|
|
1397
|
+
return reduceSessionProjectionRunEventImpl(projection, event, scope);
|
|
1280
1398
|
}
|
|
1281
1399
|
//#endregion
|
|
1282
1400
|
//#region packages/gateway-client/src/session-subscriptions.ts
|
|
@@ -1484,4 +1602,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
|
|
|
1484
1602
|
return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
|
|
1485
1603
|
}
|
|
1486
1604
|
//#endregion
|
|
1487
|
-
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/gateway-client",
|
|
3
|
-
"version": "2026.8.1-beta.
|
|
3
|
+
"version": "2026.8.1-beta.2",
|
|
4
4
|
"description": "Reference WebSocket client for the OpenClaw Gateway protocol",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client",
|
|
@@ -45,16 +45,26 @@
|
|
|
45
45
|
"import": "./dist/readiness.mjs",
|
|
46
46
|
"default": "./dist/readiness.mjs"
|
|
47
47
|
},
|
|
48
|
+
"./scope-upgrade": {
|
|
49
|
+
"types": "./dist/scope-upgrade.d.mts",
|
|
50
|
+
"import": "./dist/scope-upgrade.mjs",
|
|
51
|
+
"default": "./dist/scope-upgrade.mjs"
|
|
52
|
+
},
|
|
48
53
|
"./timeouts": {
|
|
49
54
|
"types": "./dist/timeouts.d.mts",
|
|
50
55
|
"import": "./dist/timeouts.mjs",
|
|
51
56
|
"default": "./dist/timeouts.mjs"
|
|
57
|
+
},
|
|
58
|
+
"./websocket-data": {
|
|
59
|
+
"types": "./dist/websocket-data.d.mts",
|
|
60
|
+
"import": "./dist/websocket-data.mjs",
|
|
61
|
+
"default": "./dist/websocket-data.mjs"
|
|
52
62
|
}
|
|
53
63
|
},
|
|
54
64
|
"dependencies": {
|
|
55
65
|
"ipaddr.js": "2.4.0",
|
|
56
66
|
"ws": "8.21.1",
|
|
57
|
-
"@openclaw/gateway-protocol": "2026.8.1-beta.
|
|
67
|
+
"@openclaw/gateway-protocol": "2026.8.1-beta.2"
|
|
58
68
|
},
|
|
59
69
|
"engines": {
|
|
60
70
|
"node": ">=22.19.0"
|
|
@@ -68,6 +78,6 @@
|
|
|
68
78
|
}
|
|
69
79
|
},
|
|
70
80
|
"scripts": {
|
|
71
|
-
"build": "tsdown src/index.ts src/browser.ts src/readiness.ts src/timeouts.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
|
81
|
+
"build": "tsdown src/index.ts src/browser.ts src/readiness.ts src/scope-upgrade.ts src/timeouts.ts src/websocket-data.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
|
72
82
|
}
|
|
73
83
|
}
|