@rynx-ai/daemon 0.1.0 → 0.1.9

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 (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
@@ -0,0 +1,692 @@
1
+ import { createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, randomUUID, sign, verify, } from "node:crypto";
2
+ import { chmodSync, closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, fsyncSync, } from "node:fs";
3
+ import { chmod, link, lstat, mkdir, open, readFile, rm, } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { rynxHome } from "@rynx-ai/core";
6
+ import { encodeServerAcceptedProofPayload, encodeServerHelloProofPayload, } from "@rynx-ai/protocol/direct-runtime";
7
+ const INSTALLATION_ID_FILE = "installation-id";
8
+ const INSTALLATION_KEY_FILE = "installation-key.json";
9
+ const DIRECT_RUNTIME_ENCRYPTION_KEY_FILE = "direct-runtime-encryption-key.json";
10
+ const INSTALLATION_KEY_VERSION = 1;
11
+ const DIRECT_RUNTIME_ENCRYPTION_KEY_VERSION = 1;
12
+ const INSTALLATION_PROOF_DOMAIN = "rynx-plugin-installation-proof-v1";
13
+ const INSTALLATION_HOME_MODE = 0o700;
14
+ const INSTALLATION_FILE_MODE = 0o600;
15
+ // Windows keeps the legacy compatibility path until an ACL/keychain-backed
16
+ // implementation can make equivalent ownership and no-follow guarantees.
17
+ const CHECK_POSIX_SECURITY = process.platform === "darwin" || process.platform === "linux";
18
+ const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19
+ export function daemonInstallationIdFile(env = process.env) {
20
+ return join(rynxHome(env), INSTALLATION_ID_FILE);
21
+ }
22
+ export function daemonInstallationKeyFile(env = process.env) {
23
+ return join(rynxHome(env), INSTALLATION_KEY_FILE);
24
+ }
25
+ export function daemonDirectRuntimeEncryptionKeyFile(env = process.env) {
26
+ return join(rynxHome(env), DIRECT_RUNTIME_ENCRYPTION_KEY_FILE);
27
+ }
28
+ /** Direct hosting requires ownership/mode/no-follow checks for its persistent server key. */
29
+ export function isDaemonDirectRuntimeHostingSupported(platform = process.platform) {
30
+ return platform === "darwin" || platform === "linux";
31
+ }
32
+ /** Load or atomically create the daemon's persistent Direct Runtime E2EE key. */
33
+ export async function loadOrCreateDaemonDirectRuntimeEncryptionIdentity(env = process.env) {
34
+ if (!isDaemonDirectRuntimeHostingSupported()) {
35
+ throw new Error("Direct Runtime hosting is supported only on macOS and Linux until protected server-key storage is available on other operating systems");
36
+ }
37
+ return await withSecureInstallationHome(rynxHome(env), async () => {
38
+ const key = await loadOrCreateDirectRuntimeEncryptionKey(env);
39
+ return directRuntimeEncryptionIdentity(key);
40
+ });
41
+ }
42
+ /** Load the stable installation id and proof key without exposing private key material. */
43
+ export async function loadOrCreateDaemonInstallationIdentity(env = process.env) {
44
+ return await withSecureInstallationHome(rynxHome(env), async () => {
45
+ const key = await loadOrCreateInstallationKey(env);
46
+ const installationId = await loadOrCreateDaemonInstallationIdAfterKey(env);
47
+ return installationIdentity(installationId, key);
48
+ });
49
+ }
50
+ /** Synchronous composition-root variant used before spawning an explicit plugin CLI. */
51
+ export function loadOrCreateDaemonInstallationIdentitySync(env = process.env) {
52
+ return withSecureInstallationHomeSync(rynxHome(env), () => {
53
+ const key = loadOrCreateInstallationKeySync(env);
54
+ const installationId = loadOrCreateDaemonInstallationIdAfterKeySync(env);
55
+ return installationIdentity(installationId, key);
56
+ });
57
+ }
58
+ /** Load the public ID, creating the proof key first on a new installation. */
59
+ export async function loadOrCreateDaemonInstallationId(env = process.env) {
60
+ return await withSecureInstallationHome(rynxHome(env), async () => {
61
+ await loadOrCreateInstallationKey(env);
62
+ return await loadOrCreateDaemonInstallationIdAfterKey(env);
63
+ });
64
+ }
65
+ async function loadOrCreateDaemonInstallationIdAfterKey(env) {
66
+ const file = daemonInstallationIdFile(env);
67
+ try {
68
+ return await readInstallationId(file);
69
+ }
70
+ catch (error) {
71
+ if (!hasCode(error, "ENOENT"))
72
+ throw error;
73
+ }
74
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
75
+ const candidate = randomUUID();
76
+ const handle = await open(temporary, "wx", INSTALLATION_FILE_MODE);
77
+ try {
78
+ await handle.writeFile(`${candidate}\n`, "utf8");
79
+ await handle.sync();
80
+ }
81
+ finally {
82
+ await handle.close();
83
+ }
84
+ try {
85
+ // A hard link publishes the fully written inode without replacing an ID
86
+ // concurrently created by another daemon startup.
87
+ try {
88
+ await link(temporary, file);
89
+ }
90
+ catch (error) {
91
+ if (!hasCode(error, "EEXIST"))
92
+ throw error;
93
+ }
94
+ }
95
+ finally {
96
+ await rm(temporary, { force: true });
97
+ }
98
+ return await readInstallationId(file);
99
+ }
100
+ /** Synchronous variant for CLI dispatch, which must pin the plugin before yielding. */
101
+ export function loadOrCreateDaemonInstallationIdSync(env = process.env) {
102
+ return withSecureInstallationHomeSync(rynxHome(env), () => {
103
+ loadOrCreateInstallationKeySync(env);
104
+ return loadOrCreateDaemonInstallationIdAfterKeySync(env);
105
+ });
106
+ }
107
+ function loadOrCreateDaemonInstallationIdAfterKeySync(env) {
108
+ const file = daemonInstallationIdFile(env);
109
+ try {
110
+ return readInstallationIdSync(file);
111
+ }
112
+ catch (error) {
113
+ if (!hasCode(error, "ENOENT"))
114
+ throw error;
115
+ }
116
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
117
+ const candidate = randomUUID();
118
+ const fd = openSync(temporary, "wx", INSTALLATION_FILE_MODE);
119
+ try {
120
+ writeFileSync(fd, `${candidate}\n`, "utf8");
121
+ fsyncSync(fd);
122
+ }
123
+ finally {
124
+ closeSync(fd);
125
+ }
126
+ try {
127
+ try {
128
+ linkSync(temporary, file);
129
+ }
130
+ catch (error) {
131
+ if (!hasCode(error, "EEXIST"))
132
+ throw error;
133
+ }
134
+ }
135
+ finally {
136
+ rmSync(temporary, { force: true });
137
+ }
138
+ return readInstallationIdSync(file);
139
+ }
140
+ async function readInstallationId(file) {
141
+ const raw = CHECK_POSIX_SECURITY
142
+ ? await readSecureInstallationFile(file)
143
+ : await readFile(file, "utf8");
144
+ const value = raw.trim();
145
+ if (!UUID_V4_PATTERN.test(value)) {
146
+ throw new Error(`invalid daemon installation ID in ${file}`);
147
+ }
148
+ if (!CHECK_POSIX_SECURITY)
149
+ await chmod(file, INSTALLATION_FILE_MODE);
150
+ return value;
151
+ }
152
+ function readInstallationIdSync(file) {
153
+ const raw = CHECK_POSIX_SECURITY
154
+ ? readSecureInstallationFileSync(file)
155
+ : readFileSync(file, "utf8");
156
+ const value = raw.trim();
157
+ if (!UUID_V4_PATTERN.test(value)) {
158
+ throw new Error(`invalid daemon installation ID in ${file}`);
159
+ }
160
+ if (!CHECK_POSIX_SECURITY)
161
+ chmodSync(file, INSTALLATION_FILE_MODE);
162
+ return value;
163
+ }
164
+ async function loadOrCreateInstallationKey(env) {
165
+ const file = daemonInstallationKeyFile(env);
166
+ try {
167
+ return await readInstallationKey(file);
168
+ }
169
+ catch (error) {
170
+ if (!hasCode(error, "ENOENT"))
171
+ throw error;
172
+ }
173
+ await assertNoInstallationIdWithoutKey(env);
174
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
175
+ const candidate = generateInstallationKey();
176
+ const handle = await open(temporary, "wx", INSTALLATION_FILE_MODE);
177
+ try {
178
+ await handle.writeFile(`${JSON.stringify(candidate, null, 2)}\n`, "utf8");
179
+ await handle.sync();
180
+ }
181
+ finally {
182
+ await handle.close();
183
+ }
184
+ try {
185
+ try {
186
+ await link(temporary, file);
187
+ }
188
+ catch (error) {
189
+ if (!hasCode(error, "EEXIST"))
190
+ throw error;
191
+ }
192
+ }
193
+ finally {
194
+ await rm(temporary, { force: true });
195
+ }
196
+ return await readInstallationKey(file);
197
+ }
198
+ async function loadOrCreateDirectRuntimeEncryptionKey(env) {
199
+ const file = daemonDirectRuntimeEncryptionKeyFile(env);
200
+ try {
201
+ return await readDirectRuntimeEncryptionKey(file);
202
+ }
203
+ catch (error) {
204
+ if (!hasCode(error, "ENOENT"))
205
+ throw error;
206
+ }
207
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
208
+ const candidate = generateDirectRuntimeEncryptionKey();
209
+ const handle = await open(temporary, "wx", INSTALLATION_FILE_MODE);
210
+ try {
211
+ await handle.writeFile(`${JSON.stringify(candidate, null, 2)}\n`, "utf8");
212
+ await handle.sync();
213
+ }
214
+ finally {
215
+ await handle.close();
216
+ }
217
+ try {
218
+ // Publish without replacing a key concurrently created by another caller.
219
+ try {
220
+ await link(temporary, file);
221
+ }
222
+ catch (error) {
223
+ if (!hasCode(error, "EEXIST"))
224
+ throw error;
225
+ }
226
+ }
227
+ finally {
228
+ await rm(temporary, { force: true });
229
+ }
230
+ return await readDirectRuntimeEncryptionKey(file);
231
+ }
232
+ function loadOrCreateInstallationKeySync(env) {
233
+ const file = daemonInstallationKeyFile(env);
234
+ try {
235
+ return readInstallationKeySync(file);
236
+ }
237
+ catch (error) {
238
+ if (!hasCode(error, "ENOENT"))
239
+ throw error;
240
+ }
241
+ assertNoInstallationIdWithoutKeySync(env);
242
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
243
+ const candidate = generateInstallationKey();
244
+ const fd = openSync(temporary, "wx", INSTALLATION_FILE_MODE);
245
+ try {
246
+ writeFileSync(fd, `${JSON.stringify(candidate, null, 2)}\n`, "utf8");
247
+ fsyncSync(fd);
248
+ }
249
+ finally {
250
+ closeSync(fd);
251
+ }
252
+ try {
253
+ try {
254
+ linkSync(temporary, file);
255
+ }
256
+ catch (error) {
257
+ if (!hasCode(error, "EEXIST"))
258
+ throw error;
259
+ }
260
+ }
261
+ finally {
262
+ rmSync(temporary, { force: true });
263
+ }
264
+ return readInstallationKeySync(file);
265
+ }
266
+ async function assertNoInstallationIdWithoutKey(env) {
267
+ const file = daemonInstallationIdFile(env);
268
+ try {
269
+ await readInstallationId(file);
270
+ }
271
+ catch (error) {
272
+ if (hasCode(error, "ENOENT"))
273
+ return;
274
+ throw error;
275
+ }
276
+ throw new Error(`daemon installation key is missing for existing installation ID in ${file}`);
277
+ }
278
+ function assertNoInstallationIdWithoutKeySync(env) {
279
+ const file = daemonInstallationIdFile(env);
280
+ try {
281
+ readInstallationIdSync(file);
282
+ }
283
+ catch (error) {
284
+ if (hasCode(error, "ENOENT"))
285
+ return;
286
+ throw error;
287
+ }
288
+ throw new Error(`daemon installation key is missing for existing installation ID in ${file}`);
289
+ }
290
+ async function readInstallationKey(file) {
291
+ const raw = CHECK_POSIX_SECURITY
292
+ ? await readSecureInstallationFile(file)
293
+ : await readFile(file, "utf8");
294
+ const key = parseInstallationKey(raw, file);
295
+ if (!CHECK_POSIX_SECURITY)
296
+ await chmod(file, INSTALLATION_FILE_MODE);
297
+ return key;
298
+ }
299
+ async function readDirectRuntimeEncryptionKey(file) {
300
+ const raw = CHECK_POSIX_SECURITY
301
+ ? await readSecureInstallationFile(file)
302
+ : await readFile(file, "utf8");
303
+ const key = parseDirectRuntimeEncryptionKey(raw, file);
304
+ if (!CHECK_POSIX_SECURITY)
305
+ await chmod(file, INSTALLATION_FILE_MODE);
306
+ return key;
307
+ }
308
+ function readInstallationKeySync(file) {
309
+ const raw = CHECK_POSIX_SECURITY
310
+ ? readSecureInstallationFileSync(file)
311
+ : readFileSync(file, "utf8");
312
+ const key = parseInstallationKey(raw, file);
313
+ if (!CHECK_POSIX_SECURITY)
314
+ chmodSync(file, INSTALLATION_FILE_MODE);
315
+ return key;
316
+ }
317
+ async function withSecureInstallationHome(home, operation) {
318
+ await mkdir(home, { recursive: true, mode: INSTALLATION_HOME_MODE });
319
+ if (!CHECK_POSIX_SECURITY)
320
+ return await operation();
321
+ let handle;
322
+ try {
323
+ handle = await open(home, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
324
+ }
325
+ catch (error) {
326
+ throw new Error(`daemon installation home must be a non-symlink directory: ${messageOf(error)}`, { cause: error });
327
+ }
328
+ try {
329
+ const identity = fileIdentity(await secureInstallationHandle(handle, home, "directory", INSTALLATION_HOME_MODE));
330
+ try {
331
+ return await operation();
332
+ }
333
+ finally {
334
+ await secureInstallationHandle(handle, home, "directory", INSTALLATION_HOME_MODE, identity);
335
+ }
336
+ }
337
+ finally {
338
+ await handle.close();
339
+ }
340
+ }
341
+ function withSecureInstallationHomeSync(home, operation) {
342
+ mkdirSync(home, { recursive: true, mode: INSTALLATION_HOME_MODE });
343
+ if (!CHECK_POSIX_SECURITY)
344
+ return operation();
345
+ let fd;
346
+ try {
347
+ fd = openSync(home, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
348
+ }
349
+ catch (error) {
350
+ throw new Error(`daemon installation home must be a non-symlink directory: ${messageOf(error)}`, { cause: error });
351
+ }
352
+ try {
353
+ const identity = fileIdentity(secureInstallationHandleSync(fd, home, "directory", INSTALLATION_HOME_MODE));
354
+ try {
355
+ return operation();
356
+ }
357
+ finally {
358
+ secureInstallationHandleSync(fd, home, "directory", INSTALLATION_HOME_MODE, identity);
359
+ }
360
+ }
361
+ finally {
362
+ closeSync(fd);
363
+ }
364
+ }
365
+ async function readSecureInstallationFile(file) {
366
+ let handle;
367
+ try {
368
+ handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
369
+ }
370
+ catch (error) {
371
+ if (hasCode(error, "ENOENT"))
372
+ throw error;
373
+ throw new Error(`daemon installation file must be a non-symlink regular file: ${messageOf(error)}`, { cause: error });
374
+ }
375
+ let bytes;
376
+ let before;
377
+ try {
378
+ before = await secureInstallationHandle(handle, file, "file", INSTALLATION_FILE_MODE);
379
+ const expected = fileIdentity(before);
380
+ bytes = await handle.readFile();
381
+ const after = await secureInstallationHandle(handle, file, "file", INSTALLATION_FILE_MODE, expected);
382
+ assertInstallationReadStable(before, after, bytes.byteLength);
383
+ }
384
+ finally {
385
+ await handle.close();
386
+ }
387
+ await assertSecureInstallationPath(file, before, "file", INSTALLATION_FILE_MODE);
388
+ return bytes.toString("utf8");
389
+ }
390
+ function readSecureInstallationFileSync(file) {
391
+ let fd;
392
+ try {
393
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
394
+ }
395
+ catch (error) {
396
+ if (hasCode(error, "ENOENT"))
397
+ throw error;
398
+ throw new Error(`daemon installation file must be a non-symlink regular file: ${messageOf(error)}`, { cause: error });
399
+ }
400
+ let bytes;
401
+ let before;
402
+ try {
403
+ before = secureInstallationHandleSync(fd, file, "file", INSTALLATION_FILE_MODE);
404
+ const expected = fileIdentity(before);
405
+ bytes = readFileSync(fd);
406
+ const after = secureInstallationHandleSync(fd, file, "file", INSTALLATION_FILE_MODE, expected);
407
+ assertInstallationReadStable(before, after, bytes.byteLength);
408
+ }
409
+ finally {
410
+ closeSync(fd);
411
+ }
412
+ assertSecureInstallationPathSync(file, before, "file", INSTALLATION_FILE_MODE);
413
+ return bytes.toString("utf8");
414
+ }
415
+ async function secureInstallationHandle(handle, path, kind, expectedMode, expected) {
416
+ let stats = await handle.stat({ bigint: true });
417
+ assertInstallationMetadata(stats, kind);
418
+ if (expected) {
419
+ assertSameFile(expected, stats, `daemon installation ${kind} changed during access`);
420
+ }
421
+ if (permissionMode(stats) !== expectedMode) {
422
+ try {
423
+ await handle.chmod(expectedMode);
424
+ }
425
+ catch (error) {
426
+ throw new Error(`failed to secure daemon installation ${kind} permissions: ${messageOf(error)}`);
427
+ }
428
+ stats = await handle.stat({ bigint: true });
429
+ }
430
+ assertInstallationMetadata(stats, kind);
431
+ assertMode(stats, expectedMode, `daemon installation ${kind}`);
432
+ if (expected) {
433
+ assertSameFile(expected, stats, `daemon installation ${kind} changed during access`);
434
+ }
435
+ await assertSecureInstallationPath(path, stats, kind, expectedMode);
436
+ return stats;
437
+ }
438
+ function secureInstallationHandleSync(fd, path, kind, expectedMode, expected) {
439
+ let stats = fstatSync(fd, { bigint: true });
440
+ assertInstallationMetadata(stats, kind);
441
+ if (expected) {
442
+ assertSameFile(expected, stats, `daemon installation ${kind} changed during access`);
443
+ }
444
+ if (permissionMode(stats) !== expectedMode) {
445
+ try {
446
+ fchmodSync(fd, expectedMode);
447
+ }
448
+ catch (error) {
449
+ throw new Error(`failed to secure daemon installation ${kind} permissions: ${messageOf(error)}`);
450
+ }
451
+ stats = fstatSync(fd, { bigint: true });
452
+ }
453
+ assertInstallationMetadata(stats, kind);
454
+ assertMode(stats, expectedMode, `daemon installation ${kind}`);
455
+ if (expected) {
456
+ assertSameFile(expected, stats, `daemon installation ${kind} changed during access`);
457
+ }
458
+ assertSecureInstallationPathSync(path, stats, kind, expectedMode);
459
+ return stats;
460
+ }
461
+ async function assertSecureInstallationPath(path, opened, kind, expectedMode) {
462
+ const current = await lstat(path, { bigint: true });
463
+ assertInstallationMetadata(current, kind);
464
+ assertMode(current, expectedMode, `daemon installation ${kind}`);
465
+ assertSameFile(fileIdentity(opened), current, `daemon installation ${kind} changed during access`);
466
+ }
467
+ function assertSecureInstallationPathSync(path, opened, kind, expectedMode) {
468
+ const current = lstatSync(path, { bigint: true });
469
+ assertInstallationMetadata(current, kind);
470
+ assertMode(current, expectedMode, `daemon installation ${kind}`);
471
+ assertSameFile(fileIdentity(opened), current, `daemon installation ${kind} changed during access`);
472
+ }
473
+ function assertInstallationMetadata(stats, kind) {
474
+ const valid = kind === "directory" ? stats.isDirectory() : stats.isFile();
475
+ if (stats.isSymbolicLink() || !valid) {
476
+ throw new Error(kind === "directory"
477
+ ? "daemon installation home must be a non-symlink directory"
478
+ : "daemon installation file must be a non-symlink regular file");
479
+ }
480
+ assertCurrentUserOwner(stats, kind === "directory" ? "daemon installation home" : "daemon installation file");
481
+ }
482
+ function assertCurrentUserOwner(stats, label) {
483
+ if (typeof process.getuid !== "function") {
484
+ throw new Error(`${label} ownership cannot be verified`);
485
+ }
486
+ if (stats.uid !== BigInt(process.getuid())) {
487
+ throw new Error(`${label} is not owned by the current user`);
488
+ }
489
+ }
490
+ function assertMode(stats, expectedMode, label) {
491
+ if (permissionMode(stats) !== expectedMode) {
492
+ throw new Error(`${label} permissions are unsafe`);
493
+ }
494
+ }
495
+ function permissionMode(stats) {
496
+ return Number(stats.mode & 511n);
497
+ }
498
+ function fileIdentity(stats) {
499
+ return { dev: stats.dev, ino: stats.ino };
500
+ }
501
+ function assertSameFile(expected, actual, message) {
502
+ if (actual.dev !== expected.dev || actual.ino !== expected.ino)
503
+ throw new Error(message);
504
+ }
505
+ function assertInstallationReadStable(before, after, bytesRead) {
506
+ // Atomic publication temporarily gives the inode two hard links; removing
507
+ // the private temporary name changes ctime/nlink without changing content.
508
+ const publicationCleanup = before.nlink === 2n && after.nlink === 1n;
509
+ if (before.size !== after.size ||
510
+ before.mtimeNs !== after.mtimeNs ||
511
+ (before.ctimeNs !== after.ctimeNs && !publicationCleanup) ||
512
+ after.size !== BigInt(bytesRead)) {
513
+ throw new Error("daemon installation file changed while reading");
514
+ }
515
+ }
516
+ function generateInstallationKey() {
517
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
518
+ return {
519
+ version: INSTALLATION_KEY_VERSION,
520
+ algorithm: "Ed25519",
521
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64url"),
522
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
523
+ };
524
+ }
525
+ function generateDirectRuntimeEncryptionKey() {
526
+ const { publicKey, privateKey } = generateKeyPairSync("x25519");
527
+ return {
528
+ version: DIRECT_RUNTIME_ENCRYPTION_KEY_VERSION,
529
+ algorithm: "X25519",
530
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64url"),
531
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
532
+ };
533
+ }
534
+ function parseInstallationKey(raw, file) {
535
+ let value;
536
+ try {
537
+ value = JSON.parse(raw);
538
+ }
539
+ catch (error) {
540
+ throw new Error(`invalid daemon installation key in ${file}: ${messageOf(error)}`);
541
+ }
542
+ if (!isRecord(value) || value.version !== INSTALLATION_KEY_VERSION || value.algorithm !== "Ed25519") {
543
+ throw new Error(`invalid daemon installation key in ${file}`);
544
+ }
545
+ if (typeof value.publicKey !== "string" || typeof value.privateKeyPem !== "string") {
546
+ throw new Error(`invalid daemon installation key in ${file}`);
547
+ }
548
+ try {
549
+ const privateKey = createPrivateKey(value.privateKeyPem);
550
+ const publicKey = createPublicKey({
551
+ key: Buffer.from(value.publicKey, "base64url"),
552
+ type: "spki",
553
+ format: "der",
554
+ });
555
+ if (privateKey.asymmetricKeyType !== "ed25519" || publicKey.asymmetricKeyType !== "ed25519") {
556
+ throw new Error("installation proof key must use Ed25519");
557
+ }
558
+ const probe = Buffer.from("rynx-installation-key-validation", "utf8");
559
+ const signature = sign(null, probe, privateKey);
560
+ if (!verify(null, probe, publicKey, signature))
561
+ throw new Error("public and private keys do not match");
562
+ }
563
+ catch (error) {
564
+ throw new Error(`invalid daemon installation key in ${file}: ${messageOf(error)}`);
565
+ }
566
+ return {
567
+ version: INSTALLATION_KEY_VERSION,
568
+ algorithm: "Ed25519",
569
+ publicKey: value.publicKey,
570
+ privateKeyPem: value.privateKeyPem,
571
+ };
572
+ }
573
+ function parseDirectRuntimeEncryptionKey(raw, file) {
574
+ let value;
575
+ try {
576
+ value = JSON.parse(raw);
577
+ }
578
+ catch (error) {
579
+ throw new Error(`invalid daemon Direct Runtime encryption key in ${file}: ${messageOf(error)}`);
580
+ }
581
+ if (!isRecord(value) ||
582
+ value.version !== DIRECT_RUNTIME_ENCRYPTION_KEY_VERSION ||
583
+ value.algorithm !== "X25519" ||
584
+ typeof value.publicKey !== "string" ||
585
+ typeof value.privateKeyPem !== "string") {
586
+ throw new Error(`invalid daemon Direct Runtime encryption key in ${file}`);
587
+ }
588
+ try {
589
+ const privateKey = createPrivateKey(value.privateKeyPem);
590
+ if (privateKey.asymmetricKeyType !== "x25519") {
591
+ throw new Error("Direct Runtime encryption key must use X25519");
592
+ }
593
+ const publicKey = parseX25519PublicKey(value.publicKey);
594
+ const derivedPublicKey = createPublicKey(privateKey)
595
+ .export({ type: "spki", format: "der" })
596
+ .toString("base64url");
597
+ if (derivedPublicKey !== value.publicKey) {
598
+ throw new Error("Direct Runtime encryption public and private keys do not match");
599
+ }
600
+ // Keep the parsed public key live through validation so OpenSSL also
601
+ // rejects malformed algorithm parameters, not only a matching byte shape.
602
+ if (publicKey.asymmetricKeyType !== "x25519") {
603
+ throw new Error("Direct Runtime encryption key must use X25519");
604
+ }
605
+ }
606
+ catch (error) {
607
+ throw new Error(`invalid daemon Direct Runtime encryption key in ${file}: ${messageOf(error)}`);
608
+ }
609
+ return {
610
+ version: DIRECT_RUNTIME_ENCRYPTION_KEY_VERSION,
611
+ algorithm: "X25519",
612
+ publicKey: value.publicKey,
613
+ privateKeyPem: value.privateKeyPem,
614
+ };
615
+ }
616
+ function installationIdentity(installationId, key) {
617
+ const privateKey = createPrivateKey(key.privateKeyPem);
618
+ return {
619
+ installationId,
620
+ algorithm: "Ed25519",
621
+ publicKey: key.publicKey,
622
+ sign(pluginId, challenge) {
623
+ return sign(null, installationProofPayload(installationId, pluginId, challenge), privateKey).toString("base64url");
624
+ },
625
+ signDirectRuntimeServerHello(channelBinding, hello) {
626
+ return sign(null, encodeServerHelloProofPayload(channelBinding, hello), privateKey).toString("base64url");
627
+ },
628
+ signDirectRuntimeServerAccepted(channelBinding, helloHash, authenticateHash, accepted) {
629
+ return sign(null, encodeServerAcceptedProofPayload(channelBinding, helloHash, authenticateHash, accepted), privateKey).toString("base64url");
630
+ },
631
+ };
632
+ }
633
+ function directRuntimeEncryptionIdentity(key) {
634
+ const privateKey = createPrivateKey(key.privateKeyPem);
635
+ return {
636
+ algorithm: "X25519",
637
+ publicKey: key.publicKey,
638
+ deriveSharedSecret(peerPublicKey) {
639
+ let secret;
640
+ try {
641
+ secret = diffieHellman({
642
+ privateKey,
643
+ publicKey: parseX25519PublicKey(peerPublicKey),
644
+ });
645
+ }
646
+ catch (error) {
647
+ throw new Error(`invalid Direct Runtime peer encryption key: ${messageOf(error)}`);
648
+ }
649
+ if (secret.byteLength !== 32 || secret.every((byte) => byte === 0)) {
650
+ throw new Error("invalid Direct Runtime peer encryption key: shared secret is unsafe");
651
+ }
652
+ return Uint8Array.from(secret);
653
+ },
654
+ };
655
+ }
656
+ function parseX25519PublicKey(value) {
657
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{59}$/.test(value)) {
658
+ throw new Error("X25519 public key must be canonical SPKI DER base64url");
659
+ }
660
+ const bytes = Buffer.from(value, "base64url");
661
+ if (bytes.byteLength !== 44 || bytes.toString("base64url") !== value) {
662
+ throw new Error("X25519 public key must be canonical SPKI DER base64url");
663
+ }
664
+ const publicKey = createPublicKey({ key: bytes, type: "spki", format: "der" });
665
+ if (publicKey.asymmetricKeyType !== "x25519" ||
666
+ publicKey.export({ type: "spki", format: "der" }).toString("base64url") !== value) {
667
+ throw new Error("public key must use X25519");
668
+ }
669
+ return publicKey;
670
+ }
671
+ export function installationProofPayload(installationId, pluginId, challenge) {
672
+ if (!UUID_V4_PATTERN.test(installationId)) {
673
+ throw new Error("installation proof installationId is invalid");
674
+ }
675
+ const normalizedPluginId = pluginId.trim();
676
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(normalizedPluginId)) {
677
+ throw new Error("installation proof pluginId is invalid");
678
+ }
679
+ if (!challenge || challenge.length > 16_384) {
680
+ throw new Error("installation proof challenge is invalid");
681
+ }
682
+ return Buffer.from(`${INSTALLATION_PROOF_DOMAIN}\0${installationId}\0plugin:${normalizedPluginId}\0${challenge}`, "utf8");
683
+ }
684
+ function isRecord(value) {
685
+ return typeof value === "object" && value !== null && !Array.isArray(value);
686
+ }
687
+ function messageOf(error) {
688
+ return error instanceof Error ? error.message : String(error);
689
+ }
690
+ function hasCode(error, code) {
691
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
692
+ }
@@ -1,9 +1,6 @@
1
1
  export interface CleanupOptions {
2
2
  /** CodexSessionStore JSON path; defaults to `<cwd>/.codex-proxy/sessions.json`. */
3
3
  codexStorePath?: string;
4
- /** Dir holding the channel routing maps (`lark-sessions*.json`); defaults to
5
- * `<cwd>/.codex-proxy`. */
6
- channelStoreDir?: string;
7
4
  /** Report what would be removed without deleting anything. */
8
5
  dryRun?: boolean;
9
6
  log?: (entry: Record<string, unknown>) => void;