@openclaw/gateway-client 0.0.0 → 2026.7.2-beta.4

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.
@@ -0,0 +1,850 @@
1
+ import { ConnectErrorDetailCodes, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails } from "@openclaw/gateway-protocol/connect-error-details";
2
+ import { isGatewayEventFrame, isGatewayResponseFrame } from "@openclaw/gateway-protocol/frame-guards";
3
+ //#region packages/gateway-client/src/device-auth.ts
4
+ function normalizeDeviceMetadataForAuth(value) {
5
+ if (typeof value !== "string") return "";
6
+ const trimmed = value.trim();
7
+ if (!trimmed) return "";
8
+ return trimmed.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
9
+ }
10
+ function buildDeviceAuthPayload(params) {
11
+ const scopes = params.scopes.join(",");
12
+ const token = params.token ?? "";
13
+ return [
14
+ "v2",
15
+ params.deviceId,
16
+ params.clientId,
17
+ params.clientMode,
18
+ params.role,
19
+ scopes,
20
+ String(params.signedAtMs),
21
+ token,
22
+ params.nonce
23
+ ].join("|");
24
+ }
25
+ function buildDeviceAuthPayloadV3(params) {
26
+ const scopes = params.scopes.join(",");
27
+ const token = params.token ?? "";
28
+ const platform = normalizeDeviceMetadataForAuth(params.platform);
29
+ const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily);
30
+ return [
31
+ "v3",
32
+ params.deviceId,
33
+ params.clientId,
34
+ params.clientMode,
35
+ params.role,
36
+ scopes,
37
+ String(params.signedAtMs),
38
+ token,
39
+ params.nonce,
40
+ platform,
41
+ deviceFamily
42
+ ].join("|");
43
+ }
44
+ //#endregion
45
+ //#region packages/gateway-client/src/connect-auth.ts
46
+ function normalized(value) {
47
+ return typeof value === "string" ? value.trim() || void 0 : void 0;
48
+ }
49
+ function selectGatewayConnectAuth(params) {
50
+ const authToken = normalized(params.token);
51
+ const bootstrapToken = normalized(params.bootstrapToken);
52
+ const explicitDeviceToken = normalized(params.deviceToken);
53
+ const authPassword = normalized(params.password);
54
+ const storedToken = normalized(params.storedToken);
55
+ const stored = {
56
+ storedToken,
57
+ storedScopes: params.storedScopes
58
+ };
59
+ if (params.preferBootstrapToken && bootstrapToken) return {
60
+ authBootstrapToken: bootstrapToken,
61
+ authPassword,
62
+ ...stored
63
+ };
64
+ const useRetryToken = params.pendingDeviceTokenRetry === true && !explicitDeviceToken && Boolean(authToken && storedToken && params.trustedDeviceTokenRetry);
65
+ const resolvedDeviceToken = explicitDeviceToken ?? (useRetryToken || !(authToken || authPassword) && (!bootstrapToken || storedToken) ? storedToken : void 0);
66
+ const usingStoredDeviceToken = Boolean(resolvedDeviceToken && !explicitDeviceToken && storedToken) && resolvedDeviceToken === storedToken;
67
+ const selectedToken = authToken ?? resolvedDeviceToken;
68
+ const authBootstrapToken = !authToken && !resolvedDeviceToken && !authPassword ? bootstrapToken : void 0;
69
+ return {
70
+ authToken: selectedToken,
71
+ authBootstrapToken,
72
+ authDeviceToken: useRetryToken ? storedToken : void 0,
73
+ authPassword,
74
+ authApprovalRuntimeToken: normalized(params.approvalRuntimeToken),
75
+ authAgentRuntimeIdentityToken: normalized(params.agentRuntimeIdentityToken),
76
+ signatureToken: selectedToken ?? authBootstrapToken,
77
+ resolvedDeviceToken,
78
+ usingStoredDeviceToken,
79
+ ...stored
80
+ };
81
+ }
82
+ function buildGatewayConnectAuth(selected) {
83
+ const auth = {
84
+ token: selected.authToken,
85
+ bootstrapToken: selected.authBootstrapToken,
86
+ deviceToken: selected.authDeviceToken ?? selected.resolvedDeviceToken,
87
+ password: selected.authPassword,
88
+ approvalRuntimeToken: selected.authApprovalRuntimeToken,
89
+ agentRuntimeIdentityToken: selected.authAgentRuntimeIdentityToken
90
+ };
91
+ return Object.values(auth).some(Boolean) ? auth : void 0;
92
+ }
93
+ function resolveGatewayConnectScopes(params) {
94
+ return params.requestedScopes ?? (params.usingStoredDeviceToken && params.storedScopes?.length ? params.storedScopes : [...params.defaultScopes]);
95
+ }
96
+ function shouldRetryGatewayWithDeviceToken(params) {
97
+ if (params.retryBudgetUsed || params.currentDeviceToken || !params.explicitToken || !params.storedToken || !params.trustedEndpoint) return false;
98
+ const advice = readConnectErrorRecoveryAdvice(params.errorDetails);
99
+ return params.canRetryWithDeviceTokenHint === true || advice.canRetryWithDeviceToken === true || advice.recommendedNextStep === "retry_with_device_token" || readConnectErrorDetailCode(params.errorDetails) === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH;
100
+ }
101
+ //#endregion
102
+ //#region packages/gateway-client/src/browser-device-auth.ts
103
+ /** Browser-safe device pairing and issued-token lifecycle shared by first-party UI clients. */
104
+ var GatewayBrowserDeviceAuthLifecycle = class {
105
+ constructor(deps) {
106
+ this.deps = deps;
107
+ }
108
+ async buildPlan(params) {
109
+ const identity = await this.deps.loadIdentity();
110
+ const stored = identity ? await this.deps.tokenStore.load({
111
+ clientId: params.client.id,
112
+ deviceId: identity.deviceId,
113
+ role: params.role
114
+ }) : null;
115
+ const storedValue = stored?.token;
116
+ const selectedAuth = selectGatewayConnectAuth({
117
+ token: params.token,
118
+ bootstrapToken: params.bootstrapToken,
119
+ password: params.password,
120
+ storedToken: storedValue,
121
+ storedScopes: stored?.scopes,
122
+ pendingDeviceTokenRetry: params.pendingDeviceTokenRetry,
123
+ trustedDeviceTokenRetry: params.trustedDeviceTokenRetry,
124
+ preferBootstrapToken: params.preferBootstrapToken
125
+ });
126
+ const { usingStoredDeviceToken } = selectedAuth;
127
+ const scopes = resolveGatewayConnectScopes({
128
+ requestedScopes: selectedAuth.authBootstrapToken ? params.bootstrapScopes ? [...params.bootstrapScopes] : void 0 : void 0,
129
+ usingStoredDeviceToken,
130
+ storedScopes: selectedAuth.storedScopes,
131
+ defaultScopes: params.defaultScopes
132
+ });
133
+ if (!identity) return {
134
+ clientId: params.client.id,
135
+ role: params.role,
136
+ identity,
137
+ selectedAuth,
138
+ scopes,
139
+ auth: buildGatewayConnectAuth(selectedAuth)
140
+ };
141
+ const signedAtMs = this.deps.nowMs?.() ?? Date.now();
142
+ const nonce = params.nonce ?? "";
143
+ const { authBootstrapToken: primary, signatureToken: signed } = selectedAuth;
144
+ let token = null;
145
+ if (primary) token = primary;
146
+ else if (signed) token = signed;
147
+ const payload = buildDeviceAuthPayloadV3({
148
+ deviceId: identity.deviceId,
149
+ clientId: params.client.id,
150
+ clientMode: params.client.mode,
151
+ role: params.role,
152
+ scopes,
153
+ signedAtMs,
154
+ token,
155
+ nonce,
156
+ platform: params.client.platform,
157
+ deviceFamily: params.client.deviceFamily
158
+ });
159
+ return {
160
+ clientId: params.client.id,
161
+ role: params.role,
162
+ identity,
163
+ selectedAuth,
164
+ scopes,
165
+ auth: buildGatewayConnectAuth(selectedAuth),
166
+ device: {
167
+ id: identity.deviceId,
168
+ publicKey: identity.publicKey,
169
+ signature: await identity.sign(payload),
170
+ signedAt: signedAtMs,
171
+ nonce
172
+ }
173
+ };
174
+ }
175
+ async acceptHello(hello, plan) {
176
+ const token = hello.auth?.deviceToken?.trim();
177
+ if (!token || !plan.identity) return;
178
+ await this.deps.tokenStore.store({
179
+ clientId: plan.clientId,
180
+ deviceId: plan.identity.deviceId,
181
+ role: hello.auth?.role ?? plan.role,
182
+ token,
183
+ scopes: hello.auth?.scopes ?? []
184
+ });
185
+ }
186
+ async clearStoredToken(plan) {
187
+ if (!plan.identity) return;
188
+ await this.deps.tokenStore.clear({
189
+ clientId: plan.clientId,
190
+ deviceId: plan.identity.deviceId,
191
+ role: plan.role
192
+ });
193
+ }
194
+ };
195
+ //#endregion
196
+ //#region packages/retry/src/index.ts
197
+ const MAX_TIMER_TIMEOUT_MS = 2147e6;
198
+ function computeBackoff(policy, attempt) {
199
+ const base = Math.min(policy.maxMs, policy.initialMs * policy.factor ** Math.max(attempt - 1, 0));
200
+ const jitter = base * policy.jitter * Math.random();
201
+ return Math.min(policy.maxMs, Math.round(base + jitter));
202
+ }
203
+ async function sleepWithAbort(ms, abortSignal, options = {}) {
204
+ if (!Number.isFinite(ms) || ms <= 0) return;
205
+ const delayMs = Math.min(Math.max(Math.floor(ms), 1), MAX_TIMER_TIMEOUT_MS);
206
+ await new Promise((resolve, reject) => {
207
+ let settled = false;
208
+ let timer = null;
209
+ const cleanup = () => abortSignal?.removeEventListener("abort", onAbort);
210
+ const onAbort = () => {
211
+ if (settled) return;
212
+ settled = true;
213
+ if (timer) clearTimeout(timer);
214
+ timer = null;
215
+ cleanup();
216
+ reject(new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") }));
217
+ };
218
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
219
+ if (abortSignal?.aborted) {
220
+ onAbort();
221
+ return;
222
+ }
223
+ timer = setTimeout(() => {
224
+ settled = true;
225
+ cleanup();
226
+ timer = null;
227
+ resolve();
228
+ }, delayMs);
229
+ if (options.ref === false) timer.unref?.();
230
+ if (abortSignal?.aborted) onAbort();
231
+ });
232
+ }
233
+ var RetrySupervisor = class {
234
+ constructor(policy, maxAttempts = Number.POSITIVE_INFINITY) {
235
+ this.policy = policy;
236
+ this.maxAttempts = maxAttempts;
237
+ this.attempts = 0;
238
+ this.initialMs = policy.initialMs;
239
+ }
240
+ reset(initialMs = this.policy.initialMs) {
241
+ this.cancel();
242
+ this.attempts = 0;
243
+ this.initialMs = initialMs;
244
+ this.nextDelayOverrideMs = void 0;
245
+ }
246
+ cancel(reason = /* @__PURE__ */ new Error("retry cancelled")) {
247
+ this.pendingAbort?.abort(reason);
248
+ this.pendingAbort = void 0;
249
+ }
250
+ next(abortSignal) {
251
+ const override = this.nextDelayOverrideMs;
252
+ this.nextDelayOverrideMs = void 0;
253
+ if (override === void 0 && ++this.attempts > Math.ceil(this.maxAttempts)) return;
254
+ const attempt = Math.max(this.attempts, 1);
255
+ const delayMs = override ?? computeBackoff({
256
+ ...this.policy,
257
+ initialMs: this.initialMs
258
+ }, attempt);
259
+ this.cancel();
260
+ const pendingAbort = new AbortController();
261
+ this.pendingAbort = pendingAbort;
262
+ return {
263
+ attempt,
264
+ delayMs,
265
+ signal: abortSignal ? AbortSignal.any([pendingAbort.signal, abortSignal]) : pendingAbort.signal
266
+ };
267
+ }
268
+ };
269
+ const DEFAULT_RETRY_CONFIG = {
270
+ attempts: 3,
271
+ minDelayMs: 300,
272
+ maxDelayMs: 3e4,
273
+ jitter: 0
274
+ };
275
+ const defaultSleep = (ms) => new Promise((resolve) => {
276
+ setTimeout(resolve, ms);
277
+ });
278
+ function asFiniteNumber(value) {
279
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
280
+ }
281
+ function clampNumber(value, fallback, min, max) {
282
+ const next = asFiniteNumber(value);
283
+ if (next === void 0) return fallback;
284
+ return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
285
+ }
286
+ function resolveAttemptCount(value, fallback) {
287
+ return Math.max(1, Math.round(asFiniteNumber(value) ?? fallback));
288
+ }
289
+ function resolveRetryDelayMs(value) {
290
+ const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : asFiniteNumber(value) ?? 0;
291
+ return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
292
+ }
293
+ function resolveJitterConfig(value, fallback) {
294
+ if (value === "full") return "full";
295
+ const fraction = asFiniteNumber(value);
296
+ return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
297
+ }
298
+ function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
299
+ const attempts = resolveAttemptCount(overrides?.attempts, defaults.attempts);
300
+ const minDelayMs = resolveRetryDelayMs(clampNumber(overrides?.minDelayMs, defaults.minDelayMs, 0));
301
+ return {
302
+ attempts,
303
+ minDelayMs,
304
+ maxDelayMs: Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0))),
305
+ jitter: resolveJitterConfig(overrides?.jitter, defaults.jitter)
306
+ };
307
+ }
308
+ function applyJitter(delayMs, jitter, mode, random) {
309
+ if (jitter === "full") {
310
+ if (mode === "symmetric") return Math.max(0, Math.round(delayMs * (.5 + random() * .5)));
311
+ return Math.max(0, Math.ceil(delayMs * (1 + random())));
312
+ }
313
+ if (jitter <= 0) return mode === "positive" ? Math.ceil(delayMs) : delayMs;
314
+ const fraction = random();
315
+ const raw = delayMs * (1 + (mode === "positive" ? fraction * jitter : (fraction * 2 - 1) * jitter));
316
+ return Math.max(0, mode === "positive" ? Math.ceil(raw) : Math.round(raw));
317
+ }
318
+ function toRetryError(value, fallbackMessage = "Non-Error thrown") {
319
+ if (value instanceof Error) return value;
320
+ if (typeof value === "string") return new Error(value);
321
+ const error = new Error(fallbackMessage, { cause: value });
322
+ if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
323
+ return error;
324
+ }
325
+ function createRetryRunner(runtime = {}) {
326
+ const runtimeSleep = runtime.sleep ?? defaultSleep;
327
+ const runtimeRandom = runtime.random ?? Math.random;
328
+ const createFailure = runtime.createFailure ?? ((errors) => toRetryError(errors.at(-1) ?? /* @__PURE__ */ new Error("Retry failed")));
329
+ return async function retryAsync(fn, attemptsOrOptions = 3, initialDelayMs = 300) {
330
+ const attemptErrors = [];
331
+ if (typeof attemptsOrOptions === "number") {
332
+ const attempts = resolveAttemptCount(attemptsOrOptions, DEFAULT_RETRY_CONFIG.attempts);
333
+ for (let index = 0; index < attempts; index += 1) try {
334
+ return await fn();
335
+ } catch (err) {
336
+ attemptErrors.push(err);
337
+ if (index === attempts - 1) break;
338
+ await runtimeSleep(resolveRetryDelayMs(initialDelayMs * 2 ** index));
339
+ }
340
+ throw createFailure(attemptErrors);
341
+ }
342
+ const options = attemptsOrOptions;
343
+ const resolved = resolveRetryConfig(DEFAULT_RETRY_CONFIG, options);
344
+ const maxAttempts = resolved.attempts;
345
+ const minDelayMs = resolved.minDelayMs;
346
+ const maxDelayMs = resolved.maxDelayMs > 0 ? resolved.maxDelayMs : Number.POSITIVE_INFINITY;
347
+ const retryAfterMaxDelayMs = options.retryAfterMaxDelayMs === void 0 ? maxDelayMs : Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(options.retryAfterMaxDelayMs, maxDelayMs, 0)));
348
+ const random = options.random ?? runtimeRandom;
349
+ const sleep = options.sleep ?? runtimeSleep;
350
+ const shouldRetry = options.shouldRetry ?? (() => true);
351
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
352
+ return await fn();
353
+ } catch (err) {
354
+ attemptErrors.push(err);
355
+ if (attempt >= maxAttempts || !shouldRetry(err, attempt)) break;
356
+ const context = {
357
+ attempt,
358
+ maxAttempts,
359
+ err,
360
+ label: options.label
361
+ };
362
+ const retryAfterMs = options.retryAfterMs?.(err);
363
+ const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs);
364
+ const configuredDelay = typeof options.delayMs === "function" ? options.delayMs(context) : options.delayMs;
365
+ const resolvedConfiguredDelay = configuredDelay === void 0 ? void 0 : resolveRetryDelayMs(configuredDelay);
366
+ const baseDelay = hasRetryAfter ? Math.max(retryAfterMs, minDelayMs) : resolvedConfiguredDelay === void 0 ? minDelayMs * 2 ** (attempt - 1) : Math.max(resolvedConfiguredDelay, minDelayMs);
367
+ const delayCap = hasRetryAfter ? retryAfterMaxDelayMs : maxDelayMs;
368
+ let delay = Math.min(baseDelay, delayCap);
369
+ const canHonorRetryAfter = hasRetryAfter && (retryAfterMs ?? 0) <= delayCap;
370
+ const wantsPositiveDraw = resolved.jitter === "full" ? !hasRetryAfter || canHonorRetryAfter : canHonorRetryAfter;
371
+ delay = applyJitter(delay, resolved.jitter, wantsPositiveDraw ? "positive" : "symmetric", random);
372
+ delay = Math.min(Math.max(delay, minDelayMs), delayCap);
373
+ await options.onRetry?.({
374
+ ...context,
375
+ delayMs: delay
376
+ });
377
+ if (delay > 0) await sleep(delay);
378
+ }
379
+ throw createFailure(attemptErrors);
380
+ };
381
+ }
382
+ createRetryRunner();
383
+ //#endregion
384
+ //#region packages/gateway-client/src/protocol-client.ts
385
+ var GatewayProtocolRequestError = class extends Error {
386
+ constructor(error) {
387
+ super(error.message ?? "request failed");
388
+ this.name = "GatewayProtocolRequestError";
389
+ this.code = error.code ?? "UNAVAILABLE";
390
+ this.gatewayCode = this.code;
391
+ this.details = error.details;
392
+ this.retryable = error.retryable === true;
393
+ this.retryAfterMs = error.retryAfterMs;
394
+ }
395
+ };
396
+ /**
397
+ * Browser-safe gateway wire client. Environment adapters own transport and auth
398
+ * policy; this class owns the single socket/handshake/reconnect/frame state machine.
399
+ */
400
+ var GatewayProtocolClient = class {
401
+ constructor(opts) {
402
+ this.opts = opts;
403
+ this.socket = null;
404
+ this.pending = /* @__PURE__ */ new Map();
405
+ this.listeners = /* @__PURE__ */ new Set();
406
+ this.stopped = true;
407
+ this.generation = 0;
408
+ this.lastSeq = null;
409
+ this.connectNonce = null;
410
+ this.connectSent = false;
411
+ this.connectRequestSent = false;
412
+ this.handshakeTimer = null;
413
+ this.socketOpened = false;
414
+ this.helloReceived = false;
415
+ this.connectTiming = null;
416
+ this.reconnectSupervisor = new RetrySupervisor({
417
+ initialMs: opts.reconnect.initialMs,
418
+ maxMs: opts.reconnect.maxMs,
419
+ factor: opts.reconnect.multiplier,
420
+ jitter: 0
421
+ });
422
+ }
423
+ get connected() {
424
+ return this.socket?.isOpen() ?? false;
425
+ }
426
+ get hasPendingRequests() {
427
+ return this.pending.size > 0;
428
+ }
429
+ get connecting() {
430
+ return this.connectSent && !this.helloReceived;
431
+ }
432
+ get hasUnboundedPendingRequests() {
433
+ return [...this.pending.values()].some((pending) => pending.unbounded);
434
+ }
435
+ start() {
436
+ this.stopped = false;
437
+ this.reconnectSupervisor.cancel();
438
+ this.connect();
439
+ }
440
+ stop() {
441
+ this.stopped = true;
442
+ this.clearHandshakeTimer();
443
+ this.reconnectSupervisor.reset();
444
+ const socket = this.socket;
445
+ if (socket && this.opts.notifyStoppedClose) this.stoppedSocket = {
446
+ socket,
447
+ context: this.closeContext()
448
+ };
449
+ this.socket = null;
450
+ this.connectFailure = void 0;
451
+ this.connectTiming = null;
452
+ this.flushRequests(/* @__PURE__ */ new Error("gateway client stopped"));
453
+ socket?.close();
454
+ }
455
+ request(method, params, options) {
456
+ const socket = this.socket;
457
+ if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
458
+ if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
459
+ const id = this.opts.createRequestId();
460
+ const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
461
+ return new Promise((resolve, reject) => {
462
+ let timeout;
463
+ const pending = {
464
+ resolve: (value) => resolve(value),
465
+ reject,
466
+ expectFinal: options?.expectFinal === true,
467
+ acceptedNotified: false,
468
+ onAccepted: options?.onAccepted,
469
+ unbounded: timeoutMs === void 0,
470
+ method,
471
+ startedAtMs: this.nowMs()
472
+ };
473
+ const onAbort = () => {
474
+ this.pending.delete(id);
475
+ if (timeout) clearTimeout(timeout);
476
+ this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
477
+ reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
478
+ };
479
+ const cleanup = () => {
480
+ if (timeout) clearTimeout(timeout);
481
+ options?.signal?.removeEventListener("abort", onAbort);
482
+ };
483
+ if (options?.signal?.aborted) {
484
+ reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
485
+ return;
486
+ }
487
+ pending.cleanup = cleanup;
488
+ if (timeoutMs !== void 0 && timeoutMs >= 0) {
489
+ timeout = setTimeout(() => {
490
+ this.pending.delete(id);
491
+ options?.signal?.removeEventListener("abort", onAbort);
492
+ this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
493
+ reject(this.opts.createRequestTimeoutError?.(method, timeoutMs) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
494
+ }, timeoutMs);
495
+ timeout.unref?.();
496
+ }
497
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
498
+ this.pending.set(id, pending);
499
+ try {
500
+ socket.send(JSON.stringify({
501
+ type: "req",
502
+ id,
503
+ method,
504
+ params
505
+ }));
506
+ this.invoke("sent", () => options?.onSent?.());
507
+ } catch (error) {
508
+ this.pending.delete(id);
509
+ cleanup();
510
+ this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
511
+ reject(error instanceof Error ? error : new Error(String(error)));
512
+ }
513
+ });
514
+ }
515
+ addEventListener(listener) {
516
+ this.listeners.add(listener);
517
+ return () => this.listeners.delete(listener);
518
+ }
519
+ closeSocket(code, reason) {
520
+ this.socket?.close(code, reason);
521
+ }
522
+ resetReconnectBackoff(initialMs) {
523
+ this.reconnectSupervisor.reset(initialMs);
524
+ }
525
+ recordTiming(phase, generation, plan, detail) {
526
+ const now = this.nowMs();
527
+ const state = this.connectTiming;
528
+ if (!state || state.generation !== generation) return;
529
+ state.hasChallenge ||= phase === "challenge";
530
+ state.usedFallback ||= phase === "fallback";
531
+ this.invoke("connect timing", () => this.opts.onTiming?.({
532
+ phase,
533
+ generation,
534
+ durationMs: Math.max(0, now - state.startedAtMs),
535
+ phaseDurationMs: Math.max(0, now - state.lastAtMs),
536
+ hasChallenge: state.hasChallenge,
537
+ usedFallback: state.usedFallback,
538
+ plan,
539
+ detail
540
+ }));
541
+ state.lastAtMs = now;
542
+ if (phase === "hello" || phase === "failed") this.connectTiming = null;
543
+ }
544
+ connect() {
545
+ if (this.stopped) return;
546
+ const generation = this.generation + 1;
547
+ this.connectNonce = null;
548
+ this.connectSent = false;
549
+ this.connectRequestSent = false;
550
+ this.socketOpened = false;
551
+ this.helloReceived = false;
552
+ this.connectFailure = void 0;
553
+ let socket;
554
+ try {
555
+ socket = this.opts.createSocket({
556
+ open: () => this.handleOpen(socket, generation),
557
+ message: (data) => this.handleMessage(socket, generation, data),
558
+ close: (code, reason) => this.handleClose(socket, generation, code, reason),
559
+ error: (error) => this.handleSocketError(socket, generation, error)
560
+ });
561
+ } catch (error) {
562
+ const normalized = error instanceof Error ? error : new Error(String(error));
563
+ this.opts.onSocketFactoryError?.(normalized);
564
+ this.opts.onConnectError?.(normalized);
565
+ if (this.opts.rethrowSocketFactoryError?.(normalized)) throw normalized;
566
+ return;
567
+ }
568
+ this.generation = generation;
569
+ this.socket = socket;
570
+ const now = this.nowMs();
571
+ this.connectTiming = {
572
+ generation,
573
+ startedAtMs: now,
574
+ lastAtMs: now,
575
+ hasChallenge: false,
576
+ usedFallback: false
577
+ };
578
+ }
579
+ handleOpen(socket, generation) {
580
+ if (!this.isActive(socket, generation)) return;
581
+ this.socketOpened = true;
582
+ this.recordTiming("socket-open", generation);
583
+ if (this.connectNonce) {
584
+ this.sendConnect(socket, generation);
585
+ return;
586
+ }
587
+ this.armHandshakeTimer(socket, generation);
588
+ }
589
+ armHandshakeTimer(socket, generation) {
590
+ this.clearHandshakeTimer();
591
+ const armedAt = Date.now();
592
+ this.handshakeTimer = setTimeout(() => {
593
+ this.handshakeTimer = null;
594
+ if (!this.isActive(socket, generation) || this.connectSent || !socket.isOpen()) return;
595
+ if (this.opts.handshake.mode === "fallback") {
596
+ this.recordTiming("fallback", generation);
597
+ this.sendConnect(socket, generation);
598
+ return;
599
+ }
600
+ const elapsedMs = Date.now() - armedAt;
601
+ const error = new Error(this.opts.handshake.timeoutMessage?.(elapsedMs) ?? `gateway connect challenge timeout after ${elapsedMs}ms`);
602
+ this.opts.onConnectError?.(error);
603
+ socket.close(1008, "connect challenge timeout");
604
+ }, this.opts.handshake.timeoutMs);
605
+ this.handshakeTimer.unref?.();
606
+ }
607
+ sendConnect(socket, generation) {
608
+ if (!this.isActive(socket, generation) || !socket.isOpen() || this.connectSent) return;
609
+ this.connectSent = true;
610
+ this.clearHandshakeTimer();
611
+ let planOrPromise;
612
+ try {
613
+ planOrPromise = this.opts.buildConnectPlan({
614
+ nonce: this.connectNonce,
615
+ generation
616
+ });
617
+ } catch (error) {
618
+ this.handleConnectPlanError(socket, generation, error);
619
+ return;
620
+ }
621
+ if (planOrPromise instanceof Promise) {
622
+ planOrPromise.then((plan) => this.sendConnectPlan(socket, generation, plan)).catch((error) => this.handleConnectPlanError(socket, generation, error));
623
+ return;
624
+ }
625
+ this.sendConnectPlan(socket, generation, planOrPromise);
626
+ }
627
+ handleConnectPlanError(socket, generation, error) {
628
+ if (!this.isActive(socket, generation)) return;
629
+ const normalized = error instanceof Error ? error : new Error(String(error));
630
+ const outcome = this.opts.onConnectPlanError?.(normalized) ?? {
631
+ closeCode: 1008,
632
+ closeReason: "connect failed"
633
+ };
634
+ this.opts.onConnectError?.(outcome.error ?? normalized);
635
+ if (outcome.stop) this.stopped = true;
636
+ socket.close(outcome.closeCode, outcome.closeReason);
637
+ }
638
+ sendConnectPlan(socket, generation, plan) {
639
+ if (!this.isActive(socket, generation) || !socket.isOpen()) return;
640
+ const context = {
641
+ generation,
642
+ nonce: this.connectNonce,
643
+ plan
644
+ };
645
+ this.recordTiming("connect-plan-ready", generation, plan);
646
+ this.recordTiming("request-sent", generation, plan);
647
+ this.connectRequestSent = true;
648
+ this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
649
+ if (!this.isActive(socket, generation)) return;
650
+ this.helloReceived = true;
651
+ this.connectFailure = void 0;
652
+ this.reconnectSupervisor.reset();
653
+ this.recordTiming("hello", generation, plan);
654
+ this.opts.onConnectHello?.(hello, context);
655
+ this.invoke("hello", () => this.opts.onHello?.(hello));
656
+ }).catch((error) => {
657
+ if (!this.isActive(socket, generation)) return;
658
+ const requestError = error instanceof GatewayProtocolRequestError ? error : new GatewayProtocolRequestError({ message: String(error) });
659
+ const outcome = this.opts.onConnectFailure?.(requestError, context) ?? {
660
+ closeCode: 1008,
661
+ closeReason: "connect failed"
662
+ };
663
+ this.connectFailure = {
664
+ error: requestError,
665
+ reconnectDelayMs: outcome.reconnectDelayMs
666
+ };
667
+ if (outcome.stop) this.stopped = true;
668
+ socket.close(outcome.closeCode, outcome.closeReason);
669
+ });
670
+ }
671
+ handleMessage(socket, generation, raw) {
672
+ if (!this.isActive(socket, generation)) return;
673
+ let parsed;
674
+ try {
675
+ parsed = JSON.parse(raw);
676
+ } catch (error) {
677
+ this.opts.onParseError?.(error);
678
+ return;
679
+ }
680
+ if (isGatewayEventFrame(parsed)) {
681
+ this.opts.onActivity?.();
682
+ if (parsed.event === "connect.challenge") {
683
+ const payload = parsed.payload;
684
+ const nonce = typeof payload?.nonce === "string" ? payload.nonce.trim() : "";
685
+ if (!nonce) {
686
+ if (this.opts.handshake.mode === "require-challenge") {
687
+ const error = /* @__PURE__ */ new Error("gateway connect challenge missing nonce");
688
+ this.opts.onConnectError?.(error);
689
+ socket.close(1008, "connect challenge missing nonce");
690
+ }
691
+ return;
692
+ }
693
+ this.connectNonce = nonce;
694
+ this.recordTiming("challenge", generation);
695
+ this.sendConnect(socket, generation);
696
+ return;
697
+ }
698
+ const seq = typeof parsed.seq === "number" ? parsed.seq : null;
699
+ if (seq !== null) {
700
+ if (this.lastSeq !== null && seq > this.lastSeq + 1) {
701
+ const expected = this.lastSeq + 1;
702
+ this.invoke("gap", () => this.opts.onGap?.({
703
+ expected,
704
+ received: seq
705
+ }));
706
+ }
707
+ this.lastSeq = seq;
708
+ }
709
+ this.invoke("event", () => this.opts.onEvent?.(parsed));
710
+ for (const listener of this.listeners) this.invoke("event listener", () => listener(parsed));
711
+ return;
712
+ }
713
+ if (!isGatewayResponseFrame(parsed)) return;
714
+ this.opts.onActivity?.();
715
+ this.handleResponse(parsed);
716
+ }
717
+ handleResponse(frame) {
718
+ const pending = this.pending.get(frame.id);
719
+ if (!pending) return;
720
+ const status = frame.payload?.status;
721
+ if (pending.expectFinal && status === "accepted") {
722
+ if (!pending.acceptedNotified) {
723
+ pending.acceptedNotified = true;
724
+ this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
725
+ }
726
+ return;
727
+ }
728
+ this.pending.delete(frame.id);
729
+ pending.cleanup?.();
730
+ if (frame.ok) {
731
+ this.finishRequestTiming(frame.id, pending, true);
732
+ pending.resolve(frame.payload);
733
+ return;
734
+ }
735
+ this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
736
+ pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
737
+ }
738
+ handleClose(socket, generation, code, reason) {
739
+ if (this.socket !== socket) {
740
+ if (this.stoppedSocket?.socket === socket) {
741
+ const context = {
742
+ ...this.stoppedSocket.context,
743
+ code,
744
+ reason
745
+ };
746
+ this.stoppedSocket = void 0;
747
+ this.invoke("close", () => this.opts.onClose?.(context, {
748
+ retry: false,
749
+ notify: true
750
+ }));
751
+ }
752
+ return;
753
+ }
754
+ this.socket = null;
755
+ this.clearHandshakeTimer();
756
+ const context = {
757
+ ...this.closeContext(),
758
+ code,
759
+ reason,
760
+ generation
761
+ };
762
+ this.connectFailure = void 0;
763
+ const decision = this.opts.resolveClose(context);
764
+ this.flushRequests(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
765
+ this.invoke("close", () => this.opts.onClose?.(context, decision));
766
+ if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
767
+ }
768
+ handleSocketError(socket, generation, error) {
769
+ if (!this.isActive(socket, generation) || this.connectSent) return;
770
+ this.opts.onConnectError?.(error);
771
+ }
772
+ flushRequests(error) {
773
+ for (const [id, pending] of this.pending) {
774
+ this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
775
+ pending.cleanup?.();
776
+ pending.reject(error);
777
+ }
778
+ this.pending.clear();
779
+ }
780
+ finishRequestTiming(id, pending, ok, errorCode) {
781
+ const endedAtMs = this.nowMs();
782
+ this.invoke("request timing", () => this.opts.onRequestTiming?.({
783
+ id,
784
+ method: pending.method,
785
+ ok,
786
+ durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
787
+ startedAtMs: pending.startedAtMs,
788
+ endedAtMs,
789
+ errorCode
790
+ }));
791
+ }
792
+ scheduleReconnect(overrideMs) {
793
+ if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
794
+ const retry = this.reconnectSupervisor.next();
795
+ if (!retry) return;
796
+ sleepWithAbort(retry.delayMs, retry.signal).then(() => this.connect(), () => {});
797
+ }
798
+ closeContext() {
799
+ return {
800
+ generation: this.generation,
801
+ socketOpened: this.socketOpened,
802
+ helloReceived: this.helloReceived,
803
+ connectRequestSent: this.connectRequestSent,
804
+ connectFailure: this.connectFailure
805
+ };
806
+ }
807
+ isActive(socket, generation) {
808
+ return !this.stopped && this.socket === socket && this.generation === generation;
809
+ }
810
+ nowMs() {
811
+ return this.opts.nowMs?.() ?? Date.now();
812
+ }
813
+ clearHandshakeTimer() {
814
+ if (this.handshakeTimer) {
815
+ clearTimeout(this.handshakeTimer);
816
+ this.handshakeTimer = null;
817
+ }
818
+ }
819
+ invoke(label, callback) {
820
+ try {
821
+ callback();
822
+ } catch (error) {
823
+ this.opts.onCallbackError?.(label, error);
824
+ }
825
+ }
826
+ };
827
+ //#endregion
828
+ //#region packages/gateway-client/src/reconnect-policy.ts
829
+ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
830
+ ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
831
+ ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID,
832
+ ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING,
833
+ ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
834
+ ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
835
+ ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
836
+ ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
837
+ ConnectErrorDetailCodes.PAIRING_REQUIRED,
838
+ ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
839
+ ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
840
+ ]);
841
+ function shouldPauseGatewayReconnect(params) {
842
+ const code = readConnectErrorDetailCode(params.details);
843
+ if (!code) return false;
844
+ const pairing = readPairingConnectErrorDetails(params.details);
845
+ if (code === ConnectErrorDetailCodes.PAIRING_REQUIRED && (pairing?.pauseReconnect === false || pairing?.recommendedNextStep === "wait_then_retry")) return false;
846
+ if (code === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) return params.tokenMismatchIsTerminal === true && !params.deviceTokenRetryPending;
847
+ return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
848
+ }
849
+ //#endregion
850
+ export { buildGatewayConnectAuth as a, shouldRetryGatewayWithDeviceToken as c, normalizeDeviceMetadataForAuth as d, GatewayBrowserDeviceAuthLifecycle as i, buildDeviceAuthPayload as l, GatewayProtocolClient as n, resolveGatewayConnectScopes as o, GatewayProtocolRequestError as r, selectGatewayConnectAuth as s, shouldPauseGatewayReconnect as t, buildDeviceAuthPayloadV3 as u };