@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,45 @@
1
+ export interface RemoteRuntimeClientCredential {
2
+ version: 2;
3
+ daemonId: string;
4
+ identityPublicKey: string;
5
+ serverEncryptionPublicKey: string;
6
+ clientId: string;
7
+ grantId: string;
8
+ clientPublicKey: string;
9
+ clientPrivateKeyPem: string;
10
+ }
11
+ export interface RemoteRuntimePendingClientCredential {
12
+ version: 2;
13
+ state: "pending_enrollment";
14
+ daemonId: string;
15
+ identityPublicKey: string;
16
+ serverEncryptionPublicKey: string;
17
+ claimId: string;
18
+ clientId: string;
19
+ clientPublicKey: string;
20
+ clientPrivateKeyPem: string;
21
+ }
22
+ export type RemoteRuntimeEnrollmentCredential = {
23
+ state: "pending";
24
+ credential: RemoteRuntimePendingClientCredential;
25
+ } | {
26
+ state: "accepted";
27
+ credential: RemoteRuntimeClientCredential;
28
+ };
29
+ /**
30
+ * Protected-file credential adapter used by the headless resident daemon.
31
+ * The opaque reference, never the private key, is persisted with target data.
32
+ */
33
+ export declare class FileRemoteRuntimeCredentialStore {
34
+ private readonly env;
35
+ constructor(env?: NodeJS.ProcessEnv);
36
+ save(input: RemoteRuntimeClientCredential): Promise<string>;
37
+ savePending(input: RemoteRuntimePendingClientCredential): Promise<string>;
38
+ loadEnrollment(credentialRef: string): Promise<RemoteRuntimeEnrollmentCredential>;
39
+ finalizePending(credentialRef: string, claimIdInput: string, input: RemoteRuntimeClientCredential): Promise<void>;
40
+ private saveNew;
41
+ load(credentialRef: string): Promise<RemoteRuntimeClientCredential>;
42
+ remove(credentialRef: string): Promise<void>;
43
+ private readValue;
44
+ }
45
+ export declare function credentialDirectory(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,639 @@
1
+ import { createPrivateKey, createPublicKey, randomBytes, sign, verify, } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, rename, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { rynxHome } from "@rynx-ai/core";
6
+ const CREDENTIAL_VERSION = 2;
7
+ const CREDENTIAL_DIRECTORY = "remote-runtime-credentials";
8
+ const CREDENTIAL_REF_PATTERN = /^credential_[A-Za-z0-9_-]{24}$/;
9
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
10
+ const MAX_CREDENTIAL_BYTES = 16 * 1024;
11
+ const MAX_PRIVATE_KEY_CHARS = 8 * 1024;
12
+ const CREDENTIAL_DIRECTORY_MODE = 0o700;
13
+ const CREDENTIAL_FILE_MODE = 0o600;
14
+ const CHECK_POSIX_SECURITY = process.platform !== "win32";
15
+ function assertProtectedCredentialStorageSupported() {
16
+ if (!CHECK_POSIX_SECURITY) {
17
+ throw new Error("Remote Runtime credential storage requires a verified OS-protected backend; Windows is not supported yet");
18
+ }
19
+ }
20
+ /**
21
+ * Protected-file credential adapter used by the headless resident daemon.
22
+ * The opaque reference, never the private key, is persisted with target data.
23
+ */
24
+ export class FileRemoteRuntimeCredentialStore {
25
+ env;
26
+ constructor(env = process.env) {
27
+ this.env = env;
28
+ }
29
+ async save(input) {
30
+ assertProtectedCredentialStorageSupported();
31
+ const credential = validateCredential(input);
32
+ return await this.saveNew(encodeCredential(credential));
33
+ }
34
+ async savePending(input) {
35
+ assertProtectedCredentialStorageSupported();
36
+ const credential = validatePendingCredential(input);
37
+ return await this.saveNew(encodeCredential(credential));
38
+ }
39
+ async loadEnrollment(credentialRef) {
40
+ assertProtectedCredentialStorageSupported();
41
+ const { ref, value } = await this.readValue(credentialRef);
42
+ try {
43
+ if (isRecord(value) && value.state === "pending_enrollment") {
44
+ return { state: "pending", credential: validatePendingCredential(value) };
45
+ }
46
+ return { state: "accepted", credential: validateCredential(value) };
47
+ }
48
+ catch (error) {
49
+ throw new Error(`invalid Remote Runtime credential ${ref}: ${messageOf(error)}`);
50
+ }
51
+ }
52
+ async finalizePending(credentialRef, claimIdInput, input) {
53
+ assertProtectedCredentialStorageSupported();
54
+ const ref = validateCredentialRef(credentialRef);
55
+ const claimId = opaqueId(claimIdInput, "claimId");
56
+ const credential = validateCredential(input);
57
+ const encoded = encodeCredential(credential);
58
+ const directory = credentialDirectory(this.env);
59
+ const file = credentialFile(directory, ref);
60
+ await withSecureCredentialDirectory(directory, false, async (guard) => {
61
+ const value = parseCredentialJson(ref, (await readSecureCredentialFile(file)).toString("utf8"));
62
+ if (isRecord(value) && value.state === "pending_enrollment") {
63
+ const pending = validatePendingCredential(value);
64
+ assertPendingMatchesAccepted(pending, claimId, credential);
65
+ await replaceSecureCredentialFile(directory, file, encoded);
66
+ await guard.sync();
67
+ return;
68
+ }
69
+ const accepted = validateCredential(value);
70
+ if (!sameAcceptedCredential(accepted, credential)) {
71
+ throw new Error("accepted Runtime credential differs from the enrollment result");
72
+ }
73
+ });
74
+ }
75
+ async saveNew(encoded) {
76
+ const directory = credentialDirectory(this.env);
77
+ let createdFile;
78
+ try {
79
+ const credentialRef = await withSecureCredentialDirectory(directory, true, async (guard) => {
80
+ for (let attempt = 0; attempt < 4; attempt += 1) {
81
+ const ref = `credential_${randomBytes(18).toString("base64url")}`;
82
+ const file = credentialFile(directory, ref);
83
+ if (!(await createSecureCredentialFile(file, encoded)))
84
+ continue;
85
+ createdFile = file;
86
+ await guard.sync();
87
+ return ref;
88
+ }
89
+ throw new Error("failed to allocate a Remote Runtime credential reference");
90
+ });
91
+ createdFile = undefined;
92
+ return credentialRef;
93
+ }
94
+ catch (error) {
95
+ if (createdFile)
96
+ await rm(createdFile, { force: true }).catch(() => undefined);
97
+ throw error;
98
+ }
99
+ }
100
+ async load(credentialRef) {
101
+ const enrollment = await this.loadEnrollment(credentialRef);
102
+ if (enrollment.state === "pending") {
103
+ throw new Error(`invalid Remote Runtime credential ${validateCredentialRef(credentialRef)}: enrollment is pending`);
104
+ }
105
+ return enrollment.credential;
106
+ }
107
+ async remove(credentialRef) {
108
+ const ref = validateCredentialRef(credentialRef);
109
+ const directory = credentialDirectory(this.env);
110
+ try {
111
+ await withSecureCredentialDirectory(directory, false, async (guard) => {
112
+ await rm(credentialFile(directory, ref), { force: true });
113
+ await guard.sync();
114
+ });
115
+ }
116
+ catch (error) {
117
+ if (hasCode(error, "ENOENT"))
118
+ return;
119
+ throw error;
120
+ }
121
+ }
122
+ async readValue(credentialRef) {
123
+ const ref = validateCredentialRef(credentialRef);
124
+ const directory = credentialDirectory(this.env);
125
+ const file = credentialFile(directory, ref);
126
+ let raw;
127
+ try {
128
+ raw = await withSecureCredentialDirectory(directory, false, async () => (await readSecureCredentialFile(file)).toString("utf8"));
129
+ }
130
+ catch (error) {
131
+ throw new Error(`failed to load Remote Runtime credential ${ref}: ${messageOf(error)}`);
132
+ }
133
+ return { ref, value: parseCredentialJson(ref, raw) };
134
+ }
135
+ }
136
+ export function credentialDirectory(env = process.env) {
137
+ return join(rynxHome(env), CREDENTIAL_DIRECTORY);
138
+ }
139
+ function credentialFile(directory, credentialRef) {
140
+ return join(directory, `${validateCredentialRef(credentialRef)}.json`);
141
+ }
142
+ async function withSecureCredentialDirectory(directory, create, operation) {
143
+ if (create) {
144
+ await mkdir(directory, { recursive: true, mode: CREDENTIAL_DIRECTORY_MODE });
145
+ }
146
+ const guard = await openCredentialDirectoryGuard(directory);
147
+ let result;
148
+ let operationFailed = false;
149
+ let operationError;
150
+ try {
151
+ result = await operation(guard);
152
+ }
153
+ catch (error) {
154
+ operationFailed = true;
155
+ operationError = error;
156
+ }
157
+ let verificationError;
158
+ try {
159
+ await guard.verify();
160
+ }
161
+ catch (error) {
162
+ verificationError = error;
163
+ }
164
+ try {
165
+ await guard.close();
166
+ }
167
+ catch (error) {
168
+ verificationError ??= error;
169
+ }
170
+ // A directory-integrity failure wins over an operation error: callers must
171
+ // never treat a read or write as trustworthy after its parent changed.
172
+ if (verificationError !== undefined)
173
+ throw verificationError;
174
+ if (operationFailed)
175
+ throw operationError;
176
+ return result;
177
+ }
178
+ async function openCredentialDirectoryGuard(directory) {
179
+ if (!CHECK_POSIX_SECURITY) {
180
+ const initial = await inspectCredentialDirectoryPath(directory);
181
+ const identity = fileIdentity(initial);
182
+ return {
183
+ async verify() {
184
+ const current = await inspectCredentialDirectoryPath(directory);
185
+ assertSameFile(identity, current, "credential directory changed during access");
186
+ },
187
+ async sync() { },
188
+ async close() { },
189
+ };
190
+ }
191
+ let handle;
192
+ try {
193
+ handle = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
194
+ }
195
+ catch (error) {
196
+ throw new Error(`credential directory must be a non-symlink directory: ${messageOf(error)}`, { cause: error });
197
+ }
198
+ let initial;
199
+ try {
200
+ initial = await secureCredentialDirectoryHandle(handle, directory);
201
+ }
202
+ catch (error) {
203
+ await handle.close().catch(() => undefined);
204
+ throw error;
205
+ }
206
+ const identity = fileIdentity(initial);
207
+ return {
208
+ async verify() {
209
+ await secureCredentialDirectoryHandle(handle, directory, identity);
210
+ },
211
+ async sync() {
212
+ await handle.sync();
213
+ },
214
+ async close() {
215
+ await handle.close();
216
+ },
217
+ };
218
+ }
219
+ async function secureCredentialDirectoryHandle(handle, directory, expected) {
220
+ let stats = await handle.stat({ bigint: true });
221
+ assertDirectoryMetadata(stats);
222
+ if (expected)
223
+ assertSameFile(expected, stats, "credential directory changed during access");
224
+ stats = await tightenHandlePermissions(handle, stats, CREDENTIAL_DIRECTORY_MODE, "credential directory");
225
+ assertDirectoryMetadata(stats);
226
+ if (expected)
227
+ assertSameFile(expected, stats, "credential directory changed during access");
228
+ await assertSecurePath(directory, stats, "directory", CREDENTIAL_DIRECTORY_MODE);
229
+ return stats;
230
+ }
231
+ async function inspectCredentialDirectoryPath(directory) {
232
+ const stats = await lstat(directory, { bigint: true });
233
+ assertDirectoryMetadata(stats);
234
+ assertMode(stats, CREDENTIAL_DIRECTORY_MODE, "credential directory");
235
+ return stats;
236
+ }
237
+ async function createSecureCredentialFile(file, bytes) {
238
+ let handle;
239
+ try {
240
+ handle = await open(file, constants.O_WRONLY |
241
+ constants.O_CREAT |
242
+ constants.O_EXCL |
243
+ (CHECK_POSIX_SECURITY ? constants.O_NOFOLLOW : 0), CREDENTIAL_FILE_MODE);
244
+ }
245
+ catch (error) {
246
+ if (hasCode(error, "EEXIST"))
247
+ return false;
248
+ throw new Error(`failed to create Remote Runtime credential file: ${messageOf(error)}`);
249
+ }
250
+ let identity;
251
+ try {
252
+ let stats = await secureCredentialFileHandle(handle, file);
253
+ identity = fileIdentity(stats);
254
+ if (stats.size !== 0n)
255
+ throw new Error("new credential file is not empty");
256
+ await handle.writeFile(bytes);
257
+ await handle.sync();
258
+ stats = await secureCredentialFileHandle(handle, file, identity);
259
+ if (stats.size !== BigInt(bytes.byteLength)) {
260
+ throw new Error("credential file size changed while saving");
261
+ }
262
+ await handle.close();
263
+ handle = undefined;
264
+ await assertSecureCredentialPath(file, identity);
265
+ return true;
266
+ }
267
+ catch (error) {
268
+ if (handle && identity) {
269
+ try {
270
+ await secureCredentialFileHandle(handle, file, identity);
271
+ }
272
+ catch (verificationError) {
273
+ error = verificationError;
274
+ }
275
+ }
276
+ await handle?.close().catch(() => undefined);
277
+ await rm(file, { force: true }).catch(() => undefined);
278
+ throw error;
279
+ }
280
+ }
281
+ async function replaceSecureCredentialFile(directory, file, bytes) {
282
+ let temporaryFile;
283
+ try {
284
+ for (let attempt = 0; attempt < 4; attempt += 1) {
285
+ const name = `.credential-finalize-${randomBytes(18).toString("base64url")}.tmp`;
286
+ const candidate = join(directory, name);
287
+ if (!(await createSecureCredentialFile(candidate, bytes)))
288
+ continue;
289
+ temporaryFile = candidate;
290
+ break;
291
+ }
292
+ if (!temporaryFile) {
293
+ throw new Error("failed to allocate a temporary Runtime credential file");
294
+ }
295
+ await rename(temporaryFile, file);
296
+ temporaryFile = undefined;
297
+ const persisted = await readSecureCredentialFile(file);
298
+ if (!persisted.equals(bytes)) {
299
+ throw new Error("finalized Runtime credential differs from the accepted credential");
300
+ }
301
+ }
302
+ finally {
303
+ if (temporaryFile)
304
+ await rm(temporaryFile, { force: true }).catch(() => undefined);
305
+ }
306
+ }
307
+ async function readSecureCredentialFile(file) {
308
+ await assertCredentialPathBeforeOpen(file);
309
+ let handle;
310
+ try {
311
+ handle = await open(file, constants.O_RDONLY | (CHECK_POSIX_SECURITY ? constants.O_NOFOLLOW : 0));
312
+ }
313
+ catch (error) {
314
+ throw new Error(`credential file must be a non-symlink regular file: ${messageOf(error)}`, { cause: error });
315
+ }
316
+ let identity;
317
+ let before;
318
+ let bytes;
319
+ let operationError;
320
+ try {
321
+ before = await secureCredentialFileHandle(handle, file);
322
+ identity = fileIdentity(before);
323
+ assertCredentialSize(before.size);
324
+ bytes = await readBoundedCredential(handle);
325
+ const after = await secureCredentialFileHandle(handle, file, identity);
326
+ assertCredentialReadStable(before, after, bytes.byteLength);
327
+ }
328
+ catch (error) {
329
+ operationError = error;
330
+ }
331
+ let verificationError;
332
+ if (identity) {
333
+ try {
334
+ await secureCredentialFileHandle(handle, file, identity);
335
+ }
336
+ catch (error) {
337
+ verificationError = error;
338
+ }
339
+ }
340
+ try {
341
+ await handle.close();
342
+ }
343
+ catch (error) {
344
+ verificationError ??= error;
345
+ }
346
+ if (identity) {
347
+ try {
348
+ await assertSecureCredentialPath(file, identity);
349
+ }
350
+ catch (error) {
351
+ verificationError ??= error;
352
+ }
353
+ }
354
+ if (verificationError !== undefined)
355
+ throw verificationError;
356
+ if (operationError !== undefined)
357
+ throw operationError;
358
+ return bytes;
359
+ }
360
+ async function secureCredentialFileHandle(handle, file, expected) {
361
+ let stats = await handle.stat({ bigint: true });
362
+ assertCredentialFileMetadata(stats);
363
+ if (expected)
364
+ assertSameFile(expected, stats, "credential file changed during access");
365
+ stats = await tightenHandlePermissions(handle, stats, CREDENTIAL_FILE_MODE, "credential file");
366
+ assertCredentialFileMetadata(stats);
367
+ if (expected)
368
+ assertSameFile(expected, stats, "credential file changed during access");
369
+ await assertSecurePath(file, stats, "file", CREDENTIAL_FILE_MODE);
370
+ return stats;
371
+ }
372
+ async function tightenHandlePermissions(handle, stats, expectedMode, label) {
373
+ if (!CHECK_POSIX_SECURITY || permissionMode(stats) === expectedMode)
374
+ return stats;
375
+ try {
376
+ await handle.chmod(expectedMode);
377
+ }
378
+ catch (error) {
379
+ throw new Error(`failed to secure ${label} permissions: ${messageOf(error)}`);
380
+ }
381
+ const tightened = await handle.stat({ bigint: true });
382
+ if (permissionMode(tightened) !== expectedMode) {
383
+ throw new Error(`failed to secure ${label} permissions`);
384
+ }
385
+ return tightened;
386
+ }
387
+ async function assertCredentialPathBeforeOpen(file) {
388
+ const stats = await lstat(file, { bigint: true });
389
+ assertCredentialFileMetadata(stats);
390
+ }
391
+ async function assertSecureCredentialPath(file, expected) {
392
+ const stats = await lstat(file, { bigint: true });
393
+ assertCredentialFileMetadata(stats);
394
+ assertMode(stats, CREDENTIAL_FILE_MODE, "credential file");
395
+ assertSameFile(expected, stats, "credential file changed during access");
396
+ }
397
+ async function assertSecurePath(path, opened, kind, expectedMode) {
398
+ const current = await lstat(path, { bigint: true });
399
+ if (kind === "directory")
400
+ assertDirectoryMetadata(current);
401
+ else
402
+ assertCredentialFileMetadata(current);
403
+ assertMode(current, expectedMode, `credential ${kind}`);
404
+ assertSameFile(fileIdentity(opened), current, `credential ${kind} changed during access`);
405
+ }
406
+ async function readBoundedCredential(handle) {
407
+ const buffer = Buffer.allocUnsafe(MAX_CREDENTIAL_BYTES + 1);
408
+ let offset = 0;
409
+ while (offset < buffer.byteLength) {
410
+ const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
411
+ if (bytesRead === 0)
412
+ break;
413
+ offset += bytesRead;
414
+ }
415
+ if (offset === 0 || offset > MAX_CREDENTIAL_BYTES) {
416
+ throw new Error(`credential exceeds ${MAX_CREDENTIAL_BYTES} bytes`);
417
+ }
418
+ return buffer.subarray(0, offset);
419
+ }
420
+ function assertCredentialReadStable(before, after, bytesRead) {
421
+ if (before.size !== after.size ||
422
+ before.mtimeNs !== after.mtimeNs ||
423
+ before.ctimeNs !== after.ctimeNs ||
424
+ after.size !== BigInt(bytesRead)) {
425
+ throw new Error("credential file changed while reading");
426
+ }
427
+ }
428
+ function assertCredentialSize(size) {
429
+ if (size === 0n || size > BigInt(MAX_CREDENTIAL_BYTES)) {
430
+ throw new Error(`credential exceeds ${MAX_CREDENTIAL_BYTES} bytes`);
431
+ }
432
+ }
433
+ function assertDirectoryMetadata(stats) {
434
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
435
+ throw new Error("credential directory must be a non-symlink directory");
436
+ }
437
+ assertCurrentUserOwner(stats, "credential directory");
438
+ }
439
+ function assertCredentialFileMetadata(stats) {
440
+ if (stats.isSymbolicLink() || !stats.isFile()) {
441
+ throw new Error("credential file must be a non-symlink regular file");
442
+ }
443
+ assertCurrentUserOwner(stats, "credential file");
444
+ }
445
+ function assertCurrentUserOwner(stats, label) {
446
+ if (typeof process.getuid !== "function")
447
+ return;
448
+ if (stats.uid !== BigInt(process.getuid())) {
449
+ throw new Error(`${label} is not owned by the current user`);
450
+ }
451
+ }
452
+ function assertMode(stats, expected, label) {
453
+ if (!CHECK_POSIX_SECURITY)
454
+ return;
455
+ if (permissionMode(stats) !== expected) {
456
+ throw new Error(`${label} permissions are unsafe`);
457
+ }
458
+ }
459
+ function permissionMode(stats) {
460
+ return Number(stats.mode & 511n);
461
+ }
462
+ function fileIdentity(stats) {
463
+ return { dev: stats.dev, ino: stats.ino };
464
+ }
465
+ function assertSameFile(expected, actual, message) {
466
+ if (actual.dev !== expected.dev || actual.ino !== expected.ino)
467
+ throw new Error(message);
468
+ }
469
+ function validateCredentialRef(value) {
470
+ if (typeof value !== "string" || !CREDENTIAL_REF_PATTERN.test(value)) {
471
+ throw new Error("invalid Remote Runtime credential reference");
472
+ }
473
+ return value;
474
+ }
475
+ function validateCredential(value) {
476
+ if (!isRecord(value) || value.version !== CREDENTIAL_VERSION) {
477
+ throw new Error("unsupported credential version");
478
+ }
479
+ const daemonId = opaqueId(value.daemonId, "daemonId");
480
+ const clientId = opaqueId(value.clientId, "clientId");
481
+ const grantId = opaqueId(value.grantId, "grantId");
482
+ const identityPublicKey = ed25519PublicKey(value.identityPublicKey, "identityPublicKey");
483
+ const serverEncryptionPublicKey = x25519PublicKey(value.serverEncryptionPublicKey, "serverEncryptionPublicKey");
484
+ const clientPublicKey = ed25519PublicKey(value.clientPublicKey, "clientPublicKey");
485
+ const clientPrivateKeyPem = validClientKeyPair(value.clientPrivateKeyPem, clientPublicKey);
486
+ return {
487
+ version: 2,
488
+ daemonId,
489
+ identityPublicKey,
490
+ serverEncryptionPublicKey,
491
+ clientId,
492
+ grantId,
493
+ clientPublicKey,
494
+ clientPrivateKeyPem,
495
+ };
496
+ }
497
+ function validatePendingCredential(value) {
498
+ if (!isRecord(value) ||
499
+ value.version !== CREDENTIAL_VERSION ||
500
+ value.state !== "pending_enrollment") {
501
+ throw new Error("unsupported pending credential version");
502
+ }
503
+ const daemonId = opaqueId(value.daemonId, "daemonId");
504
+ const identityPublicKey = ed25519PublicKey(value.identityPublicKey, "identityPublicKey");
505
+ const serverEncryptionPublicKey = x25519PublicKey(value.serverEncryptionPublicKey, "serverEncryptionPublicKey");
506
+ const claimId = opaqueId(value.claimId, "claimId");
507
+ const clientId = opaqueId(value.clientId, "clientId");
508
+ const clientPublicKey = ed25519PublicKey(value.clientPublicKey, "clientPublicKey");
509
+ const clientPrivateKeyPem = validClientKeyPair(value.clientPrivateKeyPem, clientPublicKey);
510
+ return {
511
+ version: 2,
512
+ state: "pending_enrollment",
513
+ daemonId,
514
+ identityPublicKey,
515
+ serverEncryptionPublicKey,
516
+ claimId,
517
+ clientId,
518
+ clientPublicKey,
519
+ clientPrivateKeyPem,
520
+ };
521
+ }
522
+ function validClientKeyPair(value, clientPublicKey) {
523
+ if (typeof value !== "string" ||
524
+ value.length === 0 ||
525
+ value.length > MAX_PRIVATE_KEY_CHARS) {
526
+ throw new Error("clientPrivateKeyPem is invalid");
527
+ }
528
+ try {
529
+ const privateKey = createPrivateKey(value);
530
+ if (privateKey.asymmetricKeyType !== "ed25519") {
531
+ throw new Error("client private key must use Ed25519");
532
+ }
533
+ const publicKey = createPublicKey({
534
+ key: Buffer.from(clientPublicKey, "base64url"),
535
+ format: "der",
536
+ type: "spki",
537
+ });
538
+ const probe = Buffer.from("rynx-remote-runtime-credential-validation-v1", "utf8");
539
+ const signature = sign(null, probe, privateKey);
540
+ if (!verify(null, probe, publicKey, signature)) {
541
+ throw new Error("client public and private keys do not match");
542
+ }
543
+ }
544
+ catch (error) {
545
+ throw new Error(`client key pair is invalid: ${messageOf(error)}`);
546
+ }
547
+ return value;
548
+ }
549
+ function assertPendingMatchesAccepted(pending, claimId, accepted) {
550
+ if (pending.claimId !== claimId ||
551
+ pending.daemonId !== accepted.daemonId ||
552
+ pending.identityPublicKey !== accepted.identityPublicKey ||
553
+ pending.serverEncryptionPublicKey !== accepted.serverEncryptionPublicKey ||
554
+ pending.clientId !== accepted.clientId ||
555
+ pending.clientPublicKey !== accepted.clientPublicKey ||
556
+ pending.clientPrivateKeyPem !== accepted.clientPrivateKeyPem) {
557
+ throw new Error("pending Runtime credential differs from the accepted enrollment");
558
+ }
559
+ }
560
+ function sameAcceptedCredential(left, right) {
561
+ return (left.version === right.version &&
562
+ left.daemonId === right.daemonId &&
563
+ left.identityPublicKey === right.identityPublicKey &&
564
+ left.serverEncryptionPublicKey === right.serverEncryptionPublicKey &&
565
+ left.clientId === right.clientId &&
566
+ left.grantId === right.grantId &&
567
+ left.clientPublicKey === right.clientPublicKey &&
568
+ left.clientPrivateKeyPem === right.clientPrivateKeyPem);
569
+ }
570
+ function encodeCredential(credential) {
571
+ const encoded = Buffer.from(`${JSON.stringify(credential, null, 2)}\n`, "utf8");
572
+ if (encoded.byteLength > MAX_CREDENTIAL_BYTES) {
573
+ throw new Error(`credential exceeds ${MAX_CREDENTIAL_BYTES} bytes`);
574
+ }
575
+ return encoded;
576
+ }
577
+ function parseCredentialJson(ref, raw) {
578
+ try {
579
+ return JSON.parse(raw);
580
+ }
581
+ catch (error) {
582
+ throw new Error(`invalid Remote Runtime credential ${ref}: ${messageOf(error)}`);
583
+ }
584
+ }
585
+ function ed25519PublicKey(value, field) {
586
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{59}$/.test(value)) {
587
+ throw new Error(`${field} is invalid`);
588
+ }
589
+ try {
590
+ const bytes = Buffer.from(value, "base64url");
591
+ if (bytes.byteLength !== 44 || bytes.toString("base64url") !== value) {
592
+ throw new Error("non-canonical key");
593
+ }
594
+ const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
595
+ if (key.asymmetricKeyType !== "ed25519")
596
+ throw new Error("not Ed25519");
597
+ return key.export({ format: "der", type: "spki" }).toString("base64url");
598
+ }
599
+ catch (error) {
600
+ throw new Error(`${field} is invalid: ${messageOf(error)}`);
601
+ }
602
+ }
603
+ function x25519PublicKey(value, field) {
604
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{59}$/.test(value)) {
605
+ throw new Error(`${field} is invalid`);
606
+ }
607
+ try {
608
+ const bytes = Buffer.from(value, "base64url");
609
+ if (bytes.byteLength !== 44 || bytes.toString("base64url") !== value) {
610
+ throw new Error("non-canonical key");
611
+ }
612
+ const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
613
+ if (key.asymmetricKeyType !== "x25519")
614
+ throw new Error("not X25519");
615
+ return key.export({ format: "der", type: "spki" }).toString("base64url");
616
+ }
617
+ catch (error) {
618
+ throw new Error(`${field} is invalid: ${messageOf(error)}`);
619
+ }
620
+ }
621
+ function opaqueId(value, field) {
622
+ if (typeof value !== "string" || !OPAQUE_ID_PATTERN.test(value)) {
623
+ throw new Error(`${field} is invalid`);
624
+ }
625
+ return value;
626
+ }
627
+ function isRecord(value) {
628
+ return typeof value === "object" && value !== null && !Array.isArray(value);
629
+ }
630
+ function hasCode(error, code) {
631
+ if (typeof error !== "object" || error === null)
632
+ return false;
633
+ if ("code" in error && error.code === code)
634
+ return true;
635
+ return "cause" in error && hasCode(error.cause, code);
636
+ }
637
+ function messageOf(error) {
638
+ return error instanceof Error ? error.message : String(error);
639
+ }