@rynx-ai/daemon 0.1.9 → 0.1.10-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.
Files changed (50) hide show
  1. package/dist/app-browser-host-supervisor.d.ts +97 -0
  2. package/dist/app-browser-host-supervisor.js +529 -0
  3. package/dist/browser-artifact-management.d.ts +20 -0
  4. package/dist/browser-artifact-management.js +61 -0
  5. package/dist/chrome-for-testing-store.js +1 -1
  6. package/dist/chrome-inspection-gateway.d.ts +58 -0
  7. package/dist/chrome-inspection-gateway.js +806 -0
  8. package/dist/chrome-inspection-manager.d.ts +94 -0
  9. package/dist/chrome-inspection-manager.js +573 -0
  10. package/dist/cli.js +13 -2
  11. package/dist/control-client.d.ts +9 -0
  12. package/dist/control-client.js +63 -0
  13. package/dist/daemon-build-status.d.ts +8 -0
  14. package/dist/daemon-build-status.js +18 -0
  15. package/dist/daemon-server.d.ts +19 -2
  16. package/dist/daemon-server.js +615 -59
  17. package/dist/db.js +56 -0
  18. package/dist/desktop-browser-host-client.d.ts +2 -1
  19. package/dist/desktop-browser-host-client.js +3 -1
  20. package/dist/direct-runtime-authenticator.js +11 -6
  21. package/dist/headless-browser-host.js +18 -1
  22. package/dist/index-daemon.js +28 -1
  23. package/dist/maintenance-management.d.ts +11 -0
  24. package/dist/maintenance-management.js +13 -0
  25. package/dist/plugin-installer.d.ts +35 -0
  26. package/dist/plugin-installer.js +195 -94
  27. package/dist/plugin-management-service.d.ts +58 -0
  28. package/dist/plugin-management-service.js +240 -0
  29. package/dist/plugin-package.d.ts +26 -3
  30. package/dist/plugin-package.js +235 -56
  31. package/dist/pm2.js +37 -5
  32. package/dist/remote-runtime-access-store.d.ts +1 -0
  33. package/dist/remote-runtime-access-store.js +7 -0
  34. package/dist/remote-runtime-admin.d.ts +3 -0
  35. package/dist/remote-runtime-admin.js +3 -0
  36. package/dist/remote-runtime-connection-manager.d.ts +12 -1
  37. package/dist/remote-runtime-connection-manager.js +170 -4
  38. package/dist/remote-runtime-target-control.d.ts +5 -1
  39. package/dist/remote-runtime-target-control.js +62 -7
  40. package/dist/remote-runtime-target-store.d.ts +4 -1
  41. package/dist/remote-runtime-target-store.js +21 -2
  42. package/dist/session-log-store.js +29 -0
  43. package/dist/session-meta-store.js +3 -1
  44. package/dist/session-pending-message-store.d.ts +2 -0
  45. package/dist/session-pending-message-store.js +41 -0
  46. package/dist/session-resource-store.d.ts +32 -0
  47. package/dist/session-resource-store.js +700 -0
  48. package/dist/setup.d.ts +16 -0
  49. package/dist/setup.js +136 -2
  50. package/package.json +14 -9
@@ -0,0 +1,94 @@
1
+ import { type RuntimeBrowserInspectMessage, type RuntimeBrowserInspectTarget } from "@rynx-ai/protocol/runtime-browser-inspect";
2
+ import type { RuntimeConnectionLease, SessionBrowserService } from "@rynx-ai/server";
3
+ import { type ChromeInspectionGateway, type StartChromeInspectionGatewayOptions } from "./chrome-inspection-gateway.js";
4
+ export interface ChromeInspectionSettings {
5
+ enabled: boolean;
6
+ includeLocal: boolean;
7
+ remoteDaemonIds: string[];
8
+ }
9
+ export interface ChromeInspectionStatus {
10
+ settings: ChromeInspectionSettings;
11
+ runtime: {
12
+ state: "disabled" | "listening" | "error";
13
+ address: string;
14
+ error?: string;
15
+ };
16
+ remotes: Array<{
17
+ daemonId: string;
18
+ displayName: string;
19
+ selected: boolean;
20
+ status: "ready" | "offline" | "incompatible";
21
+ }>;
22
+ }
23
+ export interface ChromeInspectionRemoteTarget {
24
+ daemonId: string;
25
+ displayName: string;
26
+ createdAt: string;
27
+ }
28
+ interface RemoteInspectConnection extends AsyncIterable<RuntimeBrowserInspectMessage> {
29
+ readonly daemonInstanceId: string;
30
+ send(message: RuntimeBrowserInspectMessage): Promise<void>;
31
+ close(): Promise<void>;
32
+ }
33
+ interface InspectionRuntimeLease extends RuntimeConnectionLease {
34
+ openBrowserInspect(target: RuntimeBrowserInspectTarget, options?: {
35
+ signal?: AbortSignal;
36
+ timeoutMs?: number;
37
+ }): Promise<RemoteInspectConnection>;
38
+ }
39
+ export interface ChromeInspectionRuntimeConnections {
40
+ acquire(selector: string, options?: {
41
+ signal?: AbortSignal;
42
+ }): Promise<InspectionRuntimeLease>;
43
+ }
44
+ export interface ChromeInspectionManagerOptions {
45
+ localDaemonInstanceId: string;
46
+ browsers: Pick<SessionBrowserService, "listInspectionTargets" | "getInspectionPageEndpoint" | "activatePage" | "closePage">;
47
+ runtimeTargets: {
48
+ list(): ChromeInspectionRemoteTarget[];
49
+ };
50
+ runtimeConnections: ChromeInspectionRuntimeConnections;
51
+ configFile?: string;
52
+ port?: number;
53
+ startGateway?: (options: StartChromeInspectionGatewayOptions) => Promise<ChromeInspectionGateway>;
54
+ onError?: (error: Error, context: string) => void;
55
+ }
56
+ /**
57
+ * Daemon-owned desired state and target source for Chrome inspection.
58
+ *
59
+ * The App can disappear without affecting this object. Reopening the App only
60
+ * reads its status; it never moves or recreates Browser attachments.
61
+ */
62
+ export declare class ChromeInspectionManager {
63
+ private readonly options;
64
+ private settings;
65
+ private gateway?;
66
+ private runtimeError?;
67
+ private routes;
68
+ private remoteCache;
69
+ private remoteInflight;
70
+ private operation;
71
+ private stopped;
72
+ private constructor();
73
+ static create(options: ChromeInspectionManagerOptions): Promise<ChromeInspectionManager>;
74
+ status(): Promise<ChromeInspectionStatus>;
75
+ configure(input: ChromeInspectionSettings): Promise<ChromeInspectionStatus>;
76
+ shutdown(): Promise<void>;
77
+ private source;
78
+ private listTargets;
79
+ private resolveLocalEndpoint;
80
+ private openRemoteConnection;
81
+ private mutatePage;
82
+ private remoteInventory;
83
+ private loadRemoteInventory;
84
+ private probeRemote;
85
+ private sanitizeSelections;
86
+ private currentRemoteTargets;
87
+ private isRemoteSelected;
88
+ private publicSettings;
89
+ private reconcileGateway;
90
+ private serial;
91
+ private assertRunning;
92
+ private configFile;
93
+ }
94
+ export {};
@@ -0,0 +1,573 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { rynxHome } from "@rynx-ai/core";
5
+ import { RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
6
+ import { startChromeInspectionGateway, } from "./chrome-inspection-gateway.js";
7
+ const CONFIG_SCHEMA_VERSION = 1;
8
+ const DEFAULT_PORT = 9_333;
9
+ const REMOTE_CALL_TIMEOUT_MS = 3_000;
10
+ const REMOTE_DISCOVERY_CACHE_MS = 2_000;
11
+ const MAX_REMOTE_RUNTIMES = 64;
12
+ const MAX_REMOTE_SESSIONS = 256;
13
+ const SESSION_PAGE_SIZE = 100;
14
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
15
+ /**
16
+ * Daemon-owned desired state and target source for Chrome inspection.
17
+ *
18
+ * The App can disappear without affecting this object. Reopening the App only
19
+ * reads its status; it never moves or recreates Browser attachments.
20
+ */
21
+ export class ChromeInspectionManager {
22
+ options;
23
+ settings = defaultPersistedSettings();
24
+ gateway;
25
+ runtimeError;
26
+ routes = new Map();
27
+ remoteCache = new Map();
28
+ remoteInflight = new Map();
29
+ operation = Promise.resolve();
30
+ stopped = false;
31
+ constructor(options) {
32
+ this.options = options;
33
+ }
34
+ static async create(options) {
35
+ const manager = new ChromeInspectionManager(options);
36
+ try {
37
+ manager.settings = await loadSettings(manager.configFile());
38
+ }
39
+ catch (error) {
40
+ // Corrupt local settings must fail closed without preventing the daemon
41
+ // from starting. The next explicit configure operation rewrites them.
42
+ manager.settings = defaultPersistedSettings();
43
+ options.onError?.(asError(error), "settings load");
44
+ }
45
+ manager.sanitizeSelections();
46
+ await manager.reconcileGateway();
47
+ return manager;
48
+ }
49
+ async status() {
50
+ this.sanitizeSelections();
51
+ const selected = new Set(this.settings.remotes.map((entry) => entry.daemonId));
52
+ const remoteTargets = this.currentRemoteTargets();
53
+ const probes = await Promise.all(remoteTargets.map(async (target) => ({
54
+ target,
55
+ status: await this.probeRemote(target.daemonId),
56
+ })));
57
+ return {
58
+ settings: this.publicSettings(),
59
+ runtime: {
60
+ state: !this.settings.enabled
61
+ ? "disabled"
62
+ : this.gateway
63
+ ? "listening"
64
+ : "error",
65
+ address: `127.0.0.1:${this.options.port ?? DEFAULT_PORT}`,
66
+ ...(this.runtimeError ? { error: this.runtimeError } : {}),
67
+ },
68
+ remotes: probes.map(({ target, status }) => ({
69
+ daemonId: target.daemonId,
70
+ displayName: target.displayName,
71
+ selected: selected.has(target.daemonId),
72
+ status,
73
+ })),
74
+ };
75
+ }
76
+ configure(input) {
77
+ return this.serial(async () => {
78
+ this.assertRunning();
79
+ const normalized = normalizePublicSettings(input);
80
+ const targets = new Map(this.currentRemoteTargets().map((target) => [target.daemonId, target]));
81
+ const remotes = normalized.remoteDaemonIds.map((daemonId) => {
82
+ const target = targets.get(daemonId);
83
+ if (!target) {
84
+ throw new Error(`Remote Runtime ${daemonId} is not paired on this device`);
85
+ }
86
+ return {
87
+ daemonId,
88
+ targetCreatedAt: target.createdAt,
89
+ };
90
+ });
91
+ const nextSettings = {
92
+ schemaVersion: CONFIG_SCHEMA_VERSION,
93
+ enabled: normalized.enabled,
94
+ includeLocal: normalized.includeLocal,
95
+ remotes,
96
+ };
97
+ await persistSettings(this.configFile(), nextSettings);
98
+ this.settings = nextSettings;
99
+ this.remoteCache.clear();
100
+ await this.reconcileGateway();
101
+ return await this.status();
102
+ });
103
+ }
104
+ shutdown() {
105
+ return this.serial(async () => {
106
+ if (this.stopped)
107
+ return;
108
+ this.stopped = true;
109
+ const gateway = this.gateway;
110
+ this.gateway = undefined;
111
+ if (gateway)
112
+ await gateway.shutdown();
113
+ this.routes.clear();
114
+ this.remoteCache.clear();
115
+ });
116
+ }
117
+ source() {
118
+ return {
119
+ listTargets: () => this.listTargets(),
120
+ resolveWebSocketEndpoint: (target) => this.resolveLocalEndpoint(target),
121
+ openConnection: (target, options) => this.openRemoteConnection(target, options),
122
+ activate: (target) => this.mutatePage(target, "activate"),
123
+ close: (target) => this.mutatePage(target, "close"),
124
+ };
125
+ }
126
+ async listTargets() {
127
+ this.assertRunning();
128
+ this.sanitizeSelections();
129
+ const nextRoutes = new Map();
130
+ const targets = [];
131
+ if (this.settings.includeLocal) {
132
+ for (const target of this.options.browsers.listInspectionTargets()) {
133
+ const key = localTargetKey(this.options.localDaemonInstanceId, target);
134
+ nextRoutes.set(key, {
135
+ kind: "local",
136
+ sessionId: target.sessionId,
137
+ browserGeneration: target.browserGeneration,
138
+ pageId: target.pageId,
139
+ });
140
+ targets.push(projectTarget(key, target, `This device · ${target.executionBackend}`));
141
+ }
142
+ }
143
+ const remoteTargets = new Map(this.currentRemoteTargets().map((target) => [target.daemonId, target]));
144
+ const selected = this.settings.remotes
145
+ .map((entry) => remoteTargets.get(entry.daemonId))
146
+ .filter((entry) => Boolean(entry));
147
+ const inventories = await Promise.all(selected.map((target) => this.remoteInventory(target).catch((error) => {
148
+ this.options.onError?.(asError(error), `remote discovery ${target.daemonId}`);
149
+ const stale = this.remoteCache.get(target.daemonId);
150
+ if (!stale)
151
+ return undefined;
152
+ const retained = {
153
+ ...stale,
154
+ expiresAt: Date.now() + REMOTE_DISCOVERY_CACHE_MS,
155
+ };
156
+ this.remoteCache.set(target.daemonId, retained);
157
+ return retained;
158
+ })));
159
+ for (const inventory of inventories) {
160
+ if (!inventory)
161
+ continue;
162
+ targets.push(...inventory.targets);
163
+ for (const [key, route] of inventory.routes)
164
+ nextRoutes.set(key, route);
165
+ }
166
+ this.routes = nextRoutes;
167
+ return targets;
168
+ }
169
+ async resolveLocalEndpoint(target) {
170
+ const route = this.routes.get(target.key);
171
+ if (!route || route.kind !== "local") {
172
+ throw new Error("Chrome inspection target is not a current local Page");
173
+ }
174
+ const endpoint = await this.options.browsers.getInspectionPageEndpoint({
175
+ sessionId: route.sessionId,
176
+ browserGeneration: route.browserGeneration,
177
+ pageId: route.pageId,
178
+ });
179
+ return endpoint.endpoint;
180
+ }
181
+ async openRemoteConnection(target, options = {}) {
182
+ const route = this.routes.get(target.key);
183
+ if (!route || route.kind === "local")
184
+ return undefined;
185
+ if (!this.isRemoteSelected(route.daemonId)) {
186
+ throw new Error("Remote Runtime is no longer selected for inspection");
187
+ }
188
+ const lease = await this.options.runtimeConnections.acquire(route.daemonId, {
189
+ ...(options.signal ? { signal: options.signal } : {}),
190
+ });
191
+ try {
192
+ const connection = await lease.openBrowserInspect({
193
+ sessionId: route.sessionId,
194
+ browserGeneration: route.browserGeneration,
195
+ pageId: route.pageId,
196
+ }, {
197
+ ...(options.signal ? { signal: options.signal } : {}),
198
+ timeoutMs: REMOTE_CALL_TIMEOUT_MS,
199
+ });
200
+ if (connection.daemonInstanceId !== route.daemonInstanceId) {
201
+ await connection.close().catch(() => undefined);
202
+ throw new Error("Remote Runtime restarted after target discovery");
203
+ }
204
+ return leasedConnection(connection, lease);
205
+ }
206
+ catch (error) {
207
+ lease.release();
208
+ throw error;
209
+ }
210
+ }
211
+ async mutatePage(target, action) {
212
+ const route = this.routes.get(target.key);
213
+ if (!route)
214
+ throw new Error("Chrome inspection target is stale");
215
+ const params = {
216
+ sessionId: route.sessionId,
217
+ browserGeneration: route.browserGeneration,
218
+ pageId: route.pageId,
219
+ };
220
+ if (route.kind === "local") {
221
+ if (action === "activate")
222
+ await this.options.browsers.activatePage(params);
223
+ else
224
+ await this.options.browsers.closePage(params);
225
+ return;
226
+ }
227
+ if (!this.isRemoteSelected(route.daemonId)) {
228
+ throw new Error("Remote Runtime is no longer selected for inspection");
229
+ }
230
+ const lease = await this.options.runtimeConnections.acquire(route.daemonId);
231
+ try {
232
+ await lease.call(action === "activate" ? "browser.page.activate" : "browser.page.close", params, { timeoutMs: REMOTE_CALL_TIMEOUT_MS });
233
+ }
234
+ finally {
235
+ lease.release();
236
+ }
237
+ }
238
+ async remoteInventory(target) {
239
+ const cached = this.remoteCache.get(target.daemonId);
240
+ if (cached && cached.expiresAt > Date.now())
241
+ return cached;
242
+ const existing = this.remoteInflight.get(target.daemonId);
243
+ if (existing)
244
+ return existing;
245
+ const loading = this.loadRemoteInventory(target).finally(() => {
246
+ this.remoteInflight.delete(target.daemonId);
247
+ });
248
+ this.remoteInflight.set(target.daemonId, loading);
249
+ const loaded = await loading;
250
+ this.remoteCache.set(target.daemonId, loaded);
251
+ return loaded;
252
+ }
253
+ async loadRemoteInventory(target) {
254
+ const lease = await this.options.runtimeConnections.acquire(target.daemonId);
255
+ try {
256
+ const status = await lease.call("status.get", {}, {
257
+ timeoutMs: REMOTE_CALL_TIMEOUT_MS,
258
+ });
259
+ if (!status.semanticCapabilities.includes(RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY)) {
260
+ throw new Error("Remote Runtime does not support Browser inspection");
261
+ }
262
+ const sessions = await listRemoteSessions(lease);
263
+ const states = await mapLimit(sessions, 8, async (sessionId) => lease.call("browser.state.get", { sessionId }, {
264
+ timeoutMs: REMOTE_CALL_TIMEOUT_MS,
265
+ }));
266
+ const targets = [];
267
+ const routes = new Map();
268
+ for (const state of states) {
269
+ if (!state || state.status !== "ready")
270
+ continue;
271
+ for (const page of state.pages) {
272
+ const key = remoteTargetKey(target.daemonId, status.daemonInstanceId, state.sessionId, state.browserGeneration, page.pageId);
273
+ routes.set(key, {
274
+ kind: "remote",
275
+ daemonId: target.daemonId,
276
+ daemonInstanceId: status.daemonInstanceId,
277
+ sessionId: state.sessionId,
278
+ browserGeneration: state.browserGeneration,
279
+ pageId: page.pageId,
280
+ });
281
+ targets.push(projectTarget(key, {
282
+ sessionId: state.sessionId,
283
+ browserGeneration: state.browserGeneration,
284
+ pageId: page.pageId,
285
+ executionBackend: state.executionBackend,
286
+ title: page.title,
287
+ url: page.url,
288
+ }, `${target.displayName} · ${state.executionBackend}`));
289
+ }
290
+ }
291
+ return {
292
+ expiresAt: Date.now() + REMOTE_DISCOVERY_CACHE_MS,
293
+ targets,
294
+ routes,
295
+ };
296
+ }
297
+ finally {
298
+ lease.release();
299
+ }
300
+ }
301
+ async probeRemote(daemonId) {
302
+ let lease;
303
+ try {
304
+ lease = await this.options.runtimeConnections.acquire(daemonId, {
305
+ signal: AbortSignal.timeout(REMOTE_CALL_TIMEOUT_MS),
306
+ });
307
+ const status = await lease.call("status.get", {}, {
308
+ timeoutMs: REMOTE_CALL_TIMEOUT_MS,
309
+ });
310
+ return status.semanticCapabilities.includes(RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY)
311
+ ? "ready"
312
+ : "incompatible";
313
+ }
314
+ catch {
315
+ return "offline";
316
+ }
317
+ finally {
318
+ lease?.release();
319
+ }
320
+ }
321
+ sanitizeSelections() {
322
+ const current = new Map(this.currentRemoteTargets().map((target) => [target.daemonId, target]));
323
+ this.settings.remotes = this.settings.remotes.filter((selected) => {
324
+ const target = current.get(selected.daemonId);
325
+ return target?.createdAt === selected.targetCreatedAt;
326
+ });
327
+ }
328
+ currentRemoteTargets() {
329
+ return this.options.runtimeTargets.list().slice(0, MAX_REMOTE_RUNTIMES);
330
+ }
331
+ isRemoteSelected(daemonId) {
332
+ return this.settings.remotes.some((entry) => entry.daemonId === daemonId);
333
+ }
334
+ publicSettings() {
335
+ return {
336
+ enabled: this.settings.enabled,
337
+ includeLocal: this.settings.includeLocal,
338
+ remoteDaemonIds: this.settings.remotes.map((entry) => entry.daemonId),
339
+ };
340
+ }
341
+ async reconcileGateway() {
342
+ const existing = this.gateway;
343
+ this.gateway = undefined;
344
+ if (existing)
345
+ await existing.shutdown();
346
+ this.routes.clear();
347
+ this.runtimeError = undefined;
348
+ if (!this.settings.enabled || this.stopped)
349
+ return;
350
+ try {
351
+ this.gateway = await (this.options.startGateway ?? startChromeInspectionGateway)({
352
+ source: this.source(),
353
+ port: this.options.port ?? DEFAULT_PORT,
354
+ onError: this.options.onError,
355
+ });
356
+ }
357
+ catch (error) {
358
+ const normalized = asError(error);
359
+ this.runtimeError = hasCode(normalized, "EADDRINUSE")
360
+ ? "Port 9333 is already in use."
361
+ : normalized.message;
362
+ this.options.onError?.(normalized, "gateway startup");
363
+ }
364
+ }
365
+ serial(operation) {
366
+ const run = this.operation.then(operation, operation);
367
+ this.operation = run.then(() => undefined, () => undefined);
368
+ return run;
369
+ }
370
+ assertRunning() {
371
+ if (this.stopped)
372
+ throw new Error("Chrome inspection manager is stopped");
373
+ }
374
+ configFile() {
375
+ return this.options.configFile ??
376
+ join(rynxHome(), "chrome-inspection.json");
377
+ }
378
+ }
379
+ function projectTarget(key, target, description) {
380
+ return {
381
+ key,
382
+ title: target.title || "(untitled)",
383
+ url: target.url,
384
+ description,
385
+ // Discovery does not wake or reconcile a Browser just to read its version.
386
+ // The exact engine endpoint remains attach-time-only.
387
+ engineVersion: "Chrome/0.0.0.0",
388
+ };
389
+ }
390
+ function localTargetKey(daemonInstanceId, target) {
391
+ return [
392
+ "local",
393
+ daemonInstanceId,
394
+ target.sessionId,
395
+ target.browserGeneration,
396
+ target.pageId,
397
+ ].join(":");
398
+ }
399
+ function remoteTargetKey(daemonId, daemonInstanceId, sessionId, generation, pageId) {
400
+ return [
401
+ "remote",
402
+ daemonId,
403
+ daemonInstanceId,
404
+ sessionId,
405
+ generation,
406
+ pageId,
407
+ ].join(":");
408
+ }
409
+ function leasedConnection(connection, lease) {
410
+ let closed = false;
411
+ const close = async () => {
412
+ if (closed)
413
+ return;
414
+ closed = true;
415
+ try {
416
+ await connection.close();
417
+ }
418
+ finally {
419
+ lease.release();
420
+ }
421
+ };
422
+ return {
423
+ send: (message) => connection.send(message),
424
+ close,
425
+ async *[Symbol.asyncIterator]() {
426
+ try {
427
+ for await (const message of connection)
428
+ yield message;
429
+ }
430
+ finally {
431
+ await close();
432
+ }
433
+ },
434
+ };
435
+ }
436
+ async function listRemoteSessions(lease) {
437
+ const sessions = [];
438
+ let cursor;
439
+ while (sessions.length < MAX_REMOTE_SESSIONS) {
440
+ const result = await lease.call("session.list", {
441
+ limit: SESSION_PAGE_SIZE,
442
+ ...(cursor ? { cursor } : {}),
443
+ }, {
444
+ timeoutMs: REMOTE_CALL_TIMEOUT_MS,
445
+ });
446
+ sessions.push(...result.sessions.map((session) => session.id));
447
+ if (!result.hasMore || !result.nextCursor)
448
+ break;
449
+ cursor = result.nextCursor;
450
+ }
451
+ return sessions.slice(0, MAX_REMOTE_SESSIONS);
452
+ }
453
+ async function mapLimit(values, concurrency, mapper) {
454
+ const result = new Array(values.length);
455
+ let index = 0;
456
+ const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
457
+ while (index < values.length) {
458
+ const current = index;
459
+ index += 1;
460
+ result[current] = await mapper(values[current]);
461
+ }
462
+ });
463
+ await Promise.all(workers);
464
+ return result;
465
+ }
466
+ function normalizePublicSettings(input) {
467
+ if (!input ||
468
+ typeof input !== "object" ||
469
+ typeof input.enabled !== "boolean" ||
470
+ typeof input.includeLocal !== "boolean" ||
471
+ !Array.isArray(input.remoteDaemonIds)) {
472
+ throw new TypeError("Chrome inspection settings are invalid");
473
+ }
474
+ const remoteDaemonIds = input.remoteDaemonIds.map((value) => {
475
+ if (typeof value !== "string" || !OPAQUE_ID_PATTERN.test(value)) {
476
+ throw new TypeError("Chrome inspection Remote Runtime id is invalid");
477
+ }
478
+ return value;
479
+ });
480
+ if (remoteDaemonIds.length > MAX_REMOTE_RUNTIMES ||
481
+ new Set(remoteDaemonIds).size !== remoteDaemonIds.length) {
482
+ throw new TypeError("Chrome inspection Remote Runtime selection is invalid");
483
+ }
484
+ return {
485
+ enabled: input.enabled,
486
+ includeLocal: input.includeLocal,
487
+ remoteDaemonIds,
488
+ };
489
+ }
490
+ async function loadSettings(file) {
491
+ let raw;
492
+ try {
493
+ raw = await readFile(file, "utf8");
494
+ }
495
+ catch (error) {
496
+ if (hasCode(error, "ENOENT"))
497
+ return defaultPersistedSettings();
498
+ throw error;
499
+ }
500
+ if (Buffer.byteLength(raw, "utf8") > 64 * 1024) {
501
+ throw new Error("Chrome inspection settings file is too large");
502
+ }
503
+ const value = JSON.parse(raw);
504
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
505
+ throw new Error("Chrome inspection settings file is invalid");
506
+ }
507
+ const record = value;
508
+ if (record.schemaVersion !== CONFIG_SCHEMA_VERSION ||
509
+ typeof record.enabled !== "boolean" ||
510
+ typeof record.includeLocal !== "boolean" ||
511
+ !Array.isArray(record.remotes) ||
512
+ record.remotes.length > MAX_REMOTE_RUNTIMES) {
513
+ throw new Error("Chrome inspection settings file is invalid");
514
+ }
515
+ const remotes = record.remotes.map((value) => {
516
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
517
+ throw new Error("Chrome inspection settings file is invalid");
518
+ }
519
+ const remote = value;
520
+ if (Object.keys(remote).sort().join(",") !== "daemonId,targetCreatedAt" ||
521
+ typeof remote.daemonId !== "string" ||
522
+ !OPAQUE_ID_PATTERN.test(remote.daemonId) ||
523
+ typeof remote.targetCreatedAt !== "string" ||
524
+ !Number.isFinite(Date.parse(remote.targetCreatedAt))) {
525
+ throw new Error("Chrome inspection settings file is invalid");
526
+ }
527
+ return {
528
+ daemonId: remote.daemonId,
529
+ targetCreatedAt: remote.targetCreatedAt,
530
+ };
531
+ });
532
+ if (new Set(remotes.map((remote) => remote.daemonId)).size !== remotes.length) {
533
+ throw new Error("Chrome inspection settings file contains duplicate Runtime ids");
534
+ }
535
+ return {
536
+ schemaVersion: CONFIG_SCHEMA_VERSION,
537
+ enabled: record.enabled,
538
+ includeLocal: record.includeLocal,
539
+ remotes,
540
+ };
541
+ }
542
+ async function persistSettings(file, settings) {
543
+ const directory = dirname(file);
544
+ await mkdir(directory, { recursive: true, mode: 0o700 });
545
+ const temporary = join(directory, `.chrome-inspection.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
546
+ try {
547
+ await writeFile(temporary, `${JSON.stringify(settings, null, 2)}\n`, {
548
+ mode: 0o600,
549
+ flag: "wx",
550
+ });
551
+ await rename(temporary, file);
552
+ }
553
+ finally {
554
+ await rm(temporary, { force: true }).catch(() => undefined);
555
+ }
556
+ }
557
+ function defaultPersistedSettings() {
558
+ return {
559
+ schemaVersion: CONFIG_SCHEMA_VERSION,
560
+ enabled: false,
561
+ includeLocal: true,
562
+ remotes: [],
563
+ };
564
+ }
565
+ function asError(error) {
566
+ return error instanceof Error ? error : new Error(String(error));
567
+ }
568
+ function hasCode(error, code) {
569
+ return Boolean(error &&
570
+ typeof error === "object" &&
571
+ "code" in error &&
572
+ error.code === code);
573
+ }