@powerhousedao/reactor-attachments 6.0.2-staging.6 → 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 +19 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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(
|
|
455
|
-
delete(
|
|
466
|
+
get(reservationId: string): Promise<Reservation>;
|
|
467
|
+
delete(reservationId: string): Promise<void>;
|
|
456
468
|
deleteExpired(_now?: Date): Promise<number>;
|
|
457
469
|
}
|
|
458
470
|
//#endregion
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":";;;;;;;cAGa,kBAAA,SAA2B,KAAA;cAC1B,UAAA;AAAA;;;;cASD,mBAAA,SAA4B,KAAA;cAC3B,aAAA;AAAA;;AADd;;cAUa,oBAAA,SAA6B,KAAA;cAC5B,GAAA;AAAA;;;;;;KCnBF,gBAAA;;;;;KAMA,gBAAA;EACV,IAAA,EAAM,cAAA;EACN,QAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,MAAA,EAAQ,gBAAA;EACR,MAAA;EACA,YAAA;EACA,iBAAA;AAAA;;;ADGF
|
|
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":";;;;;;;cAGa,kBAAA,SAA2B,KAAA;cAC1B,UAAA;AAAA;;;;cASD,mBAAA,SAA4B,KAAA;cAC3B,aAAA;AAAA;;AADd;;cAUa,oBAAA,SAA6B,KAAA;cAC5B,GAAA;AAAA;;;;;;KCnBF,gBAAA;;;;;KAMA,gBAAA;EACV,IAAA,EAAM,cAAA;EACN,QAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,MAAA,EAAQ,gBAAA;EACR,MAAA;EACA,YAAA;EACA,iBAAA;AAAA;;;ADGF;;;;;;;;;;;;AClBA;;;;;KAqCY,kBAAA;EACV,QAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,YAAA;EACA,iBAAA;AAAA;;;;KAMU,wBAAA;EACV,QAAA;EACA,QAAA;EACA,SAAA;AAAA;;AAfF;;KAqBY,sBAAA;EACV,IAAA,EAAM,cAAA;EACN,GAAA,EAAK,aAAA;EACL,MAAA,EAAQ,gBAAA;AAAA;;;;KAME,kBAAA;EACV,MAAA,EAAQ,gBAAA;EACR,IAAA,EAAM,cAAA,CAAe,UAAA;AAAA;;;;;;;KASX,iBAAA;EACV,IAAA,EAAM,cAAA;EACN,QAAA,EAAU,kBAAA;EACV,IAAA,EAAM,cAAA,CAAe,UAAA;AAAA;;;;KAMX,yBAAA;EACV,IAAA;EACA,UAAA,EAAY,MAAA;AAAA;;;;;;KAQF,WAAA;EACV,aAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,YAAA;EACA,YAAA;AAAA;;;;;ADzGF;;UEaiB,kBAAA;EFb4B;;;;;;AAU7C;EEWE,OAAA,CAAQ,OAAA,EAAS,wBAAA,GAA2B,OAAA,CAAQ,iBAAA;;;;;;EAOpD,IAAA,CAAK,GAAA,EAAK,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EFjBD;AASnC;;;;;EEgBE,GAAA,CAAI,GAAA,EAAK,aAAA,EAAe,MAAA,GAAS,WAAA,GAAc,OAAA,CAAQ,kBAAA;AAAA;;;;;UAOxC,iBAAA;EDzCL;;;EC6CV,aAAA;ED7C0B;AAM5B;;;;;;;;;;;;;ECuDE,IAAA,CAAK,IAAA,EAAM,cAAA,CAAe,UAAA,IAAc,OAAA,CAAQ,sBAAA;AAAA;;;ADxBlD;;;;;;;UCoCiB,iBAAA;ED/Bf;;;;AAOF;;;ECgCE,IAAA,CAAK,IAAA,EAAM,cAAA,GAAiB,OAAA,CAAQ,gBAAA;ED/BpC;;;;;AAQF;;;;;;;ECqCE,GAAA,CAAI,IAAA,EAAM,cAAA,EAAgB,MAAA,GAAS,WAAA,GAAc,OAAA,CAAQ,kBAAA;AAAA;;;;;;;UAS1C,gBAAA,SAAyB,iBAAA;EDrC9B;;;;;EC2CV,GAAA,CAAI,IAAA,EAAM,cAAA,GAAiB,OAAA;EDzCP;;;;;;;;;AAStB;;EC6CE,GAAA,CACE,IAAA,EAAM,cAAA,EACN,QAAA,EAAU,kBAAA,EACV,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA;EDhDG;;;;;;;;;;;;;;;ECiEN,KAAA,CAAM,IAAA,EAAM,cAAA,GAAiB,OAAA;EDzDM;;;;EC+DnC,WAAA,IAAe,OAAA;AAAA;;;ADrDjB;;;;;UC+DiB,oBAAA;ED5Df;;;;;;;;;ACzFF;;;EAkKE,KAAA,CACE,IAAA,EAAM,cAAA,EACN,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,iBAAA;EA7JyC;;;;;;EAqKpD,QAAA,CAAS,IAAA,EAAM,cAAA,GAAiB,OAAA;EAtJuB;;;;;;;;;;EAkKvD,IAAA,CACE,IAAA,EAAM,cAAA,EACN,MAAA,UACA,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA;AAAA;;;;;UAOY,2BAAA;EACf,QAAA,CAAS,MAAA,EAAQ,yBAAA,GAA4B,oBAAA;AAAA;;;;;UAO9B,iBAAA;EACf,MAAA,CAAO,OAAA,EAAS,wBAAA,GAA2B,OAAA,CAAQ,WAAA;EACnD,GAAA,CAAI,aAAA,WAAwB,OAAA,CAAQ,WAAA;EACpC,MAAA,CAAO,aAAA,WAAwB,OAAA;EA7JpB;;;;;;;EAsKX,aAAA,CAAc,GAAA,GAAM,IAAA,GAAO,OAAA;AAAA;;;;;;UAQZ,wBAAA;EACf,YAAA,CACE,aAAA,UACA,OAAA,EAAS,wBAAA,GACR,iBAAA;AAAA;;;cCrOQ,iBAAA,YAA6B,kBAAA;EAAA,iBAErB,KAAA;EAAA,iBACA,YAAA;EAAA,iBACA,aAAA;cAFA,KAAA,EAAO,iBAAA,EACP,YAAA,EAAc,iBAAA,EACd,aAAA,EAAe,wBAAA;EAG5B,OAAA,CAAQ,OAAA,EAAS,wBAAA,GAA2B,OAAA,CAAQ,iBAAA;EAKpD,IAAA,CAAK,GAAA,EAAK,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAKlC,GAAA,CACJ,GAAA,EAAK,aAAA,EACL,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,kBAAA;AAAA;;;KC7BD,SAAA;EACV,OAAA;EACA,IAAA,EAAM,cAAA;AAAA;AAAA,iBAGQ,QAAA,CAAS,GAAA,EAAK,aAAA,GAAgB,SAAA;AAAA,iBAW9B,SAAA,CACd,IAAA,EAAM,cAAA,EACN,OAAA,YACC,aAAA;;;UCvBc,eAAA;EACf,IAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,SAAA;EACA,MAAA;EACA,YAAA;EACA,MAAA;EACA,cAAA;EACA,oBAAA;AAAA;AAAA,UAGe,0BAAA;EACf,cAAA;EACA,SAAA;EACA,SAAA;EACA,SAAA;EACA,cAAA;EACA,cAAA;EACA,cAAA;AAAA;AAAA,UAGe,kBAAA;EACf,UAAA,EAAY,eAAA;EACZ,sBAAA,EAAwB,0BAAA;AAAA;;;cC6Cb,qBAAA,YAAiC,gBAAA;EAAA,iBAIzB,EAAA;EAAA,iBACA,SAAA;EAAA,iBACA,QAAA;EAAA,iBALF,aAAA;cAGE,EAAA,EAAI,MAAA,CAAO,kBAAA,GACX,SAAA,EAAW,oBAAA,EACX,QAAA;EAGb,IAAA,CAAK,IAAA,EAAM,cAAA,GAAiB,OAAA,CAAQ,gBAAA;EAcpC,GAAA,CAAI,IAAA,EAAM,cAAA,GAAiB,OAAA;EAU3B,GAAA,CACJ,IAAA,EAAM,cAAA,EACN,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,kBAAA;EAyCL,GAAA,CACJ,IAAA,EAAM,cAAA,EACN,QAAA,EAAU,kBAAA,EACV,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA;EAiDG,KAAA,CAAM,IAAA,EAAM,cAAA,GAAiB,OAAA;EAyB7B,WAAA,CAAA,GAAe,OAAA;EAAA,QAYb,aAAA;EAAA,QAIA,aAAA;EAAA,QASA,gBAAA;AAAA;;;cCxOG,sBAAA,YAAkC,iBAAA;EAAA,iBAI1B,EAAA;EAAA,iBAHF,KAAA;cAGE,EAAA,EAAI,MAAA,CAAO,kBAAA,GAC5B,KAAA;EAKI,MAAA,CAAO,OAAA,EAAS,wBAAA,GAA2B,OAAA,CAAQ,WAAA;EAsBnD,GAAA,CAAI,aAAA,WAAwB,OAAA,CAAQ,WAAA;EAepC,MAAA,CAAO,aAAA,WAAwB,OAAA;EAS/B,aAAA,CAAc,GAAA,GAAK,IAAA,GAAoB,OAAA;AAAA;;;cCnElC,iBAAA;AAAA,UAEI,eAAA;EACf,OAAA;EACA,kBAAA;EACA,KAAA,GAAQ,KAAA;AAAA;AAAA,iBAiBY,uBAAA,CACpB,EAAA,EAAI,MAAA,OACJ,MAAA,YACC,OAAA,CAAQ,eAAA;;;cCAE,sBAAA,YAAkC,iBAAA;EAAA,iBAK1B,OAAA;EAAA,iBACA,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,YAAA;EAAA,iBACA,QAAA;EAAA,SARV,aAAA;cAGP,aAAA,UACiB,OAAA,EAAS,wBAAA,EACT,EAAA,EAAI,MAAA,CAAO,kBAAA,GACX,QAAA,UACA,YAAA,EAAc,iBAAA,EACd,QAAA;EAKb,IAAA,CACJ,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA,CAAQ,sBAAA;AAAA;;;cCxCA,6BAAA,YAAyC,wBAAA;EAAA,iBAEjC,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,YAAA;EAAA,iBACA,QAAA;cAHA,EAAA,EAAI,MAAA,CAAO,kBAAA,GACX,QAAA,UACA,YAAA,EAAc,iBAAA,EACd,QAAA;EAGnB,YAAA,CACE,aAAA,UACA,OAAA,EAAS,wBAAA,GACR,iBAAA;AAAA;;;KCfO,0BAAA;EACV,SAAA;EACA,UAAA,GAAa,UAAA;EACb,OAAA,UAAiB,KAAA;AAAA;AAAA,cAGN,8BAAA,YAA0C,oBAAA;EAAA,iBACpC,SAAA;EAAA,iBACA,UAAA;EAAA,iBACA,OAAA;cAEL,MAAA,EAAQ,0BAAA;EAMd,KAAA,CACJ,IAAA,EAAM,cAAA,EACN,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,iBAAA;EA0BL,QAAA,CAAS,KAAA,EAAO,cAAA,GAAiB,OAAA;EAIjC,IAAA,CACJ,IAAA,EAAM,cAAA,EACN,MAAA,UACA,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA;EAAA,QAmBK,oBAAA;AAAA;;;KCzEE,uBAAA;EACV,SAAA;EACA,UAAA,GAAa,UAAA;EACb,OAAA,UAAiB,KAAA;AAAA;AAAA,cAoBN,sBAAA,YAAkC,iBAAA;EAAA,iBAC5B,SAAA;EAAA,iBACA,UAAA;EAAA,iBACA,OAAA;cAEL,MAAA,EAAQ,uBAAA;EAMd,MAAA,CAAO,OAAA,EAAS,wBAAA,GAA2B,OAAA,CAAQ,WAAA;EA0CnD,GAAA,CAAI,aAAA,WAAwB,OAAA,CAAQ,WAAA;EA6BpC,MAAA,CAAO,aAAA,WAAwB,OAAA;EAmBrC,aAAA,CAAc,IAAA,GAAO,IAAA,GAAO,OAAA;AAAA;;;cCzHjB,sBAAA,YAAkC,iBAAA;EAAA,SACpC,aAAA;EAAA,iBACQ,SAAA;EAAA,iBACA,UAAA;EAAA,iBACA,OAAA;EAAA,iBAGA,OAAA;cAGf,aAAA,UACA,OAAA,EAAS,wBAAA,EACT,MAAA,EAAQ,uBAAA;EASJ,IAAA,CACJ,IAAA,EAAM,cAAA,CAAe,UAAA,IACpB,OAAA,CAAQ,sBAAA;AAAA;;;cCxBA,6BAAA,YAAyC,wBAAA;EAAA,iBACvB,MAAA;cAAA,MAAA,EAAQ,uBAAA;EAErC,YAAA,CACE,aAAA,UACA,OAAA,EAAS,wBAAA,GACR,iBAAA;AAAA;;;cC0GQ,qBAAA,YAAiC,iBAAA;EAAA,iBAC3B,SAAA;EAAA,iBACA,UAAA;EAAA,iBACA,OAAA;cAEL,MAAA,EAAQ,uBAAA;EAMd,IAAA,CAAK,IAAA,EAAM,cAAA,GAAiB,OAAA,CAAQ,gBAAA;EAsBpC,GAAA,CACJ,IAAA,EAAM,cAAA,EACN,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,kBAAA;EAAA,QAIG,eAAA;AAAA;;;iBCvJA,6BAAA,CACd,MAAA,EAAQ,uBAAA,GACP,kBAAA;;;;;AhBRH;;ciBIa,uBAAA,YAAmC,oBAAA;EAC9C,KAAA,CAAA,GAAS,OAAA,CAAQ,iBAAA;EAIjB,QAAA,CAAA,GAAY,OAAA;EAIZ,IAAA,CAAA,GAAQ,OAAA;AAAA;;;KCAE,qBAAA;EACV,OAAA,EAAS,iBAAA;EACT,KAAA,EAAO,qBAAA;EACP,YAAA,EAAc,sBAAA;EACd,aAAA,EAAe,wBAAA;AAAA;AAAA,cAGJ,iBAAA;EAAA,iBAMQ,EAAA;EAAA,iBACA,WAAA;EAAA,QANX,SAAA;EAAA,QACA,mBAAA;EAAA,QACA,cAAA;cAGW,EAAA,EAAI,MAAA,OACJ,WAAA;EAGnB,aAAA,CAAc,SAAA,EAAW,oBAAA;EAKzB,iBAAA,CAAkB,OAAA,EAAS,wBAAA;EAK3B,kBAAA,CAAmB,QAAA;EAKb,KAAA,CAAA,GAAS,OAAA,CAAQ,qBAAA;AAAA"}
|
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.
|
|
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
|
|
375
|
-
|
|
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$
|
|
382
|
-
up: () => up$
|
|
382
|
+
down: () => down$4,
|
|
383
|
+
up: () => up$4
|
|
383
384
|
});
|
|
384
|
-
async function up$
|
|
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$
|
|
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$
|
|
396
|
-
up: () => up$
|
|
396
|
+
down: () => down$3,
|
|
397
|
+
up: () => up$3
|
|
397
398
|
});
|
|
398
|
-
async function up$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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
|
|
646
|
+
return fallback();
|
|
609
647
|
}
|
|
610
648
|
};
|
|
611
|
-
function isRecord$
|
|
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$
|
|
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
|
|
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(
|
|
677
|
-
|
|
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(
|
|
680
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
776
|
-
await
|
|
777
|
-
|
|
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.
|
|
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.
|
|
25
|
+
"@powerhousedao/reactor": "6.0.2-staging.7"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@electric-sql/pglite": "0.3.15",
|