@powerhousedao/reactor-attachments 6.0.2-staging.5 → 6.0.2-staging.7

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.
package/dist/index.d.ts CHANGED
@@ -42,13 +42,23 @@ type AttachmentHeader = {
42
42
  lastAccessedAtUtc: string;
43
43
  };
44
44
  /**
45
- * Metadata provided alongside attachment data during sync.
45
+ * Metadata provided alongside attachment data during sync, and returned
46
+ * via the switchboard's `Attachment-Metadata` header on GET/HEAD.
46
47
  * `createdAtUtc` is the original upload time, propagated from the source
47
48
  * so that the receiving store preserves it instead of synthesizing the
48
- * fetch time. `lastAccessedAtUtc` is intentionally omitted: it is a
49
- * per-reactor LRU concern that resets on every read.
50
- * The remaining fields (hash, status, source, lastAccessedAtUtc) are set
51
- * by the store when it creates the attachment record.
49
+ * fetch time. `lastAccessedAtUtc` is the source reactor's most recent
50
+ * access time; it is optional because not every producer (e.g. a fresh
51
+ * `put` from a transport) tracks access yet. Receiving stores that
52
+ * persist locally (LRU concerns) reset it on every read regardless.
53
+ *
54
+ * Reliability note: `lastAccessedAtUtc` arriving over the wire is
55
+ * best-effort. When the producer omits it, the consumer (see
56
+ * `RemoteAttachmentStore` / `SwitchboardAttachmentTransport`) coalesces
57
+ * with `createdAtUtc`, so the field can silently equal `createdAtUtc`
58
+ * even on a server that has never been read. Do NOT use the wire value
59
+ * for LRU eviction or staleness decisions on remote data; always read
60
+ * the field from the local store after persistence, where the receiving
61
+ * store is the authority.
52
62
  */
53
63
  type AttachmentMetadata = {
54
64
  mimeType: string;
@@ -56,6 +66,7 @@ type AttachmentMetadata = {
56
66
  sizeBytes: number;
57
67
  extension: string | null;
58
68
  createdAtUtc: string;
69
+ lastAccessedAtUtc?: string;
59
70
  };
60
71
  /**
61
72
  * Options provided when reserving an attachment slot.
@@ -356,6 +367,7 @@ interface AttachmentReservationTable {
356
367
  extension: string | null;
357
368
  created_at_utc: string;
358
369
  expires_at_utc: string;
370
+ deleted_at_utc: string | null;
359
371
  }
360
372
  interface AttachmentDatabase {
361
373
  attachment: AttachmentTable;
@@ -451,8 +463,8 @@ declare class RemoteReservationStore implements IReservationStore {
451
463
  private readonly fetchFn;
452
464
  constructor(config: SwitchboardClientConfig);
453
465
  create(options: ReserveAttachmentOptions): Promise<Reservation>;
454
- get(_reservationId: string): Promise<Reservation>;
455
- delete(_reservationId: string): Promise<void>;
466
+ get(reservationId: string): Promise<Reservation>;
467
+ delete(reservationId: string): Promise<void>;
456
468
  deleteExpired(_now?: Date): Promise<number>;
457
469
  }
458
470
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/interfaces.ts","../src/attachment-service.ts","../src/ref.ts","../src/storage/kysely/types.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/interfaces.ts","../src/attachment-service.ts","../src/ref.ts","../src/storage/kysely/types.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -363,67 +363,96 @@ var KyselyReservationStore = class {
363
363
  }).returningAll().executeTakeFirstOrThrow());
364
364
  }
365
365
  async get(reservationId) {
366
- const row = await this.db.selectFrom("attachment_reservation").selectAll().where("reservation_id", "=", reservationId).executeTakeFirst();
366
+ const row = await this.db.selectFrom("attachment_reservation").selectAll().where("reservation_id", "=", reservationId).where("deleted_at_utc", "is", null).executeTakeFirst();
367
367
  if (!row) throw new ReservationNotFound(reservationId);
368
368
  return rowToReservation(row);
369
369
  }
370
370
  async delete(reservationId) {
371
- await this.db.deleteFrom("attachment_reservation").where("reservation_id", "=", reservationId).execute();
371
+ await this.db.updateTable("attachment_reservation").set({ deleted_at_utc: (/* @__PURE__ */ new Date()).toISOString() }).where("reservation_id", "=", reservationId).where("deleted_at_utc", "is", null).execute();
372
372
  }
373
373
  async deleteExpired(now = /* @__PURE__ */ new Date()) {
374
- const result = await this.db.deleteFrom("attachment_reservation").where("expires_at_utc", "<=", now.toISOString()).executeTakeFirst();
375
- return Number(result.numDeletedRows ?? 0);
374
+ const nowIso = now.toISOString();
375
+ const result = await this.db.updateTable("attachment_reservation").set({ deleted_at_utc: nowIso }).where("expires_at_utc", "<=", nowIso).where("deleted_at_utc", "is", null).executeTakeFirst();
376
+ return Number(result.numUpdatedRows ?? 0);
376
377
  }
377
378
  };
378
379
  //#endregion
379
380
  //#region src/storage/migrations/001_create_attachment_table.ts
380
381
  var _001_create_attachment_table_exports = /* @__PURE__ */ __exportAll({
381
- down: () => down$2,
382
- up: () => up$2
382
+ down: () => down$4,
383
+ up: () => up$4
383
384
  });
384
- async function up$2(db) {
385
+ async function up$4(db) {
385
386
  await db.schema.createTable("attachment").addColumn("hash", "text", (col) => col.primaryKey()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("file_name", "text", (col) => col.notNull()).addColumn("size_bytes", "bigint", (col) => col.notNull()).addColumn("extension", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("available")).addColumn("storage_path", "text", (col) => col.notNull()).addColumn("source", "text", (col) => col.notNull().defaultTo("local")).addColumn("created_at_utc", "text", (col) => col.notNull()).addColumn("last_accessed_at_utc", "text", (col) => col.notNull()).execute();
386
387
  await db.schema.createIndex("idx_attachment_status").on("attachment").column("status").execute();
387
388
  await db.schema.createIndex("idx_attachment_lru").on("attachment").columns(["status", "last_accessed_at_utc"]).execute();
388
389
  }
389
- async function down$2(db) {
390
+ async function down$4(db) {
390
391
  await db.schema.dropTable("attachment").ifExists().execute();
391
392
  }
392
393
  //#endregion
393
394
  //#region src/storage/migrations/002_create_reservation_table.ts
394
395
  var _002_create_reservation_table_exports = /* @__PURE__ */ __exportAll({
395
- down: () => down$1,
396
- up: () => up$1
396
+ down: () => down$3,
397
+ up: () => up$3
397
398
  });
398
- async function up$1(db) {
399
+ async function up$3(db) {
399
400
  await db.schema.createTable("attachment_reservation").addColumn("reservation_id", "text", (col) => col.primaryKey()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("file_name", "text", (col) => col.notNull()).addColumn("extension", "text").addColumn("created_at_utc", "text", (col) => col.notNull()).execute();
400
401
  }
401
- async function down$1(db) {
402
+ async function down$3(db) {
402
403
  await db.schema.dropTable("attachment_reservation").ifExists().execute();
403
404
  }
404
405
  //#endregion
405
406
  //#region src/storage/migrations/003_add_reservation_expires_at.ts
406
407
  var _003_add_reservation_expires_at_exports = /* @__PURE__ */ __exportAll({
407
- down: () => down,
408
- up: () => up
408
+ down: () => down$2,
409
+ up: () => up$2
409
410
  });
410
- async function up(db) {
411
+ async function up$2(db) {
411
412
  await db.schema.alterTable("attachment_reservation").addColumn("expires_at_utc", "text").execute();
412
413
  await db.updateTable("attachment_reservation").set({ expires_at_utc: sql`created_at_utc` }).where("expires_at_utc", "is", null).execute();
413
414
  await db.schema.alterTable("attachment_reservation").alterColumn("expires_at_utc", (col) => col.setNotNull()).execute();
414
415
  await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
415
416
  }
416
- async function down(db) {
417
+ async function down$2(db) {
417
418
  await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
418
419
  await db.schema.alterTable("attachment_reservation").dropColumn("expires_at_utc").execute();
419
420
  }
420
421
  //#endregion
422
+ //#region src/storage/migrations/004_add_reservation_soft_delete.ts
423
+ var _004_add_reservation_soft_delete_exports = /* @__PURE__ */ __exportAll({
424
+ down: () => down$1,
425
+ up: () => up$1
426
+ });
427
+ async function up$1(db) {
428
+ await db.schema.alterTable("attachment_reservation").addColumn("deleted_at_utc", "text").execute();
429
+ }
430
+ async function down$1(db) {
431
+ await db.schema.alterTable("attachment_reservation").dropColumn("deleted_at_utc").execute();
432
+ }
433
+ //#endregion
434
+ //#region src/storage/migrations/005_add_reservation_active_index.ts
435
+ var _005_add_reservation_active_index_exports = /* @__PURE__ */ __exportAll({
436
+ down: () => down,
437
+ up: () => up
438
+ });
439
+ async function up(db) {
440
+ await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
441
+ await db.schema.createIndex("idx_reservation_expires_at_active").on("attachment_reservation").column("expires_at_utc").where(sql`deleted_at_utc IS NULL`).execute();
442
+ }
443
+ async function down(db) {
444
+ await db.schema.dropIndex("idx_reservation_expires_at_active").ifExists().execute();
445
+ await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
446
+ }
447
+ //#endregion
421
448
  //#region src/storage/migrations/migrator.ts
422
449
  const ATTACHMENT_SCHEMA = "attachments";
423
450
  const migrations = {
424
451
  "001_create_attachment_table": _001_create_attachment_table_exports,
425
452
  "002_create_reservation_table": _002_create_reservation_table_exports,
426
- "003_add_reservation_expires_at": _003_add_reservation_expires_at_exports
453
+ "003_add_reservation_expires_at": _003_add_reservation_expires_at_exports,
454
+ "004_add_reservation_soft_delete": _004_add_reservation_soft_delete_exports,
455
+ "005_add_reservation_active_index": _005_add_reservation_active_index_exports
427
456
  };
428
457
  var ProgrammaticMigrationProvider = class {
429
458
  getMigrations() {
@@ -599,29 +628,40 @@ var SwitchboardAttachmentTransport = class {
599
628
  if (!response.ok) throw new Error(`Attachment push failed: ${response.status} ${response.statusText}`);
600
629
  }
601
630
  parseMetadataHeaders(response) {
602
- const metaHeader = response.headers.get("X-Attachment-Metadata");
631
+ let fallbackCache;
632
+ const fallback = () => {
633
+ if (fallbackCache === void 0) fallbackCache = contentTypeFallback$1(response);
634
+ return fallbackCache;
635
+ };
636
+ const metaHeader = response.headers.get("Attachment-Metadata");
603
637
  if (metaHeader) try {
604
638
  const parsed = JSON.parse(metaHeader);
605
- if (isRecord$1(parsed) && parsed.extension === void 0) parsed.extension = null;
639
+ if (isRecord$2(parsed)) {
640
+ if (parsed.extension === void 0) parsed.extension = null;
641
+ if (parsed.createdAtUtc === void 0) parsed.createdAtUtc = fallback().createdAtUtc;
642
+ if (parsed.lastAccessedAtUtc === void 0) parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;
643
+ }
606
644
  if (isAttachmentMetadata$1(parsed)) return parsed;
607
645
  } catch {}
608
- return contentTypeFallback$1(response);
646
+ return fallback();
609
647
  }
610
648
  };
611
- function isRecord$1(value) {
649
+ function isRecord$2(value) {
612
650
  return typeof value === "object" && value !== null && !Array.isArray(value);
613
651
  }
614
652
  function isAttachmentMetadata$1(value) {
615
- if (!isRecord$1(value)) return false;
653
+ if (!isRecord$2(value)) return false;
616
654
  if (typeof value.mimeType !== "string") return false;
617
655
  if (typeof value.fileName !== "string") return false;
618
656
  if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
619
657
  if (value.extension !== null && typeof value.extension !== "string") return false;
658
+ if (typeof value.createdAtUtc !== "string") return false;
659
+ if (value.lastAccessedAtUtc !== void 0 && typeof value.lastAccessedAtUtc !== "string") return false;
620
660
  return true;
621
661
  }
622
662
  function contentTypeFallback$1(response) {
623
663
  const contentLength = response.headers.get("Content-Length");
624
- if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
664
+ if (contentLength === null) throw new Error("Switchboard response missing both Attachment-Metadata and Content-Length headers");
625
665
  const sizeBytes = Number(contentLength);
626
666
  if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
627
667
  const lastModified = response.headers.get("Last-Modified");
@@ -632,11 +672,25 @@ function contentTypeFallback$1(response) {
632
672
  fileName: "unknown",
633
673
  sizeBytes,
634
674
  extension: null,
635
- createdAtUtc
675
+ createdAtUtc,
676
+ lastAccessedAtUtc: createdAtUtc
636
677
  };
637
678
  }
638
679
  //#endregion
639
680
  //#region src/switchboard/remote-reservation-store.ts
681
+ function isRecord$1(value) {
682
+ return typeof value === "object" && value !== null && !Array.isArray(value);
683
+ }
684
+ function isReservation(value) {
685
+ if (!isRecord$1(value)) return false;
686
+ if (typeof value.reservationId !== "string") return false;
687
+ if (typeof value.mimeType !== "string") return false;
688
+ if (typeof value.fileName !== "string") return false;
689
+ if (value.extension !== null && typeof value.extension !== "string") return false;
690
+ if (typeof value.createdAtUtc !== "string") return false;
691
+ if (typeof value.expiresAtUtc !== "string") return false;
692
+ return true;
693
+ }
640
694
  var RemoteReservationStore = class {
641
695
  remoteUrl;
642
696
  jwtHandler;
@@ -673,11 +727,29 @@ var RemoteReservationStore = class {
673
727
  expiresAtUtc: json.expiresAtUtc ?? new Date(now.getTime() + 1440 * 60 * 1e3).toISOString()
674
728
  };
675
729
  }
676
- get(_reservationId) {
677
- return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.get is not supported"));
730
+ async get(reservationId) {
731
+ const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;
732
+ const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
733
+ const response = await this.fetchFn(url, { headers: authHeaders });
734
+ if (response.status === 404) throw new ReservationNotFound(reservationId);
735
+ if (!response.ok) throw new Error(`Reservation get failed: ${response.status} ${response.statusText}`);
736
+ let parsed;
737
+ try {
738
+ parsed = await response.json();
739
+ } catch {
740
+ throw new Error("Reservation get returned non-JSON response");
741
+ }
742
+ if (!isReservation(parsed)) throw new Error("Reservation get returned a payload that does not match the Reservation shape");
743
+ return parsed;
678
744
  }
679
- delete(_reservationId) {
680
- return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.delete is not supported"));
745
+ async delete(reservationId) {
746
+ const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;
747
+ const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
748
+ const response = await this.fetchFn(url, {
749
+ method: "DELETE",
750
+ headers: authHeaders
751
+ });
752
+ if (!response.ok && response.status !== 404 && response.status !== 410) throw new Error(`Reservation delete failed: ${response.status} ${response.statusText}`);
681
753
  }
682
754
  deleteExpired(_now) {
683
755
  return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.deleteExpired is not supported"));
@@ -735,11 +807,13 @@ function isAttachmentMetadata(value) {
735
807
  if (typeof value.fileName !== "string") return false;
736
808
  if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
737
809
  if (value.extension !== null && typeof value.extension !== "string") return false;
810
+ if (typeof value.createdAtUtc !== "string") return false;
811
+ if (value.lastAccessedAtUtc !== void 0 && typeof value.lastAccessedAtUtc !== "string") return false;
738
812
  return true;
739
813
  }
740
814
  function contentTypeFallback(response) {
741
815
  const contentLength = response.headers.get("Content-Length");
742
- if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
816
+ if (contentLength === null) throw new Error("Switchboard response missing both Attachment-Metadata and Content-Length headers");
743
817
  const sizeBytes = Number(contentLength);
744
818
  if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
745
819
  const lastModified = response.headers.get("Last-Modified");
@@ -750,17 +824,27 @@ function contentTypeFallback(response) {
750
824
  fileName: "unknown",
751
825
  sizeBytes,
752
826
  extension: null,
753
- createdAtUtc
827
+ createdAtUtc,
828
+ lastAccessedAtUtc: createdAtUtc
754
829
  };
755
830
  }
756
831
  function parseMetadata(response) {
757
- const metaHeader = response.headers.get("X-Attachment-Metadata");
832
+ let fallbackCache;
833
+ const fallback = () => {
834
+ if (fallbackCache === void 0) fallbackCache = contentTypeFallback(response);
835
+ return fallbackCache;
836
+ };
837
+ const metaHeader = response.headers.get("Attachment-Metadata");
758
838
  if (metaHeader) try {
759
839
  const parsed = JSON.parse(metaHeader);
760
- if (isRecord(parsed) && parsed.extension === void 0) parsed.extension = null;
840
+ if (isRecord(parsed)) {
841
+ if (parsed.extension === void 0) parsed.extension = null;
842
+ if (parsed.createdAtUtc === void 0) parsed.createdAtUtc = fallback().createdAtUtc;
843
+ if (parsed.lastAccessedAtUtc === void 0) parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;
844
+ }
761
845
  if (isAttachmentMetadata(parsed)) return parsed;
762
846
  } catch {}
763
- return contentTypeFallback(response);
847
+ return fallback();
764
848
  }
765
849
  var RemoteAttachmentStore = class {
766
850
  remoteUrl;
@@ -772,9 +856,15 @@ var RemoteAttachmentStore = class {
772
856
  this.fetchFn = config.fetchFn ?? globalThis.fetch;
773
857
  }
774
858
  async stat(hash) {
775
- const { header, body } = await this.fetchAttachment(hash);
776
- await body.cancel();
777
- return header;
859
+ const url = `${this.remoteUrl}/attachments/${hash}`;
860
+ const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
861
+ const response = await this.fetchFn(url, {
862
+ method: "HEAD",
863
+ headers: authHeaders
864
+ });
865
+ if (response.status === 404) throw new AttachmentNotFound(hash);
866
+ if (!response.ok) throw new Error(`Attachment stat failed: ${response.status} ${response.statusText}`);
867
+ return buildHeader(hash, parseMetadata(response));
778
868
  }
779
869
  async get(hash, signal) {
780
870
  return this.fetchAttachment(hash, signal);
@@ -789,24 +879,25 @@ var RemoteAttachmentStore = class {
789
879
  if (response.status === 404) throw new AttachmentNotFound(hash);
790
880
  if (!response.ok) throw new Error(`Attachment fetch failed: ${response.status} ${response.statusText}`);
791
881
  if (!response.body) throw new Error("Response body is null");
792
- const metadata = parseMetadata(response);
793
- const now = (/* @__PURE__ */ new Date()).toISOString();
794
882
  return {
795
- header: {
796
- hash,
797
- mimeType: metadata.mimeType,
798
- fileName: metadata.fileName,
799
- sizeBytes: metadata.sizeBytes,
800
- extension: metadata.extension,
801
- status: "available",
802
- source: "sync",
803
- createdAtUtc: now,
804
- lastAccessedAtUtc: now
805
- },
883
+ header: buildHeader(hash, parseMetadata(response)),
806
884
  body: response.body
807
885
  };
808
886
  }
809
887
  };
888
+ function buildHeader(hash, metadata) {
889
+ return {
890
+ hash,
891
+ mimeType: metadata.mimeType,
892
+ fileName: metadata.fileName,
893
+ sizeBytes: metadata.sizeBytes,
894
+ extension: metadata.extension,
895
+ status: "available",
896
+ source: "sync",
897
+ createdAtUtc: metadata.createdAtUtc,
898
+ lastAccessedAtUtc: metadata.lastAccessedAtUtc ?? metadata.createdAtUtc
899
+ };
900
+ }
810
901
  //#endregion
811
902
  //#region src/switchboard/create-remote-attachment-service.ts
812
903
  function createRemoteAttachmentService(config) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rowToHeader","up","down","up","down","migration001","migration002","migration003","isRecord","isAttachmentMetadata","contentTypeFallback"],"sources":["../src/errors.ts","../src/ref.ts","../src/attachment-service.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/003_add_reservation_expires_at.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/build-auth-headers.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"sourcesContent":["/**\n * Thrown when an attachment ref or hash is not known to the store.\n */\nexport class AttachmentNotFound extends Error {\n constructor(identifier: string) {\n super(`Attachment not found: ${identifier}`);\n this.name = \"AttachmentNotFound\";\n }\n}\n\n/**\n * Thrown when a reservation ID is not found in the reservation store.\n */\nexport class ReservationNotFound extends Error {\n constructor(reservationId: string) {\n super(`Reservation not found: ${reservationId}`);\n this.name = \"ReservationNotFound\";\n }\n}\n\n/**\n * Thrown when an attachment ref string does not match the expected format.\n */\nexport class InvalidAttachmentRef extends Error {\n constructor(ref: string) {\n super(`Invalid attachment ref: ${ref}`);\n this.name = \"InvalidAttachmentRef\";\n }\n}\n\n/**\n * Thrown when an upload exceeds the configured maximum byte cap.\n * Route handlers should map this to HTTP 413 Payload Too Large.\n */\nexport class UploadTooLarge extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`Upload exceeds maximum size of ${maxBytes} bytes`);\n this.name = \"UploadTooLarge\";\n this.maxBytes = maxBytes;\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { InvalidAttachmentRef } from \"./errors.js\";\n\nconst REF_PATTERN = /^attachment:\\/\\/v(\\d+):(.+)$/;\nconst DEFAULT_VERSION = 1;\n\nexport type ParsedRef = {\n version: number;\n hash: AttachmentHash;\n};\n\nexport function parseRef(ref: AttachmentRef): ParsedRef {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n throw new InvalidAttachmentRef(ref);\n }\n return {\n version: Number(match[1]),\n hash: match[2],\n };\n}\n\nexport function createRef(\n hash: AttachmentHash,\n version: number = DEFAULT_VERSION,\n): AttachmentRef {\n return `attachment://v${version}:${hash}`;\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n ReserveAttachmentOptions,\n} from \"./types.js\";\nimport { parseRef } from \"./ref.js\";\n\nexport class AttachmentService implements IAttachmentService {\n constructor(\n private readonly store: IAttachmentReader,\n private readonly reservations: IReservationStore,\n private readonly uploadFactory: IAttachmentUploadFactory,\n ) {}\n\n async reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload> {\n const reservation = await this.reservations.create(options);\n return this.uploadFactory.createUpload(reservation.reservationId, options);\n }\n\n async stat(ref: AttachmentRef): Promise<AttachmentHeader> {\n const { hash } = parseRef(ref);\n return this.store.stat(hash);\n }\n\n async get(\n ref: AttachmentRef,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const { hash } = parseRef(ref);\n return this.store.get(hash, signal);\n }\n}\n","import { mkdir, rm, access } from \"node:fs/promises\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { UploadTooLarge } from \"../../errors.js\";\n\n/**\n * Compute the absolute storage path for an attachment hash.\n * Uses a 2-level directory fan-out to avoid millions of files\n * in a single directory: ab/cd/abcdef123456...\n */\nexport function storagePath(basePath: string, hash: string): string {\n return join(basePath, storageRelativePath(hash));\n}\n\n/**\n * Compute the relative storage path for an attachment hash.\n * This is what gets stored in the database's storage_path column.\n */\nexport function storageRelativePath(hash: string): string {\n return join(hash.slice(0, 2), hash.slice(2, 4), hash);\n}\n\n/**\n * Write a ReadableStream to disk. Creates parent directories as needed.\n * Returns the number of bytes written.\n */\nexport async function writeAttachmentBytes(\n path: string,\n data: ReadableStream<Uint8Array>,\n): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n\n const writer = createWriteStream(path);\n const reader = data.getReader();\n let bytesWritten = 0;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve) => writer.once(\"drain\", resolve));\n }\n }\n } finally {\n reader.releaseLock();\n await new Promise<void>((resolve, reject) => {\n writer.end(() => resolve());\n writer.on(\"error\", reject);\n });\n }\n\n return bytesWritten;\n}\n\n/**\n * Open a ReadableStream from a file on disk.\n */\nexport function readAttachmentStream(path: string): ReadableStream<Uint8Array> {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;\n}\n\n/**\n * Delete a file from disk. No-op if the file does not exist.\n */\nexport async function deleteAttachmentBytes(path: string): Promise<void> {\n await rm(path, { force: true });\n}\n\n/**\n * Check whether a file exists on disk.\n */\nexport async function attachmentBytesExist(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a ReadableStream from an in-memory buffer.\n */\nexport function streamFromBuffer(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(data);\n controller.close();\n },\n });\n}\n\n/**\n * Stream bytes to a temp file under `${basePath}/.tmp/` while computing the\n * SHA-256 hash, returning the temp path, hex hash, and total bytes written.\n *\n * Bytes are never buffered in memory beyond the current chunk. The caller is\n * responsible for renaming the temp file to its final hash-derived location\n * (or removing it if the content is a duplicate).\n *\n * If `maxBytes` is set and the input exceeds it, the temp file is removed and\n * `UploadTooLarge` is thrown.\n */\nexport async function streamHashAndWrite(\n basePath: string,\n data: ReadableStream<Uint8Array>,\n options: { maxBytes?: number } = {},\n): Promise<{ tempPath: string; hash: string; sizeBytes: number }> {\n const { maxBytes } = options;\n const tmpDir = join(basePath, \".tmp\");\n await mkdir(tmpDir, { recursive: true });\n const tempPath = join(tmpDir, randomUUID());\n\n const hasher = createHash(\"sha256\");\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n let sizeBytes = 0;\n let caughtError: Error | undefined;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n sizeBytes += value.byteLength;\n if (maxBytes !== undefined && sizeBytes > maxBytes) {\n throw new UploadTooLarge(maxBytes);\n }\n hasher.update(value);\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n } catch (err) {\n caughtError = err instanceof Error ? err : new Error(String(err));\n } finally {\n reader.releaseLock();\n }\n\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.on(\"error\", reject);\n });\n\n if (caughtError) {\n await rm(tempPath, { force: true });\n throw caughtError;\n }\n\n return {\n tempPath,\n hash: hasher.digest(\"hex\"),\n sizeBytes,\n };\n}\n","import { join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentStore,\n IAttachmentTransport,\n} from \"../../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n} from \"../../types.js\";\nimport { AttachmentNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, AttachmentRow } from \"./types.js\";\nimport {\n storageRelativePath,\n writeAttachmentBytes,\n readAttachmentStream,\n deleteAttachmentBytes,\n} from \"../fs/attachment-fs.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n };\n}\n\nfunction wrapStreamWithCleanup(\n source: ReadableStream<Uint8Array>,\n cleanup: () => void,\n): ReadableStream<Uint8Array> {\n let cleaned = false;\n const doCleanup = () => {\n if (!cleaned) {\n cleaned = true;\n cleanup();\n }\n };\n\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n doCleanup();\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n doCleanup();\n controller.error(err);\n }\n },\n cancel() {\n doCleanup();\n reader.cancel().catch(() => {});\n },\n });\n}\n\nexport class KyselyAttachmentStore implements IAttachmentStore {\n private readonly activeReaders = new Map<string, number>();\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly transport: IAttachmentTransport,\n private readonly basePath: string,\n ) {}\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row) {\n throw new AttachmentNotFound(hash);\n }\n\n return rowToHeader(row);\n }\n\n async has(hash: AttachmentHash): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .select(\"status\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n return row?.status === \"available\";\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row) {\n throw new AttachmentNotFound(hash);\n }\n\n if (row.status === \"evicted\") {\n const remote = await this.transport.fetch(hash, signal);\n if (!remote) {\n throw new AttachmentNotFound(hash);\n }\n await this.put(hash, remote.metadata, remote.body);\n return this.get(hash, signal);\n }\n\n const now = new Date().toISOString();\n await this.db\n .updateTable(\"attachment\")\n .set({ last_accessed_at_utc: now })\n .where(\"hash\", \"=\", hash)\n .execute();\n\n const header = rowToHeader(row);\n header.lastAccessedAtUtc = now;\n\n this.acquireReader(hash);\n\n const fullPath = join(this.basePath, row.storage_path);\n const rawStream = readAttachmentStream(fullPath);\n const body = wrapStreamWithCleanup(rawStream, () =>\n this.releaseReader(hash),\n );\n\n return { header, body };\n }\n\n async put(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n await data.cancel();\n return;\n }\n\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, data);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: metadata.mimeType,\n file_name: metadata.fileName,\n size_bytes: metadata.sizeBytes,\n extension: metadata.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"sync\",\n created_at_utc: metadata.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n\n async evict(hash: AttachmentHash): Promise<void> {\n if (this.hasActiveReaders(hash)) {\n return;\n }\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .select([\"storage_path\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row || row.status === \"evicted\") {\n return;\n }\n\n const fullPath = join(this.basePath, row.storage_path);\n await deleteAttachmentBytes(fullPath);\n\n await this.db\n .updateTable(\"attachment\")\n .set({ status: \"evicted\" })\n .where(\"hash\", \"=\", hash)\n .execute();\n }\n\n async storageUsed(): Promise<number> {\n const result = await this.db\n .selectFrom(\"attachment\")\n .select(sql<string>`COALESCE(SUM(size_bytes), 0)`.as(\"total\"))\n .where(\"status\", \"=\", \"available\")\n .executeTakeFirst();\n\n return Number(result?.total ?? 0);\n }\n\n // Private: active reader tracking\n\n private acquireReader(hash: string): void {\n this.activeReaders.set(hash, (this.activeReaders.get(hash) ?? 0) + 1);\n }\n\n private releaseReader(hash: string): void {\n const count = (this.activeReaders.get(hash) ?? 1) - 1;\n if (count <= 0) {\n this.activeReaders.delete(hash);\n } else {\n this.activeReaders.set(hash, count);\n }\n }\n\n private hasActiveReaders(hash: string): boolean {\n return (this.activeReaders.get(hash) ?? 0) > 0;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Kysely } from \"kysely\";\nimport type { IReservationStore } from \"../../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../../types.js\";\nimport { ReservationNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, ReservationRow } from \"./types.js\";\n\nexport const DEFAULT_RESERVATION_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction rowToReservation(row: ReservationRow): Reservation {\n return {\n reservationId: row.reservation_id,\n mimeType: row.mime_type,\n fileName: row.file_name,\n extension: row.extension,\n createdAtUtc: row.created_at_utc,\n expiresAtUtc: row.expires_at_utc,\n };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n private readonly ttlMs: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n ttlMs: number = DEFAULT_RESERVATION_TTL_MS,\n ) {\n this.ttlMs = ttlMs;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const nowMs = Date.now();\n const now = new Date(nowMs).toISOString();\n const expiresAt = new Date(nowMs + this.ttlMs).toISOString();\n\n const row = await this.db\n .insertInto(\"attachment_reservation\")\n .values({\n reservation_id: reservationId,\n mime_type: options.mimeType,\n file_name: options.fileName,\n extension: options.extension ?? null,\n created_at_utc: now,\n expires_at_utc: expiresAt,\n })\n .returningAll()\n .executeTakeFirstOrThrow();\n\n return rowToReservation(row);\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const row = await this.db\n .selectFrom(\"attachment_reservation\")\n .selectAll()\n .where(\"reservation_id\", \"=\", reservationId)\n .executeTakeFirst();\n\n if (!row) {\n throw new ReservationNotFound(reservationId);\n }\n\n return rowToReservation(row);\n }\n\n async delete(reservationId: string): Promise<void> {\n await this.db\n .deleteFrom(\"attachment_reservation\")\n .where(\"reservation_id\", \"=\", reservationId)\n .execute();\n }\n\n async deleteExpired(now: Date = new Date()): Promise<number> {\n const result = await this.db\n .deleteFrom(\"attachment_reservation\")\n .where(\"expires_at_utc\", \"<=\", now.toISOString())\n .executeTakeFirst();\n\n return Number(result.numDeletedRows ?? 0);\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment\")\n .addColumn(\"hash\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"size_bytes\", \"bigint\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"status\", \"text\", (col) => col.notNull().defaultTo(\"available\"))\n .addColumn(\"storage_path\", \"text\", (col) => col.notNull())\n .addColumn(\"source\", \"text\", (col) => col.notNull().defaultTo(\"local\"))\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addColumn(\"last_accessed_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_status\")\n .on(\"attachment\")\n .column(\"status\")\n .execute();\n\n // Compound index serves the LRU eviction query:\n // SELECT ... WHERE status = 'available' ORDER BY last_accessed_at_utc ASC\n // A partial index would be ideal but raw SQL doesn't respect withSchema().\n await db.schema\n .createIndex(\"idx_attachment_lru\")\n .on(\"attachment\")\n .columns([\"status\", \"last_accessed_at_utc\"])\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment\").ifExists().execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reservation\")\n .addColumn(\"reservation_id\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reservation\").ifExists().execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"expires_at_utc\", \"text\")\n .execute();\n\n await db\n .updateTable(\"attachment_reservation\")\n .set({ expires_at_utc: sql`created_at_utc` })\n .where(\"expires_at_utc\", \"is\", null)\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .alterColumn(\"expires_at_utc\", (col) => col.setNotNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"expires_at_utc\")\n .execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { MigrationProvider, Kysely } from \"kysely\";\n\nimport * as migration001 from \"./001_create_attachment_table.js\";\nimport * as migration002 from \"./002_create_reservation_table.js\";\nimport * as migration003 from \"./003_add_reservation_expires_at.js\";\n\nexport const ATTACHMENT_SCHEMA = \"attachments\";\n\nexport interface MigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_table\": migration001,\n \"002_create_reservation_table\": migration002,\n \"003_add_reservation_expires_at\": migration003,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nexport async function runAttachmentMigrations(\n db: Kysely<any>,\n schema: string = ATTACHMENT_SCHEMA,\n): Promise<MigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n const migrator = new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n });\n\n let error: unknown;\n let results: Awaited<ReturnType<typeof migrator.migrateToLatest>>[\"results\"];\n try {\n const result = await migrator.migrateToLatest();\n error = result.error;\n results = result.results;\n } catch (e) {\n error = e;\n results = [];\n }\n\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n\n return {\n success: true,\n migrationsExecuted,\n };\n}\n","import { mkdir, rename, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n storageRelativePath,\n streamHashAndWrite,\n} from \"../storage/fs/attachment-fs.js\";\nimport type { AttachmentStatus } from \"../types.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n\n constructor(\n reservationId: string,\n private readonly options: ReserveAttachmentOptions,\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {\n this.reservationId = reservationId;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n // Stream bytes directly to a temp file while hashing. This caps memory\n // usage at one chunk regardless of payload size, and lets us enforce\n // `maxBytes` before either disk or memory grows unbounded.\n const { tempPath, hash, sizeBytes } = await streamHashAndWrite(\n this.basePath,\n data,\n { maxBytes: this.maxBytes },\n );\n\n try {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n // Dedup -- bytes already on disk, drop the temp file.\n await rm(tempPath, { force: true });\n } else {\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await mkdir(dirname(fullPath), { recursive: true });\n await rename(tempPath, fullPath);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: this.options.mimeType,\n file_name: this.options.fileName,\n size_bytes: sizeBytes,\n extension: this.options.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n created_at_utc: now,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n // Existing row was evicted — restore it\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n } catch (err) {\n await rm(tempPath, { force: true });\n throw err;\n }\n\n await this.reservations.delete(this.reservationId);\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirstOrThrow();\n\n return {\n hash,\n ref: createRef(hash),\n header: rowToHeader(row),\n };\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"../interfaces.js\";\nimport type { ReserveAttachmentOptions } from \"../types.js\";\nimport type { AttachmentDatabase } from \"../storage/kysely/types.js\";\nimport { DirectAttachmentUpload } from \"./direct-attachment-upload.js\";\n\nexport class DirectAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservationId,\n options,\n this.db,\n this.basePath,\n this.reservations,\n this.maxBytes,\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\n\nexport async function buildAuthHeaders(\n url: string,\n jwtHandler: JwtHandler | undefined,\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (jwtHandler) {\n const token = await jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { AttachmentMetadata, TransportResponse } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardTransportConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class SwitchboardAttachmentTransport implements IAttachmentTransport {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardTransportConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async fetch(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<TransportResponse | null> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n return null;\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = this.parseMetadataHeaders(response);\n const body = response.body;\n if (!body) {\n throw new Error(\"Response body is null\");\n }\n\n return { hash, metadata, body };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async announce(_hash: AttachmentHash): Promise<void> {\n // No-op for switchboard -- data is already on the server after upload.\n }\n\n async push(\n hash: AttachmentHash,\n remote: string,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const url = `${remote}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n body: data,\n headers,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment push failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed) && parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return contentTypeFallback(response);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both X-Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when X-Attachment-Metadata is absent. If that's missing too,\n // fall back to the response Date header (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n };\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IReservationStore } from \"../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardClientConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class RemoteReservationStore implements IReservationStore {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"POST\",\n headers: { ...authHeaders, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const json = (await response.json()) as {\n reservationId: string;\n createdAtUtc?: string;\n expiresAtUtc?: string;\n };\n // The server is the source of truth for both timestamps. We synthesize\n // only as a last-resort fallback for older switchboards that don't\n // include them in the response; in that case the client cannot know the\n // server's TTL, so expiresAtUtc is a best-effort placeholder.\n const now = new Date();\n return {\n reservationId: json.reservationId,\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n createdAtUtc: json.createdAtUtc ?? now.toISOString(),\n expiresAtUtc:\n json.expiresAtUtc ??\n new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),\n };\n }\n\n get(_reservationId: string): Promise<Reservation> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.get is not supported\"),\n );\n }\n\n delete(_reservationId: string): Promise<void> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.delete is not supported\"),\n );\n }\n\n // Sweeping is the server's responsibility; clients have no authority to\n // delete reservations on a remote switchboard.\n deleteExpired(_now?: Date): Promise<number> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.deleteExpired is not supported\"),\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentUpload } from \"../interfaces.js\";\nimport type {\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n // The reserve options are kept for symmetry with DirectAttachmentUpload,\n // but the server already has them tied to the reservation row.\n private readonly options: ReserveAttachmentOptions;\n\n constructor(\n reservationId: string,\n options: ReserveAttachmentOptions,\n config: SwitchboardClientConfig,\n ) {\n this.reservationId = reservationId;\n this.options = options;\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n // Always upload as octet-stream. The server reads the real mime type from\n // the reservation row; sending the user's mime type here (e.g. application/json)\n // would let Express body-parser drain the request body before our handler runs,\n // silently writing zero bytes.\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n headers: { ...authHeaders, \"Content-Type\": \"application/octet-stream\" },\n body: data,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return (await response.json()) as AttachmentUploadResult;\n }\n}\n","import type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../interfaces.js\";\nimport type { ReserveAttachmentOptions } from \"../types.js\";\nimport { RemoteAttachmentUpload } from \"./remote-attachment-upload.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(private readonly config: SwitchboardClientConfig) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new RemoteAttachmentUpload(reservationId, options, this.config);\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport { AttachmentNotFound } from \"../errors.js\";\nimport type { IAttachmentReader } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both X-Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when X-Attachment-Metadata is absent. If that's missing too,\n // fall back to the response date (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n };\n}\n\nfunction parseMetadata(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed) && parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return contentTypeFallback(response);\n}\n\nexport class RemoteAttachmentStore implements IAttachmentReader {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const { header, body } = await this.fetchAttachment(hash);\n await body.cancel();\n return header;\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n return this.fetchAttachment(hash, signal);\n }\n\n private async fetchAttachment(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n const metadata = parseMetadata(response);\n const now = new Date().toISOString();\n const header: AttachmentHeader = {\n hash,\n mimeType: metadata.mimeType,\n fileName: metadata.fileName,\n sizeBytes: metadata.sizeBytes,\n extension: metadata.extension,\n status: \"available\",\n source: \"sync\",\n createdAtUtc: now,\n lastAccessedAtUtc: now,\n };\n\n return { header, body: response.body };\n }\n}\n","import { AttachmentService } from \"../attachment-service.js\";\nimport type { IAttachmentService } from \"../interfaces.js\";\nimport { RemoteAttachmentStore } from \"./remote-attachment-store.js\";\nimport { RemoteAttachmentUploadFactory } from \"./remote-attachment-upload-factory.js\";\nimport {\n RemoteReservationStore,\n type SwitchboardClientConfig,\n} from \"./remote-reservation-store.js\";\n\nexport function createRemoteAttachmentService(\n config: SwitchboardClientConfig,\n): IAttachmentService {\n const reservations = new RemoteReservationStore(config);\n const uploadFactory = new RemoteAttachmentUploadFactory(config);\n const store = new RemoteAttachmentStore(config);\n return new AttachmentService(store, reservations, uploadFactory);\n}\n","import type { IAttachmentTransport } from \"./interfaces.js\";\nimport type { TransportResponse } from \"./types.js\";\n\n/**\n * No-op transport for deployments without remote sync.\n * fetch() always returns null, announce() and push() are no-ops.\n */\nexport class NullAttachmentTransport implements IAttachmentTransport {\n fetch(): Promise<TransportResponse | null> {\n return Promise.resolve(null);\n }\n\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n push(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentTransport,\n IAttachmentUploadFactory,\n} from \"./interfaces.js\";\nimport type { AttachmentDatabase } from \"./storage/kysely/types.js\";\nimport { AttachmentService } from \"./attachment-service.js\";\nimport { KyselyAttachmentStore } from \"./storage/kysely/attachment-store.js\";\nimport { KyselyReservationStore } from \"./storage/kysely/reservation-store.js\";\nimport { DirectAttachmentUploadFactory } from \"./direct/direct-attachment-upload-factory.js\";\nimport {\n runAttachmentMigrations,\n ATTACHMENT_SCHEMA,\n} from \"./storage/migrations/migrator.js\";\nimport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\n private maxUploadBytes?: number;\n\n constructor(\n private readonly db: Kysely<any>,\n private readonly storagePath: string,\n ) {}\n\n withTransport(transport: IAttachmentTransport): this {\n this.transport = transport;\n return this;\n }\n\n withUploadFactory(factory: IAttachmentUploadFactory): this {\n this.customUploadFactory = factory;\n return this;\n }\n\n withMaxUploadBytes(maxBytes: number): this {\n this.maxUploadBytes = maxBytes;\n return this;\n }\n\n async build(): Promise<AttachmentBuildResult> {\n const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_SCHEMA,\n ) as Kysely<AttachmentDatabase>;\n\n const store = new KyselyAttachmentStore(\n scopedDb,\n this.transport,\n this.storagePath,\n );\n const reservations = new KyselyReservationStore(scopedDb);\n\n const uploadFactory =\n this.customUploadFactory ??\n new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n this.maxUploadBytes,\n );\n\n const service = new AttachmentService(store, reservations, uploadFactory);\n\n return { service, store, reservations, uploadFactory };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["rowToHeader","up","down","up","down","up","down","up","down","migration001","migration002","migration003","migration004","migration005","contentTypeFallback","isRecord","isAttachmentMetadata","isRecord"],"sources":["../src/errors.ts","../src/ref.ts","../src/attachment-service.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/003_add_reservation_expires_at.ts","../src/storage/migrations/004_add_reservation_soft_delete.ts","../src/storage/migrations/005_add_reservation_active_index.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/build-auth-headers.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"sourcesContent":["/**\n * Thrown when an attachment ref or hash is not known to the store.\n */\nexport class AttachmentNotFound extends Error {\n constructor(identifier: string) {\n super(`Attachment not found: ${identifier}`);\n this.name = \"AttachmentNotFound\";\n }\n}\n\n/**\n * Thrown when a reservation ID is not found in the reservation store.\n */\nexport class ReservationNotFound extends Error {\n constructor(reservationId: string) {\n super(`Reservation not found: ${reservationId}`);\n this.name = \"ReservationNotFound\";\n }\n}\n\n/**\n * Thrown when an attachment ref string does not match the expected format.\n */\nexport class InvalidAttachmentRef extends Error {\n constructor(ref: string) {\n super(`Invalid attachment ref: ${ref}`);\n this.name = \"InvalidAttachmentRef\";\n }\n}\n\n/**\n * Thrown when an upload exceeds the configured maximum byte cap.\n * Route handlers should map this to HTTP 413 Payload Too Large.\n */\nexport class UploadTooLarge extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`Upload exceeds maximum size of ${maxBytes} bytes`);\n this.name = \"UploadTooLarge\";\n this.maxBytes = maxBytes;\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { InvalidAttachmentRef } from \"./errors.js\";\n\nconst REF_PATTERN = /^attachment:\\/\\/v(\\d+):(.+)$/;\nconst DEFAULT_VERSION = 1;\n\nexport type ParsedRef = {\n version: number;\n hash: AttachmentHash;\n};\n\nexport function parseRef(ref: AttachmentRef): ParsedRef {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n throw new InvalidAttachmentRef(ref);\n }\n return {\n version: Number(match[1]),\n hash: match[2],\n };\n}\n\nexport function createRef(\n hash: AttachmentHash,\n version: number = DEFAULT_VERSION,\n): AttachmentRef {\n return `attachment://v${version}:${hash}`;\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n ReserveAttachmentOptions,\n} from \"./types.js\";\nimport { parseRef } from \"./ref.js\";\n\nexport class AttachmentService implements IAttachmentService {\n constructor(\n private readonly store: IAttachmentReader,\n private readonly reservations: IReservationStore,\n private readonly uploadFactory: IAttachmentUploadFactory,\n ) {}\n\n async reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload> {\n const reservation = await this.reservations.create(options);\n return this.uploadFactory.createUpload(reservation.reservationId, options);\n }\n\n async stat(ref: AttachmentRef): Promise<AttachmentHeader> {\n const { hash } = parseRef(ref);\n return this.store.stat(hash);\n }\n\n async get(\n ref: AttachmentRef,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const { hash } = parseRef(ref);\n return this.store.get(hash, signal);\n }\n}\n","import { mkdir, rm, access } from \"node:fs/promises\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { UploadTooLarge } from \"../../errors.js\";\n\n/**\n * Compute the absolute storage path for an attachment hash.\n * Uses a 2-level directory fan-out to avoid millions of files\n * in a single directory: ab/cd/abcdef123456...\n */\nexport function storagePath(basePath: string, hash: string): string {\n return join(basePath, storageRelativePath(hash));\n}\n\n/**\n * Compute the relative storage path for an attachment hash.\n * This is what gets stored in the database's storage_path column.\n */\nexport function storageRelativePath(hash: string): string {\n return join(hash.slice(0, 2), hash.slice(2, 4), hash);\n}\n\n/**\n * Write a ReadableStream to disk. Creates parent directories as needed.\n * Returns the number of bytes written.\n */\nexport async function writeAttachmentBytes(\n path: string,\n data: ReadableStream<Uint8Array>,\n): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n\n const writer = createWriteStream(path);\n const reader = data.getReader();\n let bytesWritten = 0;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve) => writer.once(\"drain\", resolve));\n }\n }\n } finally {\n reader.releaseLock();\n await new Promise<void>((resolve, reject) => {\n writer.end(() => resolve());\n writer.on(\"error\", reject);\n });\n }\n\n return bytesWritten;\n}\n\n/**\n * Open a ReadableStream from a file on disk.\n */\nexport function readAttachmentStream(path: string): ReadableStream<Uint8Array> {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;\n}\n\n/**\n * Delete a file from disk. No-op if the file does not exist.\n */\nexport async function deleteAttachmentBytes(path: string): Promise<void> {\n await rm(path, { force: true });\n}\n\n/**\n * Check whether a file exists on disk.\n */\nexport async function attachmentBytesExist(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a ReadableStream from an in-memory buffer.\n */\nexport function streamFromBuffer(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(data);\n controller.close();\n },\n });\n}\n\n/**\n * Stream bytes to a temp file under `${basePath}/.tmp/` while computing the\n * SHA-256 hash, returning the temp path, hex hash, and total bytes written.\n *\n * Bytes are never buffered in memory beyond the current chunk. The caller is\n * responsible for renaming the temp file to its final hash-derived location\n * (or removing it if the content is a duplicate).\n *\n * If `maxBytes` is set and the input exceeds it, the temp file is removed and\n * `UploadTooLarge` is thrown.\n */\nexport async function streamHashAndWrite(\n basePath: string,\n data: ReadableStream<Uint8Array>,\n options: { maxBytes?: number } = {},\n): Promise<{ tempPath: string; hash: string; sizeBytes: number }> {\n const { maxBytes } = options;\n const tmpDir = join(basePath, \".tmp\");\n await mkdir(tmpDir, { recursive: true });\n const tempPath = join(tmpDir, randomUUID());\n\n const hasher = createHash(\"sha256\");\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n let sizeBytes = 0;\n let caughtError: Error | undefined;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n sizeBytes += value.byteLength;\n if (maxBytes !== undefined && sizeBytes > maxBytes) {\n throw new UploadTooLarge(maxBytes);\n }\n hasher.update(value);\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n } catch (err) {\n caughtError = err instanceof Error ? err : new Error(String(err));\n } finally {\n reader.releaseLock();\n }\n\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.on(\"error\", reject);\n });\n\n if (caughtError) {\n await rm(tempPath, { force: true });\n throw caughtError;\n }\n\n return {\n tempPath,\n hash: hasher.digest(\"hex\"),\n sizeBytes,\n };\n}\n","import { join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentStore,\n IAttachmentTransport,\n} from \"../../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n} from \"../../types.js\";\nimport { AttachmentNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, AttachmentRow } from \"./types.js\";\nimport {\n storageRelativePath,\n writeAttachmentBytes,\n readAttachmentStream,\n deleteAttachmentBytes,\n} from \"../fs/attachment-fs.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n };\n}\n\nfunction wrapStreamWithCleanup(\n source: ReadableStream<Uint8Array>,\n cleanup: () => void,\n): ReadableStream<Uint8Array> {\n let cleaned = false;\n const doCleanup = () => {\n if (!cleaned) {\n cleaned = true;\n cleanup();\n }\n };\n\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n doCleanup();\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n doCleanup();\n controller.error(err);\n }\n },\n cancel() {\n doCleanup();\n reader.cancel().catch(() => {});\n },\n });\n}\n\nexport class KyselyAttachmentStore implements IAttachmentStore {\n private readonly activeReaders = new Map<string, number>();\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly transport: IAttachmentTransport,\n private readonly basePath: string,\n ) {}\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row) {\n throw new AttachmentNotFound(hash);\n }\n\n return rowToHeader(row);\n }\n\n async has(hash: AttachmentHash): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .select(\"status\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n return row?.status === \"available\";\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row) {\n throw new AttachmentNotFound(hash);\n }\n\n if (row.status === \"evicted\") {\n const remote = await this.transport.fetch(hash, signal);\n if (!remote) {\n throw new AttachmentNotFound(hash);\n }\n await this.put(hash, remote.metadata, remote.body);\n return this.get(hash, signal);\n }\n\n const now = new Date().toISOString();\n await this.db\n .updateTable(\"attachment\")\n .set({ last_accessed_at_utc: now })\n .where(\"hash\", \"=\", hash)\n .execute();\n\n const header = rowToHeader(row);\n header.lastAccessedAtUtc = now;\n\n this.acquireReader(hash);\n\n const fullPath = join(this.basePath, row.storage_path);\n const rawStream = readAttachmentStream(fullPath);\n const body = wrapStreamWithCleanup(rawStream, () =>\n this.releaseReader(hash),\n );\n\n return { header, body };\n }\n\n async put(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n await data.cancel();\n return;\n }\n\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, data);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: metadata.mimeType,\n file_name: metadata.fileName,\n size_bytes: metadata.sizeBytes,\n extension: metadata.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"sync\",\n created_at_utc: metadata.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n\n async evict(hash: AttachmentHash): Promise<void> {\n if (this.hasActiveReaders(hash)) {\n return;\n }\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .select([\"storage_path\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row || row.status === \"evicted\") {\n return;\n }\n\n const fullPath = join(this.basePath, row.storage_path);\n await deleteAttachmentBytes(fullPath);\n\n await this.db\n .updateTable(\"attachment\")\n .set({ status: \"evicted\" })\n .where(\"hash\", \"=\", hash)\n .execute();\n }\n\n async storageUsed(): Promise<number> {\n const result = await this.db\n .selectFrom(\"attachment\")\n .select(sql<string>`COALESCE(SUM(size_bytes), 0)`.as(\"total\"))\n .where(\"status\", \"=\", \"available\")\n .executeTakeFirst();\n\n return Number(result?.total ?? 0);\n }\n\n // Private: active reader tracking\n\n private acquireReader(hash: string): void {\n this.activeReaders.set(hash, (this.activeReaders.get(hash) ?? 0) + 1);\n }\n\n private releaseReader(hash: string): void {\n const count = (this.activeReaders.get(hash) ?? 1) - 1;\n if (count <= 0) {\n this.activeReaders.delete(hash);\n } else {\n this.activeReaders.set(hash, count);\n }\n }\n\n private hasActiveReaders(hash: string): boolean {\n return (this.activeReaders.get(hash) ?? 0) > 0;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Kysely } from \"kysely\";\nimport type { IReservationStore } from \"../../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../../types.js\";\nimport { ReservationNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, ReservationRow } from \"./types.js\";\n\nexport const DEFAULT_RESERVATION_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction rowToReservation(row: ReservationRow): Reservation {\n return {\n reservationId: row.reservation_id,\n mimeType: row.mime_type,\n fileName: row.file_name,\n extension: row.extension,\n createdAtUtc: row.created_at_utc,\n expiresAtUtc: row.expires_at_utc,\n };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n private readonly ttlMs: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n ttlMs: number = DEFAULT_RESERVATION_TTL_MS,\n ) {\n this.ttlMs = ttlMs;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const nowMs = Date.now();\n const now = new Date(nowMs).toISOString();\n const expiresAt = new Date(nowMs + this.ttlMs).toISOString();\n\n const row = await this.db\n .insertInto(\"attachment_reservation\")\n .values({\n reservation_id: reservationId,\n mime_type: options.mimeType,\n file_name: options.fileName,\n extension: options.extension ?? null,\n created_at_utc: now,\n expires_at_utc: expiresAt,\n })\n .returningAll()\n .executeTakeFirstOrThrow();\n\n return rowToReservation(row);\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const row = await this.db\n .selectFrom(\"attachment_reservation\")\n .selectAll()\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n if (!row) {\n throw new ReservationNotFound(reservationId);\n }\n\n return rowToReservation(row);\n }\n\n async delete(reservationId: string): Promise<void> {\n await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: new Date().toISOString() })\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .execute();\n }\n\n async deleteExpired(now: Date = new Date()): Promise<number> {\n const nowIso = now.toISOString();\n const result = await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: nowIso })\n .where(\"expires_at_utc\", \"<=\", nowIso)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n return Number(result.numUpdatedRows ?? 0);\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment\")\n .addColumn(\"hash\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"size_bytes\", \"bigint\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"status\", \"text\", (col) => col.notNull().defaultTo(\"available\"))\n .addColumn(\"storage_path\", \"text\", (col) => col.notNull())\n .addColumn(\"source\", \"text\", (col) => col.notNull().defaultTo(\"local\"))\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addColumn(\"last_accessed_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_status\")\n .on(\"attachment\")\n .column(\"status\")\n .execute();\n\n // Compound index serves the LRU eviction query:\n // SELECT ... WHERE status = 'available' ORDER BY last_accessed_at_utc ASC\n // A partial index would be ideal but raw SQL doesn't respect withSchema().\n await db.schema\n .createIndex(\"idx_attachment_lru\")\n .on(\"attachment\")\n .columns([\"status\", \"last_accessed_at_utc\"])\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment\").ifExists().execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reservation\")\n .addColumn(\"reservation_id\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reservation\").ifExists().execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"expires_at_utc\", \"text\")\n .execute();\n\n await db\n .updateTable(\"attachment_reservation\")\n .set({ expires_at_utc: sql`created_at_utc` })\n .where(\"expires_at_utc\", \"is\", null)\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .alterColumn(\"expires_at_utc\", (col) => col.setNotNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"expires_at_utc\")\n .execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"deleted_at_utc\", \"text\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"deleted_at_utc\")\n .execute();\n}\n","import { sql, type Kysely, type SqlBool } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at_active\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .where(sql<SqlBool>`deleted_at_utc IS NULL`)\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .dropIndex(\"idx_reservation_expires_at_active\")\n .ifExists()\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { MigrationProvider, Kysely } from \"kysely\";\n\nimport * as migration001 from \"./001_create_attachment_table.js\";\nimport * as migration002 from \"./002_create_reservation_table.js\";\nimport * as migration003 from \"./003_add_reservation_expires_at.js\";\nimport * as migration004 from \"./004_add_reservation_soft_delete.js\";\nimport * as migration005 from \"./005_add_reservation_active_index.js\";\n\nexport const ATTACHMENT_SCHEMA = \"attachments\";\n\nexport interface MigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_table\": migration001,\n \"002_create_reservation_table\": migration002,\n \"003_add_reservation_expires_at\": migration003,\n \"004_add_reservation_soft_delete\": migration004,\n \"005_add_reservation_active_index\": migration005,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nexport async function runAttachmentMigrations(\n db: Kysely<any>,\n schema: string = ATTACHMENT_SCHEMA,\n): Promise<MigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n const migrator = new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n });\n\n let error: unknown;\n let results: Awaited<ReturnType<typeof migrator.migrateToLatest>>[\"results\"];\n try {\n const result = await migrator.migrateToLatest();\n error = result.error;\n results = result.results;\n } catch (e) {\n error = e;\n results = [];\n }\n\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n\n return {\n success: true,\n migrationsExecuted,\n };\n}\n","import { mkdir, rename, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n storageRelativePath,\n streamHashAndWrite,\n} from \"../storage/fs/attachment-fs.js\";\nimport type { AttachmentStatus } from \"../types.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n\n constructor(\n reservationId: string,\n private readonly options: ReserveAttachmentOptions,\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {\n this.reservationId = reservationId;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n // Stream bytes directly to a temp file while hashing. This caps memory\n // usage at one chunk regardless of payload size, and lets us enforce\n // `maxBytes` before either disk or memory grows unbounded.\n const { tempPath, hash, sizeBytes } = await streamHashAndWrite(\n this.basePath,\n data,\n { maxBytes: this.maxBytes },\n );\n\n try {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n // Dedup -- bytes already on disk, drop the temp file.\n await rm(tempPath, { force: true });\n } else {\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await mkdir(dirname(fullPath), { recursive: true });\n await rename(tempPath, fullPath);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: this.options.mimeType,\n file_name: this.options.fileName,\n size_bytes: sizeBytes,\n extension: this.options.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n created_at_utc: now,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n // Existing row was evicted — restore it\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n } catch (err) {\n await rm(tempPath, { force: true });\n throw err;\n }\n\n await this.reservations.delete(this.reservationId);\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirstOrThrow();\n\n return {\n hash,\n ref: createRef(hash),\n header: rowToHeader(row),\n };\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"../interfaces.js\";\nimport type { ReserveAttachmentOptions } from \"../types.js\";\nimport type { AttachmentDatabase } from \"../storage/kysely/types.js\";\nimport { DirectAttachmentUpload } from \"./direct-attachment-upload.js\";\n\nexport class DirectAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservationId,\n options,\n this.db,\n this.basePath,\n this.reservations,\n this.maxBytes,\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\n\nexport async function buildAuthHeaders(\n url: string,\n jwtHandler: JwtHandler | undefined,\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (jwtHandler) {\n const token = await jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { AttachmentMetadata, TransportResponse } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardTransportConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class SwitchboardAttachmentTransport implements IAttachmentTransport {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardTransportConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async fetch(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<TransportResponse | null> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n return null;\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = this.parseMetadataHeaders(response);\n const body = response.body;\n if (!body) {\n throw new Error(\"Response body is null\");\n }\n\n return { hash, metadata, body };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async announce(_hash: AttachmentHash): Promise<void> {\n // No-op for switchboard -- data is already on the server after upload.\n }\n\n async push(\n hash: AttachmentHash,\n remote: string,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const url = `${remote}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n body: data,\n headers,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment push failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n // Compute the fallback at most once; both the recovery path inside the\n // header parser and the outer \"no header / parse failed\" path share it.\n let fallbackCache: AttachmentMetadata | undefined;\n const fallback = (): AttachmentMetadata => {\n if (fallbackCache === undefined) {\n fallbackCache = contentTypeFallback(response);\n }\n return fallbackCache;\n };\n\n const metaHeader = response.headers.get(\"Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed)) {\n if (parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (parsed.createdAtUtc === undefined) {\n parsed.createdAtUtc = fallback().createdAtUtc;\n }\n if (parsed.lastAccessedAtUtc === undefined) {\n parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;\n }\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return fallback();\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (\n value.lastAccessedAtUtc !== undefined &&\n typeof value.lastAccessedAtUtc !== \"string\"\n ) {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when Attachment-Metadata is absent. If that's missing too,\n // fall back to the response Date header (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n lastAccessedAtUtc: createdAtUtc,\n };\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport { ReservationNotFound } from \"../errors.js\";\nimport type { IReservationStore } from \"../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardClientConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isReservation(value: unknown): value is Reservation {\n if (!isRecord(value)) return false;\n if (typeof value.reservationId !== \"string\") return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (typeof value.expiresAtUtc !== \"string\") return false;\n return true;\n}\n\nexport class RemoteReservationStore implements IReservationStore {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"POST\",\n headers: { ...authHeaders, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const json = (await response.json()) as {\n reservationId: string;\n createdAtUtc?: string;\n expiresAtUtc?: string;\n };\n // The server is the source of truth for both timestamps. We synthesize\n // only as a last-resort fallback for older switchboards that don't\n // include them in the response; in that case the client cannot know the\n // server's TTL, so expiresAtUtc is a best-effort placeholder.\n const now = new Date();\n return {\n reservationId: json.reservationId,\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n createdAtUtc: json.createdAtUtc ?? now.toISOString(),\n expiresAtUtc:\n json.expiresAtUtc ??\n new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),\n };\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { headers: authHeaders });\n\n if (response.status === 404) {\n throw new ReservationNotFound(reservationId);\n }\n if (!response.ok) {\n throw new Error(\n `Reservation get failed: ${response.status} ${response.statusText}`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n throw new Error(\"Reservation get returned non-JSON response\");\n }\n if (!isReservation(parsed)) {\n throw new Error(\n \"Reservation get returned a payload that does not match the Reservation shape\",\n );\n }\n return parsed;\n }\n\n async delete(reservationId: string): Promise<void> {\n const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"DELETE\",\n headers: authHeaders,\n });\n\n // 2xx = success; 404 / 410 = already gone, treat as idempotent success.\n if (!response.ok && response.status !== 404 && response.status !== 410) {\n throw new Error(\n `Reservation delete failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n // Sweeping is the server's responsibility; clients have no authority to\n // delete reservations on a remote switchboard.\n deleteExpired(_now?: Date): Promise<number> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.deleteExpired is not supported\"),\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentUpload } from \"../interfaces.js\";\nimport type {\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n // The reserve options are kept for symmetry with DirectAttachmentUpload,\n // but the server already has them tied to the reservation row.\n private readonly options: ReserveAttachmentOptions;\n\n constructor(\n reservationId: string,\n options: ReserveAttachmentOptions,\n config: SwitchboardClientConfig,\n ) {\n this.reservationId = reservationId;\n this.options = options;\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n // Always upload as octet-stream. The server reads the real mime type from\n // the reservation row; sending the user's mime type here (e.g. application/json)\n // would let Express body-parser drain the request body before our handler runs,\n // silently writing zero bytes.\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n headers: { ...authHeaders, \"Content-Type\": \"application/octet-stream\" },\n body: data,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return (await response.json()) as AttachmentUploadResult;\n }\n}\n","import type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../interfaces.js\";\nimport type { ReserveAttachmentOptions } from \"../types.js\";\nimport { RemoteAttachmentUpload } from \"./remote-attachment-upload.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(private readonly config: SwitchboardClientConfig) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new RemoteAttachmentUpload(reservationId, options, this.config);\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport { AttachmentNotFound } from \"../errors.js\";\nimport type { IAttachmentReader } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (\n value.lastAccessedAtUtc !== undefined &&\n typeof value.lastAccessedAtUtc !== \"string\"\n ) {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when Attachment-Metadata is absent. If that's missing too,\n // fall back to the response date (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n lastAccessedAtUtc: createdAtUtc,\n };\n}\n\nfunction parseMetadata(response: Response): AttachmentMetadata {\n // Compute the fallback at most once; both the recovery path inside the\n // header parser and the outer \"no header / parse failed\" path share it.\n let fallbackCache: AttachmentMetadata | undefined;\n const fallback = (): AttachmentMetadata => {\n if (fallbackCache === undefined) {\n fallbackCache = contentTypeFallback(response);\n }\n return fallbackCache;\n };\n\n const metaHeader = response.headers.get(\"Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed)) {\n if (parsed.extension === undefined) {\n parsed.extension = null;\n }\n // Older switchboards may omit these timestamps; fall back to the\n // Date/Last-Modified header so we never produce client-clock-stamped\n // values when the server has authority.\n if (parsed.createdAtUtc === undefined) {\n parsed.createdAtUtc = fallback().createdAtUtc;\n }\n if (parsed.lastAccessedAtUtc === undefined) {\n parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;\n }\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return fallback();\n}\n\nexport class RemoteAttachmentStore implements IAttachmentReader {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"HEAD\",\n headers: authHeaders,\n });\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment stat failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = parseMetadata(response);\n return buildHeader(hash, metadata);\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n return this.fetchAttachment(hash, signal);\n }\n\n private async fetchAttachment(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n const metadata = parseMetadata(response);\n return { header: buildHeader(hash, metadata), body: response.body };\n }\n}\n\nfunction buildHeader(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n): AttachmentHeader {\n return {\n hash,\n mimeType: metadata.mimeType,\n fileName: metadata.fileName,\n sizeBytes: metadata.sizeBytes,\n extension: metadata.extension,\n status: \"available\",\n source: \"sync\",\n createdAtUtc: metadata.createdAtUtc,\n lastAccessedAtUtc: metadata.lastAccessedAtUtc ?? metadata.createdAtUtc,\n };\n}\n","import { AttachmentService } from \"../attachment-service.js\";\nimport type { IAttachmentService } from \"../interfaces.js\";\nimport { RemoteAttachmentStore } from \"./remote-attachment-store.js\";\nimport { RemoteAttachmentUploadFactory } from \"./remote-attachment-upload-factory.js\";\nimport {\n RemoteReservationStore,\n type SwitchboardClientConfig,\n} from \"./remote-reservation-store.js\";\n\nexport function createRemoteAttachmentService(\n config: SwitchboardClientConfig,\n): IAttachmentService {\n const reservations = new RemoteReservationStore(config);\n const uploadFactory = new RemoteAttachmentUploadFactory(config);\n const store = new RemoteAttachmentStore(config);\n return new AttachmentService(store, reservations, uploadFactory);\n}\n","import type { IAttachmentTransport } from \"./interfaces.js\";\nimport type { TransportResponse } from \"./types.js\";\n\n/**\n * No-op transport for deployments without remote sync.\n * fetch() always returns null, announce() and push() are no-ops.\n */\nexport class NullAttachmentTransport implements IAttachmentTransport {\n fetch(): Promise<TransportResponse | null> {\n return Promise.resolve(null);\n }\n\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n push(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentTransport,\n IAttachmentUploadFactory,\n} from \"./interfaces.js\";\nimport type { AttachmentDatabase } from \"./storage/kysely/types.js\";\nimport { AttachmentService } from \"./attachment-service.js\";\nimport { KyselyAttachmentStore } from \"./storage/kysely/attachment-store.js\";\nimport { KyselyReservationStore } from \"./storage/kysely/reservation-store.js\";\nimport { DirectAttachmentUploadFactory } from \"./direct/direct-attachment-upload-factory.js\";\nimport {\n runAttachmentMigrations,\n ATTACHMENT_SCHEMA,\n} from \"./storage/migrations/migrator.js\";\nimport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\n private maxUploadBytes?: number;\n\n constructor(\n private readonly db: Kysely<any>,\n private readonly storagePath: string,\n ) {}\n\n withTransport(transport: IAttachmentTransport): this {\n this.transport = transport;\n return this;\n }\n\n withUploadFactory(factory: IAttachmentUploadFactory): this {\n this.customUploadFactory = factory;\n return this;\n }\n\n withMaxUploadBytes(maxBytes: number): this {\n this.maxUploadBytes = maxBytes;\n return this;\n }\n\n async build(): Promise<AttachmentBuildResult> {\n const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_SCHEMA,\n ) as Kysely<AttachmentDatabase>;\n\n const store = new KyselyAttachmentStore(\n scopedDb,\n this.transport,\n this.storagePath,\n );\n const reservations = new KyselyReservationStore(scopedDb);\n\n const uploadFactory =\n this.customUploadFactory ??\n new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n this.maxUploadBytes,\n );\n\n const service = new AttachmentService(store, reservations, uploadFactory);\n\n return { service, store, reservations, uploadFactory };\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/reactor-attachments",
3
- "version": "6.0.2-staging.5",
3
+ "version": "6.0.2-staging.7",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
24
  "kysely": "0.28.16",
25
- "@powerhousedao/reactor": "6.0.2-staging.5"
25
+ "@powerhousedao/reactor": "6.0.2-staging.7"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@electric-sql/pglite": "0.3.15",