@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,307 @@
1
+ import { createPublicKey } from "node:crypto";
2
+ import { db } from "./db.js";
3
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
4
+ const CREDENTIAL_REF_PATTERN = /^credential_[A-Za-z0-9_-]{24}$/;
5
+ const MAX_DISPLAY_NAME_CHARS = 128;
6
+ export class RemoteRuntimeTargetStore {
7
+ connection;
8
+ clock;
9
+ constructor(connection = db(), clock = () => new Date()) {
10
+ this.connection = connection;
11
+ this.clock = clock;
12
+ }
13
+ add(input) {
14
+ const normalized = normalizeTargetInput(input);
15
+ const now = validNow(this.clock()).toISOString();
16
+ const insert = this.connection.transaction(() => {
17
+ this.connection.prepare(`INSERT INTO runtime_targets
18
+ (daemon_id, display_name, identity_public_key, credential_ref, created_at, updated_at)
19
+ VALUES (?, ?, ?, ?, ?, ?)`).run(normalized.daemonId, normalized.displayName, normalized.identityPublicKey, normalized.credentialRef, now, now);
20
+ this.connection.prepare(`INSERT INTO runtime_direct_routes
21
+ (daemon_id, endpoint, updated_at)
22
+ VALUES (?, ?, ?)`).run(normalized.daemonId, normalized.directEndpoint, now);
23
+ });
24
+ try {
25
+ insert.immediate();
26
+ }
27
+ catch (error) {
28
+ if (isSqliteConstraint(error)) {
29
+ throw new RemoteRuntimeTargetConflictError("a Runtime target with this daemon identity or credential already exists", { cause: error });
30
+ }
31
+ throw error;
32
+ }
33
+ return this.get(normalized.daemonId);
34
+ }
35
+ addPendingEnrollment(input) {
36
+ const normalized = normalizePendingEnrollmentInput(input);
37
+ const createdAt = validNow(this.clock()).toISOString();
38
+ try {
39
+ this.connection.prepare(`INSERT INTO runtime_pending_enrollments
40
+ (claim_id, daemon_id, identity_public_key, credential_ref,
41
+ offer_fingerprint, expires_at, created_at)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(normalized.claimId, normalized.daemonId, normalized.identityPublicKey, normalized.credentialRef, normalized.offerFingerprint, normalized.expiresAt, createdAt);
43
+ }
44
+ catch (error) {
45
+ if (isSqliteConstraint(error)) {
46
+ throw new RemoteRuntimeTargetConflictError("a pending Runtime enrollment already exists for this claim, daemon, or credential", { cause: error });
47
+ }
48
+ throw error;
49
+ }
50
+ return this.getPendingEnrollment(normalized.claimId);
51
+ }
52
+ getPendingEnrollment(claimIdInput) {
53
+ const claimId = validId(claimIdInput, "claimId");
54
+ const row = this.connection.prepare(`SELECT claim_id, daemon_id, identity_public_key, credential_ref,
55
+ offer_fingerprint, expires_at, created_at
56
+ FROM runtime_pending_enrollments
57
+ WHERE claim_id = ?`).get(claimId);
58
+ return row ? pendingEnrollmentFromRow(row) : undefined;
59
+ }
60
+ getPendingEnrollmentForDaemon(daemonIdInput) {
61
+ const daemonId = validId(daemonIdInput, "daemonId");
62
+ const row = this.connection.prepare(`SELECT claim_id, daemon_id, identity_public_key, credential_ref,
63
+ offer_fingerprint, expires_at, created_at
64
+ FROM runtime_pending_enrollments
65
+ WHERE daemon_id = ?`).get(daemonId);
66
+ return row ? pendingEnrollmentFromRow(row) : undefined;
67
+ }
68
+ removePendingEnrollment(claimIdInput) {
69
+ const claimId = validId(claimIdInput, "claimId");
70
+ const remove = this.connection.transaction(() => {
71
+ const pending = this.getPendingEnrollment(claimId);
72
+ if (!pending)
73
+ return undefined;
74
+ const result = this.connection.prepare("DELETE FROM runtime_pending_enrollments WHERE claim_id = ?").run(claimId);
75
+ if (result.changes !== 1)
76
+ throw new Error("pending Runtime enrollment changed concurrently");
77
+ return pending;
78
+ });
79
+ return remove.immediate();
80
+ }
81
+ commitPendingEnrollment(claimIdInput, input) {
82
+ const claimId = validId(claimIdInput, "claimId");
83
+ const normalized = normalizeTargetInput(input);
84
+ const now = validNow(this.clock()).toISOString();
85
+ const commit = this.connection.transaction(() => {
86
+ const pending = this.getPendingEnrollment(claimId);
87
+ if (!pending)
88
+ throw new Error("pending Runtime enrollment was not found");
89
+ if (pending.daemonId !== normalized.daemonId ||
90
+ pending.identityPublicKey !== normalized.identityPublicKey ||
91
+ pending.credentialRef !== normalized.credentialRef) {
92
+ throw new Error("pending Runtime enrollment does not match the accepted credential");
93
+ }
94
+ insertTarget(this.connection, normalized, now);
95
+ const deleted = this.connection.prepare("DELETE FROM runtime_pending_enrollments WHERE claim_id = ?").run(claimId);
96
+ if (deleted.changes !== 1) {
97
+ throw new Error("pending Runtime enrollment changed concurrently");
98
+ }
99
+ });
100
+ try {
101
+ commit.immediate();
102
+ }
103
+ catch (error) {
104
+ if (error instanceof RemoteRuntimeTargetConflictError)
105
+ throw error;
106
+ if (isSqliteConstraint(error)) {
107
+ throw new RemoteRuntimeTargetConflictError("a Runtime target with this daemon identity or credential already exists", { cause: error });
108
+ }
109
+ throw error;
110
+ }
111
+ return this.get(normalized.daemonId);
112
+ }
113
+ get(daemonIdInput) {
114
+ const daemonId = validId(daemonIdInput, "daemonId");
115
+ const row = this.connection.prepare(`SELECT t.daemon_id, t.display_name, t.identity_public_key, t.credential_ref,
116
+ t.created_at, t.updated_at, r.endpoint
117
+ FROM runtime_targets t
118
+ JOIN runtime_direct_routes r ON r.daemon_id = t.daemon_id
119
+ WHERE t.daemon_id = ?`).get(daemonId);
120
+ return row ? targetFromRow(row) : undefined;
121
+ }
122
+ list() {
123
+ const rows = this.connection.prepare(`SELECT t.daemon_id, t.display_name, t.identity_public_key, t.credential_ref,
124
+ t.created_at, t.updated_at, r.endpoint
125
+ FROM runtime_targets t
126
+ JOIN runtime_direct_routes r ON r.daemon_id = t.daemon_id
127
+ ORDER BY lower(t.display_name), t.daemon_id`).all();
128
+ return rows.map(targetFromRow);
129
+ }
130
+ remove(daemonIdInput) {
131
+ const daemonId = validId(daemonIdInput, "daemonId");
132
+ const remove = this.connection.transaction(() => {
133
+ const target = this.get(daemonId);
134
+ if (!target)
135
+ return undefined;
136
+ const result = this.connection
137
+ .prepare("DELETE FROM runtime_targets WHERE daemon_id = ?")
138
+ .run(daemonId);
139
+ if (result.changes !== 1)
140
+ throw new Error("Runtime target changed concurrently");
141
+ return target;
142
+ });
143
+ return remove.immediate();
144
+ }
145
+ }
146
+ export class RemoteRuntimeTargetConflictError extends Error {
147
+ constructor(message, options) {
148
+ super(message, options);
149
+ this.name = "RemoteRuntimeTargetConflictError";
150
+ }
151
+ }
152
+ function normalizeTargetInput(input) {
153
+ const daemonId = validId(input.daemonId, "daemonId");
154
+ if (daemonId === "local")
155
+ throw new Error("local is a reserved Runtime target selector");
156
+ const displayName = validDisplayName(input.displayName);
157
+ const identityPublicKey = validEd25519PublicKey(input.identityPublicKey);
158
+ const credentialRef = validCredentialRef(input.credentialRef);
159
+ const directEndpoint = validDirectEndpoint(input.directEndpoint);
160
+ return {
161
+ daemonId,
162
+ displayName,
163
+ identityPublicKey,
164
+ credentialRef,
165
+ directEndpoint,
166
+ };
167
+ }
168
+ function normalizePendingEnrollmentInput(input) {
169
+ const claimId = validId(input.claimId, "claimId");
170
+ const daemonId = validId(input.daemonId, "daemonId");
171
+ if (daemonId === "local")
172
+ throw new Error("local is a reserved Runtime target selector");
173
+ const identityPublicKey = validEd25519PublicKey(input.identityPublicKey);
174
+ const credentialRef = validCredentialRef(input.credentialRef);
175
+ if (!/^[0-9a-f]{64}$/.test(input.offerFingerprint)) {
176
+ throw new Error("offerFingerprint is invalid");
177
+ }
178
+ const expiresAt = validTimestamp(input.expiresAt, "expiresAt");
179
+ return {
180
+ claimId,
181
+ daemonId,
182
+ identityPublicKey,
183
+ credentialRef,
184
+ offerFingerprint: input.offerFingerprint,
185
+ expiresAt,
186
+ };
187
+ }
188
+ function insertTarget(connection, input, now) {
189
+ connection.prepare(`INSERT INTO runtime_targets
190
+ (daemon_id, display_name, identity_public_key, credential_ref, created_at, updated_at)
191
+ VALUES (?, ?, ?, ?, ?, ?)`).run(input.daemonId, input.displayName, input.identityPublicKey, input.credentialRef, now, now);
192
+ connection.prepare(`INSERT INTO runtime_direct_routes
193
+ (daemon_id, endpoint, updated_at)
194
+ VALUES (?, ?, ?)`).run(input.daemonId, input.directEndpoint, now);
195
+ }
196
+ function targetFromRow(row) {
197
+ const input = normalizeTargetInput({
198
+ daemonId: row.daemon_id,
199
+ displayName: row.display_name,
200
+ identityPublicKey: row.identity_public_key,
201
+ credentialRef: row.credential_ref,
202
+ directEndpoint: row.endpoint,
203
+ });
204
+ const createdAt = validTimestamp(row.created_at, "createdAt");
205
+ const updatedAt = validTimestamp(row.updated_at, "updatedAt");
206
+ return {
207
+ daemonId: input.daemonId,
208
+ displayName: input.displayName,
209
+ identityPublicKey: input.identityPublicKey,
210
+ credentialRef: input.credentialRef,
211
+ directRoute: {
212
+ endpoint: input.directEndpoint,
213
+ },
214
+ createdAt,
215
+ updatedAt,
216
+ };
217
+ }
218
+ function pendingEnrollmentFromRow(row) {
219
+ return {
220
+ claimId: validId(row.claim_id, "claimId"),
221
+ daemonId: validId(row.daemon_id, "daemonId"),
222
+ identityPublicKey: validEd25519PublicKey(row.identity_public_key),
223
+ credentialRef: validCredentialRef(row.credential_ref),
224
+ offerFingerprint: /^[0-9a-f]{64}$/.test(row.offer_fingerprint)
225
+ ? row.offer_fingerprint
226
+ : invalidStoredOfferFingerprint(),
227
+ expiresAt: validTimestamp(row.expires_at, "expiresAt"),
228
+ createdAt: validTimestamp(row.created_at, "createdAt"),
229
+ };
230
+ }
231
+ function invalidStoredOfferFingerprint() {
232
+ throw new Error("stored Runtime target offerFingerprint is invalid");
233
+ }
234
+ function validDisplayName(value) {
235
+ if (typeof value !== "string" ||
236
+ value.length === 0 ||
237
+ value.length > MAX_DISPLAY_NAME_CHARS ||
238
+ value.trim() !== value ||
239
+ /[\u0000-\u001f\u007f]/.test(value)) {
240
+ throw new Error("displayName is invalid");
241
+ }
242
+ return value;
243
+ }
244
+ function validEd25519PublicKey(value) {
245
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{59}$/.test(value)) {
246
+ throw new Error("identityPublicKey is invalid");
247
+ }
248
+ try {
249
+ const bytes = Buffer.from(value, "base64url");
250
+ if (bytes.byteLength !== 44 || bytes.toString("base64url") !== value)
251
+ throw new Error();
252
+ const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
253
+ if (key.asymmetricKeyType !== "ed25519")
254
+ throw new Error();
255
+ return key.export({ format: "der", type: "spki" }).toString("base64url");
256
+ }
257
+ catch {
258
+ throw new Error("identityPublicKey is invalid");
259
+ }
260
+ }
261
+ function validCredentialRef(value) {
262
+ if (typeof value !== "string" || !CREDENTIAL_REF_PATTERN.test(value)) {
263
+ throw new Error("credentialRef is invalid");
264
+ }
265
+ return value;
266
+ }
267
+ function validDirectEndpoint(value) {
268
+ let url;
269
+ try {
270
+ url = new URL(value);
271
+ }
272
+ catch {
273
+ throw new Error("directEndpoint is invalid");
274
+ }
275
+ if ((url.protocol !== "ws:" && url.protocol !== "wss:") ||
276
+ !url.hostname ||
277
+ url.username ||
278
+ url.password ||
279
+ url.search ||
280
+ url.hash ||
281
+ url.pathname !== "/runtime/direct/v2/control") {
282
+ throw new Error("directEndpoint is invalid");
283
+ }
284
+ return url.toString();
285
+ }
286
+ function validId(value, field) {
287
+ if (typeof value !== "string" || !OPAQUE_ID_PATTERN.test(value)) {
288
+ throw new Error(`${field} is invalid`);
289
+ }
290
+ return value;
291
+ }
292
+ function validTimestamp(value, field) {
293
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
294
+ throw new Error(`stored Runtime target ${field} is invalid`);
295
+ }
296
+ return value;
297
+ }
298
+ function validNow(value) {
299
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
300
+ throw new Error("Runtime target clock returned an invalid date");
301
+ }
302
+ return value;
303
+ }
304
+ function isSqliteConstraint(error) {
305
+ return typeof error === "object" && error !== null && "code" in error &&
306
+ typeof error.code === "string" && error.code.startsWith("SQLITE_CONSTRAINT");
307
+ }
@@ -0,0 +1,21 @@
1
+ import type { ControlRuntimeShareAddress } from "@rynx-ai/protocol/control";
2
+ interface HostNetworkAddress {
3
+ address: string;
4
+ family: string | number;
5
+ internal: boolean;
6
+ }
7
+ export type HostNetworkInterfaces = NodeJS.Dict<readonly HostNetworkAddress[]>;
8
+ interface ListRuntimeShareAddressesOptions {
9
+ interfaces?: HostNetworkInterfaces;
10
+ bindHost?: string;
11
+ }
12
+ /**
13
+ * List addresses another device may be able to dial.
14
+ *
15
+ * This intentionally mirrors reference implementation's discovery policy: expose every
16
+ * non-internal IPv4 interface and prefer Tailnet addresses, while leaving the
17
+ * final reachability choice to the operator. Virtual and container interfaces
18
+ * remain visible because they can be the correct route in some deployments.
19
+ */
20
+ export declare function listRuntimeShareAddresses({ interfaces, bindHost, }?: ListRuntimeShareAddressesOptions): ControlRuntimeShareAddress[];
21
+ export {};
@@ -0,0 +1,50 @@
1
+ import { networkInterfaces } from "node:os";
2
+ /**
3
+ * List addresses another device may be able to dial.
4
+ *
5
+ * This intentionally mirrors reference implementation's discovery policy: expose every
6
+ * non-internal IPv4 interface and prefer Tailnet addresses, while leaving the
7
+ * final reachability choice to the operator. Virtual and container interfaces
8
+ * remain visible because they can be the correct route in some deployments.
9
+ */
10
+ export function listRuntimeShareAddresses({ interfaces = networkInterfaces(), bindHost = "0.0.0.0", } = {}) {
11
+ const result = [];
12
+ const seen = new Set();
13
+ const concreteBindAddress = bindHost !== "0.0.0.0" && isIpv4Address(bindHost)
14
+ ? bindHost
15
+ : undefined;
16
+ if (bindHost !== "0.0.0.0" && !concreteBindAddress)
17
+ return result;
18
+ for (const [interfaceName, addresses] of Object.entries(interfaces)) {
19
+ if (!addresses)
20
+ continue;
21
+ for (const candidate of addresses) {
22
+ if ((candidate.family === "IPv4" || candidate.family === 4) &&
23
+ !candidate.internal &&
24
+ (!concreteBindAddress || candidate.address === concreteBindAddress) &&
25
+ !seen.has(candidate.address)) {
26
+ seen.add(candidate.address);
27
+ result.push({ interfaceName, address: candidate.address });
28
+ }
29
+ }
30
+ }
31
+ return result.sort((left, right) => Number(isTailnetIpv4Address(right.address)) -
32
+ Number(isTailnetIpv4Address(left.address)));
33
+ }
34
+ function isTailnetIpv4Address(address) {
35
+ const octets = ipv4Octets(address);
36
+ return octets !== undefined &&
37
+ octets[0] === 100 &&
38
+ octets[1] >= 64 &&
39
+ octets[1] <= 127;
40
+ }
41
+ function isIpv4Address(address) {
42
+ return ipv4Octets(address) !== undefined;
43
+ }
44
+ function ipv4Octets(address) {
45
+ const octets = address.split(".").map((part) => /^\d+$/.test(part) ? Number(part) : Number.NaN);
46
+ return octets.length === 4 &&
47
+ octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)
48
+ ? octets
49
+ : undefined;
50
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Boot-live authorization for a managed Session to bootstrap its own Browser.
3
+ *
4
+ * Capabilities are stable until Session deletion or daemon shutdown. They are
5
+ * intentionally not short-lived claims and have no refresh protocol.
6
+ */
7
+ export declare class SessionBrowserCapabilityStore {
8
+ private readonly records;
9
+ issue(sessionId: string): string;
10
+ verify(sessionId: string, capability: string): boolean;
11
+ revoke(sessionId: string): boolean;
12
+ revokeAll(): void;
13
+ }
@@ -0,0 +1,58 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ const SESSION_ID_MAX_CHARS = 512;
3
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/;
4
+ const CAPABILITY_BYTES = 32;
5
+ const CAPABILITY_CHARS = 43;
6
+ /**
7
+ * Boot-live authorization for a managed Session to bootstrap its own Browser.
8
+ *
9
+ * Capabilities are stable until Session deletion or daemon shutdown. They are
10
+ * intentionally not short-lived claims and have no refresh protocol.
11
+ */
12
+ export class SessionBrowserCapabilityStore {
13
+ records = new Map();
14
+ issue(sessionId) {
15
+ requireSessionId(sessionId);
16
+ const existing = this.records.get(sessionId);
17
+ if (existing)
18
+ return existing.capability;
19
+ const capability = randomBytes(CAPABILITY_BYTES).toString("base64url");
20
+ this.records.set(sessionId, { capability, digest: digestCapability(capability) });
21
+ return capability;
22
+ }
23
+ verify(sessionId, capability) {
24
+ if (!isSessionId(sessionId) || !isCapability(capability))
25
+ return false;
26
+ const record = this.records.get(sessionId);
27
+ if (!record)
28
+ return false;
29
+ return timingSafeEqual(digestCapability(capability), record.digest);
30
+ }
31
+ revoke(sessionId) {
32
+ if (!isSessionId(sessionId))
33
+ return false;
34
+ return this.records.delete(sessionId);
35
+ }
36
+ revokeAll() {
37
+ this.records.clear();
38
+ }
39
+ }
40
+ function requireSessionId(value) {
41
+ if (!isSessionId(value)) {
42
+ throw new TypeError("Session identity must be a bounded opaque ASCII token");
43
+ }
44
+ }
45
+ function isSessionId(value) {
46
+ return typeof value === "string" &&
47
+ value.length > 0 &&
48
+ value.length <= SESSION_ID_MAX_CHARS &&
49
+ SESSION_ID_PATTERN.test(value);
50
+ }
51
+ function isCapability(value) {
52
+ return typeof value === "string" &&
53
+ value.length === CAPABILITY_CHARS &&
54
+ /^[A-Za-z0-9_-]+$/.test(value);
55
+ }
56
+ function digestCapability(value) {
57
+ return createHash("sha256").update(value, "utf8").digest();
58
+ }
@@ -0,0 +1,10 @@
1
+ import type { RemoteRuntimeEmulatorBackend } from "@rynx-ai/protocol/remote-runtime-rpc";
2
+ import { type SessionEmulatorBindingRecord, type SessionEmulatorBindingStore } from "@rynx-ai/server";
3
+ export declare class SqliteSessionEmulatorBindingStore implements SessionEmulatorBindingStore {
4
+ get(sessionId: string): SessionEmulatorBindingRecord | undefined;
5
+ findByDevice(backend: RemoteRuntimeEmulatorBackend, deviceId: string): SessionEmulatorBindingRecord | undefined;
6
+ bind(record: SessionEmulatorBindingRecord): void;
7
+ release(sessionId: string, bindingId: string): boolean;
8
+ releaseSession(sessionId: string): boolean;
9
+ releaseDevice(backend: RemoteRuntimeEmulatorBackend, deviceId: string): number;
10
+ }
@@ -0,0 +1,61 @@
1
+ import { SessionEmulatorBindingConflictError, } from "@rynx-ai/server";
2
+ import { db } from "./db.js";
3
+ export class SqliteSessionEmulatorBindingStore {
4
+ get(sessionId) {
5
+ const row = db().prepare(`SELECT session_id, binding_id, backend, device_id, device_name, attached_at
6
+ FROM session_emulator_bindings WHERE session_id = ?`).get(sessionId);
7
+ return row ? bindingRecord(row) : undefined;
8
+ }
9
+ findByDevice(backend, deviceId) {
10
+ const row = db().prepare(`SELECT session_id, binding_id, backend, device_id, device_name, attached_at
11
+ FROM session_emulator_bindings WHERE backend = ? AND device_id = ?`).get(backend, deviceId);
12
+ return row ? bindingRecord(row) : undefined;
13
+ }
14
+ bind(record) {
15
+ const connection = db();
16
+ const replace = connection.transaction(() => {
17
+ connection.prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?")
18
+ .run(record.sessionId);
19
+ connection.prepare(`INSERT INTO session_emulator_bindings
20
+ (session_id, binding_id, backend, device_id, device_name, attached_at)
21
+ VALUES (@sessionId, @bindingId, @backend, @deviceId, @deviceName, @attachedAt)`).run({
22
+ ...record,
23
+ deviceName: record.deviceName ?? null,
24
+ });
25
+ });
26
+ try {
27
+ replace();
28
+ }
29
+ catch (error) {
30
+ if (isSqliteConstraint(error)) {
31
+ throw new SessionEmulatorBindingConflictError(undefined, { cause: error });
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ release(sessionId, bindingId) {
37
+ return db().prepare("DELETE FROM session_emulator_bindings WHERE session_id = ? AND binding_id = ?").run(sessionId, bindingId).changes > 0;
38
+ }
39
+ releaseSession(sessionId) {
40
+ return db().prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?").run(sessionId).changes > 0;
41
+ }
42
+ releaseDevice(backend, deviceId) {
43
+ return db().prepare("DELETE FROM session_emulator_bindings WHERE backend = ? AND device_id = ?").run(backend, deviceId).changes;
44
+ }
45
+ }
46
+ function bindingRecord(row) {
47
+ return {
48
+ sessionId: row.session_id,
49
+ bindingId: row.binding_id,
50
+ backend: row.backend,
51
+ deviceId: row.device_id,
52
+ ...(row.device_name ? { deviceName: row.device_name } : {}),
53
+ attachedAt: row.attached_at,
54
+ };
55
+ }
56
+ function isSqliteConstraint(error) {
57
+ if (!error || typeof error !== "object")
58
+ return false;
59
+ const code = error.code;
60
+ return typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT");
61
+ }
@@ -0,0 +1,41 @@
1
+ import { type MachineSessionRecord } from "@rynx-ai/core";
2
+ type PluginSessionLaunchMeta = Omit<MachineSessionRecord, "id" | "source" | "createdAt" | "updatedAt">;
3
+ export interface EnsurePluginSessionLaunchInput {
4
+ /** Authenticated plugin identity. The future RPC host must inject this value. */
5
+ pluginId: string;
6
+ /** Opaque plugin-owned namespace, such as one connection profile. */
7
+ scopeId: string;
8
+ /** Stable across retries. A remote lease token or attempt number is not an operation id. */
9
+ operationId: string;
10
+ /** SHA-256 of the host-normalized, execution-affecting launch request. */
11
+ requestHash: string;
12
+ session: PluginSessionLaunchMeta;
13
+ }
14
+ export interface EnsurePluginSessionLaunchResult {
15
+ sessionId: string;
16
+ disposition: "created" | "replayed";
17
+ }
18
+ export declare class SessionLaunchOperationConflictError extends Error {
19
+ readonly pluginId: string;
20
+ readonly scopeId: string;
21
+ readonly operationId: string;
22
+ readonly code = "SESSION_LAUNCH_OPERATION_CONFLICT";
23
+ constructor(pluginId: string, scopeId: string, operationId: string);
24
+ }
25
+ export declare class SessionLaunchOperationTargetDeletedError extends Error {
26
+ readonly pluginId: string;
27
+ readonly scopeId: string;
28
+ readonly operationId: string;
29
+ readonly sessionId: string;
30
+ readonly code = "SESSION_LAUNCH_OPERATION_TARGET_DELETED";
31
+ constructor(pluginId: string, scopeId: string, operationId: string, sessionId: string);
32
+ }
33
+ /**
34
+ * Atomically allocate a persisted machine session for one plugin operation.
35
+ *
36
+ * The durable operation row is the idempotency boundary across runner crashes,
37
+ * daemon restarts, and remote lease re-delivery. It deliberately excludes any
38
+ * Workspace concept: `scopeId` and `operationId` are opaque to the daemon.
39
+ */
40
+ export declare function ensurePluginSessionLaunch(input: EnsurePluginSessionLaunchInput): EnsurePluginSessionLaunchResult;
41
+ export {};