@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,97 @@
1
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
+ export declare const APP_BROWSER_HOST_SIDECAR_FLAG = "--rynx-browser-host-sidecar";
3
+ export declare const APP_BROWSER_HOST_BOOTSTRAP_TYPE = "rynx.browser-host.bootstrap";
4
+ export declare const APP_BROWSER_HOST_READY_TYPE = "rynx.browser-host.ready";
5
+ declare const IPC_SCHEMA_VERSION = 1;
6
+ export interface AppBrowserHostLaunchConfig {
7
+ executable: string;
8
+ appPath?: string;
9
+ }
10
+ export interface AppBrowserHostBootstrapMessage {
11
+ type: typeof APP_BROWSER_HOST_BOOTSTRAP_TYPE;
12
+ schemaVersion: typeof IPC_SCHEMA_VERSION;
13
+ origin: string;
14
+ managementToken: string;
15
+ }
16
+ export interface AppBrowserHostReadyMessage {
17
+ type: typeof APP_BROWSER_HOST_READY_TYPE;
18
+ schemaVersion: typeof IPC_SCHEMA_VERSION;
19
+ }
20
+ export type AppBrowserHostSpawn = (executable: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
21
+ export interface AppBrowserHostSupervisorOptions {
22
+ launch: AppBrowserHostLaunchConfig;
23
+ bootstrap: AppBrowserHostBootstrapMessage;
24
+ env?: NodeJS.ProcessEnv;
25
+ spawnProcess?: AppBrowserHostSpawn;
26
+ startupTimeoutMs?: number;
27
+ stopTimeoutMs?: number;
28
+ restartDelayMs?: number;
29
+ /** Prevents the offscreen sidecar from competing with a live App-window Host. */
30
+ isDesktopHostConnected?: () => boolean;
31
+ }
32
+ /**
33
+ * Resolve the browser-host launch contract published by the desktop App.
34
+ *
35
+ * A standalone CLI/daemon never enables this supervisor. App follow and
36
+ * resident modes intentionally share the same contract; their lifetime policy
37
+ * is owned by the daemon composition root, not by this process supervisor.
38
+ */
39
+ export declare function parseAppBrowserHostLaunchConfig(env?: NodeJS.ProcessEnv): AppBrowserHostLaunchConfig | undefined;
40
+ /**
41
+ * Validate and copy the only secret-bearing message accepted by the sidecar.
42
+ * The exact-key check prevents silently widening this IPC boundary.
43
+ */
44
+ export declare function parseAppBrowserHostBootstrapMessage(value: unknown): AppBrowserHostBootstrapMessage;
45
+ /** Strict type guard for the authenticated-lease readiness acknowledgement. */
46
+ export declare function isAppBrowserHostReadyMessage(value: unknown): value is AppBrowserHostReadyMessage;
47
+ /**
48
+ * Owns the App browser-host sidecar process and its authenticated IPC
49
+ * bootstrap. `start` and `ensureConnected` are single-flight. A ready message
50
+ * means the sidecar has acquired its authenticated host lease; merely spawning
51
+ * the process is never considered success.
52
+ */
53
+ export declare class AppBrowserHostSupervisor {
54
+ private readonly launch;
55
+ private readonly bootstrap;
56
+ private readonly childEnv;
57
+ private readonly spawnProcess;
58
+ private readonly startupTimeoutMs;
59
+ private readonly stopTimeoutMs;
60
+ private readonly restartDelayMs;
61
+ private readonly isDesktopHostConnected;
62
+ private child?;
63
+ private connectedChild?;
64
+ private connecting?;
65
+ private restarting?;
66
+ private restartTimer?;
67
+ private restartStabilityTimer?;
68
+ private stopOperation?;
69
+ private readonly terminationOperations;
70
+ private desired;
71
+ private stopped;
72
+ private consecutiveRestartFailures;
73
+ constructor(options: AppBrowserHostSupervisorOptions);
74
+ start(): Promise<void>;
75
+ /**
76
+ * Replaces a live process whose authenticated Host lease is no longer
77
+ * usable. The replacement is single-flight so concurrent Browser opens do
78
+ * not fan out into multiple Electron sidecars.
79
+ */
80
+ restart(): Promise<void>;
81
+ ensureConnected(): Promise<void>;
82
+ private ensureConnection;
83
+ stop(): Promise<void>;
84
+ private launchAndConnect;
85
+ private waitForReady;
86
+ private abandonFailedChild;
87
+ private childBecameUnavailable;
88
+ private scheduleRestart;
89
+ private cancelRestart;
90
+ private armRestartStabilityReset;
91
+ private cancelRestartStabilityReset;
92
+ private stopOwnedProcess;
93
+ private replaceOwnedProcess;
94
+ private terminateChild;
95
+ private performTerminateChild;
96
+ }
97
+ export {};
@@ -0,0 +1,529 @@
1
+ import { spawn, } from "node:child_process";
2
+ import path from "node:path";
3
+ export const APP_BROWSER_HOST_SIDECAR_FLAG = "--rynx-browser-host-sidecar";
4
+ export const APP_BROWSER_HOST_BOOTSTRAP_TYPE = "rynx.browser-host.bootstrap";
5
+ export const APP_BROWSER_HOST_READY_TYPE = "rynx.browser-host.ready";
6
+ const IPC_SCHEMA_VERSION = 1;
7
+ const DEFAULT_STARTUP_TIMEOUT_MS = 15_000;
8
+ const DEFAULT_STOP_TIMEOUT_MS = 5_000;
9
+ const DEFAULT_RESTART_DELAY_MS = 250;
10
+ const MAX_RESTART_DELAY_MS = 30_000;
11
+ const RESTART_STABILITY_WINDOW_MS = 30_000;
12
+ const MAX_TIMEOUT_MS = 120_000;
13
+ const MAX_PATH_BYTES = 16 * 1024;
14
+ const MIN_MANAGEMENT_TOKEN_BYTES = 32;
15
+ const MAX_MANAGEMENT_TOKEN_BYTES = 256;
16
+ /**
17
+ * Resolve the browser-host launch contract published by the desktop App.
18
+ *
19
+ * A standalone CLI/daemon never enables this supervisor. App follow and
20
+ * resident modes intentionally share the same contract; their lifetime policy
21
+ * is owned by the daemon composition root, not by this process supervisor.
22
+ */
23
+ export function parseAppBrowserHostLaunchConfig(env = process.env) {
24
+ if (env.RYNX_DISTRIBUTION !== "app")
25
+ return undefined;
26
+ const executable = env.RYNX_BROWSER_HOST_EXECUTABLE;
27
+ if (executable === undefined) {
28
+ throw new Error("App distribution requires RYNX_BROWSER_HOST_EXECUTABLE");
29
+ }
30
+ const parsedExecutable = absolutePath(executable, "RYNX_BROWSER_HOST_EXECUTABLE");
31
+ const appPath = env.RYNX_BROWSER_HOST_APP_PATH;
32
+ return appPath === undefined
33
+ ? { executable: parsedExecutable }
34
+ : {
35
+ executable: parsedExecutable,
36
+ appPath: absolutePath(appPath, "RYNX_BROWSER_HOST_APP_PATH"),
37
+ };
38
+ }
39
+ /**
40
+ * Validate and copy the only secret-bearing message accepted by the sidecar.
41
+ * The exact-key check prevents silently widening this IPC boundary.
42
+ */
43
+ export function parseAppBrowserHostBootstrapMessage(value) {
44
+ if (!isRecordWithExactKeys(value, ["type", "schemaVersion", "origin", "managementToken"])) {
45
+ throw new Error("invalid App browser host bootstrap message");
46
+ }
47
+ if (value.type !== APP_BROWSER_HOST_BOOTSTRAP_TYPE) {
48
+ throw new Error("invalid App browser host bootstrap message type");
49
+ }
50
+ if (value.schemaVersion !== IPC_SCHEMA_VERSION) {
51
+ throw new Error("unsupported App browser host bootstrap schema version");
52
+ }
53
+ return {
54
+ type: APP_BROWSER_HOST_BOOTSTRAP_TYPE,
55
+ schemaVersion: IPC_SCHEMA_VERSION,
56
+ origin: loopbackOrigin(value.origin),
57
+ managementToken: managementToken(value.managementToken),
58
+ };
59
+ }
60
+ /** Strict type guard for the authenticated-lease readiness acknowledgement. */
61
+ export function isAppBrowserHostReadyMessage(value) {
62
+ return isRecordWithExactKeys(value, ["type", "schemaVersion"])
63
+ && value.type === APP_BROWSER_HOST_READY_TYPE
64
+ && value.schemaVersion === IPC_SCHEMA_VERSION;
65
+ }
66
+ /**
67
+ * Owns the App browser-host sidecar process and its authenticated IPC
68
+ * bootstrap. `start` and `ensureConnected` are single-flight. A ready message
69
+ * means the sidecar has acquired its authenticated host lease; merely spawning
70
+ * the process is never considered success.
71
+ */
72
+ export class AppBrowserHostSupervisor {
73
+ launch;
74
+ bootstrap;
75
+ childEnv;
76
+ spawnProcess;
77
+ startupTimeoutMs;
78
+ stopTimeoutMs;
79
+ restartDelayMs;
80
+ isDesktopHostConnected;
81
+ child;
82
+ connectedChild;
83
+ connecting;
84
+ restarting;
85
+ restartTimer;
86
+ restartStabilityTimer;
87
+ stopOperation;
88
+ terminationOperations = new WeakMap();
89
+ desired = false;
90
+ stopped = false;
91
+ consecutiveRestartFailures = 0;
92
+ constructor(options) {
93
+ this.launch = validateLaunchConfig(options.launch);
94
+ this.bootstrap = parseAppBrowserHostBootstrapMessage(options.bootstrap);
95
+ this.childEnv = childEnvironment(options.env ?? process.env, this.bootstrap);
96
+ this.spawnProcess = options.spawnProcess ?? defaultSpawn;
97
+ this.startupTimeoutMs = positiveDuration(options.startupTimeoutMs, DEFAULT_STARTUP_TIMEOUT_MS, "startupTimeoutMs");
98
+ this.stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS, "stopTimeoutMs");
99
+ this.restartDelayMs = nonNegativeDuration(options.restartDelayMs, DEFAULT_RESTART_DELAY_MS, "restartDelayMs");
100
+ this.isDesktopHostConnected = options.isDesktopHostConnected ?? (() => false);
101
+ }
102
+ start() {
103
+ return this.ensureConnected();
104
+ }
105
+ /**
106
+ * Replaces a live process whose authenticated Host lease is no longer
107
+ * usable. The replacement is single-flight so concurrent Browser opens do
108
+ * not fan out into multiple Electron sidecars.
109
+ */
110
+ restart() {
111
+ if (this.stopped) {
112
+ return Promise.reject(new Error("App browser host supervisor is stopped"));
113
+ }
114
+ if (this.restarting)
115
+ return this.restarting;
116
+ this.desired = true;
117
+ if (this.isDesktopHostConnected()) {
118
+ this.cancelRestart();
119
+ return Promise.resolve();
120
+ }
121
+ this.cancelRestart();
122
+ const operation = this.replaceOwnedProcess();
123
+ this.restarting = operation;
124
+ void operation.then(() => {
125
+ if (this.restarting === operation)
126
+ this.restarting = undefined;
127
+ }, () => {
128
+ if (this.restarting === operation)
129
+ this.restarting = undefined;
130
+ });
131
+ return operation;
132
+ }
133
+ ensureConnected() {
134
+ if (this.restarting)
135
+ return this.restarting;
136
+ return this.ensureConnection();
137
+ }
138
+ ensureConnection() {
139
+ if (this.stopped) {
140
+ return Promise.reject(new Error("App browser host supervisor is stopped"));
141
+ }
142
+ this.desired = true;
143
+ if (this.connectedChild
144
+ && this.connectedChild === this.child
145
+ && childIsRunning(this.connectedChild)) {
146
+ return Promise.resolve();
147
+ }
148
+ if (this.isDesktopHostConnected()) {
149
+ this.cancelRestart();
150
+ return Promise.resolve();
151
+ }
152
+ if (this.connecting)
153
+ return this.connecting;
154
+ this.cancelRestart();
155
+ const operation = this.launchAndConnect();
156
+ this.connecting = operation;
157
+ void operation.then(() => {
158
+ if (this.connecting === operation)
159
+ this.connecting = undefined;
160
+ }, () => {
161
+ if (this.connecting === operation)
162
+ this.connecting = undefined;
163
+ });
164
+ return operation;
165
+ }
166
+ stop() {
167
+ if (this.stopOperation)
168
+ return this.stopOperation;
169
+ this.stopped = true;
170
+ this.desired = false;
171
+ this.cancelRestart();
172
+ this.cancelRestartStabilityReset();
173
+ const operation = this.stopOwnedProcess();
174
+ this.stopOperation = operation;
175
+ return operation;
176
+ }
177
+ async launchAndConnect() {
178
+ const args = this.launch.appPath === undefined
179
+ ? [APP_BROWSER_HOST_SIDECAR_FLAG]
180
+ : [this.launch.appPath, APP_BROWSER_HOST_SIDECAR_FLAG];
181
+ let child;
182
+ try {
183
+ child = this.spawnProcess(this.launch.executable, args, {
184
+ env: { ...this.childEnv },
185
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
186
+ });
187
+ }
188
+ catch (error) {
189
+ throw supervisorError("failed to spawn App browser host", error);
190
+ }
191
+ this.child = child;
192
+ this.connectedChild = undefined;
193
+ const onLifecycleError = () => {
194
+ if (this.connectedChild === child)
195
+ this.abandonFailedChild(child);
196
+ };
197
+ child.on("error", onLifecycleError);
198
+ child.once("exit", () => {
199
+ child.off("error", onLifecycleError);
200
+ this.childBecameUnavailable(child);
201
+ });
202
+ try {
203
+ await this.waitForReady(child);
204
+ if (this.stopped
205
+ || this.child !== child
206
+ || !childIsRunning(child)) {
207
+ throw new Error("App browser host exited before it became ready");
208
+ }
209
+ this.connectedChild = child;
210
+ this.armRestartStabilityReset(child);
211
+ }
212
+ catch (error) {
213
+ this.abandonFailedChild(child);
214
+ throw supervisorError("App browser host failed to become ready", error);
215
+ }
216
+ }
217
+ waitForReady(child) {
218
+ return new Promise((resolve, reject) => {
219
+ let sent = false;
220
+ let settled = false;
221
+ const timer = setTimeout(() => finish(new Error("timed out waiting for App browser host readiness")), this.startupTimeoutMs);
222
+ timer.unref();
223
+ const onSpawn = () => {
224
+ if (settled || sent)
225
+ return;
226
+ sent = true;
227
+ try {
228
+ child.send(this.bootstrap, (error) => {
229
+ if (error) {
230
+ finish(supervisorError("failed to send App browser host bootstrap", error));
231
+ }
232
+ });
233
+ }
234
+ catch (error) {
235
+ finish(supervisorError("failed to send App browser host bootstrap", error));
236
+ }
237
+ };
238
+ const onMessage = (message) => {
239
+ if (sent && isAppBrowserHostReadyMessage(message))
240
+ finish();
241
+ };
242
+ const onError = (error) => {
243
+ finish(supervisorError("App browser host process error", error));
244
+ };
245
+ const onExit = (code, signal) => {
246
+ finish(new Error(`App browser host exited before readiness (code ${String(code)}, signal ${String(signal)})`));
247
+ };
248
+ function finish(error) {
249
+ if (settled)
250
+ return;
251
+ settled = true;
252
+ clearTimeout(timer);
253
+ child.off("spawn", onSpawn);
254
+ child.off("message", onMessage);
255
+ child.off("error", onError);
256
+ child.off("exit", onExit);
257
+ if (error)
258
+ reject(error);
259
+ else
260
+ resolve();
261
+ }
262
+ child.once("spawn", onSpawn);
263
+ child.on("message", onMessage);
264
+ child.once("error", onError);
265
+ child.once("exit", onExit);
266
+ });
267
+ }
268
+ abandonFailedChild(child) {
269
+ if (this.child !== child)
270
+ return;
271
+ if (child.pid === undefined || !childIsRunning(child)) {
272
+ this.childBecameUnavailable(child);
273
+ return;
274
+ }
275
+ void this.terminateChild(child).then(() => {
276
+ this.childBecameUnavailable(child);
277
+ }, () => {
278
+ // Keep ownership of a child that could not be terminated. A later
279
+ // explicit stop retries cleanup and surfaces the failure to its caller.
280
+ });
281
+ }
282
+ childBecameUnavailable(child) {
283
+ if (this.child !== child)
284
+ return;
285
+ this.cancelRestartStabilityReset();
286
+ this.child = undefined;
287
+ if (this.connectedChild === child)
288
+ this.connectedChild = undefined;
289
+ if (this.desired && !this.stopped)
290
+ this.scheduleRestart();
291
+ }
292
+ scheduleRestart() {
293
+ if (this.restartTimer || this.stopped || !this.desired)
294
+ return;
295
+ const maximumDelay = Math.max(this.restartDelayMs, MAX_RESTART_DELAY_MS);
296
+ const delay = Math.min(maximumDelay, this.restartDelayMs * 2 ** Math.min(this.consecutiveRestartFailures, 16));
297
+ this.consecutiveRestartFailures += 1;
298
+ this.restartTimer = setTimeout(() => {
299
+ this.restartTimer = undefined;
300
+ if (this.stopped || !this.desired)
301
+ return;
302
+ if (this.isDesktopHostConnected())
303
+ return;
304
+ void this.ensureConnected().catch(() => undefined);
305
+ }, delay);
306
+ this.restartTimer.unref();
307
+ }
308
+ cancelRestart() {
309
+ if (!this.restartTimer)
310
+ return;
311
+ clearTimeout(this.restartTimer);
312
+ this.restartTimer = undefined;
313
+ }
314
+ armRestartStabilityReset(child) {
315
+ this.cancelRestartStabilityReset();
316
+ this.restartStabilityTimer = setTimeout(() => {
317
+ this.restartStabilityTimer = undefined;
318
+ if (this.connectedChild === child
319
+ && this.child === child
320
+ && childIsRunning(child)) {
321
+ this.consecutiveRestartFailures = 0;
322
+ }
323
+ }, RESTART_STABILITY_WINDOW_MS);
324
+ this.restartStabilityTimer.unref();
325
+ }
326
+ cancelRestartStabilityReset() {
327
+ if (!this.restartStabilityTimer)
328
+ return;
329
+ clearTimeout(this.restartStabilityTimer);
330
+ this.restartStabilityTimer = undefined;
331
+ }
332
+ async stopOwnedProcess() {
333
+ const child = this.child;
334
+ if (child)
335
+ await this.terminateChild(child);
336
+ const connecting = this.connecting;
337
+ if (connecting)
338
+ await connecting.catch(() => undefined);
339
+ if (this.child && this.child !== child) {
340
+ await this.terminateChild(this.child);
341
+ }
342
+ this.child = undefined;
343
+ this.connectedChild = undefined;
344
+ }
345
+ async replaceOwnedProcess() {
346
+ const child = this.child;
347
+ this.connectedChild = undefined;
348
+ if (child) {
349
+ await this.terminateChild(child);
350
+ if (this.child === child)
351
+ this.childBecameUnavailable(child);
352
+ }
353
+ const connecting = this.connecting;
354
+ if (connecting)
355
+ await connecting.catch(() => undefined);
356
+ this.cancelRestart();
357
+ await this.ensureConnection();
358
+ }
359
+ terminateChild(child) {
360
+ const existing = this.terminationOperations.get(child);
361
+ if (existing)
362
+ return existing;
363
+ let resolveOperation;
364
+ let rejectOperation;
365
+ const operation = new Promise((resolve, reject) => {
366
+ resolveOperation = resolve;
367
+ rejectOperation = reject;
368
+ });
369
+ this.terminationOperations.set(child, operation);
370
+ void this.performTerminateChild(child).then(() => {
371
+ this.terminationOperations.delete(child);
372
+ resolveOperation();
373
+ }, (error) => {
374
+ this.terminationOperations.delete(child);
375
+ rejectOperation(error);
376
+ });
377
+ return operation;
378
+ }
379
+ async performTerminateChild(child) {
380
+ if (!childIsRunning(child))
381
+ return;
382
+ const gracefulExit = waitForExit(child, this.stopTimeoutMs);
383
+ try {
384
+ child.kill("SIGTERM");
385
+ }
386
+ catch {
387
+ // Escalation below remains the authoritative shutdown path.
388
+ }
389
+ if (await gracefulExit)
390
+ return;
391
+ const forcedExit = waitForExit(child, this.stopTimeoutMs);
392
+ let signalSent;
393
+ try {
394
+ signalSent = child.kill("SIGKILL");
395
+ }
396
+ catch (error) {
397
+ throw supervisorError("failed to force-stop App browser host", error);
398
+ }
399
+ if (!signalSent && childIsRunning(child)) {
400
+ throw new Error("failed to force-stop App browser host");
401
+ }
402
+ if (!await forcedExit) {
403
+ throw new Error("App browser host did not exit after SIGKILL");
404
+ }
405
+ }
406
+ }
407
+ const defaultSpawn = (executable, args, options) => spawn(executable, [...args], options);
408
+ function validateLaunchConfig(value) {
409
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
410
+ throw new Error("invalid App browser host launch config");
411
+ }
412
+ const keys = Object.keys(value);
413
+ if (keys.some((key) => key !== "executable" && key !== "appPath")
414
+ || !keys.includes("executable")) {
415
+ throw new Error("invalid App browser host launch config");
416
+ }
417
+ const executable = absolutePath(value.executable, "launch executable");
418
+ return value.appPath === undefined
419
+ ? { executable }
420
+ : {
421
+ executable,
422
+ appPath: absolutePath(value.appPath, "launch appPath"),
423
+ };
424
+ }
425
+ function childEnvironment(source, bootstrap) {
426
+ const result = {};
427
+ for (const [key, value] of Object.entries(source)) {
428
+ if (key.toUpperCase() === "ELECTRON_RUN_AS_NODE")
429
+ continue;
430
+ if (value === bootstrap.origin || value === bootstrap.managementToken)
431
+ continue;
432
+ result[key] = value;
433
+ }
434
+ return result;
435
+ }
436
+ function absolutePath(value, name) {
437
+ if (typeof value !== "string"
438
+ || value.length === 0
439
+ || value.trim() !== value
440
+ || Buffer.byteLength(value, "utf8") > MAX_PATH_BYTES
441
+ || /[\u0000-\u001f\u007f]/u.test(value)
442
+ || !path.isAbsolute(value)) {
443
+ throw new Error(`${name} must be an absolute path`);
444
+ }
445
+ return value;
446
+ }
447
+ function loopbackOrigin(value) {
448
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
449
+ throw new Error("bootstrap origin must be a canonical loopback HTTP origin");
450
+ }
451
+ let parsed;
452
+ try {
453
+ parsed = new URL(value);
454
+ }
455
+ catch {
456
+ throw new Error("bootstrap origin must be a canonical loopback HTTP origin");
457
+ }
458
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:")
459
+ || parsed.origin !== value
460
+ || parsed.username
461
+ || parsed.password
462
+ || parsed.hostname !== "localhost"
463
+ && parsed.hostname !== "127.0.0.1"
464
+ && parsed.hostname !== "[::1]") {
465
+ throw new Error("bootstrap origin must be a canonical loopback HTTP origin");
466
+ }
467
+ return parsed.origin;
468
+ }
469
+ function managementToken(value) {
470
+ if (typeof value !== "string"
471
+ || value.trim() !== value
472
+ || /[\u0000-\u001f\u007f]/u.test(value)) {
473
+ throw new Error("invalid App browser host management token");
474
+ }
475
+ const length = Buffer.byteLength(value, "utf8");
476
+ if (length < MIN_MANAGEMENT_TOKEN_BYTES || length > MAX_MANAGEMENT_TOKEN_BYTES) {
477
+ throw new Error("invalid App browser host management token");
478
+ }
479
+ return value;
480
+ }
481
+ function isRecordWithExactKeys(value, expectedKeys) {
482
+ if (!value || typeof value !== "object" || Array.isArray(value))
483
+ return false;
484
+ const keys = Object.keys(value);
485
+ return keys.length === expectedKeys.length
486
+ && keys.every((key) => expectedKeys.includes(key));
487
+ }
488
+ function positiveDuration(value, fallback, name) {
489
+ const resolved = value ?? fallback;
490
+ if (!Number.isInteger(resolved) || resolved <= 0 || resolved > MAX_TIMEOUT_MS) {
491
+ throw new Error(`${name} must be an integer between 1 and ${MAX_TIMEOUT_MS}`);
492
+ }
493
+ return resolved;
494
+ }
495
+ function nonNegativeDuration(value, fallback, name) {
496
+ const resolved = value ?? fallback;
497
+ if (!Number.isInteger(resolved) || resolved < 0 || resolved > MAX_TIMEOUT_MS) {
498
+ throw new Error(`${name} must be an integer between 0 and ${MAX_TIMEOUT_MS}`);
499
+ }
500
+ return resolved;
501
+ }
502
+ function childIsRunning(child) {
503
+ return child.exitCode === null && child.signalCode === null;
504
+ }
505
+ function waitForExit(child, timeoutMs) {
506
+ if (!childIsRunning(child))
507
+ return Promise.resolve(true);
508
+ return new Promise((resolve) => {
509
+ let settled = false;
510
+ const timer = setTimeout(() => finish(false), timeoutMs);
511
+ timer.unref();
512
+ const onExit = () => finish(true);
513
+ const onClose = () => finish(true);
514
+ function finish(exited) {
515
+ if (settled)
516
+ return;
517
+ settled = true;
518
+ clearTimeout(timer);
519
+ child.off("exit", onExit);
520
+ child.off("close", onClose);
521
+ resolve(exited);
522
+ }
523
+ child.once("exit", onExit);
524
+ child.once("close", onClose);
525
+ });
526
+ }
527
+ function supervisorError(message, cause) {
528
+ return new Error(message, { cause });
529
+ }
@@ -0,0 +1,20 @@
1
+ import type { DaemonBrowserArtifactCleanResult, DaemonBrowserArtifactInstallInput, DaemonBrowserArtifactInstallResult, DaemonBrowserArtifactUpdateInput, DaemonBrowserArtifactVersionResult } from "@rynx-ai/protocol/control";
2
+ import { type ChromeForTestingReleaseSelector, type ResolvedChromeForTestingRelease } from "./chrome-for-testing-release-resolver.js";
3
+ import { type ChromeForTestingArtifactStore } from "./chrome-for-testing-store.js";
4
+ export interface BrowserArtifactManagementContext {
5
+ signal?: AbortSignal;
6
+ }
7
+ export interface BrowserArtifactManagementService {
8
+ install(input: DaemonBrowserArtifactInstallInput, context?: BrowserArtifactManagementContext): Promise<DaemonBrowserArtifactInstallResult>;
9
+ update(input: DaemonBrowserArtifactUpdateInput, context?: BrowserArtifactManagementContext): Promise<DaemonBrowserArtifactInstallResult>;
10
+ version(): DaemonBrowserArtifactVersionResult;
11
+ clean(): DaemonBrowserArtifactCleanResult;
12
+ }
13
+ export interface BrowserArtifactManagementServiceOptions {
14
+ resolveRelease?: (selector: ChromeForTestingReleaseSelector, options?: {
15
+ signal?: AbortSignal;
16
+ }) => Promise<ResolvedChromeForTestingRelease>;
17
+ createStore?: () => ChromeForTestingArtifactStore;
18
+ }
19
+ /** Daemon-owned Browser artifact operations exposed through local management. */
20
+ export declare function createBrowserArtifactManagementService(options?: BrowserArtifactManagementServiceOptions): BrowserArtifactManagementService;