@runtypelabs/cli 2.59.1 → 2.59.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.
Files changed (84) hide show
  1. package/dist/chunk-0-T9NWjH.js +3899 -0
  2. package/dist/chunk-0x7UKC1U.js +17 -0
  3. package/dist/chunk-2ySxXtcg.js +418 -0
  4. package/dist/chunk-3An_BRHv.js +501 -0
  5. package/dist/chunk-6uTxgdiY.js +16218 -0
  6. package/dist/chunk-87nRnNRG.js +1363 -0
  7. package/dist/chunk-B3wo8uRx.js +322 -0
  8. package/dist/chunk-BGp3ZdBy.js +560 -0
  9. package/dist/chunk-BJQv9Elp.js +107 -0
  10. package/dist/chunk-BOpO6xE-.js +6053 -0
  11. package/dist/chunk-BQXXCL8B.js +102 -0
  12. package/dist/chunk-BV-8isOd.js +34 -0
  13. package/dist/chunk-BVMhjGVe.js +826 -0
  14. package/dist/chunk-BW2fg46T.js +141 -0
  15. package/dist/chunk-BWJdY5Ag.js +36 -0
  16. package/dist/chunk-BYJOrLhh.js +302 -0
  17. package/dist/chunk-BYab8EVL.js +127 -0
  18. package/dist/chunk-BfEKvp7h.js +82 -0
  19. package/dist/chunk-BhgLSfH6.js +1123 -0
  20. package/dist/chunk-BirUxTtH.js +296 -0
  21. package/dist/chunk-BmB6UVZ9.js +1559 -0
  22. package/dist/chunk-BnWhIqW1.js +109 -0
  23. package/dist/chunk-Bpfnfk0t.js +123 -0
  24. package/dist/chunk-BrhgdnM3.js +108 -0
  25. package/dist/chunk-Bt0XAgX5.js +327 -0
  26. package/dist/chunk-Bu0FJr6S.js +84 -0
  27. package/dist/chunk-BvEoKpN3.js +255 -0
  28. package/dist/chunk-BxNJgYbE.js +28 -0
  29. package/dist/chunk-C1OFhd7F.js +6131 -0
  30. package/dist/chunk-C7e7WwbU.js +331 -0
  31. package/dist/chunk-CDTYP6bN.js +558 -0
  32. package/dist/chunk-CGDQoqDC.js +24 -0
  33. package/dist/chunk-CJraEBVd.js +127 -0
  34. package/dist/chunk-CQrNhOtv.js +40 -0
  35. package/dist/chunk-CW6AzBH6.js +128 -0
  36. package/dist/chunk-CXgL4w-9.js +317 -0
  37. package/dist/chunk-CZrqoPyf.js +233 -0
  38. package/dist/chunk-CcFCyqbB.js +83 -0
  39. package/dist/chunk-CdaIvO-y.js +485 -0
  40. package/dist/chunk-CehSXtgK.js +624 -0
  41. package/dist/chunk-Ciqkop65.js +218 -0
  42. package/dist/chunk-CoAWdSiE.js +43 -0
  43. package/dist/chunk-Crb4y7t4.js +830 -0
  44. package/dist/chunk-CrlqIfmb.js +337 -0
  45. package/dist/chunk-Cs4pwCQ-.js +324 -0
  46. package/dist/chunk-CwM_9y01.js +519 -0
  47. package/dist/chunk-CyRbIJg8.js +440 -0
  48. package/dist/chunk-D0OHsyMy.js +212 -0
  49. package/dist/chunk-D3y2bURs.js +1347 -0
  50. package/dist/chunk-D4_mJk03.js +2338 -0
  51. package/dist/chunk-D6C8rned.js +578 -0
  52. package/dist/chunk-D9ZbUnks.js +11 -0
  53. package/dist/chunk-DEPmLshX.js +834 -0
  54. package/dist/chunk-DFj1UPB2.js +330 -0
  55. package/dist/chunk-DGdFdZVf.js +197 -0
  56. package/dist/chunk-DGv7X9S_.js +63 -0
  57. package/dist/chunk-DJQ47WYZ.js +664 -0
  58. package/dist/chunk-DJw3O61j.js +1974 -0
  59. package/dist/chunk-DK4FbWVK.js +100 -0
  60. package/dist/chunk-DSRYhUus.js +993 -0
  61. package/dist/chunk-DajZ9Eg4.js +299 -0
  62. package/dist/chunk-Df4dunvU.js +195 -0
  63. package/dist/chunk-DjNTPhfO.js +481 -0
  64. package/dist/chunk-Dn8eaJuu.js +1042 -0
  65. package/dist/chunk-DwDWqSxM.js +637 -0
  66. package/dist/chunk-Dz7cEtxl.js +84 -0
  67. package/dist/chunk-E_0bgjrS.js +359 -0
  68. package/dist/chunk-KPYN2vUp.js +776 -0
  69. package/dist/chunk-KcqPQJBi.js +86 -0
  70. package/dist/chunk-N9H0JnoG.js +349 -0
  71. package/dist/chunk-NONFcXkn.js +728 -0
  72. package/dist/chunk-PrFjcpmH.js +256 -0
  73. package/dist/chunk-QSfNYiw9.js +11 -0
  74. package/dist/chunk-R265bzj9.js +226 -0
  75. package/dist/chunk-X9V_r6jM.js +166 -0
  76. package/dist/chunk-ZBtPUtyA.js +55 -0
  77. package/dist/chunk-ZCTfmOs7.js +4939 -0
  78. package/dist/chunk-h6oavj0t.js +97 -0
  79. package/dist/chunk-i7DwkvvD.js +42 -0
  80. package/dist/chunk-mG-4eZ6S.js +203 -0
  81. package/dist/chunk-o0ygUbCf.js +313 -0
  82. package/dist/chunk-ucu5lrDZ.js +5715 -0
  83. package/dist/index.js +11367 -73789
  84. package/package.json +4 -4
@@ -0,0 +1,1042 @@
1
+ #!/usr/bin/env node
2
+ import { F as getCliVersion, M as Conf, P as getRuntypeHomeDir, l as normalizeApiUrl } from "./index.js";
3
+ import { C as unknown, b as string, c as boolean, f as literal, h as object, i as _enum, m as number, s as array, u as discriminatedUnion, v as record } from "./chunk-BOpO6xE-.js";
4
+ import { n as datetime } from "./chunk-D9ZbUnks.js";
5
+ import chalk from "chalk";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import os from "node:os";
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
+ import WebSocket from "ws";
12
+ object({
13
+ audience: literal("runtype-device-relay"),
14
+ deviceId: string().min(1).max(128),
15
+ connectionId: string().min(1).max(128),
16
+ nonce: string().min(16).max(128),
17
+ issuedAt: number().int().nonnegative(),
18
+ expiresAt: number().int().positive()
19
+ }).strict();
20
+ new TextEncoder();
21
+ function bytesToBase64Url(bytes) {
22
+ let binary = "";
23
+ for (const byte of bytes) binary += String.fromCharCode(byte);
24
+ return btoa(binary).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
25
+ }
26
+ function base64UrlToBytes(value) {
27
+ if (!/^[A-Za-z0-9_-]+$/u.test(value)) return null;
28
+ const padded = value.replace(/-/gu, "+").replace(/_/gu, "/") + "=".repeat((4 - value.length % 4) % 4);
29
+ try {
30
+ const binary = atob(padded);
31
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ // ../device-bridge/src/device-identity.ts
37
+ const encoder = new TextEncoder();
38
+ function toArrayBuffer(bytes) {
39
+ const copy = new Uint8Array(bytes.byteLength);
40
+ copy.set(bytes);
41
+ return copy.buffer;
42
+ }
43
+ function exportedKeyBytes(value) {
44
+ if (!(value instanceof ArrayBuffer)) throw new Error("Expected a binary key export");
45
+ return new Uint8Array(value);
46
+ }
47
+ /**
48
+ * Builds the exact string a device signs to open a relay connection.
49
+ * Format: `runtype-device-connect-v1`, device id, timestamp, and nonce, separated by newlines.
50
+ */
51
+ function deviceConnectionSignaturePayload(input) {
52
+ return `runtype-device-connect-v1\n${input.deviceId}\n${input.timestamp}\n${input.nonce}`;
53
+ }
54
+ function requireCryptoKeyPair(value) {
55
+ if ("publicKey" in value && "privateKey" in value) return value;
56
+ throw new Error("Ed25519 identity must be a key pair");
57
+ }
58
+ async function generateDeviceIdentity() {
59
+ const keyPair = requireCryptoKeyPair(await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]));
60
+ const rawPublicKey = exportedKeyBytes(await crypto.subtle.exportKey("raw", keyPair.publicKey));
61
+ if (rawPublicKey.byteLength !== 32) throw new Error("Ed25519 public key must be 32 bytes");
62
+ return {
63
+ keyPair,
64
+ publicKey: bytesToBase64Url(rawPublicKey)
65
+ };
66
+ }
67
+ async function exportDevicePrivateKey(privateKey) {
68
+ return bytesToBase64Url(exportedKeyBytes(await crypto.subtle.exportKey("pkcs8", privateKey)));
69
+ }
70
+ async function importDevicePrivateKey(privateKey) {
71
+ const pkcs8 = base64UrlToBytes(privateKey);
72
+ if (!pkcs8) throw new Error("Device private key must be base64url PKCS8");
73
+ return crypto.subtle.importKey("pkcs8", toArrayBuffer(pkcs8), { name: "Ed25519" }, false, ["sign"]);
74
+ }
75
+ async function signDeviceConnectionChallenge(input) {
76
+ const signature = new Uint8Array(await crypto.subtle.sign("Ed25519", input.privateKey, encoder.encode(deviceConnectionSignaturePayload({
77
+ deviceId: input.deviceId,
78
+ timestamp: input.timestamp,
79
+ nonce: input.nonce
80
+ }))));
81
+ if (signature.byteLength !== 64) throw new Error("Ed25519 signature must be 64 bytes");
82
+ return bytesToBase64Url(signature);
83
+ }
84
+ const DEVICE_MAX_FRAME_BYTES = 1048576;
85
+ const identifierSchema = string().min(1).max(128);
86
+ const isoDateSchema = datetime({ offset: true });
87
+ const jsonObjectSchema = record(string(), unknown());
88
+ const devicePlatformSchema = _enum(["macos", "cli"]);
89
+ const devicePermissionStateSchema = _enum([
90
+ "granted",
91
+ "denied",
92
+ "not_determined",
93
+ "restricted",
94
+ "unavailable"
95
+ ]);
96
+ const devicePluginHealthSchema = _enum([
97
+ "healthy",
98
+ "degraded",
99
+ "unavailable",
100
+ "disabled"
101
+ ]);
102
+ const deviceToolSchema = object({
103
+ name: identifierSchema.regex(/^[a-z][a-z0-9_]*$/),
104
+ title: string().min(1).max(120),
105
+ description: string().min(1).max(2e3),
106
+ inputSchema: jsonObjectSchema,
107
+ sideEffect: boolean().default(false)
108
+ }).strict();
109
+ const devicePluginManifestSchema = object({
110
+ id: identifierSchema.regex(/^[a-z][a-z0-9_-]*$/),
111
+ displayName: string().min(1).max(120),
112
+ version: string().min(1).max(64),
113
+ health: devicePluginHealthSchema,
114
+ statusMessage: string().max(500).optional(),
115
+ permissions: record(string(), devicePermissionStateSchema).default({}),
116
+ tools: array(deviceToolSchema).max(50)
117
+ }).strict();
118
+ const deviceCapabilityManifestSchema = object({
119
+ deviceId: identifierSchema,
120
+ generatedAt: isoDateSchema,
121
+ plugins: array(devicePluginManifestSchema).max(20)
122
+ }).strict().superRefine((manifest, ctx) => {
123
+ const toolNames = /* @__PURE__ */ new Set();
124
+ let toolCount = 0;
125
+ for (const plugin of manifest.plugins) for (const tool of plugin.tools) {
126
+ toolCount++;
127
+ if (toolNames.has(tool.name)) ctx.addIssue({
128
+ code: "custom",
129
+ message: `Duplicate device tool name: ${tool.name}`,
130
+ path: ["plugins"]
131
+ });
132
+ toolNames.add(tool.name);
133
+ }
134
+ if (toolCount > 50) ctx.addIssue({
135
+ code: "custom",
136
+ message: `Device manifests may advertise at most 50 tools`,
137
+ path: ["plugins"]
138
+ });
139
+ });
140
+ const deviceErrorCodeSchema = _enum([
141
+ "DEVICE_OFFLINE",
142
+ "DEVICE_REVOKED",
143
+ "DEVICE_BUSY",
144
+ "PLUGIN_UNAVAILABLE",
145
+ "PERMISSION_DENIED",
146
+ "MESSAGES_NOT_READY",
147
+ "APP_VERSION_UNSUPPORTED",
148
+ "PROTOCOL_VERSION_UNSUPPORTED",
149
+ "REQUEST_EXPIRED",
150
+ "AMBIGUOUS_SIDE_EFFECT",
151
+ "POLICY_DENIED",
152
+ "TOOL_NOT_FOUND",
153
+ "INVALID_REQUEST",
154
+ "DUPLICATE_INVOCATION",
155
+ "INTERNAL_ERROR"
156
+ ]);
157
+ const deviceErrorSchema = object({
158
+ code: deviceErrorCodeSchema,
159
+ message: string().min(1).max(1e3),
160
+ retryable: boolean(),
161
+ details: jsonObjectSchema.optional()
162
+ }).strict();
163
+ const deviceInvocationSchema = object({
164
+ id: identifierSchema,
165
+ idempotencyKey: string().min(16).max(256),
166
+ toolName: identifierSchema,
167
+ sideEffect: boolean(),
168
+ arguments: jsonObjectSchema,
169
+ requestedAt: isoDateSchema,
170
+ deadline: isoDateSchema
171
+ }).strict();
172
+ const deviceInvocationResultSchema = object({
173
+ invocationId: identifierSchema,
174
+ status: _enum([
175
+ "success",
176
+ "error",
177
+ "ambiguous"
178
+ ]),
179
+ completedAt: isoDateSchema,
180
+ output: unknown().optional(),
181
+ error: deviceErrorSchema.optional()
182
+ }).strict().superRefine((result, ctx) => {
183
+ if (result.status === "success" && result.error) ctx.addIssue({
184
+ code: "custom",
185
+ message: "Successful results cannot include an error"
186
+ });
187
+ if (result.status !== "success" && !result.error) ctx.addIssue({
188
+ code: "custom",
189
+ message: "Non-success results must include an error"
190
+ });
191
+ });
192
+ const protocolEnvelopeSchema = object({ protocolVersion: literal(1) });
193
+ const relayToDeviceMessageSchema = discriminatedUnion("type", [
194
+ protocolEnvelopeSchema.extend({
195
+ type: literal("invoke"),
196
+ invocation: deviceInvocationSchema
197
+ }).strict(),
198
+ protocolEnvelopeSchema.extend({
199
+ type: literal("cancel"),
200
+ invocationId: identifierSchema,
201
+ reason: string().max(500).optional()
202
+ }).strict(),
203
+ protocolEnvelopeSchema.extend({
204
+ type: literal("event_ack"),
205
+ eventId: identifierSchema
206
+ }).strict(),
207
+ protocolEnvelopeSchema.extend({
208
+ type: literal("shutdown"),
209
+ reason: _enum([
210
+ "revoked",
211
+ "replaced",
212
+ "maintenance",
213
+ "unsupported_version"
214
+ ])
215
+ }).strict()
216
+ ]);
217
+ const deviceToRelayMessageSchema = discriminatedUnion("type", [
218
+ protocolEnvelopeSchema.extend({
219
+ type: literal("hello"),
220
+ deviceId: identifierSchema,
221
+ connectionId: identifierSchema,
222
+ platform: devicePlatformSchema,
223
+ appVersion: string().min(1).max(64),
224
+ connectedAt: isoDateSchema
225
+ }).strict(),
226
+ protocolEnvelopeSchema.extend({
227
+ type: literal("capabilities"),
228
+ manifest: deviceCapabilityManifestSchema
229
+ }).strict(),
230
+ protocolEnvelopeSchema.extend({
231
+ type: literal("heartbeat"),
232
+ sentAt: isoDateSchema
233
+ }).strict(),
234
+ protocolEnvelopeSchema.extend({
235
+ type: literal("invocation_result"),
236
+ result: deviceInvocationResultSchema
237
+ }).strict(),
238
+ protocolEnvelopeSchema.extend({
239
+ type: literal("event"),
240
+ eventId: identifierSchema,
241
+ pluginId: identifierSchema,
242
+ eventName: identifierSchema,
243
+ occurredAt: isoDateSchema,
244
+ payload: jsonObjectSchema
245
+ }).strict()
246
+ ]);
247
+ object({
248
+ deviceId: identifierSchema,
249
+ online: boolean(),
250
+ lastSeenAt: isoDateSchema.optional(),
251
+ connectionId: identifierSchema.optional(),
252
+ appVersion: string().max(64).optional(),
253
+ protocolVersion: number().int().positive().optional(),
254
+ manifest: deviceCapabilityManifestSchema.optional()
255
+ }).strict();
256
+ function deviceError(code, message, retryable, details) {
257
+ return deviceErrorSchema.parse({
258
+ code,
259
+ message,
260
+ retryable,
261
+ details
262
+ });
263
+ }
264
+ // ../device-bridge/src/localhost-http.ts
265
+ const LOCALHOST_HTTP_TOOL_NAME = "localhost_http_call";
266
+ const LOCALHOST_HTTP_MAX_BODY_BYTES = DEVICE_MAX_FRAME_BYTES;
267
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
268
+ "127.0.0.1",
269
+ "localhost",
270
+ "::1"
271
+ ]);
272
+ const localhostHttpMethodSchema = _enum([
273
+ "GET",
274
+ "POST",
275
+ "PUT",
276
+ "PATCH",
277
+ "DELETE",
278
+ "HEAD"
279
+ ]);
280
+ const localhostHttpCallArgsSchema = object({
281
+ method: localhostHttpMethodSchema,
282
+ url: string().url(),
283
+ headers: record(string(), string().max(8192)).optional(),
284
+ allowedAuthHeaderName: string().regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/).max(256).optional(),
285
+ body: string().max(LOCALHOST_HTTP_MAX_BODY_BYTES).optional(),
286
+ timeoutMs: number().int().min(100).max(3e5).optional()
287
+ }).strict();
288
+ const localhostHttpCallResultSchema = object({
289
+ status: number().int().min(100).max(599),
290
+ statusText: string().max(120),
291
+ headers: record(string(), string()),
292
+ body: string().max(LOCALHOST_HTTP_MAX_BODY_BYTES)
293
+ }).strict();
294
+ var LocalhostHttpCallError = class extends Error {
295
+ code;
296
+ constructor(message, code) {
297
+ super(message);
298
+ this.code = code;
299
+ this.name = "LocalhostHttpCallError";
300
+ }
301
+ };
302
+ function parseLoopbackHttpUrl(urlString) {
303
+ let url;
304
+ try {
305
+ url = new URL(urlString);
306
+ } catch {
307
+ throw new LocalhostHttpCallError("URL is not valid", "INVALID_REQUEST");
308
+ }
309
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new LocalhostHttpCallError("Only http and https loopback URLs are allowed", "NOT_LOOPBACK");
310
+ if (url.username || url.password) throw new LocalhostHttpCallError("Loopback URLs cannot include credentials", "INVALID_REQUEST");
311
+ const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
312
+ if (!LOOPBACK_HOSTS.has(hostname)) throw new LocalhostHttpCallError("Device HTTP calls may only target 127.0.0.1, localhost, or ::1", "NOT_LOOPBACK");
313
+ return url;
314
+ }
315
+ function normalizedLoopbackPort(url) {
316
+ if (url.port !== "") return url.port;
317
+ return url.protocol === "https:" ? "443" : "80";
318
+ }
319
+ /**
320
+ * Reports whether a device HTTP call may be forwarded to the listen target.
321
+ * Loopback host aliases compare equal. A URL that is not loopback HTTP is refused.
322
+ */
323
+ function isAllowedListenTarget(requestUrl, forwardTo) {
324
+ try {
325
+ const request = parseLoopbackHttpUrl(requestUrl);
326
+ const target = parseLoopbackHttpUrl(forwardTo);
327
+ return request.protocol === target.protocol && normalizedLoopbackPort(request) === normalizedLoopbackPort(target);
328
+ } catch (error) {
329
+ if (error instanceof LocalhostHttpCallError) return false;
330
+ throw error;
331
+ }
332
+ }
333
+ const FORWARDED_REQUEST_HEADERS = /* @__PURE__ */ new Set([
334
+ "accept",
335
+ "authorization",
336
+ "a2a-version",
337
+ "content-type",
338
+ "x-api-key",
339
+ "x-runtype-identity"
340
+ ]);
341
+ const BLOCKED_AUTH_HEADERS = /* @__PURE__ */ new Set([
342
+ "host",
343
+ "cookie",
344
+ "connection",
345
+ "content-length",
346
+ "transfer-encoding",
347
+ "upgrade",
348
+ "proxy-authorization",
349
+ "proxy-authenticate",
350
+ "te",
351
+ "trailer",
352
+ "keep-alive"
353
+ ]);
354
+ function forwardedRequestHeaders(headers, allowedAuthHeaderName) {
355
+ const out = new Headers();
356
+ if (!headers) return out;
357
+ for (const [name, value] of Object.entries(headers)) {
358
+ const normalized = name.toLowerCase();
359
+ const configuredAuth = normalized === allowedAuthHeaderName?.toLowerCase() && !BLOCKED_AUTH_HEADERS.has(normalized) && !normalized.startsWith("sec-") && !normalized.startsWith("proxy-");
360
+ if (!FORWARDED_REQUEST_HEADERS.has(normalized) && !configuredAuth) continue;
361
+ out.set(name, value);
362
+ }
363
+ return out;
364
+ }
365
+ async function readResponseBody(response) {
366
+ if (!response.body) return "";
367
+ const reader = response.body.getReader();
368
+ const decoder = new TextDecoder();
369
+ const chunks = [];
370
+ let bytes = 0;
371
+ try {
372
+ while (true) {
373
+ const { done, value } = await reader.read();
374
+ if (done) break;
375
+ bytes += value.byteLength;
376
+ if (bytes > LOCALHOST_HTTP_MAX_BODY_BYTES) {
377
+ await reader.cancel();
378
+ throw new LocalhostHttpCallError(`Response body exceeds ${LOCALHOST_HTTP_MAX_BODY_BYTES} bytes`, "BODY_TOO_LARGE");
379
+ }
380
+ chunks.push(decoder.decode(value, { stream: true }));
381
+ }
382
+ chunks.push(decoder.decode());
383
+ return chunks.join("");
384
+ } finally {
385
+ reader.releaseLock();
386
+ }
387
+ }
388
+ async function executeLocalhostHttpCall(args, fetchImpl = fetch, signal) {
389
+ const url = parseLoopbackHttpUrl(args.url);
390
+ const timeoutMs = args.timeoutMs ?? 3e4;
391
+ const response = await fetchImpl(url, {
392
+ method: args.method,
393
+ headers: forwardedRequestHeaders(args.headers, args.allowedAuthHeaderName),
394
+ body: args.method === "GET" || args.method === "HEAD" ? void 0 : args.body,
395
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
396
+ redirect: "error"
397
+ });
398
+ const body = await readResponseBody(response);
399
+ const headers = {};
400
+ response.headers.forEach((value, key) => {
401
+ headers[key] = value;
402
+ });
403
+ return localhostHttpCallResultSchema.parse({
404
+ status: response.status,
405
+ statusText: response.statusText,
406
+ headers,
407
+ body
408
+ });
409
+ }
410
+ // src/lib/device-listen-identity.ts
411
+ const DEVICE_LISTEN_CONFIG = "device-listen";
412
+ function isRecord$1(value) {
413
+ return typeof value === "object" && value !== null && !Array.isArray(value);
414
+ }
415
+ function copyIdentity(identity) {
416
+ return {
417
+ deviceId: identity.deviceId,
418
+ name: identity.name,
419
+ publicKey: identity.publicKey,
420
+ privateKey: identity.privateKey
421
+ };
422
+ }
423
+ function readIdentity(value) {
424
+ if (!isRecord$1(value)) throw new Error("Stored device identity is invalid");
425
+ const { deviceId, name, publicKey, privateKey } = value;
426
+ if (deviceId !== null && (typeof deviceId !== "string" || deviceId.length === 0)) throw new Error("Stored device identity is invalid");
427
+ if (typeof name !== "string" || name.trim().length === 0 || name.length > 120) throw new Error("Stored device identity is invalid");
428
+ if (typeof publicKey !== "string" || !/^[A-Za-z0-9_-]{43}$/u.test(publicKey)) throw new Error("Stored device identity is invalid");
429
+ if (typeof privateKey !== "string" || !/^[A-Za-z0-9_-]{40,}$/u.test(privateKey)) throw new Error("Stored device identity is invalid");
430
+ return {
431
+ deviceId,
432
+ name,
433
+ publicKey,
434
+ privateKey
435
+ };
436
+ }
437
+ /** Hostname shown in Runtype until the operator passes --name. */
438
+ function resolveDeviceListenName(requested) {
439
+ const explicit = requested?.trim();
440
+ if (explicit) return explicit.slice(0, 120);
441
+ const hostname = os.hostname().trim();
442
+ if (!hostname) return "This computer";
443
+ return hostname.slice(0, 120);
444
+ }
445
+ async function createDeviceListenIdentity(name) {
446
+ const { keyPair, publicKey } = await generateDeviceIdentity();
447
+ return {
448
+ deviceId: null,
449
+ name,
450
+ publicKey,
451
+ privateKey: await exportDevicePrivateKey(keyPair.privateKey)
452
+ };
453
+ }
454
+ async function signDeviceListenChallenge(identity, input) {
455
+ return signDeviceConnectionChallenge({
456
+ privateKey: await importDevicePrivateKey(identity.privateKey),
457
+ ...input
458
+ });
459
+ }
460
+ function deviceListenEncryptionKey() {
461
+ const hostname = os.hostname();
462
+ const username = os.userInfo().username;
463
+ return createHash("sha256").update(`runtype-device-listen-${hostname}-${username}`).digest("hex");
464
+ }
465
+ function isCorruptStoreError(error) {
466
+ if (error instanceof SyntaxError) return true;
467
+ const message = error instanceof Error ? error.message : String(error);
468
+ return /JSON|bad decrypt|authenticate data|unable to authenticate/iu.test(message);
469
+ }
470
+ function createConfig(cwd, encryptionKey, configName) {
471
+ return new Conf({
472
+ projectName: "runtype-cli",
473
+ projectSuffix: "",
474
+ configName,
475
+ cwd,
476
+ encryptionKey,
477
+ defaults: { identities: {} }
478
+ });
479
+ }
480
+ function openConfig(cwd, encryptionKey, configName) {
481
+ try {
482
+ return createConfig(cwd, encryptionKey, configName);
483
+ } catch (error) {
484
+ if (!isCorruptStoreError(error)) throw error;
485
+ const filePath = path.join(cwd, `${configName}.json`);
486
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
487
+ return createConfig(cwd, encryptionKey, configName);
488
+ }
489
+ }
490
+ function createDeviceListenIdentityStore(options) {
491
+ const config = openConfig(options.cwd ?? getRuntypeHomeDir(), options.encryptionKey ?? deviceListenEncryptionKey(), workspaceConfigName(options.workspace, options.namespace));
492
+ return {
493
+ async load(apiUrl) {
494
+ const value = (config.get("identities") ?? {})[normalizeApiUrl(apiUrl)];
495
+ if (value === void 0) return null;
496
+ return readIdentity(value);
497
+ },
498
+ async save(apiUrl, identity) {
499
+ const key = normalizeApiUrl(apiUrl);
500
+ const identities = {
501
+ ...config.get("identities"),
502
+ [key]: copyIdentity(readIdentity(identity))
503
+ };
504
+ config.set("identities", identities);
505
+ }
506
+ };
507
+ }
508
+ function workspaceConfigName(workspace, namespace) {
509
+ if (!workspace.userId) throw new Error("Authenticated device owner is required");
510
+ const scope = createHash("sha256").update(JSON.stringify(namespace ? [
511
+ workspace.userId,
512
+ workspace.orgId,
513
+ namespace
514
+ ] : [workspace.userId, workspace.orgId])).digest("hex");
515
+ return `${DEVICE_LISTEN_CONFIG}-${scope}`;
516
+ }
517
+ function frameText(data) {
518
+ if (typeof data === "string") return data;
519
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
520
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
521
+ return Buffer.concat(data).toString("utf8");
522
+ }
523
+ function closeReason(reason) {
524
+ return typeof reason === "string" ? reason : reason.toString("utf8");
525
+ }
526
+ function localhostFailureCode(error) {
527
+ if (error.code === "NOT_LOOPBACK") return "POLICY_DENIED";
528
+ if (error.code === "REQUEST_EXPIRED") return "REQUEST_EXPIRED";
529
+ if (error.code === "INTERNAL_ERROR") return "INTERNAL_ERROR";
530
+ return "INVALID_REQUEST";
531
+ }
532
+ function resultMessage(invocationId, status, output, error) {
533
+ return deviceToRelayMessageSchema.parse({
534
+ type: "invocation_result",
535
+ protocolVersion: 1,
536
+ result: {
537
+ invocationId,
538
+ status,
539
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
540
+ ...status === "success" ? { output } : { error }
541
+ }
542
+ });
543
+ }
544
+ function failureMessage(invocationId, code, message) {
545
+ return resultMessage(invocationId, "error", void 0, deviceError(code, message.slice(0, 1e3), false));
546
+ }
547
+ async function executeInvocation(invocation, forwardTo, fetchImpl, signal) {
548
+ if (invocation.toolName !== "localhost_http_call") return failureMessage(invocation.id, "TOOL_NOT_FOUND", `Tool ${invocation.toolName} is not available`);
549
+ const deadline = Date.parse(invocation.deadline);
550
+ if (Number.isNaN(deadline) || deadline <= Date.now()) return failureMessage(invocation.id, "REQUEST_EXPIRED", "Device invocation expired before local execution");
551
+ const parsed = localhostHttpCallArgsSchema.safeParse(invocation.arguments);
552
+ if (!parsed.success) return failureMessage(invocation.id, "INVALID_REQUEST", "localhost_http_call arguments are invalid");
553
+ if (!isAllowedListenTarget(parsed.data.url, forwardTo)) return failureMessage(invocation.id, "POLICY_DENIED", "That URL is outside the forwarded loopback port");
554
+ try {
555
+ const timeoutMs = Math.min(parsed.data.timeoutMs ?? 3e4, deadline - Date.now());
556
+ if (timeoutMs <= 0) return failureMessage(invocation.id, "REQUEST_EXPIRED", "Device invocation expired");
557
+ const output = await executeLocalhostHttpCall({
558
+ ...parsed.data,
559
+ timeoutMs
560
+ }, fetchImpl, signal);
561
+ return resultMessage(invocation.id, "success", output);
562
+ } catch (error) {
563
+ if (error instanceof LocalhostHttpCallError) return failureMessage(invocation.id, localhostFailureCode(error), error.message);
564
+ const message = error instanceof Error ? error.message : "The localhost request failed";
565
+ return failureMessage(invocation.id, "INTERNAL_ERROR", message || "The localhost request failed");
566
+ }
567
+ }
568
+ function connectRelaySocket(relayUrl, headers) {
569
+ const socket = new WebSocket(relayUrl, { headers });
570
+ return {
571
+ send(data) {
572
+ socket.send(data);
573
+ },
574
+ close(code, reason) {
575
+ socket.close(code, reason);
576
+ },
577
+ terminate() {
578
+ socket.terminate();
579
+ },
580
+ onOpen(listener) {
581
+ socket.on("open", listener);
582
+ },
583
+ onMessage(listener) {
584
+ socket.on("message", (data) => listener(data));
585
+ },
586
+ onClose(listener) {
587
+ socket.on("close", (code, reason) => listener(code, reason));
588
+ },
589
+ onError(listener) {
590
+ socket.on("error", listener);
591
+ }
592
+ };
593
+ }
594
+ function startDeviceListenSession(options) {
595
+ const fetchImpl = options.fetchImpl ?? fetch;
596
+ const socket = (options.socketFactory ?? connectRelaySocket)(options.relayUrl, { Authorization: `Bearer ${options.token}` });
597
+ let settled = false;
598
+ let terminal = null;
599
+ let heartbeat = null;
600
+ let closeTimer;
601
+ const active = /* @__PURE__ */ new Map();
602
+ const abortActive = () => {
603
+ for (const controller of active.values()) controller.abort();
604
+ active.clear();
605
+ };
606
+ let resolveClosed = () => {};
607
+ const closed = new Promise((resolve) => {
608
+ resolveClosed = resolve;
609
+ });
610
+ const finish = (code, reason, cleanupFailed = false) => {
611
+ if (settled) return;
612
+ settled = true;
613
+ abortActive();
614
+ if (heartbeat) clearInterval(heartbeat);
615
+ if (closeTimer) clearTimeout(closeTimer);
616
+ heartbeat = null;
617
+ resolveClosed({
618
+ code,
619
+ reason,
620
+ intent: terminal ?? "unexpected",
621
+ ...cleanupFailed ? { cleanupFailed: true } : {}
622
+ });
623
+ try {
624
+ socket.close(code, reason);
625
+ } catch {
626
+ return;
627
+ }
628
+ };
629
+ const send = (message) => {
630
+ socket.send(JSON.stringify(deviceToRelayMessageSchema.parse(message)));
631
+ };
632
+ const requestClose = (code, reason) => {
633
+ if (settled || closeTimer) return;
634
+ abortActive();
635
+ closeTimer = setTimeout(() => {
636
+ try {
637
+ socket.terminate();
638
+ } catch {
639
+ finish(1011, "Relay socket termination failed", true);
640
+ return;
641
+ }
642
+ finish(code, reason);
643
+ }, 500);
644
+ try {
645
+ socket.close(code, reason);
646
+ } catch {
647
+ try {
648
+ socket.terminate();
649
+ } catch {
650
+ finish(1011, "Relay socket termination failed", true);
651
+ return;
652
+ }
653
+ finish(code, reason);
654
+ }
655
+ };
656
+ const localhostManifest = () => ({
657
+ deviceId: options.deviceId,
658
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
659
+ plugins: [{
660
+ id: "localhost-http",
661
+ displayName: "Localhost HTTP",
662
+ version: "1.0.0",
663
+ health: "healthy",
664
+ permissions: {},
665
+ tools: [{
666
+ name: LOCALHOST_HTTP_TOOL_NAME,
667
+ title: "Call a loopback HTTP origin",
668
+ description: "Fetch a 127.0.0.1, localhost, or ::1 URL from the paired device. Used to call a local agent.",
669
+ inputSchema: {
670
+ type: "object",
671
+ properties: {
672
+ method: {
673
+ type: "string",
674
+ enum: [
675
+ "GET",
676
+ "POST",
677
+ "PUT",
678
+ "PATCH",
679
+ "DELETE",
680
+ "HEAD"
681
+ ]
682
+ },
683
+ url: { type: "string" },
684
+ headers: {
685
+ type: "object",
686
+ additionalProperties: { type: "string" }
687
+ },
688
+ body: { type: "string" },
689
+ timeoutMs: {
690
+ type: "integer",
691
+ minimum: 100,
692
+ maximum: 3e5
693
+ },
694
+ allowedAuthHeaderName: { type: "string" }
695
+ },
696
+ required: ["method", "url"],
697
+ additionalProperties: false
698
+ },
699
+ sideEffect: true
700
+ }]
701
+ }]
702
+ });
703
+ socket.onOpen(() => {
704
+ if (settled || closeTimer) return;
705
+ try {
706
+ send({
707
+ type: "hello",
708
+ protocolVersion: 1,
709
+ deviceId: options.deviceId,
710
+ connectionId: options.connectionId,
711
+ platform: "cli",
712
+ appVersion: options.appVersion,
713
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
714
+ });
715
+ send({
716
+ type: "capabilities",
717
+ protocolVersion: 1,
718
+ manifest: localhostManifest()
719
+ });
720
+ heartbeat = setInterval(() => {
721
+ if (settled) return;
722
+ try {
723
+ send({
724
+ type: "heartbeat",
725
+ protocolVersion: 1,
726
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
727
+ });
728
+ } catch {
729
+ finish(1011, "Heartbeat failed");
730
+ }
731
+ }, options.heartbeatIntervalMs ?? 2e4);
732
+ options.onListening?.();
733
+ } catch (error) {
734
+ finish(1011, error instanceof Error ? error.message : "Device hello failed");
735
+ }
736
+ });
737
+ socket.onMessage((data) => {
738
+ handleFrame(data).catch(() => {
739
+ finish(1011, "Device session failed");
740
+ });
741
+ });
742
+ socket.onClose((code, reason) => {
743
+ finish(code, closeReason(reason));
744
+ });
745
+ socket.onError(() => {
746
+ requestClose(1011, "Relay socket error");
747
+ });
748
+ async function handleFrame(data) {
749
+ let parsed;
750
+ try {
751
+ parsed = relayToDeviceMessageSchema.safeParse(JSON.parse(frameText(data)));
752
+ } catch {
753
+ return;
754
+ }
755
+ if (!parsed.success || settled) return;
756
+ if (parsed.data.type === "shutdown") {
757
+ abortActive();
758
+ terminal = parsed.data.reason === "revoked" ? "revoked" : terminal;
759
+ requestClose(1e3, parsed.data.reason);
760
+ return;
761
+ }
762
+ if (parsed.data.type === "cancel") {
763
+ active.get(parsed.data.invocationId)?.abort();
764
+ return;
765
+ }
766
+ if (parsed.data.type !== "invoke") return;
767
+ const invocation = parsed.data.invocation;
768
+ if (active.has(invocation.id)) return;
769
+ const controller = new AbortController();
770
+ active.set(invocation.id, controller);
771
+ try {
772
+ const message = await executeInvocation(invocation, options.forwardTo, fetchImpl, controller.signal);
773
+ if (!settled && !controller.signal.aborted) send(message);
774
+ } finally {
775
+ active.delete(invocation.id);
776
+ }
777
+ }
778
+ return {
779
+ closed,
780
+ close(code = 1e3, reason = "Interrupted") {
781
+ if (settled) return;
782
+ terminal = terminal ?? "stop";
783
+ requestClose(code, reason);
784
+ }
785
+ };
786
+ }
787
+ // src/lib/device-listen.ts
788
+ const DEVICE_LISTEN_RECONNECT_DELAYS_MS = [
789
+ 250,
790
+ 500,
791
+ 1e3
792
+ ];
793
+ var DeviceListenError = class extends Error {
794
+ constructor(message) {
795
+ super(message);
796
+ this.name = "DeviceListenError";
797
+ }
798
+ };
799
+ var DeviceListenCleanupError = class extends DeviceListenError {};
800
+ function isRecord(value) {
801
+ return typeof value === "object" && value !== null && !Array.isArray(value);
802
+ }
803
+ function httpStatus(error) {
804
+ if (!isRecord(error) || typeof error.statusCode !== "number") return null;
805
+ return error.statusCode;
806
+ }
807
+ function unwrapData(response) {
808
+ if (!isRecord(response) || !("data" in response)) throw new DeviceListenError("Runtype API returned an invalid device response");
809
+ return response.data;
810
+ }
811
+ function requireDeviceId(value) {
812
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) throw new DeviceListenError("Runtype API returned an invalid device response");
813
+ return value.id;
814
+ }
815
+ function readRelayConnection(value) {
816
+ if (!isRecord(value) || typeof value.connectionId !== "string" || value.connectionId.length === 0 || typeof value.token !== "string" || value.token.length === 0 || typeof value.relayUrl !== "string" || value.relayUrl.length === 0) throw new DeviceListenError("Runtype API returned an invalid device response");
817
+ return {
818
+ connectionId: value.connectionId,
819
+ token: value.token,
820
+ relayUrl: value.relayUrl
821
+ };
822
+ }
823
+ async function untilStopped(work, signal) {
824
+ let abort = () => {};
825
+ const stopped = new Promise((_, reject) => {
826
+ abort = () => reject(new DeviceListenError("Device listener stopped"));
827
+ signal.addEventListener("abort", abort, { once: true });
828
+ if (signal.aborted) abort();
829
+ });
830
+ try {
831
+ return await Promise.race([work, stopped]);
832
+ } finally {
833
+ signal.removeEventListener("abort", abort);
834
+ }
835
+ }
836
+ function installProcessSignalHandlers(stop) {
837
+ const onStop = () => stop();
838
+ process.on("SIGINT", onStop);
839
+ process.on("SIGTERM", onStop);
840
+ return () => {
841
+ process.off("SIGINT", onStop);
842
+ process.off("SIGTERM", onStop);
843
+ };
844
+ }
845
+ function deviceListenAppVersion(version = getCliVersion()) {
846
+ const value = `runtype-cli/${version}`;
847
+ return value.length <= 64 ? value : value.slice(0, 64);
848
+ }
849
+ function assertListenForwardTarget(value) {
850
+ if (!value?.trim()) throw new DeviceListenError("Missing required option --forward-to <url>");
851
+ try {
852
+ const url = parseLoopbackHttpUrl(value.trim());
853
+ return `${url.protocol}//${url.host}`;
854
+ } catch (error) {
855
+ throw new DeviceListenError(error instanceof Error ? error.message : String(error));
856
+ }
857
+ }
858
+ function printListening(deviceId, forwardTo) {
859
+ console.log("Listening for this workspace");
860
+ console.log(` Device ${deviceId}`);
861
+ console.log(` Forward ${forwardTo}`);
862
+ console.log(" Only this Runtype workspace can call this process.");
863
+ console.log(" Keep this running. Ctrl-C to stop.");
864
+ }
865
+ async function pairIdentity(client, identity, appVersion) {
866
+ const session = unwrapData(await client.post("/devices/pairing-sessions", {}));
867
+ if (!isRecord(session) || typeof session.code !== "string" || session.code.length === 0) throw new DeviceListenError("Runtype API returned an invalid device response");
868
+ identity.deviceId = requireDeviceId(unwrapData(await client.post("/devices/pair", {
869
+ code: session.code,
870
+ name: identity.name,
871
+ platform: "cli",
872
+ publicKey: identity.publicKey,
873
+ appVersion
874
+ })));
875
+ }
876
+ async function requestConnectionToken(client, identity, nonce, timestamp) {
877
+ if (!identity.deviceId) throw new DeviceListenError("Device is not paired");
878
+ const signature = await signDeviceListenChallenge(identity, {
879
+ deviceId: identity.deviceId,
880
+ timestamp,
881
+ nonce
882
+ });
883
+ return readRelayConnection(unwrapData(await client.post(`/devices/${encodeURIComponent(identity.deviceId)}/connection-token`, {
884
+ timestamp,
885
+ nonce,
886
+ signature
887
+ })));
888
+ }
889
+ async function verifyStoredDeviceOwnership(client, deviceId) {
890
+ let response;
891
+ try {
892
+ response = await client.get(`/devices/${encodeURIComponent(deviceId)}`);
893
+ } catch (error) {
894
+ if (httpStatus(error) === 403 || httpStatus(error) === 404) throw new DeviceListenError("Cannot reuse this device in the authenticated workspace. Verify ownership and DEVICES:READ permission.");
895
+ throw error;
896
+ }
897
+ if (requireDeviceId(unwrapData(response)) !== deviceId) throw new DeviceListenError("Runtype API returned a different device during ownership verification");
898
+ }
899
+ async function recoverRevokedIdentity(client, store, apiUrl, identity, appVersion) {
900
+ const rotated = await createDeviceListenIdentity(identity.name);
901
+ identity.publicKey = rotated.publicKey;
902
+ identity.privateKey = rotated.privateKey;
903
+ identity.deviceId = null;
904
+ await store.save(apiUrl, identity);
905
+ await pairIdentity(client, identity, appVersion);
906
+ await store.save(apiUrl, identity);
907
+ }
908
+ async function obtainConnection(client, store, apiUrl, identity, appVersion, createNonce, now, repaired, recoverRevoked = true) {
909
+ if (!identity.deviceId) {
910
+ await pairIdentity(client, identity, appVersion);
911
+ await store.save(apiUrl, identity);
912
+ } else await verifyStoredDeviceOwnership(client, identity.deviceId);
913
+ try {
914
+ return await requestConnectionToken(client, identity, createNonce(), now());
915
+ } catch (error) {
916
+ if (httpStatus(error) === 503) throw new DeviceListenError("Device relay is not configured");
917
+ if (httpStatus(error) !== 410) throw error;
918
+ if (!recoverRevoked || repaired.done) throw new DeviceListenError("This device was revoked");
919
+ repaired.done = true;
920
+ await recoverRevokedIdentity(client, store, apiUrl, identity, appVersion);
921
+ try {
922
+ return await requestConnectionToken(client, identity, createNonce(), now());
923
+ } catch (retryError) {
924
+ if (httpStatus(retryError) === 503) throw new DeviceListenError("Device relay is not configured");
925
+ if (httpStatus(retryError) === 410) throw new DeviceListenError("This device was revoked");
926
+ throw retryError;
927
+ }
928
+ }
929
+ }
930
+ async function runDeviceListen(options) {
931
+ const forwardTo = assertListenForwardTarget(options.forwardTo);
932
+ const stopped = new AbortController();
933
+ const sleep = options.sleep ?? ((milliseconds) => setTimeout$1(milliseconds, void 0, { signal: stopped.signal }));
934
+ const setExitCode = options.setExitCode ?? ((code) => {
935
+ process.exitCode = code;
936
+ });
937
+ const reconnectDelays = options.reconnectDelaysMs ?? DEVICE_LISTEN_RECONNECT_DELAYS_MS;
938
+ const createNonce = options.createNonce ?? (() => randomUUID());
939
+ const now = options.now ?? (() => Date.now());
940
+ const appVersion = options.appVersion ?? deviceListenAppVersion();
941
+ const runtime = {
942
+ stopping: false,
943
+ active: null
944
+ };
945
+ const requestStop = () => {
946
+ runtime.stopping = true;
947
+ stopped.abort();
948
+ runtime.active?.close(1e3, "Interrupted");
949
+ };
950
+ options.signal?.addEventListener("abort", requestStop, { once: true });
951
+ if (options.signal?.aborted) requestStop();
952
+ const uninstall = (options.installSignalHandlers ?? installProcessSignalHandlers)(requestStop);
953
+ const repaired = { done: false };
954
+ let announcedJson = false;
955
+ let reconnects = 0;
956
+ try {
957
+ let identity = await options.identityStore.load(options.apiUrl);
958
+ if (!identity) {
959
+ identity = await createDeviceListenIdentity(resolveDeviceListenName(options.name));
960
+ await options.identityStore.save(options.apiUrl, identity);
961
+ } else if (options.name?.trim()) {
962
+ identity = {
963
+ ...identity,
964
+ name: resolveDeviceListenName(options.name)
965
+ };
966
+ await options.identityStore.save(options.apiUrl, identity);
967
+ }
968
+ while (!runtime.stopping) {
969
+ options.onStatus?.("connecting");
970
+ const credentials = await obtainConnection(options.client, options.identityStore, options.apiUrl, identity, appVersion, createNonce, now, repaired, options.recoverRevokedIdentity);
971
+ if (runtime.stopping || !identity.deviceId) break;
972
+ const deviceId = identity.deviceId;
973
+ const session = startDeviceListenSession({
974
+ relayUrl: credentials.relayUrl,
975
+ token: credentials.token,
976
+ deviceId,
977
+ connectionId: credentials.connectionId,
978
+ forwardTo,
979
+ appVersion,
980
+ socketFactory: options.socketFactory,
981
+ fetchImpl: options.fetchImpl,
982
+ onListening: () => {
983
+ reconnects = 0;
984
+ options.onStatus?.("listening");
985
+ options.onReady?.({
986
+ deviceId,
987
+ forwardTo
988
+ });
989
+ if (options.quiet) return;
990
+ if (options.json) {
991
+ if (announcedJson) return;
992
+ announcedJson = true;
993
+ console.log(JSON.stringify({
994
+ deviceId,
995
+ forwardTo,
996
+ relayUrl: credentials.relayUrl
997
+ }));
998
+ return;
999
+ }
1000
+ printListening(deviceId, forwardTo);
1001
+ }
1002
+ });
1003
+ runtime.active = session;
1004
+ if (runtime.stopping) session.close(1e3, "Interrupted");
1005
+ const close = await session.closed;
1006
+ runtime.active = null;
1007
+ if (close.cleanupFailed) throw new DeviceListenCleanupError("Device relay cleanup failed");
1008
+ if (runtime.stopping || close.intent === "stop") {
1009
+ setExitCode(0);
1010
+ return;
1011
+ }
1012
+ if (close.intent === "revoked") throw new DeviceListenError("This device was revoked");
1013
+ if (reconnects >= reconnectDelays.length) {
1014
+ options.onStatus?.("offline");
1015
+ if (!options.quiet) console.error(chalk.red("Offline. Stopped listening after repeated disconnects."));
1016
+ setExitCode(1);
1017
+ return;
1018
+ }
1019
+ options.onStatus?.("offline");
1020
+ if (!options.quiet) console.error(chalk.yellow("Offline. Reconnecting."));
1021
+ await untilStopped(sleep(reconnectDelays[reconnects] ?? 0), stopped.signal);
1022
+ reconnects += 1;
1023
+ if (runtime.stopping) break;
1024
+ }
1025
+ setExitCode(0);
1026
+ } catch (error) {
1027
+ if (runtime.stopping && !(error instanceof DeviceListenCleanupError)) {
1028
+ setExitCode(0);
1029
+ return;
1030
+ }
1031
+ setExitCode(1);
1032
+ throw error;
1033
+ } finally {
1034
+ runtime.active?.close(1e3, "Stopped");
1035
+ runtime.active = null;
1036
+ uninstall();
1037
+ options.signal?.removeEventListener("abort", requestStop);
1038
+ options.onStatus?.("stopped");
1039
+ }
1040
+ }
1041
+ export { createDeviceListenIdentityStore as i, assertListenForwardTarget as n, runDeviceListen as r, DeviceListenCleanupError as t };
1042
+