@rynx-ai/daemon 0.1.0 → 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 (109) 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/browser-cli-args.d.ts +20 -0
  6. package/dist/browser-cli-args.js +100 -0
  7. package/dist/browser-runner-session-context.d.ts +24 -0
  8. package/dist/browser-runner-session-context.js +113 -0
  9. package/dist/cdp-keyboard-input.d.ts +6 -0
  10. package/dist/cdp-keyboard-input.js +209 -0
  11. package/dist/channel-store.d.ts +0 -6
  12. package/dist/channel-store.js +2 -18
  13. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  14. package/dist/chrome-for-testing-release-resolver.js +214 -0
  15. package/dist/chrome-for-testing-store.d.ts +85 -0
  16. package/dist/chrome-for-testing-store.js +836 -0
  17. package/dist/chrome-inspection-gateway.d.ts +58 -0
  18. package/dist/chrome-inspection-gateway.js +806 -0
  19. package/dist/chrome-inspection-manager.d.ts +94 -0
  20. package/dist/chrome-inspection-manager.js +573 -0
  21. package/dist/cli.js +478 -30
  22. package/dist/control-client.d.ts +122 -0
  23. package/dist/control-client.js +855 -0
  24. package/dist/control-deps.js +37 -6
  25. package/dist/control-endpoint.d.ts +14 -0
  26. package/dist/control-endpoint.js +124 -0
  27. package/dist/control-link-store.d.ts +16 -0
  28. package/dist/control-link-store.js +53 -0
  29. package/dist/daemon-build-status.d.ts +8 -0
  30. package/dist/daemon-build-status.js +18 -0
  31. package/dist/daemon-owner.d.ts +14 -0
  32. package/dist/daemon-owner.js +104 -0
  33. package/dist/daemon-server.d.ts +29 -2
  34. package/dist/daemon-server.js +1113 -26
  35. package/dist/db.js +196 -10
  36. package/dist/desktop-browser-host-client.d.ts +35 -0
  37. package/dist/desktop-browser-host-client.js +415 -0
  38. package/dist/direct-runtime-authenticator.d.ts +13 -0
  39. package/dist/direct-runtime-authenticator.js +132 -0
  40. package/dist/direct-runtime-config.d.ts +20 -0
  41. package/dist/direct-runtime-config.js +101 -0
  42. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  43. package/dist/embedded-browser-profile-cleanup.js +338 -0
  44. package/dist/headless-browser-host.d.ts +14 -0
  45. package/dist/headless-browser-host.js +2173 -0
  46. package/dist/index-daemon.js +55 -5
  47. package/dist/installation-id.d.ts +33 -0
  48. package/dist/installation-id.js +692 -0
  49. package/dist/maintenance-management.d.ts +11 -0
  50. package/dist/maintenance-management.js +13 -0
  51. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  52. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  53. package/dist/plugin-cli.d.ts +30 -0
  54. package/dist/plugin-cli.js +270 -0
  55. package/dist/plugin-host-rpc.d.ts +85 -0
  56. package/dist/plugin-host-rpc.js +878 -0
  57. package/dist/plugin-install-restart.d.ts +13 -0
  58. package/dist/plugin-install-restart.js +24 -0
  59. package/dist/plugin-installer.d.ts +71 -15
  60. package/dist/plugin-installer.js +521 -155
  61. package/dist/plugin-management-service.d.ts +58 -0
  62. package/dist/plugin-management-service.js +240 -0
  63. package/dist/plugin-package.d.ts +61 -0
  64. package/dist/plugin-package.js +732 -0
  65. package/dist/plugin-runtime-control.d.ts +2 -0
  66. package/dist/plugin-runtime-control.js +37 -0
  67. package/dist/plugin-store.d.ts +37 -8
  68. package/dist/plugin-store.js +80 -15
  69. package/dist/plugin-supervisor.d.ts +160 -0
  70. package/dist/plugin-supervisor.js +1143 -0
  71. package/dist/pm2.d.ts +3 -1
  72. package/dist/pm2.js +92 -18
  73. package/dist/provider-cli-service.d.ts +46 -0
  74. package/dist/provider-cli-service.js +510 -0
  75. package/dist/registry.d.ts +6 -19
  76. package/dist/registry.js +22 -87
  77. package/dist/remote-runtime-access-store.d.ts +62 -0
  78. package/dist/remote-runtime-access-store.js +393 -0
  79. package/dist/remote-runtime-admin.d.ts +35 -0
  80. package/dist/remote-runtime-admin.js +110 -0
  81. package/dist/remote-runtime-connection-manager.d.ts +103 -0
  82. package/dist/remote-runtime-connection-manager.js +1575 -0
  83. package/dist/remote-runtime-credential-store.d.ts +45 -0
  84. package/dist/remote-runtime-credential-store.js +639 -0
  85. package/dist/remote-runtime-target-control.d.ts +51 -0
  86. package/dist/remote-runtime-target-control.js +488 -0
  87. package/dist/remote-runtime-target-store.d.ts +56 -0
  88. package/dist/remote-runtime-target-store.js +326 -0
  89. package/dist/runtime-share-addresses.d.ts +21 -0
  90. package/dist/runtime-share-addresses.js +50 -0
  91. package/dist/session-browser-capability-store.d.ts +13 -0
  92. package/dist/session-browser-capability-store.js +58 -0
  93. package/dist/session-emulator-binding-store.d.ts +10 -0
  94. package/dist/session-emulator-binding-store.js +61 -0
  95. package/dist/session-launch-operations.d.ts +41 -0
  96. package/dist/session-launch-operations.js +124 -0
  97. package/dist/session-log-store.js +29 -0
  98. package/dist/session-meta-store.d.ts +3 -2
  99. package/dist/session-meta-store.js +59 -7
  100. package/dist/session-pending-message-store.d.ts +2 -0
  101. package/dist/session-pending-message-store.js +41 -0
  102. package/dist/session-resource-store.d.ts +32 -0
  103. package/dist/session-resource-store.js +700 -0
  104. package/dist/setup.d.ts +16 -0
  105. package/dist/setup.js +144 -5
  106. package/dist/skills-catalog.js +8 -4
  107. package/dist/update.d.ts +11 -2
  108. package/dist/update.js +0 -27
  109. package/package.json +23 -10
@@ -0,0 +1,62 @@
1
+ import Database from "better-sqlite3";
2
+ declare const DEFAULT_AUTHORITIES: readonly ["daemon.full"];
3
+ export type RuntimeAuthority = (typeof DEFAULT_AUTHORITIES)[number];
4
+ export type RemoteRuntimeAccessErrorCode = "invalid_claim" | "claim_expired" | "claim_used" | "grant_not_found" | "grant_revoked" | "grant_identity_mismatch" | "conflict" | "corrupt_state";
5
+ export declare class RemoteRuntimeAccessError extends Error {
6
+ readonly code: RemoteRuntimeAccessErrorCode;
7
+ constructor(code: RemoteRuntimeAccessErrorCode, message: string, options?: ErrorOptions);
8
+ }
9
+ export interface RuntimePairingClaim {
10
+ claimId: string;
11
+ claimSecret: string;
12
+ authorities: readonly RuntimeAuthority[];
13
+ clientLabel: string | null;
14
+ createdAt: string;
15
+ expiresAt: string;
16
+ }
17
+ export interface RuntimeClientGrant {
18
+ grantId: string;
19
+ clientId: string;
20
+ /** Canonical Ed25519 SubjectPublicKeyInfo DER, base64url encoded. */
21
+ clientPublicKey: string;
22
+ clientLabel: string | null;
23
+ authorities: readonly RuntimeAuthority[];
24
+ createdAt: string;
25
+ revokedAt: string | null;
26
+ lastUsedAt: string | null;
27
+ }
28
+ export interface CreatePairingClaimInput {
29
+ ttlMs: number;
30
+ label?: string;
31
+ }
32
+ export interface RedeemPairingClaimInput {
33
+ claimId: string;
34
+ claimSecret: string;
35
+ clientId: string;
36
+ clientPublicKey: string;
37
+ label?: string;
38
+ }
39
+ export interface ResolveActiveGrantInput {
40
+ grantId: string;
41
+ clientId: string;
42
+ clientPublicKey: string;
43
+ }
44
+ type Clock = () => Date;
45
+ /** SQLite-backed enrollment and client authorization state for a direct runtime. */
46
+ export declare class RemoteRuntimeAccessStore {
47
+ private readonly connection;
48
+ private readonly clock;
49
+ constructor(connection?: Database.Database, clock?: Clock);
50
+ createPairingClaim(input: CreatePairingClaimInput): RuntimePairingClaim;
51
+ redeemPairingClaim(input: RedeemPairingClaimInput): RuntimeClientGrant;
52
+ resolveActiveGrant(input: ResolveActiveGrantInput): RuntimeClientGrant;
53
+ listClientGrants(): RuntimeClientGrant[];
54
+ /** Recheck an already authenticated peer before each dispatched operation. */
55
+ isClientGrantActive(grantIdInput: string, clientIdInput: string): boolean;
56
+ revokeClientGrant(grantIdInput: string): boolean;
57
+ deleteRevokedClientGrant(grantIdInput: string): boolean;
58
+ private replayConsumedClaim;
59
+ private deleteRetainedExpiredClaims;
60
+ private now;
61
+ }
62
+ export {};
@@ -0,0 +1,393 @@
1
+ import { createHash, createPublicKey, randomBytes, timingSafeEqual, } from "node:crypto";
2
+ import { db } from "./db.js";
3
+ const CLAIM_SECRET_HASH_DOMAIN = "rynx-runtime-pairing-claim-secret-v1\0";
4
+ const DEFAULT_AUTHORITIES = Object.freeze(["daemon.full"]);
5
+ const MIN_TTL_MS = 1_000;
6
+ const MAX_TTL_MS = 15 * 60_000;
7
+ const EXPIRED_UNCONSUMED_CLAIM_RETENTION_MS = 24 * 60 * 60_000;
8
+ const CLAIM_CLEANUP_BATCH_SIZE = 256;
9
+ const MAX_ID_LENGTH = 256;
10
+ const MAX_KEY_LENGTH = 512;
11
+ const MAX_LABEL_LENGTH = 128;
12
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/;
13
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
14
+ export class RemoteRuntimeAccessError extends Error {
15
+ code;
16
+ constructor(code, message, options) {
17
+ super(message, options);
18
+ this.name = "RemoteRuntimeAccessError";
19
+ this.code = code;
20
+ }
21
+ }
22
+ /** SQLite-backed enrollment and client authorization state for a direct runtime. */
23
+ export class RemoteRuntimeAccessStore {
24
+ connection;
25
+ clock;
26
+ constructor(connection = db(), clock = () => new Date()) {
27
+ this.connection = connection;
28
+ this.clock = clock;
29
+ }
30
+ createPairingClaim(input) {
31
+ const ttlMs = validTtl(input.ttlMs);
32
+ const clientLabel = optionalLabel(input.label, "conflict");
33
+ const claimId = `claim_${randomBytes(18).toString("base64url")}`;
34
+ const claimSecret = randomBytes(32).toString("base64url");
35
+ const created = this.now();
36
+ const createdAt = created.toISOString();
37
+ const expiresAt = new Date(created.getTime() + ttlMs).toISOString();
38
+ const create = this.connection.transaction(() => {
39
+ this.deleteRetainedExpiredClaims(created);
40
+ this.connection
41
+ .prepare(`INSERT INTO runtime_pairing_claims
42
+ (claim_id, secret_hash, authorities, client_label, created_at, expires_at,
43
+ consumed_at, consumed_client_id, consumed_client_public_key, consumed_grant_id)
44
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL)`)
45
+ .run(claimId, hashClaimSecret(claimSecret).toString("hex"), JSON.stringify(DEFAULT_AUTHORITIES), clientLabel, createdAt, expiresAt);
46
+ });
47
+ create.immediate();
48
+ return {
49
+ claimId,
50
+ claimSecret,
51
+ authorities: DEFAULT_AUTHORITIES,
52
+ clientLabel,
53
+ createdAt,
54
+ expiresAt,
55
+ };
56
+ }
57
+ redeemPairingClaim(input) {
58
+ const claimId = validId(input.claimId, "claimId", "invalid_claim");
59
+ const claimSecret = validClaimSecret(input.claimSecret);
60
+ const clientId = validId(input.clientId, "clientId", "invalid_claim");
61
+ const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "invalid_claim");
62
+ const requestedLabel = optionalLabel(input.label, "invalid_claim");
63
+ const redeem = this.connection.transaction(() => {
64
+ const now = this.now().toISOString();
65
+ const claim = this.connection
66
+ .prepare(`SELECT claim_id, secret_hash, authorities, client_label, created_at, expires_at,
67
+ consumed_at, consumed_client_id, consumed_client_public_key, consumed_grant_id
68
+ FROM runtime_pairing_claims
69
+ WHERE claim_id = ?`)
70
+ .get(claimId);
71
+ if (!claim) {
72
+ throw accessError("invalid_claim", "pairing claim is invalid");
73
+ }
74
+ assertAuthorities(claim.authorities);
75
+ assertClaimSecret(claim.secret_hash, claimSecret);
76
+ if (claim.consumed_at !== null) {
77
+ return this.replayConsumedClaim(claim, clientId, clientPublicKey);
78
+ }
79
+ assertUnconsumedClaimShape(claim);
80
+ if (claim.expires_at <= now) {
81
+ throw accessError("claim_expired", "pairing claim has expired");
82
+ }
83
+ const grantId = `grant_${randomBytes(18).toString("base64url")}`;
84
+ const clientLabel = requestedLabel ?? claim.client_label;
85
+ this.connection
86
+ .prepare(`INSERT INTO runtime_client_grants
87
+ (grant_id, client_id, client_public_key, client_label, authorities,
88
+ created_at, revoked_at, last_used_at)
89
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)`)
90
+ .run(grantId, clientId, clientPublicKey, clientLabel, JSON.stringify(DEFAULT_AUTHORITIES), now);
91
+ const consumed = this.connection
92
+ .prepare(`UPDATE runtime_pairing_claims
93
+ SET consumed_at = ?, consumed_client_id = ?, consumed_client_public_key = ?,
94
+ consumed_grant_id = ?
95
+ WHERE claim_id = ? AND consumed_at IS NULL`)
96
+ .run(now, clientId, clientPublicKey, grantId, claimId);
97
+ if (consumed.changes !== 1) {
98
+ throw accessError("conflict", "pairing claim was consumed concurrently");
99
+ }
100
+ return {
101
+ grantId,
102
+ clientId,
103
+ clientPublicKey,
104
+ clientLabel,
105
+ authorities: DEFAULT_AUTHORITIES,
106
+ createdAt: now,
107
+ revokedAt: null,
108
+ lastUsedAt: null,
109
+ };
110
+ });
111
+ try {
112
+ return redeem.immediate();
113
+ }
114
+ catch (error) {
115
+ if (error instanceof RemoteRuntimeAccessError)
116
+ throw error;
117
+ if (isSqliteConstraint(error)) {
118
+ throw accessError("conflict", "client identity already has a runtime grant", error);
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ resolveActiveGrant(input) {
124
+ const grantId = validId(input.grantId, "grantId", "grant_not_found");
125
+ const clientId = validId(input.clientId, "clientId", "grant_identity_mismatch");
126
+ const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "grant_identity_mismatch");
127
+ const resolve = this.connection.transaction(() => {
128
+ const row = this.connection
129
+ .prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
130
+ created_at, revoked_at, last_used_at
131
+ FROM runtime_client_grants
132
+ WHERE grant_id = ?`)
133
+ .get(grantId);
134
+ if (!row)
135
+ throw accessError("grant_not_found", "runtime client grant was not found");
136
+ const grant = grantFromRow(row);
137
+ if (grant.clientId !== clientId || grant.clientPublicKey !== clientPublicKey) {
138
+ throw accessError("grant_identity_mismatch", "runtime client grant identity does not match");
139
+ }
140
+ if (grant.revokedAt !== null) {
141
+ throw accessError("grant_revoked", "runtime client grant has been revoked");
142
+ }
143
+ const lastUsedAt = this.now().toISOString();
144
+ const updated = this.connection
145
+ .prepare(`UPDATE runtime_client_grants
146
+ SET last_used_at = ?
147
+ WHERE grant_id = ? AND client_id = ? AND client_public_key = ? AND revoked_at IS NULL`)
148
+ .run(lastUsedAt, grantId, clientId, clientPublicKey);
149
+ if (updated.changes !== 1) {
150
+ throw accessError("grant_revoked", "runtime client grant is no longer active");
151
+ }
152
+ return { ...grant, lastUsedAt };
153
+ });
154
+ return resolve.immediate();
155
+ }
156
+ listClientGrants() {
157
+ const rows = this.connection
158
+ .prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
159
+ created_at, revoked_at, last_used_at
160
+ FROM runtime_client_grants
161
+ ORDER BY created_at, grant_id`)
162
+ .all();
163
+ return rows.map(grantFromRow);
164
+ }
165
+ /** Recheck an already authenticated peer before each dispatched operation. */
166
+ isClientGrantActive(grantIdInput, clientIdInput) {
167
+ const grantId = validId(grantIdInput, "grantId", "grant_not_found");
168
+ const clientId = validId(clientIdInput, "clientId", "grant_identity_mismatch");
169
+ const row = this.connection
170
+ .prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
171
+ created_at, revoked_at, last_used_at
172
+ FROM runtime_client_grants
173
+ WHERE grant_id = ?`)
174
+ .get(grantId);
175
+ if (!row)
176
+ return false;
177
+ const grant = grantFromRow(row);
178
+ return grant.clientId === clientId && grant.revokedAt === null;
179
+ }
180
+ revokeClientGrant(grantIdInput) {
181
+ const grantId = validId(grantIdInput, "grantId", "grant_not_found");
182
+ const revokedAt = this.now().toISOString();
183
+ const revoke = this.connection.transaction(() => {
184
+ const revoked = this.connection
185
+ .prepare(`UPDATE runtime_client_grants
186
+ SET revoked_at = ?
187
+ WHERE grant_id = ? AND revoked_at IS NULL`)
188
+ .run(revokedAt, grantId).changes === 1;
189
+ // An active grant keeps its consumed claim so the enrolling client can
190
+ // recover an accepted response it missed. Revocation ends that retry
191
+ // contract, so remove the no-longer-secret enrollment record atomically.
192
+ this.connection
193
+ .prepare(`DELETE FROM runtime_pairing_claims
194
+ WHERE consumed_grant_id = ? AND consumed_at IS NOT NULL`)
195
+ .run(grantId);
196
+ return revoked;
197
+ });
198
+ return revoke.immediate();
199
+ }
200
+ deleteRevokedClientGrant(grantIdInput) {
201
+ const grantId = validId(grantIdInput, "grantId", "grant_not_found");
202
+ return this.connection
203
+ .prepare(`DELETE FROM runtime_client_grants
204
+ WHERE grant_id = ? AND revoked_at IS NOT NULL`)
205
+ .run(grantId).changes === 1;
206
+ }
207
+ replayConsumedClaim(claim, clientId, clientPublicKey) {
208
+ if (claim.consumed_client_id === null ||
209
+ claim.consumed_client_public_key === null ||
210
+ claim.consumed_grant_id === null) {
211
+ throw accessError("corrupt_state", "consumed pairing claim is incomplete");
212
+ }
213
+ let consumedPublicKey;
214
+ try {
215
+ consumedPublicKey = canonicalEd25519PublicKey(claim.consumed_client_public_key, "corrupt_state");
216
+ }
217
+ catch (error) {
218
+ if (error instanceof RemoteRuntimeAccessError)
219
+ throw error;
220
+ throw accessError("corrupt_state", "consumed pairing claim has an invalid client key", error);
221
+ }
222
+ if (claim.consumed_client_id !== clientId || consumedPublicKey !== clientPublicKey) {
223
+ throw accessError("claim_used", "pairing claim has already been used");
224
+ }
225
+ const row = this.connection
226
+ .prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
227
+ created_at, revoked_at, last_used_at
228
+ FROM runtime_client_grants
229
+ WHERE grant_id = ?`)
230
+ .get(claim.consumed_grant_id);
231
+ if (!row) {
232
+ throw accessError("corrupt_state", "consumed pairing claim has no associated grant");
233
+ }
234
+ const grant = grantFromRow(row);
235
+ if (grant.clientId !== clientId || grant.clientPublicKey !== clientPublicKey) {
236
+ throw accessError("corrupt_state", "consumed pairing claim and grant identities differ");
237
+ }
238
+ if (grant.revokedAt !== null) {
239
+ throw accessError("grant_revoked", "runtime client grant has been revoked");
240
+ }
241
+ return grant;
242
+ }
243
+ deleteRetainedExpiredClaims(now) {
244
+ const cutoff = new Date(now.getTime() - EXPIRED_UNCONSUMED_CLAIM_RETENTION_MS).toISOString();
245
+ this.connection
246
+ .prepare(`DELETE FROM runtime_pairing_claims
247
+ WHERE claim_id IN (
248
+ SELECT claim_id
249
+ FROM runtime_pairing_claims
250
+ WHERE expires_at <= ? AND consumed_at IS NULL
251
+ ORDER BY expires_at
252
+ LIMIT ?
253
+ )`)
254
+ .run(cutoff, CLAIM_CLEANUP_BATCH_SIZE);
255
+ }
256
+ now() {
257
+ const value = this.clock();
258
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
259
+ throw accessError("corrupt_state", "runtime access clock returned an invalid date");
260
+ }
261
+ return new Date(value.getTime());
262
+ }
263
+ }
264
+ function validTtl(value) {
265
+ if (!Number.isSafeInteger(value) || value < MIN_TTL_MS || value > MAX_TTL_MS) {
266
+ throw accessError("conflict", `ttlMs must be an integer between ${MIN_TTL_MS} and ${MAX_TTL_MS}`);
267
+ }
268
+ return value;
269
+ }
270
+ function validId(value, label, code) {
271
+ if (typeof value !== "string" ||
272
+ value.length === 0 ||
273
+ value.length > MAX_ID_LENGTH ||
274
+ value.trim() !== value ||
275
+ !ID_PATTERN.test(value)) {
276
+ throw accessError(code, `${label} is invalid`);
277
+ }
278
+ return value;
279
+ }
280
+ function validClaimSecret(value) {
281
+ if (typeof value !== "string" ||
282
+ value.length !== 43 ||
283
+ !BASE64URL_PATTERN.test(value) ||
284
+ Buffer.from(value, "base64url").length !== 32 ||
285
+ Buffer.from(value, "base64url").toString("base64url") !== value) {
286
+ throw accessError("invalid_claim", "pairing claim is invalid");
287
+ }
288
+ return value;
289
+ }
290
+ function optionalLabel(value, code) {
291
+ if (value === undefined)
292
+ return null;
293
+ if (typeof value !== "string" ||
294
+ value.length === 0 ||
295
+ value.length > MAX_LABEL_LENGTH ||
296
+ value.trim() !== value ||
297
+ /[\u0000-\u001f\u007f]/.test(value)) {
298
+ throw accessError(code, "client label is invalid");
299
+ }
300
+ return value;
301
+ }
302
+ function canonicalEd25519PublicKey(value, code) {
303
+ if (typeof value !== "string" ||
304
+ value.length === 0 ||
305
+ value.length > MAX_KEY_LENGTH ||
306
+ !BASE64URL_PATTERN.test(value)) {
307
+ throw accessError(code, "client public key is invalid");
308
+ }
309
+ try {
310
+ const bytes = Buffer.from(value, "base64url");
311
+ if (bytes.toString("base64url") !== value) {
312
+ throw new Error("non-canonical base64url");
313
+ }
314
+ const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
315
+ if (key.asymmetricKeyType !== "ed25519") {
316
+ throw new Error("not an Ed25519 key");
317
+ }
318
+ return key.export({ format: "der", type: "spki" }).toString("base64url");
319
+ }
320
+ catch (error) {
321
+ if (error instanceof RemoteRuntimeAccessError)
322
+ throw error;
323
+ throw accessError(code, "client public key is invalid", error);
324
+ }
325
+ }
326
+ function assertAuthorities(raw) {
327
+ try {
328
+ const parsed = JSON.parse(raw);
329
+ if (!Array.isArray(parsed) ||
330
+ parsed.length !== 1 ||
331
+ parsed[0] !== DEFAULT_AUTHORITIES[0]) {
332
+ throw new Error("unsupported authorities");
333
+ }
334
+ }
335
+ catch (error) {
336
+ throw accessError("corrupt_state", "runtime authorities are corrupt", error);
337
+ }
338
+ }
339
+ function assertClaimSecret(storedHash, claimSecret) {
340
+ const expected = hashClaimSecret(claimSecret);
341
+ let actual;
342
+ try {
343
+ if (!/^[0-9a-f]{64}$/.test(storedHash))
344
+ throw new Error("invalid secret hash");
345
+ actual = Buffer.from(storedHash, "hex");
346
+ }
347
+ catch (error) {
348
+ throw accessError("corrupt_state", "pairing claim secret hash is corrupt", error);
349
+ }
350
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
351
+ throw accessError("invalid_claim", "pairing claim is invalid");
352
+ }
353
+ }
354
+ function assertUnconsumedClaimShape(claim) {
355
+ if (claim.consumed_client_id !== null ||
356
+ claim.consumed_client_public_key !== null ||
357
+ claim.consumed_grant_id !== null) {
358
+ throw accessError("corrupt_state", "unconsumed pairing claim has consumed identity state");
359
+ }
360
+ }
361
+ function hashClaimSecret(secret) {
362
+ return createHash("sha256").update(CLAIM_SECRET_HASH_DOMAIN).update(secret).digest();
363
+ }
364
+ function grantFromRow(row) {
365
+ validId(row.grant_id, "stored grantId", "corrupt_state");
366
+ validId(row.client_id, "stored clientId", "corrupt_state");
367
+ const clientPublicKey = canonicalEd25519PublicKey(row.client_public_key, "corrupt_state");
368
+ if (clientPublicKey !== row.client_public_key) {
369
+ throw accessError("corrupt_state", "stored client public key is not canonical");
370
+ }
371
+ const clientLabel = row.client_label === null ? null : optionalLabel(row.client_label, "corrupt_state");
372
+ assertAuthorities(row.authorities);
373
+ return {
374
+ grantId: row.grant_id,
375
+ clientId: row.client_id,
376
+ clientPublicKey,
377
+ clientLabel,
378
+ authorities: DEFAULT_AUTHORITIES,
379
+ createdAt: row.created_at,
380
+ revokedAt: row.revoked_at,
381
+ lastUsedAt: row.last_used_at,
382
+ };
383
+ }
384
+ function isSqliteConstraint(error) {
385
+ return (typeof error === "object" &&
386
+ error !== null &&
387
+ "code" in error &&
388
+ typeof error.code === "string" &&
389
+ error.code.startsWith("SQLITE_CONSTRAINT"));
390
+ }
391
+ function accessError(code, message, cause) {
392
+ return new RemoteRuntimeAccessError(code, message, cause === undefined ? undefined : { cause });
393
+ }
@@ -0,0 +1,35 @@
1
+ import { type PairingOffer } from "@rynx-ai/protocol/direct-runtime";
2
+ import type { DirectRuntimeConnectionHub, RemoteRuntimeAdminHost } from "@rynx-ai/server";
3
+ import type { DirectRuntimeConfig } from "./direct-runtime-config.js";
4
+ import type { DaemonDirectRuntimeEncryptionIdentity, DaemonInstallationIdentity } from "./installation-id.js";
5
+ import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
6
+ export declare class DaemonRemoteRuntimeAdmin implements RemoteRuntimeAdminHost {
7
+ private readonly identity;
8
+ private readonly encryption;
9
+ private readonly access;
10
+ private readonly directConfig;
11
+ private readonly connections;
12
+ constructor(identity: DaemonInstallationIdentity, encryption: DaemonDirectRuntimeEncryptionIdentity, access: RemoteRuntimeAccessStore, directConfig: DirectRuntimeConfig, connections: DirectRuntimeConnectionHub);
13
+ listShareAddresses(): import("@rynx-ai/protocol/control").ControlRuntimeShareAddress[];
14
+ createPairingOffer(input?: {
15
+ clientLabel?: string;
16
+ address?: string;
17
+ }): PairingOffer;
18
+ listClientGrants(): {
19
+ grantId: string;
20
+ clientId: string;
21
+ clientLabel: string | null;
22
+ clientKeyFingerprint: string;
23
+ authorities: "daemon.full"[];
24
+ createdAt: string;
25
+ revokedAt: string | null;
26
+ lastUsedAt: string | null;
27
+ }[];
28
+ revokeClientGrant(grantId: string): {
29
+ revoked: boolean;
30
+ closedConnections: number;
31
+ };
32
+ deleteRevokedClientGrant(grantId: string): {
33
+ deleted: boolean;
34
+ };
35
+ }
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { DIRECT_RUNTIME_CONTROL_PATH, parsePairingOffer, } from "@rynx-ai/protocol/direct-runtime";
3
+ import { listRuntimeShareAddresses } from "./runtime-share-addresses.js";
4
+ const DEFAULT_PAIRING_TTL_MS = 5 * 60_000;
5
+ export class DaemonRemoteRuntimeAdmin {
6
+ identity;
7
+ encryption;
8
+ access;
9
+ directConfig;
10
+ connections;
11
+ constructor(identity, encryption, access, directConfig, connections) {
12
+ this.identity = identity;
13
+ this.encryption = encryption;
14
+ this.access = access;
15
+ this.directConfig = directConfig;
16
+ this.connections = connections;
17
+ }
18
+ listShareAddresses() {
19
+ return listRuntimeShareAddresses({ bindHost: this.directConfig.bindHost });
20
+ }
21
+ createPairingOffer(input = {}) {
22
+ const directEndpoint = advertisedEndpoint(input.address, this.directConfig.port);
23
+ const claim = this.access.createPairingClaim({
24
+ ttlMs: DEFAULT_PAIRING_TTL_MS,
25
+ ...(input.clientLabel ? { label: input.clientLabel } : {}),
26
+ });
27
+ return parsePairingOffer({
28
+ type: "rynx.remote-runtime.pairing-offer",
29
+ enrollmentVersion: 2,
30
+ daemonId: this.identity.installationId,
31
+ identityAlgorithm: "Ed25519",
32
+ identityPublicKey: this.identity.publicKey,
33
+ security: "e2ee-v1",
34
+ encryptionAlgorithm: "X25519",
35
+ serverEncryptionPublicKey: this.encryption.publicKey,
36
+ directEndpoint,
37
+ claimId: claim.claimId,
38
+ claimSecret: claim.claimSecret,
39
+ expiresAt: claim.expiresAt,
40
+ });
41
+ }
42
+ listClientGrants() {
43
+ return this.access.listClientGrants().map(redactGrant);
44
+ }
45
+ revokeClientGrant(grantId) {
46
+ const revoked = this.access.revokeClientGrant(grantId);
47
+ const closedConnections = revoked ? this.connections.revokeGrant(grantId) : 0;
48
+ return { revoked, closedConnections };
49
+ }
50
+ deleteRevokedClientGrant(grantId) {
51
+ return { deleted: this.access.deleteRevokedClientGrant(grantId) };
52
+ }
53
+ }
54
+ function advertisedEndpoint(address, listenerPort) {
55
+ const input = address?.trim() || "127.0.0.1";
56
+ let endpoint;
57
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
58
+ try {
59
+ endpoint = new URL(input);
60
+ }
61
+ catch {
62
+ throw new Error("Direct Runtime address must be a host or ws(s) URL");
63
+ }
64
+ }
65
+ else {
66
+ if (/[/\\?#@\s]/.test(input)) {
67
+ throw new Error("Direct Runtime address must be a host or ws(s) URL");
68
+ }
69
+ const host = input.includes(":") ? `[${input}]` : input;
70
+ try {
71
+ endpoint = new URL(`ws://${host}:${listenerPort}`);
72
+ }
73
+ catch {
74
+ throw new Error("Direct Runtime address must be a host or ws(s) URL");
75
+ }
76
+ }
77
+ if ((endpoint.protocol !== "ws:" && endpoint.protocol !== "wss:") ||
78
+ endpoint.username ||
79
+ endpoint.password ||
80
+ endpoint.search ||
81
+ endpoint.hash ||
82
+ !endpoint.hostname ||
83
+ endpoint.hostname === "0.0.0.0" ||
84
+ endpoint.hostname === "[::]" ||
85
+ endpoint.hostname === "::") {
86
+ throw new Error("Direct Runtime address must be a credential-free dialable ws(s) route");
87
+ }
88
+ if (endpoint.pathname !== "/" && endpoint.pathname !== DIRECT_RUNTIME_CONTROL_PATH) {
89
+ throw new Error(`Direct Runtime address path must be / or ${DIRECT_RUNTIME_CONTROL_PATH}`);
90
+ }
91
+ endpoint.pathname = DIRECT_RUNTIME_CONTROL_PATH;
92
+ return endpoint.toString();
93
+ }
94
+ function redactGrant(grant) {
95
+ return {
96
+ grantId: grant.grantId,
97
+ clientId: grant.clientId,
98
+ clientLabel: grant.clientLabel,
99
+ clientKeyFingerprint: publicKeyFingerprint(grant.clientPublicKey),
100
+ authorities: [...grant.authorities],
101
+ createdAt: grant.createdAt,
102
+ revokedAt: grant.revokedAt,
103
+ lastUsedAt: grant.lastUsedAt,
104
+ };
105
+ }
106
+ function publicKeyFingerprint(publicKey) {
107
+ return `SHA256:${createHash("sha256")
108
+ .update(Buffer.from(publicKey, "base64url"))
109
+ .digest("base64url")}`;
110
+ }