@powerhousedao/reactor-attachments 6.0.0-dev.208 → 6.0.0-dev.209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -43,14 +43,19 @@ type AttachmentHeader = {
43
43
  };
44
44
  /**
45
45
  * Metadata provided alongside attachment data during sync.
46
- * The remaining fields (hash, status, source, createdAtUtc, lastAccessedAtUtc)
47
- * are set by the store when it creates the attachment record.
46
+ * `createdAtUtc` is the original upload time, propagated from the source
47
+ * so that the receiving store preserves it instead of synthesizing the
48
+ * fetch time. `lastAccessedAtUtc` is intentionally omitted: it is a
49
+ * per-reactor LRU concern that resets on every read.
50
+ * The remaining fields (hash, status, source, lastAccessedAtUtc) are set
51
+ * by the store when it creates the attachment record.
48
52
  */
49
53
  type AttachmentMetadata = {
50
54
  mimeType: string;
51
55
  fileName: string;
52
56
  sizeBytes: number;
53
57
  extension: string | null;
58
+ createdAtUtc: string;
54
59
  };
55
60
  /**
56
61
  * Options provided when reserving an attachment slot.
@@ -95,7 +100,8 @@ type AttachmentTransportConfig = {
95
100
  };
96
101
  /**
97
102
  * A reservation for an in-progress attachment upload.
98
- * Created by reserve(), deleted when upload.send() completes.
103
+ * Created by reserve(), deleted when upload.send() completes or
104
+ * once expiresAtUtc has passed and a sweep runs.
99
105
  */
100
106
  type Reservation = {
101
107
  reservationId: string;
@@ -103,6 +109,7 @@ type Reservation = {
103
109
  fileName: string;
104
110
  extension: string | null;
105
111
  createdAtUtc: string;
112
+ expiresAtUtc: string;
106
113
  };
107
114
  //#endregion
108
115
  //#region src/interfaces.d.ts
@@ -159,12 +166,15 @@ interface IAttachmentUpload {
159
166
  send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
160
167
  }
161
168
  /**
162
- * Reactor-facing interface for managing local attachment data.
163
- * The IAttachmentTransport calls put when it receives data from a remote.
164
- * The store notifies its configured transport when new data arrives,
165
- * forming a bidirectional store-transport pair.
169
+ * Read-only subset of IAttachmentStore.
170
+ *
171
+ * Adapters that cannot safely support the local-only write/GC surface
172
+ * (remote stores, forwarding caches) implement this narrow interface
173
+ * instead of stub-rejecting unsupported methods. Consumers that only
174
+ * need to query metadata or stream attachment bytes can take this type
175
+ * to make their dependency requirements explicit.
166
176
  */
167
- interface IAttachmentStore {
177
+ interface IAttachmentReader {
168
178
  /**
169
179
  * Get attachment metadata without streaming body data.
170
180
  * Does NOT update lastAccessedAtUtc -- this is a metadata check,
@@ -173,12 +183,6 @@ interface IAttachmentStore {
173
183
  * @throws AttachmentNotFound if the hash is unknown.
174
184
  */
175
185
  stat(hash: AttachmentHash): Promise<AttachmentHeader>;
176
- /**
177
- * Check whether attachment data is available locally.
178
- * Returns true if the bytes can be served from this reactor's store
179
- * without a transport round-trip. Does not trigger a remote fetch.
180
- */
181
- has(hash: AttachmentHash): Promise<boolean>;
182
186
  /**
183
187
  * Retrieve attachment header and data stream by hash.
184
188
  * Updates lastAccessedAtUtc on access.
@@ -192,6 +196,20 @@ interface IAttachmentStore {
192
196
  * record exists).
193
197
  */
194
198
  get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
199
+ }
200
+ /**
201
+ * Reactor-facing interface for managing local attachment data.
202
+ * The IAttachmentTransport calls put when it receives data from a remote.
203
+ * The store notifies its configured transport when new data arrives,
204
+ * forming a bidirectional store-transport pair.
205
+ */
206
+ interface IAttachmentStore extends IAttachmentReader {
207
+ /**
208
+ * Check whether attachment data is available locally.
209
+ * Returns true if the bytes can be served from this reactor's store
210
+ * without a transport round-trip. Does not trigger a remote fetch.
211
+ */
212
+ has(hash: AttachmentHash): Promise<boolean>;
195
213
  /**
196
214
  * Store attachment data received from a remote (during sync or re-fetch).
197
215
  * Called by IAttachmentTransport implementations during sync, and
@@ -281,6 +299,14 @@ interface IReservationStore {
281
299
  create(options: ReserveAttachmentOptions): Promise<Reservation>;
282
300
  get(reservationId: string): Promise<Reservation>;
283
301
  delete(reservationId: string): Promise<void>;
302
+ /**
303
+ * Delete reservations whose expires_at_utc is at or before `now`.
304
+ * Returns the number of rows deleted.
305
+ *
306
+ * Reservations are not auto-swept; consumers should call this on a
307
+ * cron / interval to clean up rows left behind by aborted uploads.
308
+ */
309
+ deleteExpired(now?: Date): Promise<number>;
284
310
  }
285
311
  /**
286
312
  * Factory for creating transport-specific upload handles.
@@ -296,7 +322,7 @@ declare class AttachmentService implements IAttachmentService {
296
322
  private readonly store;
297
323
  private readonly reservations;
298
324
  private readonly uploadFactory;
299
- constructor(store: IAttachmentStore, reservations: IReservationStore, uploadFactory: IAttachmentUploadFactory);
325
+ constructor(store: IAttachmentReader, reservations: IReservationStore, uploadFactory: IAttachmentUploadFactory);
300
326
  reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload>;
301
327
  stat(ref: AttachmentRef): Promise<AttachmentHeader>;
302
328
  get(ref: AttachmentRef, signal?: AbortSignal): Promise<AttachmentResponse>;
@@ -329,6 +355,7 @@ interface AttachmentReservationTable {
329
355
  file_name: string;
330
356
  extension: string | null;
331
357
  created_at_utc: string;
358
+ expires_at_utc: string;
332
359
  }
333
360
  interface AttachmentDatabase {
334
361
  attachment: AttachmentTable;
@@ -356,10 +383,12 @@ declare class KyselyAttachmentStore implements IAttachmentStore {
356
383
  //#region src/storage/kysely/reservation-store.d.ts
357
384
  declare class KyselyReservationStore implements IReservationStore {
358
385
  private readonly db;
359
- constructor(db: Kysely<AttachmentDatabase>);
386
+ private readonly ttlMs;
387
+ constructor(db: Kysely<AttachmentDatabase>, ttlMs?: number);
360
388
  create(options: ReserveAttachmentOptions): Promise<Reservation>;
361
389
  get(reservationId: string): Promise<Reservation>;
362
390
  delete(reservationId: string): Promise<void>;
391
+ deleteExpired(now?: Date): Promise<number>;
363
392
  }
364
393
  //#endregion
365
394
  //#region src/storage/migrations/migrator.d.ts
@@ -377,8 +406,9 @@ declare class DirectAttachmentUpload implements IAttachmentUpload {
377
406
  private readonly db;
378
407
  private readonly basePath;
379
408
  private readonly reservations;
409
+ private readonly maxBytes?;
380
410
  readonly reservationId: string;
381
- constructor(reservationId: string, options: ReserveAttachmentOptions, db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore);
411
+ constructor(reservationId: string, options: ReserveAttachmentOptions, db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
382
412
  send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
383
413
  }
384
414
  //#endregion
@@ -387,7 +417,8 @@ declare class DirectAttachmentUploadFactory implements IAttachmentUploadFactory
387
417
  private readonly db;
388
418
  private readonly basePath;
389
419
  private readonly reservations;
390
- constructor(db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore);
420
+ private readonly maxBytes?;
421
+ constructor(db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
391
422
  createUpload(reservationId: string, options: ReserveAttachmentOptions): IAttachmentUpload;
392
423
  }
393
424
  //#endregion
@@ -405,10 +436,58 @@ declare class SwitchboardAttachmentTransport implements IAttachmentTransport {
405
436
  fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<TransportResponse | null>;
406
437
  announce(_hash: AttachmentHash): Promise<void>;
407
438
  push(hash: AttachmentHash, remote: string, data: ReadableStream<Uint8Array>): Promise<void>;
408
- private buildHeaders;
409
439
  private parseMetadataHeaders;
410
440
  }
411
441
  //#endregion
442
+ //#region src/switchboard/remote-reservation-store.d.ts
443
+ type SwitchboardClientConfig = {
444
+ remoteUrl: string;
445
+ jwtHandler?: JwtHandler;
446
+ fetchFn?: typeof fetch;
447
+ };
448
+ declare class RemoteReservationStore implements IReservationStore {
449
+ private readonly remoteUrl;
450
+ private readonly jwtHandler?;
451
+ private readonly fetchFn;
452
+ constructor(config: SwitchboardClientConfig);
453
+ create(options: ReserveAttachmentOptions): Promise<Reservation>;
454
+ get(_reservationId: string): Promise<Reservation>;
455
+ delete(_reservationId: string): Promise<void>;
456
+ deleteExpired(_now?: Date): Promise<number>;
457
+ }
458
+ //#endregion
459
+ //#region src/switchboard/remote-attachment-upload.d.ts
460
+ declare class RemoteAttachmentUpload implements IAttachmentUpload {
461
+ readonly reservationId: string;
462
+ private readonly remoteUrl;
463
+ private readonly jwtHandler?;
464
+ private readonly fetchFn;
465
+ private readonly options;
466
+ constructor(reservationId: string, options: ReserveAttachmentOptions, config: SwitchboardClientConfig);
467
+ send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
468
+ }
469
+ //#endregion
470
+ //#region src/switchboard/remote-attachment-upload-factory.d.ts
471
+ declare class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {
472
+ private readonly config;
473
+ constructor(config: SwitchboardClientConfig);
474
+ createUpload(reservationId: string, options: ReserveAttachmentOptions): IAttachmentUpload;
475
+ }
476
+ //#endregion
477
+ //#region src/switchboard/remote-attachment-store.d.ts
478
+ declare class RemoteAttachmentStore implements IAttachmentReader {
479
+ private readonly remoteUrl;
480
+ private readonly jwtHandler?;
481
+ private readonly fetchFn;
482
+ constructor(config: SwitchboardClientConfig);
483
+ stat(hash: AttachmentHash): Promise<AttachmentHeader>;
484
+ get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
485
+ private fetchAttachment;
486
+ }
487
+ //#endregion
488
+ //#region src/switchboard/create-remote-attachment-service.d.ts
489
+ declare function createRemoteAttachmentService(config: SwitchboardClientConfig): IAttachmentService;
490
+ //#endregion
412
491
  //#region src/null-attachment-transport.d.ts
413
492
  /**
414
493
  * No-op transport for deployments without remote sync.
@@ -432,11 +511,13 @@ declare class AttachmentBuilder {
432
511
  private readonly storagePath;
433
512
  private transport;
434
513
  private customUploadFactory?;
514
+ private maxUploadBytes?;
435
515
  constructor(db: Kysely<any>, storagePath: string);
436
516
  withTransport(transport: IAttachmentTransport): this;
437
517
  withUploadFactory(factory: IAttachmentUploadFactory): this;
518
+ withMaxUploadBytes(maxBytes: number): this;
438
519
  build(): Promise<AttachmentBuildResult>;
439
520
  }
440
521
  //#endregion
441
- export { ATTACHMENT_SCHEMA, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DirectAttachmentUpload, DirectAttachmentUploadFactory, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SwitchboardAttachmentTransport, type SwitchboardTransportConfig, type TransportResponse, createRef, parseRef, runAttachmentMigrations };
522
+ export { ATTACHMENT_SCHEMA, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DirectAttachmentUpload, DirectAttachmentUploadFactory, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportResponse, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
442
523
  //# sourceMappingURL=index.d.ts.map
@@ -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/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/interfaces.ts","../src/attachment-service.ts","../src/ref.ts","../src/storage/kysely/types.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { Migrator, sql } from "kysely";
3
- import { mkdir, rm } from "node:fs/promises";
3
+ import { mkdir, rename, rm } from "node:fs/promises";
4
4
  import { createReadStream, createWriteStream } from "node:fs";
5
- import { Readable } from "node:stream";
6
5
  import { createHash, randomUUID } from "node:crypto";
6
+ import { Readable } from "node:stream";
7
7
  //#region \0rolldown/runtime.js
8
8
  var __defProp = Object.defineProperty;
9
9
  var __exportAll = (all, no_symbols) => {
@@ -44,6 +44,18 @@ var InvalidAttachmentRef = class extends Error {
44
44
  this.name = "InvalidAttachmentRef";
45
45
  }
46
46
  };
47
+ /**
48
+ * Thrown when an upload exceeds the configured maximum byte cap.
49
+ * Route handlers should map this to HTTP 413 Payload Too Large.
50
+ */
51
+ var UploadTooLarge = class extends Error {
52
+ maxBytes;
53
+ constructor(maxBytes) {
54
+ super(`Upload exceeds maximum size of ${maxBytes} bytes`);
55
+ this.name = "UploadTooLarge";
56
+ this.maxBytes = maxBytes;
57
+ }
58
+ };
47
59
  //#endregion
48
60
  //#region src/ref.ts
49
61
  const REF_PATTERN = /^attachment:\/\/v(\d+):(.+)$/;
@@ -128,13 +140,67 @@ async function deleteAttachmentBytes(path) {
128
140
  await rm(path, { force: true });
129
141
  }
130
142
  /**
131
- * Create a ReadableStream from an in-memory buffer.
143
+ * Stream bytes to a temp file under `${basePath}/.tmp/` while computing the
144
+ * SHA-256 hash, returning the temp path, hex hash, and total bytes written.
145
+ *
146
+ * Bytes are never buffered in memory beyond the current chunk. The caller is
147
+ * responsible for renaming the temp file to its final hash-derived location
148
+ * (or removing it if the content is a duplicate).
149
+ *
150
+ * If `maxBytes` is set and the input exceeds it, the temp file is removed and
151
+ * `UploadTooLarge` is thrown.
132
152
  */
133
- function streamFromBuffer(data) {
134
- return new ReadableStream({ start(controller) {
135
- controller.enqueue(data);
136
- controller.close();
137
- } });
153
+ async function streamHashAndWrite(basePath, data, options = {}) {
154
+ const { maxBytes } = options;
155
+ const tmpDir = join(basePath, ".tmp");
156
+ await mkdir(tmpDir, { recursive: true });
157
+ const tempPath = join(tmpDir, randomUUID());
158
+ const hasher = createHash("sha256");
159
+ const writer = createWriteStream(tempPath);
160
+ const reader = data.getReader();
161
+ let sizeBytes = 0;
162
+ let caughtError;
163
+ try {
164
+ for (;;) {
165
+ const { done, value } = await reader.read();
166
+ if (done) break;
167
+ sizeBytes += value.byteLength;
168
+ if (maxBytes !== void 0 && sizeBytes > maxBytes) throw new UploadTooLarge(maxBytes);
169
+ hasher.update(value);
170
+ if (!writer.write(value)) await new Promise((resolve, reject) => {
171
+ const onDrain = () => {
172
+ writer.off("error", onError);
173
+ resolve();
174
+ };
175
+ const onError = (err) => {
176
+ writer.off("drain", onDrain);
177
+ reject(err);
178
+ };
179
+ writer.once("drain", onDrain);
180
+ writer.once("error", onError);
181
+ });
182
+ }
183
+ } catch (err) {
184
+ caughtError = err instanceof Error ? err : new Error(String(err));
185
+ } finally {
186
+ reader.releaseLock();
187
+ }
188
+ await new Promise((resolve, reject) => {
189
+ writer.end((err) => {
190
+ if (err) reject(err);
191
+ else resolve();
192
+ });
193
+ writer.on("error", reject);
194
+ });
195
+ if (caughtError) {
196
+ await rm(tempPath, { force: true });
197
+ throw caughtError;
198
+ }
199
+ return {
200
+ tempPath,
201
+ hash: hasher.digest("hex"),
202
+ sizeBytes
203
+ };
138
204
  }
139
205
  //#endregion
140
206
  //#region src/storage/kysely/attachment-store.ts
@@ -231,7 +297,7 @@ var KyselyAttachmentStore = class {
231
297
  status: "available",
232
298
  storage_path: relPath,
233
299
  source: "sync",
234
- created_at_utc: now,
300
+ created_at_utc: metadata.createdAtUtc,
235
301
  last_accessed_at_utc: now
236
302
  }).onConflict((oc) => oc.column("hash").doNothing()).execute();
237
303
  else await this.db.updateTable("attachment").set({
@@ -265,28 +331,35 @@ var KyselyAttachmentStore = class {
265
331
  };
266
332
  //#endregion
267
333
  //#region src/storage/kysely/reservation-store.ts
334
+ const DEFAULT_RESERVATION_TTL_MS = 1440 * 60 * 1e3;
268
335
  function rowToReservation(row) {
269
336
  return {
270
337
  reservationId: row.reservation_id,
271
338
  mimeType: row.mime_type,
272
339
  fileName: row.file_name,
273
340
  extension: row.extension,
274
- createdAtUtc: row.created_at_utc
341
+ createdAtUtc: row.created_at_utc,
342
+ expiresAtUtc: row.expires_at_utc
275
343
  };
276
344
  }
277
345
  var KyselyReservationStore = class {
278
- constructor(db) {
346
+ ttlMs;
347
+ constructor(db, ttlMs = DEFAULT_RESERVATION_TTL_MS) {
279
348
  this.db = db;
349
+ this.ttlMs = ttlMs;
280
350
  }
281
351
  async create(options) {
282
352
  const reservationId = randomUUID();
283
- const now = (/* @__PURE__ */ new Date()).toISOString();
353
+ const nowMs = Date.now();
354
+ const now = new Date(nowMs).toISOString();
355
+ const expiresAt = new Date(nowMs + this.ttlMs).toISOString();
284
356
  return rowToReservation(await this.db.insertInto("attachment_reservation").values({
285
357
  reservation_id: reservationId,
286
358
  mime_type: options.mimeType,
287
359
  file_name: options.fileName,
288
360
  extension: options.extension ?? null,
289
- created_at_utc: now
361
+ created_at_utc: now,
362
+ expires_at_utc: expiresAt
290
363
  }).returningAll().executeTakeFirstOrThrow());
291
364
  }
292
365
  async get(reservationId) {
@@ -297,39 +370,60 @@ var KyselyReservationStore = class {
297
370
  async delete(reservationId) {
298
371
  await this.db.deleteFrom("attachment_reservation").where("reservation_id", "=", reservationId).execute();
299
372
  }
373
+ async deleteExpired(now = /* @__PURE__ */ new Date()) {
374
+ const result = await this.db.deleteFrom("attachment_reservation").where("expires_at_utc", "<=", now.toISOString()).executeTakeFirst();
375
+ return Number(result.numDeletedRows ?? 0);
376
+ }
300
377
  };
301
378
  //#endregion
302
379
  //#region src/storage/migrations/001_create_attachment_table.ts
303
380
  var _001_create_attachment_table_exports = /* @__PURE__ */ __exportAll({
304
- down: () => down$1,
305
- up: () => up$1
381
+ down: () => down$2,
382
+ up: () => up$2
306
383
  });
307
- async function up$1(db) {
384
+ async function up$2(db) {
308
385
  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();
309
386
  await db.schema.createIndex("idx_attachment_status").on("attachment").column("status").execute();
310
387
  await db.schema.createIndex("idx_attachment_lru").on("attachment").columns(["status", "last_accessed_at_utc"]).execute();
311
388
  }
312
- async function down$1(db) {
389
+ async function down$2(db) {
313
390
  await db.schema.dropTable("attachment").ifExists().execute();
314
391
  }
315
392
  //#endregion
316
393
  //#region src/storage/migrations/002_create_reservation_table.ts
317
394
  var _002_create_reservation_table_exports = /* @__PURE__ */ __exportAll({
395
+ down: () => down$1,
396
+ up: () => up$1
397
+ });
398
+ async function up$1(db) {
399
+ 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
+ async function down$1(db) {
402
+ await db.schema.dropTable("attachment_reservation").ifExists().execute();
403
+ }
404
+ //#endregion
405
+ //#region src/storage/migrations/003_add_reservation_expires_at.ts
406
+ var _003_add_reservation_expires_at_exports = /* @__PURE__ */ __exportAll({
318
407
  down: () => down,
319
408
  up: () => up
320
409
  });
321
410
  async function up(db) {
322
- 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();
411
+ await db.schema.alterTable("attachment_reservation").addColumn("expires_at_utc", "text").execute();
412
+ await db.updateTable("attachment_reservation").set({ expires_at_utc: sql`created_at_utc` }).where("expires_at_utc", "is", null).execute();
413
+ await db.schema.alterTable("attachment_reservation").alterColumn("expires_at_utc", (col) => col.setNotNull()).execute();
414
+ await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
323
415
  }
324
416
  async function down(db) {
325
- await db.schema.dropTable("attachment_reservation").ifExists().execute();
417
+ await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
418
+ await db.schema.alterTable("attachment_reservation").dropColumn("expires_at_utc").execute();
326
419
  }
327
420
  //#endregion
328
421
  //#region src/storage/migrations/migrator.ts
329
422
  const ATTACHMENT_SCHEMA = "attachments";
330
423
  const migrations = {
331
424
  "001_create_attachment_table": _001_create_attachment_table_exports,
332
- "002_create_reservation_table": _002_create_reservation_table_exports
425
+ "002_create_reservation_table": _002_create_reservation_table_exports,
426
+ "003_add_reservation_expires_at": _003_add_reservation_expires_at_exports
333
427
  };
334
428
  var ProgrammaticMigrationProvider = class {
335
429
  getMigrations() {
@@ -387,62 +481,49 @@ function rowToHeader(row) {
387
481
  lastAccessedAtUtc: row.last_accessed_at_utc
388
482
  };
389
483
  }
390
- async function collectAndHash(data) {
391
- const hasher = createHash("sha256");
392
- const chunks = [];
393
- const reader = data.getReader();
394
- for (;;) {
395
- const { done, value } = await reader.read();
396
- if (done) break;
397
- hasher.update(value);
398
- chunks.push(value);
399
- }
400
- const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);
401
- const bytes = new Uint8Array(totalLength);
402
- let offset = 0;
403
- for (const chunk of chunks) {
404
- bytes.set(chunk, offset);
405
- offset += chunk.byteLength;
406
- }
407
- return {
408
- bytes,
409
- hash: hasher.digest("hex")
410
- };
411
- }
412
484
  var DirectAttachmentUpload = class {
413
485
  reservationId;
414
- constructor(reservationId, options, db, basePath, reservations) {
486
+ constructor(reservationId, options, db, basePath, reservations, maxBytes) {
415
487
  this.options = options;
416
488
  this.db = db;
417
489
  this.basePath = basePath;
418
490
  this.reservations = reservations;
491
+ this.maxBytes = maxBytes;
419
492
  this.reservationId = reservationId;
420
493
  }
421
494
  async send(data) {
422
- const { bytes, hash } = await collectAndHash(data);
423
- const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
424
- if (existing?.status !== "available") {
425
- const relPath = storageRelativePath(hash);
426
- await writeAttachmentBytes(join(this.basePath, relPath), streamFromBuffer(bytes));
427
- const now = (/* @__PURE__ */ new Date()).toISOString();
428
- if (!existing) await this.db.insertInto("attachment").values({
429
- hash,
430
- mime_type: this.options.mimeType,
431
- file_name: this.options.fileName,
432
- size_bytes: bytes.byteLength,
433
- extension: this.options.extension ?? null,
434
- status: "available",
435
- storage_path: relPath,
436
- source: "local",
437
- created_at_utc: now,
438
- last_accessed_at_utc: now
439
- }).onConflict((oc) => oc.column("hash").doNothing()).execute();
440
- else await this.db.updateTable("attachment").set({
441
- status: "available",
442
- storage_path: relPath,
443
- source: "local",
444
- last_accessed_at_utc: now
445
- }).where("hash", "=", hash).where("status", "=", "evicted").execute();
495
+ const { tempPath, hash, sizeBytes } = await streamHashAndWrite(this.basePath, data, { maxBytes: this.maxBytes });
496
+ try {
497
+ const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
498
+ if (existing?.status === "available") await rm(tempPath, { force: true });
499
+ else {
500
+ const relPath = storageRelativePath(hash);
501
+ const fullPath = join(this.basePath, relPath);
502
+ await mkdir(dirname(fullPath), { recursive: true });
503
+ await rename(tempPath, fullPath);
504
+ const now = (/* @__PURE__ */ new Date()).toISOString();
505
+ if (!existing) await this.db.insertInto("attachment").values({
506
+ hash,
507
+ mime_type: this.options.mimeType,
508
+ file_name: this.options.fileName,
509
+ size_bytes: sizeBytes,
510
+ extension: this.options.extension ?? null,
511
+ status: "available",
512
+ storage_path: relPath,
513
+ source: "local",
514
+ created_at_utc: now,
515
+ last_accessed_at_utc: now
516
+ }).onConflict((oc) => oc.column("hash").doNothing()).execute();
517
+ else await this.db.updateTable("attachment").set({
518
+ status: "available",
519
+ storage_path: relPath,
520
+ source: "local",
521
+ last_accessed_at_utc: now
522
+ }).where("hash", "=", hash).where("status", "=", "evicted").execute();
523
+ }
524
+ } catch (err) {
525
+ await rm(tempPath, { force: true });
526
+ throw err;
446
527
  }
447
528
  await this.reservations.delete(this.reservationId);
448
529
  const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirstOrThrow();
@@ -456,16 +537,27 @@ var DirectAttachmentUpload = class {
456
537
  //#endregion
457
538
  //#region src/direct/direct-attachment-upload-factory.ts
458
539
  var DirectAttachmentUploadFactory = class {
459
- constructor(db, basePath, reservations) {
540
+ constructor(db, basePath, reservations, maxBytes) {
460
541
  this.db = db;
461
542
  this.basePath = basePath;
462
543
  this.reservations = reservations;
544
+ this.maxBytes = maxBytes;
463
545
  }
464
546
  createUpload(reservationId, options) {
465
- return new DirectAttachmentUpload(reservationId, options, this.db, this.basePath, this.reservations);
547
+ return new DirectAttachmentUpload(reservationId, options, this.db, this.basePath, this.reservations, this.maxBytes);
466
548
  }
467
549
  };
468
550
  //#endregion
551
+ //#region src/switchboard/build-auth-headers.ts
552
+ async function buildAuthHeaders(url, jwtHandler) {
553
+ const headers = {};
554
+ if (jwtHandler) {
555
+ const token = await jwtHandler(url);
556
+ if (token) headers["Authorization"] = `Bearer ${token}`;
557
+ }
558
+ return headers;
559
+ }
560
+ //#endregion
469
561
  //#region src/switchboard/switchboard-attachment-transport.ts
470
562
  var SwitchboardAttachmentTransport = class {
471
563
  remoteUrl;
@@ -478,7 +570,7 @@ var SwitchboardAttachmentTransport = class {
478
570
  }
479
571
  async fetch(hash, signal) {
480
572
  const url = `${this.remoteUrl}/attachments/${hash}`;
481
- const headers = await this.buildHeaders(url);
573
+ const headers = await buildAuthHeaders(url, this.jwtHandler);
482
574
  const response = await this.fetchFn(url, {
483
575
  signal,
484
576
  headers
@@ -497,7 +589,7 @@ var SwitchboardAttachmentTransport = class {
497
589
  async announce(_hash) {}
498
590
  async push(hash, remote, data) {
499
591
  const url = `${remote}/attachments/${hash}`;
500
- const headers = await this.buildHeaders(url);
592
+ const headers = await buildAuthHeaders(url, this.jwtHandler);
501
593
  const response = await this.fetchFn(url, {
502
594
  method: "PUT",
503
595
  body: data,
@@ -506,26 +598,223 @@ var SwitchboardAttachmentTransport = class {
506
598
  });
507
599
  if (!response.ok) throw new Error(`Attachment push failed: ${response.status} ${response.statusText}`);
508
600
  }
509
- async buildHeaders(url) {
510
- const headers = {};
511
- if (this.jwtHandler) {
512
- const token = await this.jwtHandler(url);
513
- if (token) headers["Authorization"] = `Bearer ${token}`;
514
- }
515
- return headers;
516
- }
517
601
  parseMetadataHeaders(response) {
518
602
  const metaHeader = response.headers.get("X-Attachment-Metadata");
519
- if (metaHeader) return JSON.parse(metaHeader);
603
+ if (metaHeader) try {
604
+ const parsed = JSON.parse(metaHeader);
605
+ if (isRecord$1(parsed) && parsed.extension === void 0) parsed.extension = null;
606
+ if (isAttachmentMetadata$1(parsed)) return parsed;
607
+ } catch {}
608
+ return contentTypeFallback$1(response);
609
+ }
610
+ };
611
+ function isRecord$1(value) {
612
+ return typeof value === "object" && value !== null && !Array.isArray(value);
613
+ }
614
+ function isAttachmentMetadata$1(value) {
615
+ if (!isRecord$1(value)) return false;
616
+ if (typeof value.mimeType !== "string") return false;
617
+ if (typeof value.fileName !== "string") return false;
618
+ if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
619
+ if (value.extension !== null && typeof value.extension !== "string") return false;
620
+ return true;
621
+ }
622
+ function contentTypeFallback$1(response) {
623
+ const contentLength = response.headers.get("Content-Length");
624
+ if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
625
+ const sizeBytes = Number(contentLength);
626
+ if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
627
+ const lastModified = response.headers.get("Last-Modified");
628
+ const dateHeader = response.headers.get("Date");
629
+ const createdAtUtc = lastModified ? new Date(lastModified).toISOString() : dateHeader ? new Date(dateHeader).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
630
+ return {
631
+ mimeType: response.headers.get("Content-Type") ?? "application/octet-stream",
632
+ fileName: "unknown",
633
+ sizeBytes,
634
+ extension: null,
635
+ createdAtUtc
636
+ };
637
+ }
638
+ //#endregion
639
+ //#region src/switchboard/remote-reservation-store.ts
640
+ var RemoteReservationStore = class {
641
+ remoteUrl;
642
+ jwtHandler;
643
+ fetchFn;
644
+ constructor(config) {
645
+ this.remoteUrl = config.remoteUrl;
646
+ this.jwtHandler = config.jwtHandler;
647
+ this.fetchFn = config.fetchFn ?? globalThis.fetch;
648
+ }
649
+ async create(options) {
650
+ const url = `${this.remoteUrl}/attachments/reservations`;
651
+ const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
652
+ const response = await this.fetchFn(url, {
653
+ method: "POST",
654
+ headers: {
655
+ ...authHeaders,
656
+ "Content-Type": "application/json"
657
+ },
658
+ body: JSON.stringify({
659
+ mimeType: options.mimeType,
660
+ fileName: options.fileName,
661
+ extension: options.extension ?? null
662
+ })
663
+ });
664
+ if (!response.ok) throw new Error(`Reservation create failed: ${response.status} ${response.statusText}`);
665
+ const json = await response.json();
666
+ const now = /* @__PURE__ */ new Date();
667
+ return {
668
+ reservationId: json.reservationId,
669
+ mimeType: options.mimeType,
670
+ fileName: options.fileName,
671
+ extension: options.extension ?? null,
672
+ createdAtUtc: json.createdAtUtc ?? now.toISOString(),
673
+ expiresAtUtc: json.expiresAtUtc ?? new Date(now.getTime() + 1440 * 60 * 1e3).toISOString()
674
+ };
675
+ }
676
+ get(_reservationId) {
677
+ return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.get is not supported"));
678
+ }
679
+ delete(_reservationId) {
680
+ return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.delete is not supported"));
681
+ }
682
+ deleteExpired(_now) {
683
+ return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.deleteExpired is not supported"));
684
+ }
685
+ };
686
+ //#endregion
687
+ //#region src/switchboard/remote-attachment-upload.ts
688
+ var RemoteAttachmentUpload = class {
689
+ reservationId;
690
+ remoteUrl;
691
+ jwtHandler;
692
+ fetchFn;
693
+ options;
694
+ constructor(reservationId, options, config) {
695
+ this.reservationId = reservationId;
696
+ this.options = options;
697
+ this.remoteUrl = config.remoteUrl;
698
+ this.jwtHandler = config.jwtHandler;
699
+ this.fetchFn = config.fetchFn ?? globalThis.fetch;
700
+ }
701
+ async send(data) {
702
+ const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;
703
+ const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
704
+ const response = await this.fetchFn(url, {
705
+ method: "PUT",
706
+ headers: {
707
+ ...authHeaders,
708
+ "Content-Type": "application/octet-stream"
709
+ },
710
+ body: data,
711
+ duplex: "half"
712
+ });
713
+ if (!response.ok) throw new Error(`Attachment upload failed: ${response.status} ${response.statusText}`);
714
+ return await response.json();
715
+ }
716
+ };
717
+ //#endregion
718
+ //#region src/switchboard/remote-attachment-upload-factory.ts
719
+ var RemoteAttachmentUploadFactory = class {
720
+ constructor(config) {
721
+ this.config = config;
722
+ }
723
+ createUpload(reservationId, options) {
724
+ return new RemoteAttachmentUpload(reservationId, options, this.config);
725
+ }
726
+ };
727
+ //#endregion
728
+ //#region src/switchboard/remote-attachment-store.ts
729
+ function isRecord(value) {
730
+ return typeof value === "object" && value !== null && !Array.isArray(value);
731
+ }
732
+ function isAttachmentMetadata(value) {
733
+ if (!isRecord(value)) return false;
734
+ if (typeof value.mimeType !== "string") return false;
735
+ if (typeof value.fileName !== "string") return false;
736
+ if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
737
+ if (value.extension !== null && typeof value.extension !== "string") return false;
738
+ return true;
739
+ }
740
+ function contentTypeFallback(response) {
741
+ const contentLength = response.headers.get("Content-Length");
742
+ if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
743
+ const sizeBytes = Number(contentLength);
744
+ if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
745
+ const lastModified = response.headers.get("Last-Modified");
746
+ const dateHeader = response.headers.get("Date");
747
+ const createdAtUtc = lastModified ? new Date(lastModified).toISOString() : dateHeader ? new Date(dateHeader).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
748
+ return {
749
+ mimeType: response.headers.get("Content-Type") ?? "application/octet-stream",
750
+ fileName: "unknown",
751
+ sizeBytes,
752
+ extension: null,
753
+ createdAtUtc
754
+ };
755
+ }
756
+ function parseMetadata(response) {
757
+ const metaHeader = response.headers.get("X-Attachment-Metadata");
758
+ if (metaHeader) try {
759
+ const parsed = JSON.parse(metaHeader);
760
+ if (isRecord(parsed) && parsed.extension === void 0) parsed.extension = null;
761
+ if (isAttachmentMetadata(parsed)) return parsed;
762
+ } catch {}
763
+ return contentTypeFallback(response);
764
+ }
765
+ var RemoteAttachmentStore = class {
766
+ remoteUrl;
767
+ jwtHandler;
768
+ fetchFn;
769
+ constructor(config) {
770
+ this.remoteUrl = config.remoteUrl;
771
+ this.jwtHandler = config.jwtHandler;
772
+ this.fetchFn = config.fetchFn ?? globalThis.fetch;
773
+ }
774
+ async stat(hash) {
775
+ const { header, body } = await this.fetchAttachment(hash);
776
+ await body.cancel();
777
+ return header;
778
+ }
779
+ async get(hash, signal) {
780
+ return this.fetchAttachment(hash, signal);
781
+ }
782
+ async fetchAttachment(hash, signal) {
783
+ const url = `${this.remoteUrl}/attachments/${hash}`;
784
+ const headers = await buildAuthHeaders(url, this.jwtHandler);
785
+ const response = await this.fetchFn(url, {
786
+ signal,
787
+ headers
788
+ });
789
+ if (response.status === 404) throw new AttachmentNotFound(hash);
790
+ if (!response.ok) throw new Error(`Attachment fetch failed: ${response.status} ${response.statusText}`);
791
+ if (!response.body) throw new Error("Response body is null");
792
+ const metadata = parseMetadata(response);
793
+ const now = (/* @__PURE__ */ new Date()).toISOString();
520
794
  return {
521
- mimeType: response.headers.get("Content-Type") ?? "application/octet-stream",
522
- fileName: "unknown",
523
- sizeBytes: Number(response.headers.get("Content-Length") ?? 0),
524
- extension: null
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
+ },
806
+ body: response.body
525
807
  };
526
808
  }
527
809
  };
528
810
  //#endregion
811
+ //#region src/switchboard/create-remote-attachment-service.ts
812
+ function createRemoteAttachmentService(config) {
813
+ const reservations = new RemoteReservationStore(config);
814
+ const uploadFactory = new RemoteAttachmentUploadFactory(config);
815
+ return new AttachmentService(new RemoteAttachmentStore(config), reservations, uploadFactory);
816
+ }
817
+ //#endregion
529
818
  //#region src/null-attachment-transport.ts
530
819
  /**
531
820
  * No-op transport for deployments without remote sync.
@@ -547,6 +836,7 @@ var NullAttachmentTransport = class {
547
836
  var AttachmentBuilder = class {
548
837
  transport = new NullAttachmentTransport();
549
838
  customUploadFactory;
839
+ maxUploadBytes;
550
840
  constructor(db, storagePath) {
551
841
  this.db = db;
552
842
  this.storagePath = storagePath;
@@ -559,13 +849,17 @@ var AttachmentBuilder = class {
559
849
  this.customUploadFactory = factory;
560
850
  return this;
561
851
  }
852
+ withMaxUploadBytes(maxBytes) {
853
+ this.maxUploadBytes = maxBytes;
854
+ return this;
855
+ }
562
856
  async build() {
563
857
  const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);
564
858
  if (!result.success && result.error) throw result.error;
565
859
  const scopedDb = this.db.withSchema(ATTACHMENT_SCHEMA);
566
860
  const store = new KyselyAttachmentStore(scopedDb, this.transport, this.storagePath);
567
861
  const reservations = new KyselyReservationStore(scopedDb);
568
- const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations);
862
+ const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations, this.maxUploadBytes);
569
863
  return {
570
864
  service: new AttachmentService(store, reservations, uploadFactory),
571
865
  store,
@@ -575,6 +869,6 @@ var AttachmentBuilder = class {
575
869
  }
576
870
  };
577
871
  //#endregion
578
- export { ATTACHMENT_SCHEMA, AttachmentBuilder, AttachmentNotFound, AttachmentService, DirectAttachmentUpload, DirectAttachmentUploadFactory, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, ReservationNotFound, SwitchboardAttachmentTransport, createRef, parseRef, runAttachmentMigrations };
872
+ export { ATTACHMENT_SCHEMA, AttachmentBuilder, AttachmentNotFound, AttachmentService, DirectAttachmentUpload, DirectAttachmentUploadFactory, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SwitchboardAttachmentTransport, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
579
873
 
580
874
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rowToHeader","up","down","migration001","migration002"],"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/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/switchboard-attachment-transport.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","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 IAttachmentService,\n IAttachmentStore,\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: IAttachmentStore,\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 { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\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","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: now,\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\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 };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n constructor(private readonly db: Kysely<AttachmentDatabase>) {}\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const now = new Date().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 })\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","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 { 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\";\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};\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 { createHash } from \"node:crypto\";\nimport { 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 writeAttachmentBytes,\n streamFromBuffer,\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\nasync function collectAndHash(\n data: ReadableStream<Uint8Array>,\n): Promise<{ bytes: Uint8Array; hash: string }> {\n const hasher = createHash(\"sha256\");\n const chunks: Uint8Array[] = [];\n const reader = data.getReader();\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hasher.update(value);\n chunks.push(value);\n }\n\n const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);\n const bytes = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return { bytes, hash: hasher.digest(\"hex\") };\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 ) {\n this.reservationId = reservationId;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const { bytes, hash } = await collectAndHash(data);\n\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 const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, streamFromBuffer(bytes));\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: bytes.byteLength,\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\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 ) {}\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 );\n }\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\";\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 this.buildHeaders(url);\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 this.buildHeaders(url);\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 async buildHeaders(url: string): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (this.jwtHandler) {\n const token = await this.jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n return JSON.parse(metaHeader) as AttachmentMetadata;\n }\n return {\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes: Number(response.headers.get(\"Content-Length\") ?? 0),\n extension: null,\n };\n }\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\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 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 );\n\n const service = new AttachmentService(store, reservations, uploadFactory);\n\n return { service, store, reservations, uploadFactory };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAGA,IAAa,qBAAb,cAAwC,MAAM;CAC5C,YAAY,YAAoB;AAC9B,QAAM,yBAAyB,aAAa;AAC5C,OAAK,OAAO;;;;;;AAOhB,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,eAAuB;AACjC,QAAM,0BAA0B,gBAAgB;AAChD,OAAK,OAAO;;;;;;AAOhB,IAAa,uBAAb,cAA0C,MAAM;CAC9C,YAAY,KAAa;AACvB,QAAM,2BAA2B,MAAM;AACvC,OAAK,OAAO;;;;;ACvBhB,MAAM,cAAc;AACpB,MAAM,kBAAkB;AAOxB,SAAgB,SAAS,KAA+B;CACtD,MAAM,QAAQ,YAAY,KAAK,IAAI;AACnC,KAAI,CAAC,MACH,OAAM,IAAI,qBAAqB,IAAI;AAErC,QAAO;EACL,SAAS,OAAO,MAAM,GAAG;EACzB,MAAM,MAAM;EACb;;AAGH,SAAgB,UACd,MACA,UAAkB,iBACH;AACf,QAAO,iBAAiB,QAAQ,GAAG;;;;ACXrC,IAAa,oBAAb,MAA6D;CAC3D,YACE,OACA,cACA,eACA;AAHiB,OAAA,QAAA;AACA,OAAA,eAAA;AACA,OAAA,gBAAA;;CAGnB,MAAM,QAAQ,SAA+D;EAC3E,MAAM,cAAc,MAAM,KAAK,aAAa,OAAO,QAAQ;AAC3D,SAAO,KAAK,cAAc,aAAa,YAAY,eAAe,QAAQ;;CAG5E,MAAM,KAAK,KAA+C;EACxD,MAAM,EAAE,SAAS,SAAS,IAAI;AAC9B,SAAO,KAAK,MAAM,KAAK,KAAK;;CAG9B,MAAM,IACJ,KACA,QAC6B;EAC7B,MAAM,EAAE,SAAS,SAAS,IAAI;AAC9B,SAAO,KAAK,MAAM,IAAI,MAAM,OAAO;;;;;;;;;ACnBvC,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;;;;;;AAOvD,eAAsB,qBACpB,MACA,MACiB;AACjB,OAAM,MAAM,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE/C,MAAM,SAAS,kBAAkB,KAAK;CACtC,MAAM,SAAS,KAAK,WAAW;CAC/B,IAAI,eAAe;AAEnB,KAAI;AACF,WAAS;GACP,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AACV,mBAAgB,MAAM;AAEtB,OAAI,CADgB,OAAO,MAAM,MAAM,CAErC,OAAM,IAAI,SAAe,YAAY,OAAO,KAAK,SAAS,QAAQ,CAAC;;WAG/D;AACR,SAAO,aAAa;AACpB,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,UAAO,UAAU,SAAS,CAAC;AAC3B,UAAO,GAAG,SAAS,OAAO;IAC1B;;AAGJ,QAAO;;;;;AAMT,SAAgB,qBAAqB,MAA0C;CAC7E,MAAM,aAAa,iBAAiB,KAAK;AACzC,QAAO,SAAS,MAAM,WAAW;;;;;AAMnC,eAAsB,sBAAsB,MAA6B;AACvE,OAAM,GAAG,MAAM,EAAE,OAAO,MAAM,CAAC;;;;;AAkBjC,SAAgB,iBAAiB,MAA8C;AAC7E,QAAO,IAAI,eAAe,EACxB,MAAM,YAAY;AAChB,aAAW,QAAQ,KAAK;AACxB,aAAW,OAAO;IAErB,CAAC;;;;ACtEJ,SAASA,cAAY,KAAsC;AACzD,QAAO;EACL,MAAM,IAAI;EACV,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,OAAO,IAAI,WAAW;EACjC,WAAW,IAAI;EACf,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,mBAAmB,IAAI;EACxB;;AAGH,SAAS,sBACP,QACA,SAC4B;CAC5B,IAAI,UAAU;CACd,MAAM,kBAAkB;AACtB,MAAI,CAAC,SAAS;AACZ,aAAU;AACV,YAAS;;;CAIb,MAAM,SAAS,OAAO,WAAW;AACjC,QAAO,IAAI,eAA2B;EACpC,MAAM,KAAK,YAAY;AACrB,OAAI;IACF,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,gBAAW;AACX,gBAAW,OAAO;UAElB,YAAW,QAAQ,MAAM;YAEpB,KAAK;AACZ,eAAW;AACX,eAAW,MAAM,IAAI;;;EAGzB,SAAS;AACP,cAAW;AACX,UAAO,QAAQ,CAAC,YAAY,GAAG;;EAElC,CAAC;;AAGJ,IAAa,wBAAb,MAA+D;CAC7D,gCAAiC,IAAI,KAAqB;CAE1D,YACE,IACA,WACA,UACA;AAHiB,OAAA,KAAA;AACA,OAAA,YAAA;AACA,OAAA,WAAA;;CAGnB,MAAM,KAAK,MAAiD;EAC1D,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,CAAC,IACH,OAAM,IAAI,mBAAmB,KAAK;AAGpC,SAAOA,cAAY,IAAI;;CAGzB,MAAM,IAAI,MAAwC;AAOhD,UANY,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,OAAO,SAAS,CAChB,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB,GAET,WAAW;;CAGzB,MAAM,IACJ,MACA,QAC6B;EAC7B,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,CAAC,IACH,OAAM,IAAI,mBAAmB,KAAK;AAGpC,MAAI,IAAI,WAAW,WAAW;GAC5B,MAAM,SAAS,MAAM,KAAK,UAAU,MAAM,MAAM,OAAO;AACvD,OAAI,CAAC,OACH,OAAM,IAAI,mBAAmB,KAAK;AAEpC,SAAM,KAAK,IAAI,MAAM,OAAO,UAAU,OAAO,KAAK;AAClD,UAAO,KAAK,IAAI,MAAM,OAAO;;EAG/B,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AACpC,QAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI,EAAE,sBAAsB,KAAK,CAAC,CAClC,MAAM,QAAQ,KAAK,KAAK,CACxB,SAAS;EAEZ,MAAM,SAASA,cAAY,IAAI;AAC/B,SAAO,oBAAoB;AAE3B,OAAK,cAAc,KAAK;AAQxB,SAAO;GAAE;GAAQ,MAJJ,sBADK,qBADD,KAAK,KAAK,UAAU,IAAI,aAAa,CACN,QAE9C,KAAK,cAAc,KAAK,CACzB;GAEsB;;CAGzB,MAAM,IACJ,MACA,UACA,MACe;EACf,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,aAAa,CACxB,OAAO,CAAC,QAAQ,SAAS,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,UAAU,WAAW,aAAa;AACpC,SAAM,KAAK,QAAQ;AACnB;;EAGF,MAAM,UAAU,oBAAoB,KAAK;AAEzC,QAAM,qBADW,KAAK,KAAK,UAAU,QAAQ,EACR,KAAK;EAE1C,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,MAAI,CAAC,SACH,OAAM,KAAK,GACR,WAAW,aAAa,CACxB,OAAO;GACN;GACA,WAAW,SAAS;GACpB,WAAW,SAAS;GACpB,YAAY,SAAS;GACrB,WAAW,SAAS,aAAa;GACjC,QAAQ;GACR,cAAc;GACd,QAAQ;GACR,gBAAgB;GAChB,sBAAsB;GACvB,CAAC,CACD,YAAY,OAAO,GAAG,OAAO,OAAO,CAAC,WAAW,CAAC,CACjD,SAAS;MAEZ,OAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI;GACH,QAAQ;GACR,cAAc;GACd,sBAAsB;GACvB,CAAC,CACD,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,UAAU,KAAK,UAAU,CAC/B,SAAS;;CAIhB,MAAM,MAAM,MAAqC;AAC/C,MAAI,KAAK,iBAAiB,KAAK,CAC7B;EAGF,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,OAAO,CAAC,gBAAgB,SAAS,CAAC,CAClC,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,CAAC,OAAO,IAAI,WAAW,UACzB;AAIF,QAAM,sBADW,KAAK,KAAK,UAAU,IAAI,aAAa,CACjB;AAErC,QAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI,EAAE,QAAQ,WAAW,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,SAAS;;CAGd,MAAM,cAA+B;EACnC,MAAM,SAAS,MAAM,KAAK,GACvB,WAAW,aAAa,CACxB,OAAO,GAAW,+BAA+B,GAAG,QAAQ,CAAC,CAC7D,MAAM,UAAU,KAAK,YAAY,CACjC,kBAAkB;AAErB,SAAO,OAAO,QAAQ,SAAS,EAAE;;CAKnC,cAAsB,MAAoB;AACxC,OAAK,cAAc,IAAI,OAAO,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK,EAAE;;CAGvE,cAAsB,MAAoB;EACxC,MAAM,SAAS,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK;AACpD,MAAI,SAAS,EACX,MAAK,cAAc,OAAO,KAAK;MAE/B,MAAK,cAAc,IAAI,MAAM,MAAM;;CAIvC,iBAAyB,MAAuB;AAC9C,UAAQ,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK;;;;;ACtPjD,SAAS,iBAAiB,KAAkC;AAC1D,QAAO;EACL,eAAe,IAAI;EACnB,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,IAAI;EACf,cAAc,IAAI;EACnB;;AAGH,IAAa,yBAAb,MAAiE;CAC/D,YAAY,IAAiD;AAAhC,OAAA,KAAA;;CAE7B,MAAM,OAAO,SAAyD;EACpE,MAAM,gBAAgB,YAAY;EAClC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAcpC,SAAO,iBAZK,MAAM,KAAK,GACpB,WAAW,yBAAyB,CACpC,OAAO;GACN,gBAAgB;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,WAAW,QAAQ,aAAa;GAChC,gBAAgB;GACjB,CAAC,CACD,cAAc,CACd,yBAAyB,CAEA;;CAG9B,MAAM,IAAI,eAA6C;EACrD,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,yBAAyB,CACpC,WAAW,CACX,MAAM,kBAAkB,KAAK,cAAc,CAC3C,kBAAkB;AAErB,MAAI,CAAC,IACH,OAAM,IAAI,oBAAoB,cAAc;AAG9C,SAAO,iBAAiB,IAAI;;CAG9B,MAAM,OAAO,eAAsC;AACjD,QAAM,KAAK,GACR,WAAW,yBAAyB,CACpC,MAAM,kBAAkB,KAAK,cAAc,CAC3C,SAAS;;;;;;;;;ACvDhB,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,YAAY,aAAa,CACzB,UAAU,QAAQ,SAAS,QAAQ,IAAI,YAAY,CAAC,CACpD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,cAAc,WAAW,QAAQ,IAAI,SAAS,CAAC,CACzD,UAAU,aAAa,OAAO,CAC9B,UAAU,UAAU,SAAS,QAAQ,IAAI,SAAS,CAAC,UAAU,YAAY,CAAC,CAC1E,UAAU,gBAAgB,SAAS,QAAQ,IAAI,SAAS,CAAC,CACzD,UAAU,UAAU,SAAS,QAAQ,IAAI,SAAS,CAAC,UAAU,QAAQ,CAAC,CACtE,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,UAAU,wBAAwB,SAAS,QAAQ,IAAI,SAAS,CAAC,CACjE,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,wBAAwB,CACpC,GAAG,aAAa,CAChB,OAAO,SAAS,CAChB,SAAS;AAKZ,OAAM,GAAG,OACN,YAAY,qBAAqB,CACjC,GAAG,aAAa,CAChB,QAAQ,CAAC,UAAU,uBAAuB,CAAC,CAC3C,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,aAAa,CAAC,UAAU,CAAC,SAAS;;;;;;;;AChC9D,eAAsB,GAAG,IAAgC;AACvD,OAAM,GAAG,OACN,YAAY,yBAAyB,CACrC,UAAU,kBAAkB,SAAS,QAAQ,IAAI,YAAY,CAAC,CAC9D,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,OAAO,CAC9B,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,SAAS;;AAGd,eAAsB,KAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,yBAAyB,CAAC,UAAU,CAAC,SAAS;;;;ACR1E,MAAa,oBAAoB;AAQjC,MAAM,aAAa;CACjB,+BAA+BC;CAC/B,gCAAgCC;CACjC;AAED,IAAM,gCAAN,MAAiE;CAC/D,gBAAgB;AACd,SAAO,QAAQ,QAAQ,WAAW;;;AAItC,eAAsB,wBACpB,IACA,SAAiB,mBACS;AAC1B,KAAI;AACF,QAAM,GAAG,+BAA+B,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG;UAC7D,OAAO;AACd,SAAO;GACL,SAAS;GACT,oBAAoB,EAAE;GACtB,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;GACxE;;CAGH,MAAM,WAAW,IAAI,SAAS;EAC5B,IAAI,GAAG,WAAW,OAAO;EACzB,UAAU,IAAI,+BAA+B;EAC7C,sBAAsB;EACvB,CAAC;CAEF,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,MAAM,SAAS,iBAAiB;AAC/C,UAAQ,OAAO;AACf,YAAU,OAAO;UACV,GAAG;AACV,UAAQ;AACR,YAAU,EAAE;;CAGd,MAAM,qBACJ,SAAS,KAAK,WAAW,OAAO,cAAc,IAAI,EAAE;AAEtD,KAAI,MACF,QAAO;EACL,SAAS;EACT;EACA,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;EACxE;AAGH,QAAO;EACL,SAAS;EACT;EACD;;;;ACnDH,SAAS,YAAY,KAAsC;AACzD,QAAO;EACL,MAAM,IAAI;EACV,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,OAAO,IAAI,WAAW;EACjC,WAAW,IAAI;EACf,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,mBAAmB,IAAI;EACxB;;AAGH,eAAe,eACb,MAC8C;CAC9C,MAAM,SAAS,WAAW,SAAS;CACnC,MAAM,SAAuB,EAAE;CAC/B,MAAM,SAAS,KAAK,WAAW;AAE/B,UAAS;EACP,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,MAAI,KAAM;AACV,SAAO,OAAO,MAAM;AACpB,SAAO,KAAK,MAAM;;CAGpB,MAAM,cAAc,OAAO,QAAQ,KAAK,MAAM,MAAM,EAAE,YAAY,EAAE;CACpE,MAAM,QAAQ,IAAI,WAAW,YAAY;CACzC,IAAI,SAAS;AACb,MAAK,MAAM,SAAS,QAAQ;AAC1B,QAAM,IAAI,OAAO,OAAO;AACxB,YAAU,MAAM;;AAGlB,QAAO;EAAE;EAAO,MAAM,OAAO,OAAO,MAAM;EAAE;;AAG9C,IAAa,yBAAb,MAAiE;CAC/D;CAEA,YACE,eACA,SACA,IACA,UACA,cACA;AAJiB,OAAA,UAAA;AACA,OAAA,KAAA;AACA,OAAA,WAAA;AACA,OAAA,eAAA;AAEjB,OAAK,gBAAgB;;CAGvB,MAAM,KACJ,MACiC;EACjC,MAAM,EAAE,OAAO,SAAS,MAAM,eAAe,KAAK;EAElD,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,aAAa,CACxB,OAAO,CAAC,QAAQ,SAAS,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,UAAU,WAAW,aAAa;GACpC,MAAM,UAAU,oBAAoB,KAAK;AAEzC,SAAM,qBADW,KAAK,KAAK,UAAU,QAAQ,EACR,iBAAiB,MAAM,CAAC;GAE7D,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,OAAI,CAAC,SACH,OAAM,KAAK,GACR,WAAW,aAAa,CACxB,OAAO;IACN;IACA,WAAW,KAAK,QAAQ;IACxB,WAAW,KAAK,QAAQ;IACxB,YAAY,MAAM;IAClB,WAAW,KAAK,QAAQ,aAAa;IACrC,QAAQ;IACR,cAAc;IACd,QAAQ;IACR,gBAAgB;IAChB,sBAAsB;IACvB,CAAC,CACD,YAAY,OAAO,GAAG,OAAO,OAAO,CAAC,WAAW,CAAC,CACjD,SAAS;OAGZ,OAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI;IACH,QAAQ;IACR,cAAc;IACd,QAAQ;IACR,sBAAsB;IACvB,CAAC,CACD,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,UAAU,KAAK,UAAU,CAC/B,SAAS;;AAIhB,QAAM,KAAK,aAAa,OAAO,KAAK,cAAc;EAElD,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,yBAAyB;AAE5B,SAAO;GACL;GACA,KAAK,UAAU,KAAK;GACpB,QAAQ,YAAY,IAAI;GACzB;;;;;AC9HL,IAAa,gCAAb,MAA+E;CAC7E,YACE,IACA,UACA,cACA;AAHiB,OAAA,KAAA;AACA,OAAA,WAAA;AACA,OAAA,eAAA;;CAGnB,aACE,eACA,SACmB;AACnB,SAAO,IAAI,uBACT,eACA,SACA,KAAK,IACL,KAAK,UACL,KAAK,aACN;;;;;AChBL,IAAa,iCAAb,MAA4E;CAC1E;CACA;CACA;CAEA,YAAY,QAAoC;AAC9C,OAAK,YAAY,OAAO;AACxB,OAAK,aAAa,OAAO;AACzB,OAAK,UAAU,OAAO,WAAW,WAAW;;CAG9C,MAAM,MACJ,MACA,QACmC;EACnC,MAAM,MAAM,GAAG,KAAK,UAAU,eAAe;EAC7C,MAAM,UAAU,MAAM,KAAK,aAAa,IAAI;EAE5C,MAAM,WAAW,MAAM,KAAK,QAAQ,KAAK;GAAE;GAAQ;GAAS,CAAC;AAE7D,MAAI,SAAS,WAAW,IACtB,QAAO;AAGT,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MACR,4BAA4B,SAAS,OAAO,GAAG,SAAS,aACzD;EAGH,MAAM,WAAW,KAAK,qBAAqB,SAAS;EACpD,MAAM,OAAO,SAAS;AACtB,MAAI,CAAC,KACH,OAAM,IAAI,MAAM,wBAAwB;AAG1C,SAAO;GAAE;GAAM;GAAU;GAAM;;CAIjC,MAAM,SAAS,OAAsC;CAIrD,MAAM,KACJ,MACA,QACA,MACe;EACf,MAAM,MAAM,GAAG,OAAO,eAAe;EACrC,MAAM,UAAU,MAAM,KAAK,aAAa,IAAI;EAE5C,MAAM,WAAW,MAAM,KAAK,QAAQ,KAAK;GACvC,QAAQ;GACR,MAAM;GACN;GAEA,QAAQ;GACT,CAAC;AAEF,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MACR,2BAA2B,SAAS,OAAO,GAAG,SAAS,aACxD;;CAIL,MAAc,aAAa,KAA8C;EACvE,MAAM,UAAkC,EAAE;AAC1C,MAAI,KAAK,YAAY;GACnB,MAAM,QAAQ,MAAM,KAAK,WAAW,IAAI;AACxC,OAAI,MACF,SAAQ,mBAAmB,UAAU;;AAGzC,SAAO;;CAGT,qBAA6B,UAAwC;EACnE,MAAM,aAAa,SAAS,QAAQ,IAAI,wBAAwB;AAChE,MAAI,WACF,QAAO,KAAK,MAAM,WAAW;AAE/B,SAAO;GACL,UACE,SAAS,QAAQ,IAAI,eAAe,IAAI;GAC1C,UAAU;GACV,WAAW,OAAO,SAAS,QAAQ,IAAI,iBAAiB,IAAI,EAAE;GAC9D,WAAW;GACZ;;;;;;;;;AC7FL,IAAa,0BAAb,MAAqE;CACnE,QAA2C;AACzC,SAAO,QAAQ,QAAQ,KAAK;;CAG9B,WAA0B;AACxB,SAAO,QAAQ,SAAS;;CAG1B,OAAsB;AACpB,SAAO,QAAQ,SAAS;;;;;ACM5B,IAAa,oBAAb,MAA+B;CAC7B,YAA0C,IAAI,yBAAyB;CACvE;CAEA,YACE,IACA,aACA;AAFiB,OAAA,KAAA;AACA,OAAA,cAAA;;CAGnB,cAAc,WAAuC;AACnD,OAAK,YAAY;AACjB,SAAO;;CAGT,kBAAkB,SAAyC;AACzD,OAAK,sBAAsB;AAC3B,SAAO;;CAGT,MAAM,QAAwC;EAC5C,MAAM,SAAS,MAAM,wBAAwB,KAAK,IAAI,kBAAkB;AACxE,MAAI,CAAC,OAAO,WAAW,OAAO,MAC5B,OAAM,OAAO;EAGf,MAAM,WAAW,KAAK,GAAG,WACvB,kBACD;EAED,MAAM,QAAQ,IAAI,sBAChB,UACA,KAAK,WACL,KAAK,YACN;EACD,MAAM,eAAe,IAAI,uBAAuB,SAAS;EAEzD,MAAM,gBACJ,KAAK,uBACL,IAAI,8BACF,UACA,KAAK,aACL,aACD;AAIH,SAAO;GAAE,SAFO,IAAI,kBAAkB,OAAO,cAAc,cAAc;GAEvD;GAAO;GAAc;GAAe"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/reactor-attachments",
3
- "version": "6.0.0-dev.208",
3
+ "version": "6.0.0-dev.209",
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.0-dev.208"
25
+ "@powerhousedao/reactor": "6.0.0-dev.209"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@electric-sql/pglite": "0.3.15",