@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,700 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { rynxHome, } from "@rynx-ai/core";
5
+ import { defaultSessionResourcePolicy, SESSION_RESOURCE_MAX_IMAGE_PIXELS, SESSION_RESOURCE_MAX_IMAGE_SIDE, SESSION_RESOURCE_MAX_MESSAGE_BYTES, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, } from "@rynx-ai/protocol/remote-runtime-rpc";
6
+ import { MachineSessionServiceFailure, } from "@rynx-ai/server";
7
+ import { db } from "./db.js";
8
+ const MAX_STAGED_RESOURCES_PER_SESSION = 16;
9
+ const STAGED_TTL_MS = 24 * 60 * 60 * 1_000;
10
+ export class SqliteSessionResourceStore {
11
+ root;
12
+ constructor(root = join(rynxHome(), "resources")) {
13
+ this.root = root;
14
+ ensurePrivateDirectory(this.root);
15
+ this.sweepExpiredStagedResources();
16
+ }
17
+ policy(_sessionId) {
18
+ return defaultSessionResourcePolicy();
19
+ }
20
+ async beginUpload(input) {
21
+ this.sweepExpiredStagedResources();
22
+ const filename = safeFilename(input.filename);
23
+ const existing = db()
24
+ .prepare(`SELECT * FROM session_resources
25
+ WHERE session_id = ? AND client_upload_id = ?`)
26
+ .get(input.sessionId, input.clientUploadId);
27
+ if (existing) {
28
+ if (existing.filename !== filename ||
29
+ existing.media_type !== input.mediaType ||
30
+ existing.byte_length !== input.byteLength) {
31
+ throw conflict("clientUploadId was already used for different content");
32
+ }
33
+ if (existing.state === "uploading")
34
+ this.reconcileUploadOffset(existing);
35
+ const refreshed = this.rowByUpload(input.sessionId, existing.upload_id);
36
+ return {
37
+ uploadId: refreshed.upload_id,
38
+ resourceId: refreshed.id,
39
+ offset: refreshed.uploaded_bytes,
40
+ complete: refreshed.state !== "uploading",
41
+ ...(refreshed.state === "uploading"
42
+ ? {}
43
+ : { resource: toMetadata(refreshed) }),
44
+ };
45
+ }
46
+ const staged = db()
47
+ .prepare(`SELECT COUNT(*) AS count, COALESCE(SUM(byte_length), 0) AS bytes
48
+ FROM session_resources
49
+ WHERE session_id = ? AND state IN ('uploading', 'staged')`)
50
+ .get(input.sessionId);
51
+ if (staged.count >= MAX_STAGED_RESOURCES_PER_SESSION ||
52
+ staged.bytes + input.byteLength > SESSION_RESOURCE_MAX_MESSAGE_BYTES) {
53
+ throw unavailable("Session staged image quota exceeded");
54
+ }
55
+ const resourceId = `res_${randomUUID()}`;
56
+ const uploadId = `upl_${randomUUID()}`;
57
+ const createdAt = Date.now();
58
+ ensurePrivateDirectory(this.sessionDirectory(input.sessionId));
59
+ db().prepare(`INSERT INTO session_resources
60
+ (id, session_id, client_upload_id, upload_id, filename, media_type,
61
+ byte_length, uploaded_bytes, state, created_at)
62
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'uploading', ?)`).run(resourceId, input.sessionId, input.clientUploadId, uploadId, filename, input.mediaType, input.byteLength, createdAt);
63
+ return { uploadId, resourceId, offset: 0, complete: false };
64
+ }
65
+ async writeUploadChunk(input) {
66
+ const row = this.requireUpload(input.sessionId, input.uploadId);
67
+ if (row.state !== "uploading")
68
+ return { offset: row.byte_length };
69
+ const bytes = decodeBase64(input.data);
70
+ if (bytes.byteLength === 0 || bytes.byteLength > SESSION_RESOURCE_TRANSFER_CHUNK_BYTES) {
71
+ throw invalid("Upload chunk length is invalid");
72
+ }
73
+ this.reconcileUploadOffset(row);
74
+ const current = this.requireUpload(input.sessionId, input.uploadId);
75
+ if (input.offset > current.uploaded_bytes) {
76
+ throw conflict("Upload chunk offset is ahead of the stored resource");
77
+ }
78
+ if (input.offset < current.uploaded_bytes) {
79
+ const end = input.offset + bytes.byteLength;
80
+ if (end > current.uploaded_bytes) {
81
+ throw conflict("Upload chunk overlaps the stored resource tail");
82
+ }
83
+ const existing = Buffer.alloc(bytes.byteLength);
84
+ const fd = openSync(this.partialPath(current), constants.O_RDONLY | noFollowFlag());
85
+ try {
86
+ readSync(fd, existing, 0, existing.length, input.offset);
87
+ }
88
+ finally {
89
+ closeSync(fd);
90
+ }
91
+ if (!existing.equals(bytes)) {
92
+ throw conflict("Upload chunk retry does not match stored bytes");
93
+ }
94
+ return { offset: current.uploaded_bytes };
95
+ }
96
+ if (current.uploaded_bytes + bytes.byteLength > current.byte_length) {
97
+ throw invalid("Upload exceeds its declared byteLength");
98
+ }
99
+ const file = this.partialPath(current);
100
+ const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | noFollowFlag(), 0o600);
101
+ try {
102
+ writeSync(fd, bytes);
103
+ }
104
+ finally {
105
+ closeSync(fd);
106
+ }
107
+ const nextOffset = current.uploaded_bytes + bytes.byteLength;
108
+ db().prepare(`UPDATE session_resources
109
+ SET uploaded_bytes = ?
110
+ WHERE id = ? AND state = 'uploading'`).run(nextOffset, current.id);
111
+ return { offset: nextOffset };
112
+ }
113
+ async commitUpload(input) {
114
+ let row = this.requireUpload(input.sessionId, input.uploadId);
115
+ if (row.state !== "uploading")
116
+ return { resource: toMetadata(row) };
117
+ this.reconcileUploadOffset(row);
118
+ row = this.requireUpload(input.sessionId, input.uploadId);
119
+ if (row.uploaded_bytes !== row.byte_length) {
120
+ throw conflict("Upload is incomplete");
121
+ }
122
+ const partial = this.partialPath(row);
123
+ const final = this.finalPath(row);
124
+ if (!existsSync(partial) && existsSync(final)) {
125
+ const bytes = readFileSync(final);
126
+ return { resource: this.finalizeUpload(row, bytes) };
127
+ }
128
+ const bytes = readFileSync(partial);
129
+ const image = inspectImage(bytes, row.media_type);
130
+ if (image.width > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
131
+ image.height > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
132
+ image.width * image.height > SESSION_RESOURCE_MAX_IMAGE_PIXELS) {
133
+ throw invalid("Image dimensions exceed the Session resource policy");
134
+ }
135
+ renameSync(partial, final);
136
+ chmodSync(final, 0o600);
137
+ return { resource: this.finalizeUpload(row, bytes, image) };
138
+ }
139
+ async getResource(input) {
140
+ return { resource: toMetadata(this.requireResource(input.sessionId, input.resourceId)) };
141
+ }
142
+ async readResource(input) {
143
+ const row = this.requireResource(input.sessionId, input.resourceId);
144
+ if (row.state === "uploading")
145
+ throw unavailable("Resource upload is incomplete");
146
+ const length = Math.min(input.length, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES);
147
+ const available = Math.max(0, row.byte_length - input.offset);
148
+ const readLength = Math.min(length, available);
149
+ const data = Buffer.alloc(readLength);
150
+ if (readLength > 0) {
151
+ const fd = openSync(this.finalPath(row), constants.O_RDONLY | noFollowFlag());
152
+ try {
153
+ readSync(fd, data, 0, readLength, input.offset);
154
+ }
155
+ finally {
156
+ closeSync(fd);
157
+ }
158
+ }
159
+ const offset = input.offset + readLength;
160
+ return { data: data.toString("base64"), offset, eof: offset >= row.byte_length };
161
+ }
162
+ async deleteResource(input) {
163
+ const row = db()
164
+ .prepare("SELECT * FROM session_resources WHERE session_id = ? AND id = ?")
165
+ .get(input.sessionId, input.resourceId);
166
+ if (!row)
167
+ return { deleted: false };
168
+ if (row.state === "committed") {
169
+ throw conflict("Committed message resources cannot be deleted independently");
170
+ }
171
+ rmSync(this.partialPath(row), { force: true });
172
+ rmSync(this.finalPath(row), { force: true });
173
+ db().prepare("DELETE FROM session_resources WHERE id = ?").run(row.id);
174
+ return { deleted: true };
175
+ }
176
+ async prepareInput(sessionId, content, clientMessageId) {
177
+ const runtimeContent = [];
178
+ const canonicalContent = [];
179
+ const resourceIds = [];
180
+ let title = "";
181
+ let totalImageBytes = 0;
182
+ for (const part of content) {
183
+ if (part.type === "input_text") {
184
+ runtimeContent.push({ type: "text", text: part.text });
185
+ canonicalContent.push({ type: "input_text", text: part.text });
186
+ title ||= part.text.trim();
187
+ continue;
188
+ }
189
+ const row = this.requireResource(sessionId, part.resourceId);
190
+ if (row.state === "uploading")
191
+ throw unavailable("Image upload is incomplete");
192
+ const resource = toImagePart(row);
193
+ if (resource.mediaType !== part.mediaType ||
194
+ (part.byteLength !== undefined && part.byteLength !== resource.byteLength)) {
195
+ throw conflict("Image reference metadata does not match the stored resource");
196
+ }
197
+ totalImageBytes += row.byte_length;
198
+ resourceIds.push(row.id);
199
+ canonicalContent.push(resource);
200
+ runtimeContent.push({
201
+ type: "local_image",
202
+ path: this.finalPath(row),
203
+ resource,
204
+ });
205
+ title ||= row.filename;
206
+ }
207
+ if (runtimeContent.length === 0 || totalImageBytes > SESSION_RESOURCE_MAX_MESSAGE_BYTES) {
208
+ throw invalid("Message content is empty or exceeds the image byte limit");
209
+ }
210
+ if (!runtimeContent.some((part) => part.type === "local_image" || part.text.trim())) {
211
+ throw invalid("Message content must contain text or an image");
212
+ }
213
+ let operationState;
214
+ if (clientMessageId) {
215
+ const requestHash = hashMessageContent(canonicalContent);
216
+ operationState = this.prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds);
217
+ }
218
+ return {
219
+ input: {
220
+ content: runtimeContent,
221
+ ...(clientMessageId ? { clientMessageId } : {}),
222
+ },
223
+ title: title || "Image",
224
+ ...(operationState ? { operationState } : {}),
225
+ };
226
+ }
227
+ markMessageInjecting(sessionId, clientMessageId) {
228
+ this.transitionOperation(sessionId, clientMessageId, ["prepared", "failed_not_started"], "injecting");
229
+ }
230
+ markMessageInjected(sessionId, clientMessageId) {
231
+ this.transitionOperation(sessionId, clientMessageId, ["injecting"], "injected");
232
+ }
233
+ markMessageOutcomeUnknown(sessionId, clientMessageId, reason) {
234
+ this.transitionOperation(sessionId, clientMessageId, ["injecting"], "outcome_unknown", reason);
235
+ }
236
+ markMessageFailedNotStarted(sessionId, clientMessageId, reason) {
237
+ this.transitionOperation(sessionId, clientMessageId, ["prepared", "injecting"], "failed_not_started", reason);
238
+ }
239
+ deleteSession(sessionId) {
240
+ const rows = db()
241
+ .prepare("SELECT * FROM session_resources WHERE session_id = ?")
242
+ .all(sessionId);
243
+ for (const row of rows) {
244
+ rmSync(this.partialPath(row), { force: true });
245
+ rmSync(this.finalPath(row), { force: true });
246
+ }
247
+ db().transaction(() => {
248
+ db().prepare("DELETE FROM session_message_operations WHERE session_id = ?").run(sessionId);
249
+ db().prepare("DELETE FROM session_resources WHERE session_id = ?").run(sessionId);
250
+ })();
251
+ rmSync(this.sessionDirectory(sessionId), { recursive: true, force: true });
252
+ }
253
+ prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds) {
254
+ const conn = db();
255
+ return conn.transaction(() => {
256
+ const existing = conn.prepare(`SELECT request_hash, state
257
+ FROM session_message_operations
258
+ WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
259
+ if (existing) {
260
+ if (existing.request_hash !== requestHash) {
261
+ throw conflict("clientMessageId was already used for different content");
262
+ }
263
+ if (existing.state === "failed_not_started") {
264
+ this.bindOperationResources(conn, sessionId, clientMessageId, resourceIds);
265
+ conn.prepare(`UPDATE session_message_operations
266
+ SET state = 'prepared', error = NULL, updated_at = ?
267
+ WHERE session_id = ? AND client_message_id = ?`).run(Date.now(), sessionId, clientMessageId);
268
+ return "prepared";
269
+ }
270
+ return existing.state;
271
+ }
272
+ const now = Date.now();
273
+ conn.prepare(`INSERT INTO session_message_operations
274
+ (session_id, client_message_id, request_hash, state, created_at, updated_at)
275
+ VALUES (?, ?, ?, 'prepared', ?, ?)`).run(sessionId, clientMessageId, requestHash, now, now);
276
+ this.bindOperationResources(conn, sessionId, clientMessageId, resourceIds);
277
+ return "prepared";
278
+ })();
279
+ }
280
+ bindOperationResources(conn, sessionId, clientMessageId, resourceIds) {
281
+ conn.prepare(`DELETE FROM session_message_operation_resources
282
+ WHERE session_id = ? AND client_message_id = ?`).run(sessionId, clientMessageId);
283
+ const releaseFailed = conn.prepare(`DELETE FROM session_message_operation_resources
284
+ WHERE resource_id = ?
285
+ AND EXISTS (
286
+ SELECT 1
287
+ FROM session_message_operations operation
288
+ WHERE operation.session_id = session_message_operation_resources.session_id
289
+ AND operation.client_message_id =
290
+ session_message_operation_resources.client_message_id
291
+ AND operation.state = 'failed_not_started'
292
+ )`);
293
+ const bind = conn.prepare(`INSERT INTO session_message_operation_resources
294
+ (session_id, client_message_id, resource_id)
295
+ VALUES (?, ?, ?)`);
296
+ try {
297
+ for (const resourceId of resourceIds) {
298
+ releaseFailed.run(resourceId);
299
+ bind.run(sessionId, clientMessageId, resourceId);
300
+ }
301
+ }
302
+ catch {
303
+ throw conflict("An image resource is already bound to another message");
304
+ }
305
+ }
306
+ transitionOperation(sessionId, clientMessageId, from, to, error) {
307
+ const placeholders = from.map(() => "?").join(", ");
308
+ const result = db().prepare(`UPDATE session_message_operations
309
+ SET state = ?, error = ?, updated_at = ?
310
+ WHERE session_id = ? AND client_message_id = ?
311
+ AND state IN (${placeholders})`).run(to, error ?? null, Date.now(), sessionId, clientMessageId, ...from);
312
+ if (result.changes === 0) {
313
+ const current = db().prepare(`SELECT state FROM session_message_operations
314
+ WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
315
+ if (current?.state === to || current?.state === "mirrored")
316
+ return;
317
+ throw conflict("Message operation state changed");
318
+ }
319
+ }
320
+ finalizeUpload(row, bytes, inspected = inspectImage(bytes, row.media_type)) {
321
+ const digest = createHash("sha256").update(bytes).digest("hex");
322
+ db().prepare(`UPDATE session_resources
323
+ SET state = 'staged', uploaded_bytes = byte_length, sha256 = ?,
324
+ width = ?, height = ?
325
+ WHERE id = ? AND state = 'uploading'`).run(digest, inspected.width, inspected.height, row.id);
326
+ return toMetadata(this.requireResource(row.session_id, row.id));
327
+ }
328
+ reconcileUploadOffset(row) {
329
+ const partial = this.partialPath(row);
330
+ const final = this.finalPath(row);
331
+ if (!existsSync(partial)) {
332
+ if (existsSync(final))
333
+ return;
334
+ const fd = openSync(partial, constants.O_WRONLY | constants.O_CREAT | noFollowFlag(), 0o600);
335
+ closeSync(fd);
336
+ }
337
+ const size = statSync(partial).size;
338
+ if (size > row.byte_length)
339
+ throw conflict("Stored upload exceeds its declared byteLength");
340
+ if (size !== row.uploaded_bytes) {
341
+ db().prepare("UPDATE session_resources SET uploaded_bytes = ? WHERE id = ? AND state = 'uploading'").run(size, row.id);
342
+ }
343
+ }
344
+ rowByUpload(sessionId, uploadId) {
345
+ return db()
346
+ .prepare("SELECT * FROM session_resources WHERE session_id = ? AND upload_id = ?")
347
+ .get(sessionId, uploadId);
348
+ }
349
+ requireUpload(sessionId, uploadId) {
350
+ const row = this.rowByUpload(sessionId, uploadId);
351
+ if (!row)
352
+ throw notFound();
353
+ return row;
354
+ }
355
+ requireResource(sessionId, resourceId) {
356
+ const row = db()
357
+ .prepare("SELECT * FROM session_resources WHERE session_id = ? AND id = ?")
358
+ .get(sessionId, resourceId);
359
+ if (!row)
360
+ throw notFound();
361
+ return row;
362
+ }
363
+ sessionDirectory(sessionId) {
364
+ const sessionKey = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
365
+ return join(this.root, sessionKey);
366
+ }
367
+ partialPath(row) {
368
+ return join(this.sessionDirectory(row.session_id), `${row.id}.part`);
369
+ }
370
+ finalPath(row) {
371
+ return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row.media_type)}`);
372
+ }
373
+ sweepExpiredStagedResources(now = Date.now()) {
374
+ const rows = db()
375
+ .prepare(`SELECT * FROM session_resources
376
+ WHERE state IN ('uploading', 'staged') AND created_at < ?`)
377
+ .all(now - STAGED_TTL_MS);
378
+ for (const row of rows) {
379
+ rmSync(this.partialPath(row), { force: true });
380
+ rmSync(this.finalPath(row), { force: true });
381
+ db().prepare("DELETE FROM session_resources WHERE id = ?").run(row.id);
382
+ }
383
+ }
384
+ }
385
+ function toImagePart(row) {
386
+ if (row.state === "uploading" ||
387
+ !row.sha256 ||
388
+ !row.width ||
389
+ !row.height) {
390
+ throw unavailable("Resource upload is incomplete");
391
+ }
392
+ return {
393
+ type: "input_image",
394
+ resourceId: row.id,
395
+ mediaType: row.media_type,
396
+ filename: row.filename,
397
+ byteLength: row.byte_length,
398
+ width: row.width,
399
+ height: row.height,
400
+ };
401
+ }
402
+ function toMetadata(row) {
403
+ const part = toImagePart(row);
404
+ if (row.state === "uploading")
405
+ throw unavailable("Resource upload is incomplete");
406
+ return {
407
+ ...part,
408
+ state: row.state,
409
+ sha256: row.sha256,
410
+ createdAt: row.created_at,
411
+ };
412
+ }
413
+ function hashMessageContent(content) {
414
+ return createHash("sha256")
415
+ .update(JSON.stringify(content.map((part) => part.type === "input_text"
416
+ ? { type: part.type, text: part.text }
417
+ : { type: part.type, resourceId: part.resourceId })))
418
+ .digest("hex");
419
+ }
420
+ function inspectImage(bytes, declared) {
421
+ const sniffed = sniffMediaType(bytes);
422
+ if (sniffed !== declared)
423
+ throw invalid("Image bytes do not match the declared media type");
424
+ const structurallyValid = sniffed === "image/png"
425
+ ? validatePngContainer(bytes)
426
+ : sniffed === "image/jpeg"
427
+ ? validateJpegContainer(bytes)
428
+ : validateWebpContainer(bytes);
429
+ if (!structurallyValid)
430
+ throw invalid("Image file is incomplete or malformed");
431
+ const dimensions = sniffed === "image/png"
432
+ ? pngDimensions(bytes)
433
+ : sniffed === "image/jpeg"
434
+ ? jpegDimensions(bytes)
435
+ : webpDimensions(bytes);
436
+ if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {
437
+ throw invalid("Image dimensions could not be decoded");
438
+ }
439
+ return dimensions;
440
+ }
441
+ function validatePngContainer(bytes) {
442
+ let offset = 8;
443
+ let sawHeader = false;
444
+ let sawImageData = false;
445
+ while (offset + 12 <= bytes.length) {
446
+ const length = bytes.readUInt32BE(offset);
447
+ const typeStart = offset + 4;
448
+ const dataStart = offset + 8;
449
+ const dataEnd = dataStart + length;
450
+ const chunkEnd = dataEnd + 4;
451
+ if (dataEnd < dataStart || chunkEnd > bytes.length)
452
+ return false;
453
+ const type = bytes.toString("ascii", typeStart, dataStart);
454
+ if (!sawHeader && (type !== "IHDR" || length !== 13))
455
+ return false;
456
+ const expectedCrc = bytes.readUInt32BE(dataEnd);
457
+ if (pngCrc32(bytes.subarray(typeStart, dataEnd)) !== expectedCrc)
458
+ return false;
459
+ if (type === "IHDR") {
460
+ if (sawHeader || length !== 13)
461
+ return false;
462
+ sawHeader = true;
463
+ }
464
+ else if (type === "IDAT") {
465
+ if (!sawHeader)
466
+ return false;
467
+ sawImageData = true;
468
+ }
469
+ else if (type === "IEND") {
470
+ return length === 0 && sawHeader && sawImageData && chunkEnd === bytes.length;
471
+ }
472
+ offset = chunkEnd;
473
+ }
474
+ return false;
475
+ }
476
+ const PNG_CRC_TABLE = Array.from({ length: 256 }, (_, index) => {
477
+ let value = index;
478
+ for (let bit = 0; bit < 8; bit += 1) {
479
+ value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
480
+ }
481
+ return value >>> 0;
482
+ });
483
+ function pngCrc32(bytes) {
484
+ let crc = 0xffffffff;
485
+ for (const byte of bytes) {
486
+ crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
487
+ }
488
+ return (crc ^ 0xffffffff) >>> 0;
489
+ }
490
+ function validateJpegContainer(bytes) {
491
+ if (bytes.length < 8 || bytes[0] !== 0xff || bytes[1] !== 0xd8)
492
+ return false;
493
+ let offset = 2;
494
+ let sawFrame = false;
495
+ let sawScan = false;
496
+ let entropyBytes = 0;
497
+ while (offset < bytes.length) {
498
+ if (bytes[offset] !== 0xff)
499
+ return false;
500
+ while (offset < bytes.length && bytes[offset] === 0xff)
501
+ offset += 1;
502
+ if (offset >= bytes.length)
503
+ return false;
504
+ const marker = bytes[offset++];
505
+ if (marker === 0xd9) {
506
+ return sawFrame && sawScan && entropyBytes > 0 && offset === bytes.length;
507
+ }
508
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
509
+ return false;
510
+ }
511
+ if (offset + 2 > bytes.length)
512
+ return false;
513
+ const length = bytes.readUInt16BE(offset);
514
+ if (length < 2 || offset + length > bytes.length)
515
+ return false;
516
+ const segmentEnd = offset + length;
517
+ if ((marker >= 0xc0 && marker <= 0xc3) ||
518
+ (marker >= 0xc5 && marker <= 0xc7) ||
519
+ (marker >= 0xc9 && marker <= 0xcb) ||
520
+ (marker >= 0xcd && marker <= 0xcf)) {
521
+ sawFrame = true;
522
+ }
523
+ offset = segmentEnd;
524
+ if (marker !== 0xda)
525
+ continue;
526
+ sawScan = true;
527
+ const scanStart = offset;
528
+ while (offset < bytes.length) {
529
+ if (bytes[offset] !== 0xff) {
530
+ offset += 1;
531
+ continue;
532
+ }
533
+ const markerStart = offset;
534
+ while (offset < bytes.length && bytes[offset] === 0xff)
535
+ offset += 1;
536
+ if (offset >= bytes.length)
537
+ return false;
538
+ const scanMarker = bytes[offset];
539
+ if (scanMarker === 0x00) {
540
+ offset += 1;
541
+ continue;
542
+ }
543
+ if (scanMarker >= 0xd0 && scanMarker <= 0xd7) {
544
+ offset += 1;
545
+ continue;
546
+ }
547
+ entropyBytes += markerStart - scanStart;
548
+ offset = markerStart;
549
+ break;
550
+ }
551
+ }
552
+ return false;
553
+ }
554
+ function validateWebpContainer(bytes) {
555
+ if (bytes.length < 20 ||
556
+ bytes.toString("ascii", 0, 4) !== "RIFF" ||
557
+ bytes.toString("ascii", 8, 12) !== "WEBP" ||
558
+ bytes.readUInt32LE(4) + 8 !== bytes.length)
559
+ return false;
560
+ let offset = 12;
561
+ let sawImage = false;
562
+ while (offset + 8 <= bytes.length) {
563
+ const type = bytes.toString("ascii", offset, offset + 4);
564
+ const size = bytes.readUInt32LE(offset + 4);
565
+ const dataStart = offset + 8;
566
+ const dataEnd = dataStart + size;
567
+ const chunkEnd = dataEnd + (size & 1);
568
+ if (dataEnd < dataStart || chunkEnd > bytes.length)
569
+ return false;
570
+ if (type === "ANIM" || type === "ANMF")
571
+ return false;
572
+ if (type === "VP8X") {
573
+ if (size < 10 || (bytes[dataStart] & 0x02) !== 0)
574
+ return false;
575
+ }
576
+ else if (type === "VP8 " || type === "VP8L") {
577
+ sawImage = true;
578
+ }
579
+ offset = chunkEnd;
580
+ }
581
+ return sawImage && offset === bytes.length;
582
+ }
583
+ function sniffMediaType(bytes) {
584
+ if (bytes.length >= 24 &&
585
+ bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
586
+ return "image/png";
587
+ if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8)
588
+ return "image/jpeg";
589
+ if (bytes.length >= 30 &&
590
+ bytes.toString("ascii", 0, 4) === "RIFF" &&
591
+ bytes.toString("ascii", 8, 12) === "WEBP")
592
+ return "image/webp";
593
+ return undefined;
594
+ }
595
+ function pngDimensions(bytes) {
596
+ if (bytes.toString("ascii", 12, 16) !== "IHDR")
597
+ return undefined;
598
+ return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
599
+ }
600
+ function jpegDimensions(bytes) {
601
+ let offset = 2;
602
+ while (offset + 9 < bytes.length) {
603
+ if (bytes[offset] !== 0xff) {
604
+ offset += 1;
605
+ continue;
606
+ }
607
+ const marker = bytes[offset + 1];
608
+ offset += 2;
609
+ if (marker === 0xd8 || marker === 0xd9)
610
+ continue;
611
+ if (offset + 2 > bytes.length)
612
+ return undefined;
613
+ const length = bytes.readUInt16BE(offset);
614
+ if (length < 2 || offset + length > bytes.length)
615
+ return undefined;
616
+ if ((marker >= 0xc0 && marker <= 0xc3) ||
617
+ (marker >= 0xc5 && marker <= 0xc7) ||
618
+ (marker >= 0xc9 && marker <= 0xcb) ||
619
+ (marker >= 0xcd && marker <= 0xcf)) {
620
+ return {
621
+ height: bytes.readUInt16BE(offset + 3),
622
+ width: bytes.readUInt16BE(offset + 5),
623
+ };
624
+ }
625
+ offset += length;
626
+ }
627
+ return undefined;
628
+ }
629
+ function webpDimensions(bytes) {
630
+ const chunk = bytes.toString("ascii", 12, 16);
631
+ if (chunk === "VP8X" && bytes.length >= 30) {
632
+ return {
633
+ width: 1 + readUInt24LE(bytes, 24),
634
+ height: 1 + readUInt24LE(bytes, 27),
635
+ };
636
+ }
637
+ if (chunk === "VP8L" && bytes.length >= 25 && bytes[20] === 0x2f) {
638
+ const b1 = bytes[21];
639
+ const b2 = bytes[22];
640
+ const b3 = bytes[23];
641
+ const b4 = bytes[24];
642
+ return {
643
+ width: 1 + (((b2 & 0x3f) << 8) | b1),
644
+ height: 1 + (((b4 & 0x0f) << 10) | (b3 << 2) | (b2 >> 6)),
645
+ };
646
+ }
647
+ if (chunk === "VP8 " &&
648
+ bytes.length >= 30 &&
649
+ bytes[23] === 0x9d &&
650
+ bytes[24] === 0x01 &&
651
+ bytes[25] === 0x2a) {
652
+ return {
653
+ width: bytes.readUInt16LE(26) & 0x3fff,
654
+ height: bytes.readUInt16LE(28) & 0x3fff,
655
+ };
656
+ }
657
+ return undefined;
658
+ }
659
+ function readUInt24LE(bytes, offset) {
660
+ return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16);
661
+ }
662
+ function decodeBase64(value) {
663
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
664
+ throw invalid("Upload chunk is not valid base64");
665
+ }
666
+ return Buffer.from(value, "base64");
667
+ }
668
+ function extension(mediaType) {
669
+ return mediaType === "image/png" ? "png" : mediaType === "image/jpeg" ? "jpg" : "webp";
670
+ }
671
+ function safeFilename(value) {
672
+ const name = basename(value).replace(/[\u0000-\u001f\u007f]/g, "").trim();
673
+ if (!name || name === "." || name === "..")
674
+ throw invalid("Image filename is invalid");
675
+ return name;
676
+ }
677
+ function ensurePrivateDirectory(path) {
678
+ if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
679
+ throw new Error("Session resource directory must not be a symlink");
680
+ }
681
+ mkdirSync(path, { recursive: true, mode: 0o700 });
682
+ chmodSync(path, 0o700);
683
+ }
684
+ function noFollowFlag() {
685
+ return "O_NOFOLLOW" in constants
686
+ ? constants.O_NOFOLLOW
687
+ : 0;
688
+ }
689
+ function invalid(message) {
690
+ return new MachineSessionServiceFailure("failed_precondition", message);
691
+ }
692
+ function unavailable(message) {
693
+ return new MachineSessionServiceFailure("failed_precondition", message);
694
+ }
695
+ function conflict(message) {
696
+ return new MachineSessionServiceFailure("conflict", message);
697
+ }
698
+ function notFound() {
699
+ return new MachineSessionServiceFailure("not_found", "Session resource not found");
700
+ }