@agentshouse/kit 0.0.1 → 0.1.0-alpha.1

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
@@ -0,0 +1,631 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ import { basename } from "node:path";
5
+ import { transferBytes } from "./bytes.js";
6
+ import {
7
+ ATTACHMENT_REFERENCE,
8
+ ATTACHMENT_TRANSFER_CONTRACT,
9
+ BYTE_GRANT_POST_BYTES_MAX,
10
+ ITEM_ATTACHMENTS_MAX,
11
+ KIT_ATTACHMENTS_GET_PATH,
12
+ KIT_ATTACHMENTS_UPLOAD_PATH,
13
+ KIT_CAPTURE_ORIGINALS_SAVE_PATH,
14
+ KIT_CONVERSATION_ORIGINALS_SAVE_PATH,
15
+ ORIGINAL_MEDIA_TYPE_MAX,
16
+ ORIGINAL_NAME_MAX,
17
+ ORIGINAL_UUID,
18
+ SHA256_DIGEST,
19
+ } from "./contract.js";
20
+ import { heldControl, houseRequest, maintainControl, openSession } from "./house.js";
21
+ import { operationId } from "./operation.js";
22
+ import { KitRefusal } from "./refusal.js";
23
+ import { readAttachmentState, writeAttachmentState, writeText } from "./state.js";
24
+
25
+ function refuse(code, detail = null) {
26
+ throw new KitRefusal(code, detail);
27
+ }
28
+
29
+ function active(signal) {
30
+ if (signal?.aborted === true) refuse("kit_command_cancelled");
31
+ }
32
+
33
+ function digestOf(bytes) {
34
+ return createHash("sha256").update(bytes).digest("hex");
35
+ }
36
+
37
+ export function originalName(value) {
38
+ if (typeof value !== "string" || value.length === 0 || value.length > ORIGINAL_NAME_MAX) {
39
+ refuse("kit_original_name_invalid");
40
+ }
41
+ if (value.includes("/") || value.includes("\\") || value === "." || value === "..") {
42
+ refuse("kit_original_name_invalid");
43
+ }
44
+ return value;
45
+ }
46
+
47
+ function originalUuid(value, code) {
48
+ if (typeof value !== "string" || !ORIGINAL_UUID.test(value)) refuse(code);
49
+ return value;
50
+ }
51
+
52
+ function originalMediaType(value) {
53
+ if (value === undefined || value === null) return undefined;
54
+ if (typeof value !== "string" || value.length === 0 || value.length > ORIGINAL_MEDIA_TYPE_MAX) {
55
+ refuse("kit_original_media_type_invalid");
56
+ }
57
+ return value;
58
+ }
59
+
60
+ export function originalDescriptor(value) {
61
+ if (
62
+ !Number.isSafeInteger(value?.bytes) ||
63
+ value.bytes < 0 ||
64
+ value.bytes > BYTE_GRANT_POST_BYTES_MAX ||
65
+ typeof value.sha256 !== "string" ||
66
+ !SHA256_DIGEST.test(value.sha256)
67
+ ) {
68
+ refuse("kit_original_descriptor_invalid");
69
+ }
70
+ const mediaType = originalMediaType(value.media_type);
71
+ return {
72
+ bytes: value.bytes,
73
+ sha256: value.sha256,
74
+ ...(mediaType === undefined ? {} : { media_type: mediaType }),
75
+ };
76
+ }
77
+
78
+ export function describedBytes(bytes) {
79
+ if (bytes.length > BYTE_GRANT_POST_BYTES_MAX) refuse("original_bytes_exceeded");
80
+ return { bytes: bytes.length, sha256: digestOf(bytes) };
81
+ }
82
+
83
+ function describes(body, descriptor) {
84
+ return (
85
+ Buffer.isBuffer(body) &&
86
+ descriptor !== null &&
87
+ body.length === descriptor.bytes &&
88
+ digestOf(body) === descriptor.sha256
89
+ );
90
+ }
91
+
92
+ function sessionDependencies(options, dependencies) {
93
+ return {
94
+ fetch: dependencies.fetch ?? options.fetch,
95
+ now: dependencies.now ?? options.now,
96
+ };
97
+ }
98
+
99
+ async function withSession(options, dependencies, operation) {
100
+ const session = await openSession(options, sessionDependencies(options, dependencies));
101
+ return maintainControl(session, (signal) => operation(session, signal), options.signal);
102
+ }
103
+
104
+ async function originalRequest(session, path, body, signal) {
105
+ return houseRequest(
106
+ session.house,
107
+ await heldControl(session, signal),
108
+ path,
109
+ body,
110
+ session.dependencies,
111
+ signal,
112
+ );
113
+ }
114
+
115
+ function outcomeOf(answer) {
116
+ if (
117
+ typeof answer?.original !== "string" ||
118
+ typeof answer?.version !== "string" ||
119
+ typeof answer?.save?.status !== "string"
120
+ ) {
121
+ refuse("kit_original_save_rejected");
122
+ }
123
+ return {
124
+ original: answer.original,
125
+ version: answer.version,
126
+ save: {
127
+ status: answer.save.status,
128
+ failure: answer.save.failure ?? null,
129
+ },
130
+ upload: answer.upload,
131
+ };
132
+ }
133
+
134
+ function grantOf(value, kind) {
135
+ if (value === undefined) return null;
136
+ if (typeof value?.url !== "string" || typeof value?.operation !== "string") {
137
+ refuse("kit_original_grant_rejected", kind);
138
+ }
139
+ return { url: value.url, operation: value.operation };
140
+ }
141
+
142
+ function sendBytes(dependencies, grant, body, signal) {
143
+ return transferBytes(
144
+ { url: grant.url, method: "POST", operation: grant.operation, body, signal },
145
+ dependencies,
146
+ );
147
+ }
148
+
149
+ async function receiveBytes(session, grant, signal) {
150
+ const transferred = await transferBytes(
151
+ { url: grant.url, method: "GET", operation: grant.operation, signal },
152
+ session.dependencies,
153
+ );
154
+ return transferred.bytes;
155
+ }
156
+
157
+ async function writeOutput(path, bytes, signal) {
158
+ if (path === undefined) return;
159
+ await writeText(path, bytes, signal);
160
+ }
161
+
162
+ const UNREADABLE = new Set(["ENOENT", "EACCES", "EISDIR", "ELOOP", "ENOTDIR"]);
163
+
164
+ export async function readOriginalFile(path, expectedBytes = null) {
165
+ let handle;
166
+ try {
167
+ handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
168
+ } catch (error) {
169
+ if (UNREADABLE.has(error?.code)) return null;
170
+ throw error;
171
+ }
172
+ try {
173
+ const state = await handle.stat();
174
+ if (!state.isFile() || (expectedBytes !== null && state.size !== expectedBytes)) return null;
175
+ if (state.size > BYTE_GRANT_POST_BYTES_MAX) refuse("original_bytes_exceeded");
176
+ const bytes = await handle.readFile();
177
+ if (bytes.length > BYTE_GRANT_POST_BYTES_MAX) refuse("original_bytes_exceeded");
178
+ return bytes;
179
+ } finally {
180
+ await handle.close();
181
+ }
182
+ }
183
+
184
+ async function publish(requested, body, accepted, dependencies, signal) {
185
+ const answer = accepted(await requested());
186
+ if (body === null || answer.save.status === "saved" || answer.upload === undefined) {
187
+ return answer;
188
+ }
189
+ return accepted(await sendBytes(dependencies, grantOf(answer.upload, "upload"), body, signal));
190
+ }
191
+
192
+ function houseRequested(session, path, request, signal) {
193
+ return () => originalRequest(session, path, request, signal);
194
+ }
195
+
196
+ function associated(answer, expected) {
197
+ const outcome = outcomeOf(answer);
198
+ originalUuid(outcome.original, "kit_original_association_rejected");
199
+ originalUuid(outcome.version, "kit_original_association_rejected");
200
+ for (const [field, value] of Object.entries(expected)) {
201
+ if (answer[field] !== value) refuse("kit_original_association_rejected", field);
202
+ }
203
+ return {
204
+ original: outcome.original,
205
+ version: outcome.version,
206
+ save: outcome.save,
207
+ ...(outcome.upload === undefined ? {} : { upload: outcome.upload }),
208
+ };
209
+ }
210
+
211
+ function capturedReference(options) {
212
+ if (
213
+ typeof options.room_ref !== "string" ||
214
+ options.room_ref.length === 0 ||
215
+ !Number.isInteger(options.position) ||
216
+ options.position < 0 ||
217
+ options.position >= ITEM_ATTACHMENTS_MAX
218
+ ) {
219
+ refuse("kit_original_association_invalid");
220
+ }
221
+ return {
222
+ room_ref: options.room_ref,
223
+ record: originalUuid(options.record, "kit_original_association_invalid"),
224
+ position: options.position,
225
+ accepted_version: originalUuid(options.accepted_version, "kit_original_version_invalid"),
226
+ };
227
+ }
228
+
229
+ export async function saveCapturedOriginal(session, options) {
230
+ active(options.signal);
231
+ const reference = capturedReference(options);
232
+ const descriptor = originalDescriptor(options.descriptor);
233
+ const body = describes(options.body, descriptor) ? options.body : null;
234
+ const expected = {
235
+ record: reference.record,
236
+ position: reference.position,
237
+ accepted_version: reference.accepted_version,
238
+ bytes: descriptor.bytes,
239
+ sha256: descriptor.sha256,
240
+ };
241
+ const signal = options.signal ?? session.signal;
242
+ const answered = await publish(
243
+ houseRequested(
244
+ session,
245
+ KIT_CAPTURE_ORIGINALS_SAVE_PATH,
246
+ body === null ? { ...reference, source_failure: "source_unavailable" } : reference,
247
+ signal,
248
+ ),
249
+ body,
250
+ (answer) => associated(answer, expected),
251
+ session.dependencies,
252
+ signal,
253
+ );
254
+ return { ...reference, original: answered.original, version: answered.version, save: answered.save };
255
+ }
256
+
257
+ export async function saveConversationOriginal(session, options) {
258
+ active(options.signal);
259
+ const attachment = originalUuid(options.attachment, "kit_original_association_invalid");
260
+ const descriptor =
261
+ options.descriptor === null || options.descriptor === undefined
262
+ ? null
263
+ : originalDescriptor(options.descriptor);
264
+ const body = describes(options.body, descriptor) ? options.body : null;
265
+ const expected = {
266
+ direction: "incoming",
267
+ ...(descriptor === null ? {} : { bytes: descriptor.bytes, sha256: descriptor.sha256 }),
268
+ };
269
+ const signal = options.signal ?? session.signal;
270
+ const answered = await publish(
271
+ houseRequested(
272
+ session,
273
+ KIT_CONVERSATION_ORIGINALS_SAVE_PATH,
274
+ {
275
+ attachment,
276
+ ...(descriptor === null ? {} : { bytes: descriptor.bytes, sha256: descriptor.sha256 }),
277
+ ...(body === null ? { source_failure: "source_unavailable" } : {}),
278
+ },
279
+ signal,
280
+ ),
281
+ body,
282
+ (answer) => associated(answer, expected),
283
+ session.dependencies,
284
+ signal,
285
+ );
286
+ return { attachment, original: answered.original, version: answered.version, save: answered.save };
287
+ }
288
+
289
+ export function attachmentReference(value) {
290
+ if (typeof value !== "string" || !ATTACHMENT_REFERENCE.test(value)) {
291
+ refuse("kit_attachment_reference_invalid");
292
+ }
293
+ return value;
294
+ }
295
+
296
+ export function attachmentPlace(options) {
297
+ const room = options.room;
298
+ const privatePlace = options.private === true;
299
+ if (room !== undefined && privatePlace) refuse("kit_attachment_place_ambiguous");
300
+ if (room === undefined && !privatePlace) refuse("kit_attachment_place_missing");
301
+ if (privatePlace) return {};
302
+ if (typeof room !== "string" || room.length === 0) refuse("kit_attachment_place_missing");
303
+ return { room_ref: room };
304
+ }
305
+
306
+ function uploadRequest(place, version, name, descriptor, mediaType) {
307
+ return {
308
+ ...place,
309
+ version,
310
+ name,
311
+ bytes: descriptor.bytes,
312
+ sha256: descriptor.sha256,
313
+ ...(mediaType === undefined ? {} : { media_type: mediaType }),
314
+ };
315
+ }
316
+
317
+ function attachmentOf(answer, expected) {
318
+ if (
319
+ typeof answer?.original !== "string" ||
320
+ !ORIGINAL_UUID.test(answer.original) ||
321
+ typeof answer.version !== "string" ||
322
+ !ORIGINAL_UUID.test(answer.version) ||
323
+ typeof answer.attachment !== "string" ||
324
+ !ATTACHMENT_REFERENCE.test(answer.attachment) ||
325
+ typeof answer.name !== "string" ||
326
+ !Number.isSafeInteger(answer.bytes) ||
327
+ typeof answer.sha256 !== "string" ||
328
+ !SHA256_DIGEST.test(answer.sha256) ||
329
+ typeof answer.save?.status !== "string"
330
+ ) {
331
+ refuse("kit_attachment_rejected");
332
+ }
333
+ for (const [field, value] of Object.entries(expected)) {
334
+ if (answer[field] !== value) refuse("kit_attachment_association_rejected", field);
335
+ }
336
+ return {
337
+ attachment: answer.attachment,
338
+ original: answer.original,
339
+ version: answer.version,
340
+ name: answer.name,
341
+ bytes: answer.bytes,
342
+ sha256: answer.sha256,
343
+ media_type: answer.media_type ?? null,
344
+ save: { status: answer.save.status, failure: answer.save.failure ?? null },
345
+ upload_failure: answer.upload_failure ?? null,
346
+ upload: answer.upload,
347
+ read: answer.read,
348
+ download: answer.download,
349
+ };
350
+ }
351
+
352
+ function uploadedExpectation(request) {
353
+ return { version: request.version, name: request.name, bytes: request.bytes, sha256: request.sha256 };
354
+ }
355
+
356
+ function deliver(session, request, body, signal) {
357
+ return publish(
358
+ houseRequested(session, KIT_ATTACHMENTS_UPLOAD_PATH, request, signal),
359
+ body,
360
+ (answer) => attachmentOf(answer, uploadedExpectation(request)),
361
+ session.dependencies,
362
+ signal,
363
+ );
364
+ }
365
+
366
+ const IMAGE_SIGNATURES = [
367
+ { mediaType: "image/png", prefix: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
368
+ { mediaType: "image/jpeg", prefix: [0xff, 0xd8, 0xff] },
369
+ { mediaType: "image/gif", prefix: [0x47, 0x49, 0x46, 0x38] },
370
+ {
371
+ mediaType: "image/webp",
372
+ prefix: [0x52, 0x49, 0x46, 0x46],
373
+ suffix: { offset: 8, bytes: [0x57, 0x45, 0x42, 0x50] },
374
+ },
375
+ { mediaType: "image/bmp", prefix: [0x42, 0x4d] },
376
+ ];
377
+
378
+ function signed(header, offset, bytes) {
379
+ return (
380
+ header.length >= offset + bytes.length &&
381
+ bytes.every((byte, index) => header[offset + index] === byte)
382
+ );
383
+ }
384
+
385
+ function observedMediaType(header) {
386
+ for (const signature of IMAGE_SIGNATURES) {
387
+ if (!signed(header, 0, signature.prefix)) continue;
388
+ if (signature.suffix !== undefined && !signed(header, signature.suffix.offset, signature.suffix.bytes)) {
389
+ continue;
390
+ }
391
+ return signature.mediaType;
392
+ }
393
+ return null;
394
+ }
395
+
396
+ export async function localAttachment(path, version) {
397
+ const body = await readOriginalFile(path);
398
+ if (body === null) refuse("attachment_source_unreadable", path);
399
+ const mediaType = observedMediaType(body);
400
+ return {
401
+ body,
402
+ file: {
403
+ version,
404
+ name: originalName(basename(path)),
405
+ ...describedBytes(body),
406
+ ...(mediaType === null ? {} : { media_type: mediaType }),
407
+ },
408
+ };
409
+ }
410
+
411
+ export function uploadLocalAttachment(requested, local, dependencies, signal) {
412
+ return publish(
413
+ requested,
414
+ local.body,
415
+ (answer) => attachmentOf(answer, uploadedExpectation(local.file)),
416
+ dependencies,
417
+ signal,
418
+ );
419
+ }
420
+
421
+ export function sendLocalConversationFile(requested, local, dependencies, signal) {
422
+ const { version, bytes, sha256 } = local.file;
423
+ return publish(
424
+ requested,
425
+ local.body,
426
+ (answer) => associated(answer, { version, direction: "outgoing", bytes, sha256 }),
427
+ dependencies,
428
+ signal,
429
+ );
430
+ }
431
+
432
+ function unreadable(answer) {
433
+ const code = answer.save.status !== "saved" ? "attachment_read_not_saved" : "attachment_read_unavailable";
434
+ const refusal = new KitRefusal(code, answer.read?.status ?? answer.save.status);
435
+ refusal.payload = {
436
+ attachment: answer.attachment,
437
+ version: answer.version,
438
+ save: answer.save,
439
+ read: answer.read ?? null,
440
+ };
441
+ return refusal;
442
+ }
443
+
444
+ async function readAttachment(session, place, attachment, expected, signal) {
445
+ const answer = attachmentOf(
446
+ await originalRequest(
447
+ session,
448
+ KIT_ATTACHMENTS_GET_PATH,
449
+ { ...place, attachment, download: true },
450
+ signal,
451
+ ),
452
+ { ...expected, attachment },
453
+ );
454
+ if (answer.save.status !== "saved" || answer.read?.status !== "available") throw unreadable(answer);
455
+ const grant = grantOf(answer.download, "download");
456
+ if (grant === null) refuse("kit_attachment_grant_rejected", "download");
457
+ const bytes = await receiveBytes(session, grant, signal);
458
+ if (bytes.length !== answer.bytes || digestOf(bytes) !== answer.sha256) {
459
+ refuse("attachment_bytes_mismatch");
460
+ }
461
+ return { answer, bytes };
462
+ }
463
+
464
+ function failure(error) {
465
+ return error instanceof KitRefusal ? error.code : "kit_attachment_transfer_failed";
466
+ }
467
+
468
+ export async function placeAttachment(session, options) {
469
+ active(options.signal);
470
+ const place = options.room === undefined || options.room === null ? {} : { room_ref: options.room };
471
+ const version = originalUuid(options.version, "kit_attachment_version_invalid");
472
+ const name = originalName(options.name);
473
+ const mediaType = originalMediaType(options.mediaType);
474
+ if (!Buffer.isBuffer(options.body)) refuse("kit_attachment_file_missing");
475
+ const request = uploadRequest(place, version, name, describedBytes(options.body), mediaType);
476
+ const signal = options.signal ?? session.signal;
477
+ const uploaded = await deliver(session, request, options.body, signal);
478
+ const placed = {
479
+ attachment: uploaded.attachment,
480
+ original: uploaded.original,
481
+ version: uploaded.version,
482
+ name: uploaded.name,
483
+ bytes: uploaded.bytes,
484
+ sha256: uploaded.sha256,
485
+ save: uploaded.save,
486
+ };
487
+ if (uploaded.save.status !== "saved") return placed;
488
+ try {
489
+ await readAttachment(session, place, uploaded.attachment, uploadedExpectation(request), signal);
490
+ } catch (error) {
491
+ placed.save = { status: "failed", failure: failure(error) };
492
+ }
493
+ return placed;
494
+ }
495
+
496
+ function stateKey(house, request) {
497
+ return createHash("sha256")
498
+ .update(
499
+ JSON.stringify([
500
+ house,
501
+ request.room_ref ?? null,
502
+ request.name,
503
+ request.bytes,
504
+ request.sha256,
505
+ request.media_type ?? null,
506
+ ]),
507
+ )
508
+ .digest("hex");
509
+ }
510
+
511
+ function persist(directory, key, house, request, answered, signal) {
512
+ return writeAttachmentState(
513
+ directory,
514
+ key,
515
+ {
516
+ contract: ATTACHMENT_TRANSFER_CONTRACT,
517
+ house,
518
+ room_ref: request.room_ref ?? null,
519
+ name: request.name,
520
+ bytes: request.bytes,
521
+ sha256: request.sha256,
522
+ media_type: request.media_type ?? null,
523
+ version: request.version,
524
+ attachment: answered.attachment,
525
+ save: answered.save,
526
+ },
527
+ signal,
528
+ );
529
+ }
530
+
531
+ function heldVersion(held) {
532
+ return typeof held?.version === "string" && ORIGINAL_UUID.test(held.version)
533
+ ? held.version
534
+ : operationId();
535
+ }
536
+
537
+ export async function uploadAttachment(options, dependencies = {}) {
538
+ active(options.signal);
539
+ const place = attachmentPlace(options);
540
+ if (typeof options.file !== "string" || options.file.length === 0) {
541
+ refuse("kit_attachment_file_missing");
542
+ }
543
+ const name = originalName(options.name ?? basename(options.file));
544
+ const mediaType = originalMediaType(options.mediaType);
545
+ const body = await readOriginalFile(options.file);
546
+ if (body === null) refuse("attachment_source_unreadable");
547
+ const descriptor = describedBytes(body);
548
+ const provisional = uploadRequest(place, null, name, descriptor, mediaType);
549
+ const key = stateKey(options.house, provisional);
550
+ const held = await readAttachmentState(options.directory, key);
551
+ const request = { ...provisional, version: heldVersion(held) };
552
+ await persist(
553
+ options.directory,
554
+ key,
555
+ options.house,
556
+ request,
557
+ {
558
+ attachment: held?.version === request.version ? held.attachment ?? null : null,
559
+ save: { status: "pending", failure: null },
560
+ },
561
+ options.signal,
562
+ );
563
+ return withSession(options, dependencies, async (session, signal) => {
564
+ const uploaded = await deliver(session, request, body, signal);
565
+ await persist(options.directory, key, options.house, request, uploaded, signal);
566
+ if (uploaded.save.status !== "saved") {
567
+ const refusal = new KitRefusal(
568
+ "attachment_upload_failed",
569
+ uploaded.upload_failure ?? uploaded.save.failure ?? uploaded.save.status,
570
+ );
571
+ refusal.payload = {
572
+ attachment: uploaded.attachment,
573
+ version: uploaded.version,
574
+ save: uploaded.save,
575
+ };
576
+ throw refusal;
577
+ }
578
+ const read = await readAttachment(
579
+ session,
580
+ place,
581
+ uploaded.attachment,
582
+ uploadedExpectation(request),
583
+ signal,
584
+ );
585
+ await writeOutput(options.output, read.bytes, signal);
586
+ return {
587
+ attachment: uploaded.attachment,
588
+ original: uploaded.original,
589
+ version: uploaded.version,
590
+ name: uploaded.name,
591
+ save: uploaded.save,
592
+ read: { status: "available" },
593
+ bytes: read.bytes.length,
594
+ sha256: digestOf(read.bytes),
595
+ };
596
+ });
597
+ }
598
+
599
+ export async function getAttachment(options, dependencies = {}) {
600
+ active(options.signal);
601
+ const place = attachmentPlace(options);
602
+ const attachment = attachmentReference(options.attachment);
603
+ return withSession(options, dependencies, async (session, signal) => {
604
+ const read = await readAttachment(session, place, attachment, {}, signal);
605
+ await writeOutput(options.output, read.bytes, signal);
606
+ return {
607
+ attachment: read.answer.attachment,
608
+ original: read.answer.original,
609
+ version: read.answer.version,
610
+ name: read.answer.name,
611
+ media_type: read.answer.media_type,
612
+ save: read.answer.save,
613
+ read: read.answer.read,
614
+ bytes: read.bytes.length,
615
+ sha256: digestOf(read.bytes),
616
+ };
617
+ });
618
+ }
619
+
620
+ export function formatAttachmentUpload(result) {
621
+ return [
622
+ `Uploaded attachment ${result.attachment} as ${result.name} (${result.save.status}).`,
623
+ `Read ${result.bytes} bytes sha256=${result.sha256}.`,
624
+ ];
625
+ }
626
+
627
+ export function formatAttachmentGet(result) {
628
+ return [
629
+ `Read attachment ${result.attachment} (${result.name}) ${result.bytes} bytes sha256=${result.sha256}.`,
630
+ ];
631
+ }