@powerhousedao/reactor-attachments 6.1.0-dev.16 → 6.1.0-dev.18

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.js CHANGED
@@ -56,6 +56,80 @@ var UploadTooLarge = class extends Error {
56
56
  this.maxBytes = maxBytes;
57
57
  }
58
58
  };
59
+ /**
60
+ * Thrown by reserve() when the claimed hash is already available in the store.
61
+ * The caller should use err.ref directly and upload nothing -- this is the
62
+ * dedup fast path: duplicate content never leaves the client.
63
+ */
64
+ var AttachmentAlreadyExists = class extends Error {
65
+ hash;
66
+ ref;
67
+ constructor(hash, ref) {
68
+ super(`Attachment already exists for hash: ${hash}`);
69
+ this.name = "AttachmentAlreadyExists";
70
+ this.hash = hash;
71
+ this.ref = ref;
72
+ }
73
+ };
74
+ /**
75
+ * Thrown by send() when the server-computed hash of the uploaded bytes
76
+ * does not match the hash claimed at reservation time. Nothing is committed;
77
+ * the reservation is retained so the client can retry with correct bytes.
78
+ */
79
+ var HashMismatch = class extends Error {
80
+ claimed;
81
+ actual;
82
+ constructor(claimed, actual) {
83
+ super(`Hash mismatch: claimed ${claimed} but computed ${actual}`);
84
+ this.name = "HashMismatch";
85
+ this.claimed = claimed;
86
+ this.actual = actual;
87
+ }
88
+ };
89
+ /**
90
+ * Thrown by send() when the uploaded byte count does not equal the
91
+ * sizeBytes declared at reservation time. The handle may reject
92
+ * mid-stream as soon as the count exceeds the declaration.
93
+ * Nothing is committed; the reservation is retained for retry.
94
+ *
95
+ * "actual" is the byte count received from the stream before aborting --
96
+ * it includes the chunk that crossed the declaration and can exceed bytes
97
+ * persisted. On mid-stream aborts the true total is unknown; at least
98
+ * "actual" bytes were sent.
99
+ */
100
+ var SizeMismatch = class extends Error {
101
+ declared;
102
+ actual;
103
+ constructor(declared, actual) {
104
+ super(`Size mismatch: declared ${declared} bytes but received ${actual}`);
105
+ this.name = "SizeMismatch";
106
+ this.declared = declared;
107
+ this.actual = actual;
108
+ }
109
+ };
110
+ /**
111
+ * Thrown by get() when the hash is reserved by an in-flight upload and
112
+ * bytes are not yet available anywhere. Deliberately NOT a subclass of
113
+ * AttachmentNotFound -- callers must distinguish "retry later" from "unknown".
114
+ * After expiresAtUtc has passed the hash reads as not found.
115
+ *
116
+ * metadata is populated when the reservation is local and its fields are
117
+ * known (mimeType, fileName, sizeBytes). It is undefined when the pending
118
+ * state is learned from a remote transport that did not supply the full
119
+ * Attachment-Pending header (transport-pending / degraded wire case).
120
+ */
121
+ var AttachmentPending = class extends Error {
122
+ hash;
123
+ expiresAtUtc;
124
+ metadata;
125
+ constructor(hash, expiresAtUtc, meta) {
126
+ super(`Attachment pending upload for hash: ${hash}, expires: ${expiresAtUtc}`);
127
+ this.name = "AttachmentPending";
128
+ this.hash = hash;
129
+ this.expiresAtUtc = expiresAtUtc;
130
+ this.metadata = meta;
131
+ }
132
+ };
59
133
  //#endregion
60
134
  //#region src/ref.ts
61
135
  const REF_PATTERN = /^attachment:\/\/v(\d+):(.+)$/;
@@ -73,6 +147,7 @@ function createRef(hash, version = DEFAULT_VERSION) {
73
147
  }
74
148
  //#endregion
75
149
  //#region src/attachment-service.ts
150
+ const CLIENT_HASH_PATTERN = /^[a-f0-9]{64}$/;
76
151
  var AttachmentService = class {
77
152
  constructor(store, reservations, uploadFactory) {
78
153
  this.store = store;
@@ -80,8 +155,9 @@ var AttachmentService = class {
80
155
  this.uploadFactory = uploadFactory;
81
156
  }
82
157
  async reserve(options) {
158
+ if (options.clientHash !== void 0) return this.reserveHashFirst(options);
83
159
  const reservation = await this.reservations.create(options);
84
- return this.uploadFactory.createUpload(reservation.reservationId, options);
160
+ return this.uploadFactory.createUpload(reservation);
85
161
  }
86
162
  async stat(ref) {
87
163
  const { hash } = parseRef(ref);
@@ -91,6 +167,24 @@ var AttachmentService = class {
91
167
  const { hash } = parseRef(ref);
92
168
  return this.store.get(hash, signal);
93
169
  }
170
+ async reserveHashFirst(options) {
171
+ const normalized = options.clientHash.toLowerCase();
172
+ if (!CLIENT_HASH_PATTERN.test(normalized)) throw new Error(`clientHash must be a 64-character lowercase hex string, got: ${options.clientHash}`);
173
+ if (options.sizeBytes === void 0 || !Number.isInteger(options.sizeBytes) || options.sizeBytes <= 0 || !Number.isSafeInteger(options.sizeBytes)) throw new Error("sizeBytes must be a positive safe integer when clientHash is provided");
174
+ const normalizedOptions = {
175
+ ...options,
176
+ clientHash: normalized
177
+ };
178
+ let existingHeader = null;
179
+ try {
180
+ existingHeader = await this.store.stat(normalized);
181
+ } catch (err) {
182
+ if (!(err instanceof AttachmentNotFound) && !(err instanceof AttachmentPending)) throw err;
183
+ }
184
+ if (existingHeader !== null && existingHeader.status === "available") throw new AttachmentAlreadyExists(normalized, createRef(normalized));
185
+ const reservation = await this.reservations.create(normalizedOptions);
186
+ return this.uploadFactory.createUpload(reservation);
187
+ }
94
188
  };
95
189
  //#endregion
96
190
  //#region src/storage/fs/attachment-fs.ts
@@ -115,13 +209,24 @@ async function writeAttachmentBytes(path, data) {
115
209
  const { done, value } = await reader.read();
116
210
  if (done) break;
117
211
  bytesWritten += value.byteLength;
118
- if (!writer.write(value)) await new Promise((resolve) => writer.once("drain", resolve));
212
+ if (!writer.write(value)) await new Promise((resolve, reject) => {
213
+ const onDrain = () => {
214
+ writer.off("error", onError);
215
+ resolve();
216
+ };
217
+ const onError = (err) => {
218
+ writer.off("drain", onDrain);
219
+ reject(err);
220
+ };
221
+ writer.once("drain", onDrain);
222
+ writer.once("error", onError);
223
+ });
119
224
  }
120
225
  } finally {
121
226
  reader.releaseLock();
122
227
  await new Promise((resolve, reject) => {
123
228
  writer.end(() => resolve());
124
- writer.on("error", reject);
229
+ writer.once("error", reject);
125
230
  });
126
231
  }
127
232
  return bytesWritten;
@@ -149,9 +254,16 @@ async function deleteAttachmentBytes(path) {
149
254
  *
150
255
  * If `maxBytes` is set and the input exceeds it, the temp file is removed and
151
256
  * `UploadTooLarge` is thrown.
257
+ *
258
+ * If `declaredSizeBytes` is set, the byte count is enforced as a contract:
259
+ * mid-stream, the moment the count exceeds the declaration the reader is
260
+ * released and `SizeMismatch` is thrown without consuming the rest of the
261
+ * stream. At stream end, if the count does not equal the declaration,
262
+ * `SizeMismatch` is thrown. Both the `maxBytes` and `declaredSizeBytes`
263
+ * checks apply; `maxBytes` is evaluated first on each chunk.
152
264
  */
153
265
  async function streamHashAndWrite(basePath, data, options = {}) {
154
- const { maxBytes } = options;
266
+ const { maxBytes, declaredSizeBytes } = options;
155
267
  const tmpDir = join(basePath, ".tmp");
156
268
  await mkdir(tmpDir, { recursive: true });
157
269
  const tempPath = join(tmpDir, randomUUID());
@@ -166,6 +278,7 @@ async function streamHashAndWrite(basePath, data, options = {}) {
166
278
  if (done) break;
167
279
  sizeBytes += value.byteLength;
168
280
  if (maxBytes !== void 0 && sizeBytes > maxBytes) throw new UploadTooLarge(maxBytes);
281
+ if (declaredSizeBytes !== void 0 && sizeBytes > declaredSizeBytes) throw new SizeMismatch(declaredSizeBytes, sizeBytes);
169
282
  hasher.update(value);
170
283
  if (!writer.write(value)) await new Promise((resolve, reject) => {
171
284
  const onDrain = () => {
@@ -185,17 +298,30 @@ async function streamHashAndWrite(basePath, data, options = {}) {
185
298
  } finally {
186
299
  reader.releaseLock();
187
300
  }
188
- await new Promise((resolve, reject) => {
189
- writer.end((err) => {
190
- if (err) reject(err);
191
- else resolve();
301
+ let endError;
302
+ try {
303
+ await new Promise((resolve, reject) => {
304
+ writer.end((err) => {
305
+ if (err) reject(err);
306
+ else resolve();
307
+ });
308
+ writer.once("error", reject);
192
309
  });
193
- writer.on("error", reject);
194
- });
310
+ } catch (err) {
311
+ endError = err instanceof Error ? err : new Error(String(err));
312
+ }
195
313
  if (caughtError) {
196
314
  await rm(tempPath, { force: true });
197
315
  throw caughtError;
198
316
  }
317
+ if (endError) {
318
+ await rm(tempPath, { force: true });
319
+ throw endError;
320
+ }
321
+ if (declaredSizeBytes !== void 0 && sizeBytes !== declaredSizeBytes) {
322
+ await rm(tempPath, { force: true });
323
+ throw new SizeMismatch(declaredSizeBytes, sizeBytes);
324
+ }
199
325
  return {
200
326
  tempPath,
201
327
  hash: hasher.digest("hex"),
@@ -214,7 +340,8 @@ function rowToHeader$1(row) {
214
340
  status: row.status,
215
341
  source: row.source,
216
342
  createdAtUtc: row.created_at_utc,
217
- lastAccessedAtUtc: row.last_accessed_at_utc
343
+ lastAccessedAtUtc: row.last_accessed_at_utc,
344
+ expiresAtUtc: null
218
345
  };
219
346
  }
220
347
  function wrapStreamWithCleanup(source, cleanup) {
@@ -254,30 +381,62 @@ var KyselyAttachmentStore = class {
254
381
  }
255
382
  async stat(hash) {
256
383
  const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirst();
257
- if (!row) throw new AttachmentNotFound(hash);
258
- return rowToHeader$1(row);
384
+ if (row) return rowToHeader$1(row);
385
+ const now = (/* @__PURE__ */ new Date()).toISOString();
386
+ const pending = await this.findPendingReservation(hash, now);
387
+ if (pending) return {
388
+ hash,
389
+ mimeType: pending.mime_type,
390
+ fileName: pending.file_name,
391
+ sizeBytes: Number(pending.size_bytes),
392
+ extension: pending.extension,
393
+ status: "pending",
394
+ source: "local",
395
+ createdAtUtc: pending.created_at_utc,
396
+ lastAccessedAtUtc: pending.created_at_utc,
397
+ expiresAtUtc: pending.expires_at_utc
398
+ };
399
+ throw new AttachmentNotFound(hash);
259
400
  }
260
401
  async has(hash) {
261
402
  return (await this.db.selectFrom("attachment").select("status").where("hash", "=", hash).executeTakeFirst())?.status === "available";
262
403
  }
263
404
  async get(hash, signal) {
264
405
  const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirst();
265
- if (!row) throw new AttachmentNotFound(hash);
266
- if (row.status === "evicted") {
267
- const remote = await this.transport.fetch(hash, signal);
268
- if (!remote) throw new AttachmentNotFound(hash);
269
- await this.put(hash, remote.metadata, remote.body);
270
- return this.get(hash, signal);
406
+ if (row) {
407
+ if (row.status === "evicted") {
408
+ const remote = await this.transport.fetch(hash, signal);
409
+ if (remote.kind === "data") {
410
+ await this.put(hash, remote.response.metadata, remote.response.body);
411
+ return this.get(hash, signal);
412
+ }
413
+ if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
414
+ throw new AttachmentNotFound(hash);
415
+ }
416
+ const now = (/* @__PURE__ */ new Date()).toISOString();
417
+ await this.db.updateTable("attachment").set({ last_accessed_at_utc: now }).where("hash", "=", hash).execute();
418
+ const header = rowToHeader$1(row);
419
+ header.lastAccessedAtUtc = now;
420
+ this.acquireReader(hash);
421
+ return {
422
+ header,
423
+ body: wrapStreamWithCleanup(readAttachmentStream(join(this.basePath, row.storage_path)), () => this.releaseReader(hash))
424
+ };
271
425
  }
272
426
  const now = (/* @__PURE__ */ new Date()).toISOString();
273
- await this.db.updateTable("attachment").set({ last_accessed_at_utc: now }).where("hash", "=", hash).execute();
274
- const header = rowToHeader$1(row);
275
- header.lastAccessedAtUtc = now;
276
- this.acquireReader(hash);
277
- return {
278
- header,
279
- body: wrapStreamWithCleanup(readAttachmentStream(join(this.basePath, row.storage_path)), () => this.releaseReader(hash))
280
- };
427
+ const pending = await this.findPendingReservation(hash, now);
428
+ if (pending) throw new AttachmentPending(hash, pending.expires_at_utc, {
429
+ mimeType: pending.mime_type,
430
+ fileName: pending.file_name,
431
+ sizeBytes: pending.size_bytes
432
+ });
433
+ const remote = await this.transport.fetch(hash, signal);
434
+ if (remote.kind === "data") {
435
+ await this.put(hash, remote.response.metadata, remote.response.body);
436
+ return this.get(hash, signal);
437
+ }
438
+ if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
439
+ throw new AttachmentNotFound(hash);
281
440
  }
282
441
  async put(hash, metadata, data) {
283
442
  const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
@@ -317,6 +476,25 @@ var KyselyAttachmentStore = class {
317
476
  const result = await this.db.selectFrom("attachment").select(sql`COALESCE(SUM(size_bytes), 0)`.as("total")).where("status", "=", "available").executeTakeFirst();
318
477
  return Number(result?.total ?? 0);
319
478
  }
479
+ async findPendingReservation(hash, now) {
480
+ const row = await this.db.selectFrom("attachment_reservation as r").leftJoin("attachment as a", "a.hash", "r.client_hash").select([
481
+ "r.mime_type",
482
+ "r.file_name",
483
+ "r.extension",
484
+ "r.size_bytes",
485
+ "r.created_at_utc",
486
+ "r.expires_at_utc"
487
+ ]).where("r.client_hash", "=", hash).where("r.deleted_at_utc", "is", null).where("r.expires_at_utc", ">", now).where("r.size_bytes", "is not", null).where("a.hash", "is", null).orderBy("r.expires_at_utc", "desc").executeTakeFirst();
488
+ if (!row) return null;
489
+ return {
490
+ mime_type: row.mime_type,
491
+ file_name: row.file_name,
492
+ extension: row.extension,
493
+ size_bytes: Number(row.size_bytes),
494
+ created_at_utc: row.created_at_utc,
495
+ expires_at_utc: row.expires_at_utc
496
+ };
497
+ }
320
498
  acquireReader(hash) {
321
499
  this.activeReaders.set(hash, (this.activeReaders.get(hash) ?? 0) + 1);
322
500
  }
@@ -339,7 +517,9 @@ function rowToReservation(row) {
339
517
  fileName: row.file_name,
340
518
  extension: row.extension,
341
519
  createdAtUtc: row.created_at_utc,
342
- expiresAtUtc: row.expires_at_utc
520
+ expiresAtUtc: row.expires_at_utc,
521
+ clientHash: row.client_hash,
522
+ sizeBytes: row.size_bytes !== null ? Number(row.size_bytes) : null
343
523
  };
344
524
  }
345
525
  var KyselyReservationStore = class {
@@ -359,7 +539,9 @@ var KyselyReservationStore = class {
359
539
  file_name: options.fileName,
360
540
  extension: options.extension ?? null,
361
541
  created_at_utc: now,
362
- expires_at_utc: expiresAt
542
+ expires_at_utc: expiresAt,
543
+ client_hash: options.clientHash ?? null,
544
+ size_bytes: options.sizeBytes ?? null
363
545
  }).returningAll().executeTakeFirstOrThrow());
364
546
  }
365
547
  async get(reservationId) {
@@ -379,72 +561,89 @@ var KyselyReservationStore = class {
379
561
  //#endregion
380
562
  //#region src/storage/migrations/001_create_attachment_table.ts
381
563
  var _001_create_attachment_table_exports = /* @__PURE__ */ __exportAll({
382
- down: () => down$4,
383
- up: () => up$4
564
+ down: () => down$5,
565
+ up: () => up$5
384
566
  });
385
- async function up$4(db) {
567
+ async function up$5(db) {
386
568
  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();
387
569
  await db.schema.createIndex("idx_attachment_status").on("attachment").column("status").execute();
388
570
  await db.schema.createIndex("idx_attachment_lru").on("attachment").columns(["status", "last_accessed_at_utc"]).execute();
389
571
  }
390
- async function down$4(db) {
572
+ async function down$5(db) {
391
573
  await db.schema.dropTable("attachment").ifExists().execute();
392
574
  }
393
575
  //#endregion
394
576
  //#region src/storage/migrations/002_create_reservation_table.ts
395
577
  var _002_create_reservation_table_exports = /* @__PURE__ */ __exportAll({
396
- down: () => down$3,
397
- up: () => up$3
578
+ down: () => down$4,
579
+ up: () => up$4
398
580
  });
399
- async function up$3(db) {
581
+ async function up$4(db) {
400
582
  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();
401
583
  }
402
- async function down$3(db) {
584
+ async function down$4(db) {
403
585
  await db.schema.dropTable("attachment_reservation").ifExists().execute();
404
586
  }
405
587
  //#endregion
406
588
  //#region src/storage/migrations/003_add_reservation_expires_at.ts
407
589
  var _003_add_reservation_expires_at_exports = /* @__PURE__ */ __exportAll({
408
- down: () => down$2,
409
- up: () => up$2
590
+ down: () => down$3,
591
+ up: () => up$3
410
592
  });
411
- async function up$2(db) {
593
+ async function up$3(db) {
412
594
  await db.schema.alterTable("attachment_reservation").addColumn("expires_at_utc", "text").execute();
413
595
  await db.updateTable("attachment_reservation").set({ expires_at_utc: sql`created_at_utc` }).where("expires_at_utc", "is", null).execute();
414
596
  await db.schema.alterTable("attachment_reservation").alterColumn("expires_at_utc", (col) => col.setNotNull()).execute();
415
597
  await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
416
598
  }
417
- async function down$2(db) {
599
+ async function down$3(db) {
418
600
  await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
419
601
  await db.schema.alterTable("attachment_reservation").dropColumn("expires_at_utc").execute();
420
602
  }
421
603
  //#endregion
422
604
  //#region src/storage/migrations/004_add_reservation_soft_delete.ts
423
605
  var _004_add_reservation_soft_delete_exports = /* @__PURE__ */ __exportAll({
424
- down: () => down$1,
425
- up: () => up$1
606
+ down: () => down$2,
607
+ up: () => up$2
426
608
  });
427
- async function up$1(db) {
609
+ async function up$2(db) {
428
610
  await db.schema.alterTable("attachment_reservation").addColumn("deleted_at_utc", "text").execute();
429
611
  }
430
- async function down$1(db) {
612
+ async function down$2(db) {
431
613
  await db.schema.alterTable("attachment_reservation").dropColumn("deleted_at_utc").execute();
432
614
  }
433
615
  //#endregion
434
616
  //#region src/storage/migrations/005_add_reservation_active_index.ts
435
617
  var _005_add_reservation_active_index_exports = /* @__PURE__ */ __exportAll({
436
- down: () => down,
437
- up: () => up
618
+ down: () => down$1,
619
+ up: () => up$1
438
620
  });
439
- async function up(db) {
621
+ async function up$1(db) {
440
622
  await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
441
623
  await db.schema.createIndex("idx_reservation_expires_at_active").on("attachment_reservation").column("expires_at_utc").where(sql`deleted_at_utc IS NULL`).execute();
442
624
  }
443
- async function down(db) {
625
+ async function down$1(db) {
444
626
  await db.schema.dropIndex("idx_reservation_expires_at_active").ifExists().execute();
445
627
  await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
446
628
  }
447
629
  //#endregion
630
+ //#region src/storage/migrations/006_add_reservation_client_hash.ts
631
+ var _006_add_reservation_client_hash_exports = /* @__PURE__ */ __exportAll({
632
+ down: () => down,
633
+ up: () => up
634
+ });
635
+ async function up(db) {
636
+ await db.schema.alterTable("attachment_reservation").addColumn("client_hash", "text").execute();
637
+ await db.schema.alterTable("attachment_reservation").addColumn("size_bytes", "bigint").execute();
638
+ await db.schema.alterTable("attachment_reservation").addCheckConstraint("attachment_reservation_hash_size_check", sql`client_hash is null or size_bytes is not null`).execute();
639
+ await db.schema.createIndex("idx_reservation_client_hash").on("attachment_reservation").column("client_hash").execute();
640
+ }
641
+ async function down(db) {
642
+ await db.schema.dropIndex("idx_reservation_client_hash").ifExists().execute();
643
+ await db.schema.alterTable("attachment_reservation").dropColumn("size_bytes").execute();
644
+ await db.schema.alterTable("attachment_reservation").dropColumn("client_hash").execute();
645
+ }
646
+ //#endregion
448
647
  //#region src/storage/migrations/migrator.ts
449
648
  const ATTACHMENT_SCHEMA = "attachments";
450
649
  const migrations = {
@@ -452,7 +651,8 @@ const migrations = {
452
651
  "002_create_reservation_table": _002_create_reservation_table_exports,
453
652
  "003_add_reservation_expires_at": _003_add_reservation_expires_at_exports,
454
653
  "004_add_reservation_soft_delete": _004_add_reservation_soft_delete_exports,
455
- "005_add_reservation_active_index": _005_add_reservation_active_index_exports
654
+ "005_add_reservation_active_index": _005_add_reservation_active_index_exports,
655
+ "006_add_reservation_client_hash": _006_add_reservation_client_hash_exports
456
656
  };
457
657
  var ProgrammaticMigrationProvider = class {
458
658
  getMigrations() {
@@ -507,21 +707,35 @@ function rowToHeader(row) {
507
707
  status: row.status,
508
708
  source: row.source,
509
709
  createdAtUtc: row.created_at_utc,
510
- lastAccessedAtUtc: row.last_accessed_at_utc
710
+ lastAccessedAtUtc: row.last_accessed_at_utc,
711
+ expiresAtUtc: null
511
712
  };
512
713
  }
513
714
  var DirectAttachmentUpload = class {
514
715
  reservationId;
515
- constructor(reservationId, options, db, basePath, reservations, maxBytes) {
516
- this.options = options;
716
+ ref;
717
+ expiresAtUtc;
718
+ constructor(reservation, db, basePath, reservations, maxBytes) {
719
+ this.reservation = reservation;
517
720
  this.db = db;
518
721
  this.basePath = basePath;
519
722
  this.reservations = reservations;
520
723
  this.maxBytes = maxBytes;
521
- this.reservationId = reservationId;
724
+ this.reservationId = reservation.reservationId;
725
+ this.ref = reservation.clientHash != null ? createRef(reservation.clientHash) : null;
726
+ this.expiresAtUtc = reservation.expiresAtUtc;
522
727
  }
523
728
  async send(data) {
524
- const { tempPath, hash, sizeBytes } = await streamHashAndWrite(this.basePath, data, { maxBytes: this.maxBytes });
729
+ if (this.reservation.clientHash != null && this.reservation.sizeBytes == null) throw new Error("hash-first reservation missing sizeBytes");
730
+ const declaredSizeBytes = this.reservation.clientHash != null ? this.reservation.sizeBytes ?? void 0 : void 0;
731
+ const { tempPath, hash, sizeBytes } = await streamHashAndWrite(this.basePath, data, {
732
+ maxBytes: this.maxBytes,
733
+ declaredSizeBytes
734
+ });
735
+ if (this.reservation.clientHash != null && hash !== this.reservation.clientHash) {
736
+ await rm(tempPath, { force: true });
737
+ throw new HashMismatch(this.reservation.clientHash, hash);
738
+ }
525
739
  try {
526
740
  const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
527
741
  if (existing?.status === "available") await rm(tempPath, { force: true });
@@ -533,10 +747,10 @@ var DirectAttachmentUpload = class {
533
747
  const now = (/* @__PURE__ */ new Date()).toISOString();
534
748
  if (!existing) await this.db.insertInto("attachment").values({
535
749
  hash,
536
- mime_type: this.options.mimeType,
537
- file_name: this.options.fileName,
750
+ mime_type: this.reservation.mimeType,
751
+ file_name: this.reservation.fileName,
538
752
  size_bytes: sizeBytes,
539
- extension: this.options.extension ?? null,
753
+ extension: this.reservation.extension ?? null,
540
754
  status: "available",
541
755
  storage_path: relPath,
542
756
  source: "local",
@@ -572,8 +786,8 @@ var DirectAttachmentUploadFactory = class {
572
786
  this.reservations = reservations;
573
787
  this.maxBytes = maxBytes;
574
788
  }
575
- createUpload(reservationId, options) {
576
- return new DirectAttachmentUpload(reservationId, options, this.db, this.basePath, this.reservations, this.maxBytes);
789
+ createUpload(reservation) {
790
+ return new DirectAttachmentUpload(reservation, this.db, this.basePath, this.reservations, this.maxBytes);
577
791
  }
578
792
  };
579
793
  //#endregion
@@ -604,15 +818,28 @@ var SwitchboardAttachmentTransport = class {
604
818
  signal,
605
819
  headers
606
820
  });
607
- if (response.status === 404) return null;
821
+ if (response.status === 202) {
822
+ const expiresAtUtc = this.parsePendingExpiry(response);
823
+ if (!expiresAtUtc) throw new Error("Attachment fetch returned 202 with missing or malformed Attachment-Pending header");
824
+ return {
825
+ kind: "pending",
826
+ hash,
827
+ expiresAtUtc,
828
+ retryAfterMs: parseRetryAfterMs(response)
829
+ };
830
+ }
831
+ if (response.status === 404) return { kind: "not-found" };
608
832
  if (!response.ok) throw new Error(`Attachment fetch failed: ${response.status} ${response.statusText}`);
609
833
  const metadata = this.parseMetadataHeaders(response);
610
834
  const body = response.body;
611
835
  if (!body) throw new Error("Response body is null");
612
836
  return {
613
- hash,
614
- metadata,
615
- body
837
+ kind: "data",
838
+ response: {
839
+ hash,
840
+ metadata,
841
+ body
842
+ }
616
843
  };
617
844
  }
618
845
  async announce(_hash) {}
@@ -627,6 +854,18 @@ var SwitchboardAttachmentTransport = class {
627
854
  });
628
855
  if (!response.ok) throw new Error(`Attachment push failed: ${response.status} ${response.statusText}`);
629
856
  }
857
+ parsePendingExpiry(response) {
858
+ const header = response.headers.get("Attachment-Pending");
859
+ if (!header) return null;
860
+ try {
861
+ const parsed = JSON.parse(header);
862
+ if (!isRecord$3(parsed)) return null;
863
+ if (typeof parsed.expiresAtUtc !== "string") return null;
864
+ return parsed.expiresAtUtc;
865
+ } catch {
866
+ return null;
867
+ }
868
+ }
630
869
  parseMetadataHeaders(response) {
631
870
  let fallbackCache;
632
871
  const fallback = () => {
@@ -636,7 +875,7 @@ var SwitchboardAttachmentTransport = class {
636
875
  const metaHeader = response.headers.get("Attachment-Metadata");
637
876
  if (metaHeader) try {
638
877
  const parsed = JSON.parse(metaHeader);
639
- if (isRecord$2(parsed)) {
878
+ if (isRecord$3(parsed)) {
640
879
  if (parsed.extension === void 0) parsed.extension = null;
641
880
  if (parsed.createdAtUtc === void 0) parsed.createdAtUtc = fallback().createdAtUtc;
642
881
  if (parsed.lastAccessedAtUtc === void 0) parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;
@@ -646,11 +885,19 @@ var SwitchboardAttachmentTransport = class {
646
885
  return fallback();
647
886
  }
648
887
  };
649
- function isRecord$2(value) {
888
+ const DEFAULT_RETRY_AFTER_MS = 5e3;
889
+ function parseRetryAfterMs(response) {
890
+ const retryAfter = response.headers.get("Retry-After");
891
+ if (!retryAfter) return DEFAULT_RETRY_AFTER_MS;
892
+ const seconds = Number(retryAfter);
893
+ if (!Number.isFinite(seconds) || seconds < 0) return DEFAULT_RETRY_AFTER_MS;
894
+ return Math.round(seconds * 1e3);
895
+ }
896
+ function isRecord$3(value) {
650
897
  return typeof value === "object" && value !== null && !Array.isArray(value);
651
898
  }
652
899
  function isAttachmentMetadata$1(value) {
653
- if (!isRecord$2(value)) return false;
900
+ if (!isRecord$3(value)) return false;
654
901
  if (typeof value.mimeType !== "string") return false;
655
902
  if (typeof value.fileName !== "string") return false;
656
903
  if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
@@ -678,7 +925,7 @@ function contentTypeFallback$1(response) {
678
925
  }
679
926
  //#endregion
680
927
  //#region src/switchboard/remote-reservation-store.ts
681
- function isRecord$1(value) {
928
+ function isRecord$2(value) {
682
929
  return typeof value === "object" && value !== null && !Array.isArray(value);
683
930
  }
684
931
  function deriveExtension(fileName) {
@@ -686,8 +933,8 @@ function deriveExtension(fileName) {
686
933
  if (idx <= 0 || idx === fileName.length - 1) return null;
687
934
  return fileName.slice(idx + 1).toLowerCase();
688
935
  }
689
- function isReservation(value) {
690
- if (!isRecord$1(value)) return false;
936
+ function isReservationBase(value) {
937
+ if (!isRecord$2(value)) return false;
691
938
  if (typeof value.reservationId !== "string") return false;
692
939
  if (typeof value.mimeType !== "string") return false;
693
940
  if (typeof value.fileName !== "string") return false;
@@ -696,6 +943,12 @@ function isReservation(value) {
696
943
  if (typeof value.expiresAtUtc !== "string") return false;
697
944
  return true;
698
945
  }
946
+ function isReservation(value) {
947
+ if (!isReservationBase(value)) return false;
948
+ if (value.clientHash !== void 0 && value.clientHash !== null && typeof value.clientHash !== "string") return false;
949
+ if (value.sizeBytes !== void 0 && value.sizeBytes !== null && typeof value.sizeBytes !== "number") return false;
950
+ return true;
951
+ }
699
952
  var RemoteReservationStore = class {
700
953
  remoteUrl;
701
954
  jwtHandler;
@@ -709,28 +962,60 @@ var RemoteReservationStore = class {
709
962
  const url = `${this.remoteUrl}/attachments/reservations`;
710
963
  const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
711
964
  const extension = options.extension ?? deriveExtension(options.fileName);
965
+ const bodyObj = {
966
+ mimeType: options.mimeType,
967
+ fileName: options.fileName,
968
+ extension
969
+ };
970
+ if (options.clientHash !== void 0) bodyObj.clientHash = options.clientHash;
971
+ if (options.sizeBytes !== void 0) bodyObj.sizeBytes = options.sizeBytes;
712
972
  const response = await this.fetchFn(url, {
713
973
  method: "POST",
714
974
  headers: {
715
975
  ...authHeaders,
716
976
  "Content-Type": "application/json"
717
977
  },
718
- body: JSON.stringify({
719
- mimeType: options.mimeType,
720
- fileName: options.fileName,
721
- extension
722
- })
978
+ body: JSON.stringify(bodyObj)
723
979
  });
980
+ if (response.status === 409) {
981
+ let body;
982
+ try {
983
+ body = await response.json();
984
+ } catch {
985
+ throw new Error(`Reservation create failed: ${response.status} ${response.statusText}`);
986
+ }
987
+ if (isRecord$2(body) && body.error === "already_exists" && options.clientHash !== void 0) {
988
+ let ref;
989
+ if (typeof body.ref === "string") try {
990
+ parseRef(body.ref);
991
+ ref = body.ref;
992
+ } catch {
993
+ ref = createRef(options.clientHash);
994
+ }
995
+ else ref = createRef(options.clientHash);
996
+ throw new AttachmentAlreadyExists(options.clientHash, ref);
997
+ }
998
+ throw new Error(`Reservation create failed: ${response.status} ${response.statusText}`);
999
+ }
724
1000
  if (!response.ok) throw new Error(`Reservation create failed: ${response.status} ${response.statusText}`);
725
- const json = await response.json();
1001
+ let json;
1002
+ try {
1003
+ json = await response.json();
1004
+ } catch {
1005
+ throw new Error("Reservation create returned non-JSON response");
1006
+ }
1007
+ if (typeof json !== "object" || json === null || typeof json.reservationId !== "string" || json.reservationId.length === 0) throw new Error("Reservation create returned a payload missing a non-empty reservationId string");
1008
+ const body = json;
726
1009
  const now = /* @__PURE__ */ new Date();
727
1010
  return {
728
- reservationId: json.reservationId,
1011
+ reservationId: body.reservationId,
729
1012
  mimeType: options.mimeType,
730
1013
  fileName: options.fileName,
731
1014
  extension,
732
- createdAtUtc: json.createdAtUtc ?? now.toISOString(),
733
- expiresAtUtc: json.expiresAtUtc ?? new Date(now.getTime() + 1440 * 60 * 1e3).toISOString()
1015
+ createdAtUtc: body.createdAtUtc ?? now.toISOString(),
1016
+ expiresAtUtc: body.expiresAtUtc ?? new Date(now.getTime() + 1440 * 60 * 1e3).toISOString(),
1017
+ clientHash: options.clientHash ?? null,
1018
+ sizeBytes: options.sizeBytes ?? null
734
1019
  };
735
1020
  }
736
1021
  async get(reservationId) {
@@ -746,7 +1031,16 @@ var RemoteReservationStore = class {
746
1031
  throw new Error("Reservation get returned non-JSON response");
747
1032
  }
748
1033
  if (!isReservation(parsed)) throw new Error("Reservation get returned a payload that does not match the Reservation shape");
749
- return parsed;
1034
+ return {
1035
+ reservationId: parsed.reservationId,
1036
+ mimeType: parsed.mimeType,
1037
+ fileName: parsed.fileName,
1038
+ extension: parsed.extension,
1039
+ createdAtUtc: parsed.createdAtUtc,
1040
+ expiresAtUtc: parsed.expiresAtUtc,
1041
+ clientHash: parsed.clientHash ?? null,
1042
+ sizeBytes: parsed.sizeBytes ?? null
1043
+ };
750
1044
  }
751
1045
  async delete(reservationId) {
752
1046
  const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;
@@ -763,15 +1057,20 @@ var RemoteReservationStore = class {
763
1057
  };
764
1058
  //#endregion
765
1059
  //#region src/switchboard/remote-attachment-upload.ts
1060
+ function isRecord$1(value) {
1061
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1062
+ }
766
1063
  var RemoteAttachmentUpload = class {
767
1064
  reservationId;
1065
+ ref;
1066
+ expiresAtUtc;
768
1067
  remoteUrl;
769
1068
  jwtHandler;
770
1069
  fetchFn;
771
- options;
772
- constructor(reservationId, options, config) {
773
- this.reservationId = reservationId;
774
- this.options = options;
1070
+ constructor(reservation, config) {
1071
+ this.reservationId = reservation.reservationId;
1072
+ this.ref = reservation.clientHash !== null ? createRef(reservation.clientHash) : null;
1073
+ this.expiresAtUtc = reservation.expiresAtUtc;
775
1074
  this.remoteUrl = config.remoteUrl;
776
1075
  this.jwtHandler = config.jwtHandler;
777
1076
  this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);
@@ -788,6 +1087,19 @@ var RemoteAttachmentUpload = class {
788
1087
  },
789
1088
  body
790
1089
  });
1090
+ if (response.status === 422) {
1091
+ let errorBody;
1092
+ try {
1093
+ errorBody = await response.json();
1094
+ } catch {
1095
+ throw new Error(`Attachment upload failed: ${response.status} ${response.statusText}`);
1096
+ }
1097
+ if (isRecord$1(errorBody)) {
1098
+ if (errorBody.error === "hash_mismatch" && typeof errorBody.claimed === "string" && typeof errorBody.actual === "string") throw new HashMismatch(errorBody.claimed, errorBody.actual);
1099
+ if (errorBody.error === "size_mismatch" && typeof errorBody.declared === "number" && typeof errorBody.actual === "number") throw new SizeMismatch(errorBody.declared, errorBody.actual);
1100
+ }
1101
+ throw new Error(`Attachment upload failed: ${response.status} ${response.statusText}`);
1102
+ }
791
1103
  if (!response.ok) throw new Error(`Attachment upload failed: ${response.status} ${response.statusText}`);
792
1104
  return await response.json();
793
1105
  }
@@ -798,8 +1110,8 @@ var RemoteAttachmentUploadFactory = class {
798
1110
  constructor(config) {
799
1111
  this.config = config;
800
1112
  }
801
- createUpload(reservationId, options) {
802
- return new RemoteAttachmentUpload(reservationId, options, this.config);
1113
+ createUpload(reservation) {
1114
+ return new RemoteAttachmentUpload(reservation, this.config);
803
1115
  }
804
1116
  };
805
1117
  //#endregion
@@ -852,6 +1164,33 @@ function parseMetadata(response) {
852
1164
  } catch {}
853
1165
  return fallback();
854
1166
  }
1167
+ function parsePendingExpiry(response) {
1168
+ const header = response.headers.get("Attachment-Pending");
1169
+ if (!header) return null;
1170
+ try {
1171
+ const parsed = JSON.parse(header);
1172
+ if (!isRecord(parsed)) return null;
1173
+ if (typeof parsed.expiresAtUtc !== "string") return null;
1174
+ const result = { expiresAtUtc: parsed.expiresAtUtc };
1175
+ if (typeof parsed.mimeType === "string") result.mimeType = parsed.mimeType;
1176
+ if (typeof parsed.fileName === "string") result.fileName = parsed.fileName;
1177
+ if (typeof parsed.sizeBytes === "number" && Number.isFinite(parsed.sizeBytes) && parsed.sizeBytes >= 0) result.sizeBytes = parsed.sizeBytes;
1178
+ return result;
1179
+ } catch {
1180
+ return null;
1181
+ }
1182
+ }
1183
+ function parsePendingHeader(response) {
1184
+ const partial = parsePendingExpiry(response);
1185
+ if (!partial) return null;
1186
+ if (typeof partial.mimeType !== "string" || typeof partial.fileName !== "string" || partial.sizeBytes === void 0) return null;
1187
+ return {
1188
+ expiresAtUtc: partial.expiresAtUtc,
1189
+ mimeType: partial.mimeType,
1190
+ fileName: partial.fileName,
1191
+ sizeBytes: partial.sizeBytes
1192
+ };
1193
+ }
855
1194
  var RemoteAttachmentStore = class {
856
1195
  remoteUrl;
857
1196
  jwtHandler;
@@ -861,6 +1200,13 @@ var RemoteAttachmentStore = class {
861
1200
  this.jwtHandler = config.jwtHandler;
862
1201
  this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);
863
1202
  }
1203
+ /**
1204
+ * Get attachment metadata. Normally returns a pending AttachmentHeader
1205
+ * (status: 'pending') when the server responds 202 with a full
1206
+ * Attachment-Pending header. When only expiresAtUtc is present in the
1207
+ * header (degraded wire), throws AttachmentPending instead -- the
1208
+ * AttachmentPending throw is the degraded-wire case.
1209
+ */
864
1210
  async stat(hash) {
865
1211
  const url = `${this.remoteUrl}/attachments/${hash}`;
866
1212
  const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
@@ -868,6 +1214,13 @@ var RemoteAttachmentStore = class {
868
1214
  method: "HEAD",
869
1215
  headers: authHeaders
870
1216
  });
1217
+ if (response.status === 202) {
1218
+ const fullPending = parsePendingHeader(response);
1219
+ if (fullPending) return buildPendingHeader(hash, fullPending);
1220
+ const partial = parsePendingExpiry(response);
1221
+ if (partial) throw new AttachmentPending(hash, partial.expiresAtUtc);
1222
+ throw new Error("Attachment stat returned 202 with missing or malformed Attachment-Pending header");
1223
+ }
871
1224
  if (response.status === 404) throw new AttachmentNotFound(hash);
872
1225
  if (!response.ok) throw new Error(`Attachment stat failed: ${response.status} ${response.statusText}`);
873
1226
  return buildHeader(hash, parseMetadata(response));
@@ -882,6 +1235,11 @@ var RemoteAttachmentStore = class {
882
1235
  signal,
883
1236
  headers
884
1237
  });
1238
+ if (response.status === 202) {
1239
+ const pending = parsePendingExpiry(response);
1240
+ if (!pending) throw new Error("Attachment fetch returned 202 with missing or malformed Attachment-Pending header");
1241
+ throw new AttachmentPending(hash, pending.expiresAtUtc);
1242
+ }
885
1243
  if (response.status === 404) throw new AttachmentNotFound(hash);
886
1244
  if (!response.ok) throw new Error(`Attachment fetch failed: ${response.status} ${response.statusText}`);
887
1245
  if (!response.body) throw new Error("Response body is null");
@@ -901,7 +1259,23 @@ function buildHeader(hash, metadata) {
901
1259
  status: "available",
902
1260
  source: "sync",
903
1261
  createdAtUtc: metadata.createdAtUtc,
904
- lastAccessedAtUtc: metadata.lastAccessedAtUtc ?? metadata.createdAtUtc
1262
+ lastAccessedAtUtc: metadata.lastAccessedAtUtc ?? metadata.createdAtUtc,
1263
+ expiresAtUtc: null
1264
+ };
1265
+ }
1266
+ function buildPendingHeader(hash, pending) {
1267
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1268
+ return {
1269
+ hash,
1270
+ mimeType: pending.mimeType,
1271
+ fileName: pending.fileName,
1272
+ sizeBytes: pending.sizeBytes,
1273
+ extension: null,
1274
+ status: "pending",
1275
+ source: "sync",
1276
+ createdAtUtc: now,
1277
+ lastAccessedAtUtc: now,
1278
+ expiresAtUtc: pending.expiresAtUtc
905
1279
  };
906
1280
  }
907
1281
  //#endregion
@@ -915,11 +1289,11 @@ function createRemoteAttachmentService(config) {
915
1289
  //#region src/null-attachment-transport.ts
916
1290
  /**
917
1291
  * No-op transport for deployments without remote sync.
918
- * fetch() always returns null, announce() and push() are no-ops.
1292
+ * fetch() always returns not-found, announce() and push() are no-ops.
919
1293
  */
920
1294
  var NullAttachmentTransport = class {
921
1295
  fetch() {
922
- return Promise.resolve(null);
1296
+ return Promise.resolve({ kind: "not-found" });
923
1297
  }
924
1298
  announce() {
925
1299
  return Promise.resolve();
@@ -934,6 +1308,7 @@ var AttachmentBuilder = class {
934
1308
  transport = new NullAttachmentTransport();
935
1309
  customUploadFactory;
936
1310
  maxUploadBytes;
1311
+ reservationSweepMs;
937
1312
  constructor(db, storagePath) {
938
1313
  this.db = db;
939
1314
  this.storagePath = storagePath;
@@ -950,6 +1325,18 @@ var AttachmentBuilder = class {
950
1325
  this.maxUploadBytes = maxBytes;
951
1326
  return this;
952
1327
  }
1328
+ /**
1329
+ * Configure a recurring sweep that deletes expired reservations.
1330
+ * The sweep calls reservations.deleteExpired() on the given interval.
1331
+ * When set, the built result's destroy() clears the timer.
1332
+ * Without this option no sweep runs -- deleteExpired() is never called
1333
+ * automatically. Call withReservationSweepMs in production to prevent
1334
+ * expired reservation rows from accumulating indefinitely.
1335
+ */
1336
+ withReservationSweepMs(intervalMs) {
1337
+ this.reservationSweepMs = intervalMs;
1338
+ return this;
1339
+ }
953
1340
  async build() {
954
1341
  const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);
955
1342
  if (!result.success && result.error) throw result.error;
@@ -957,15 +1344,31 @@ var AttachmentBuilder = class {
957
1344
  const store = new KyselyAttachmentStore(scopedDb, this.transport, this.storagePath);
958
1345
  const reservations = new KyselyReservationStore(scopedDb);
959
1346
  const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations, this.maxUploadBytes);
1347
+ const service = new AttachmentService(store, reservations, uploadFactory);
1348
+ let sweepTimer;
1349
+ if (this.reservationSweepMs !== void 0) {
1350
+ const intervalMs = this.reservationSweepMs;
1351
+ sweepTimer = setInterval(() => {
1352
+ reservations.deleteExpired().catch(() => {});
1353
+ }, intervalMs);
1354
+ if (typeof sweepTimer.unref === "function") sweepTimer.unref();
1355
+ }
1356
+ const destroy = () => {
1357
+ if (sweepTimer !== void 0) {
1358
+ clearInterval(sweepTimer);
1359
+ sweepTimer = void 0;
1360
+ }
1361
+ };
960
1362
  return {
961
- service: new AttachmentService(store, reservations, uploadFactory),
1363
+ service,
962
1364
  store,
963
1365
  reservations,
964
- uploadFactory
1366
+ uploadFactory,
1367
+ destroy
965
1368
  };
966
1369
  }
967
1370
  };
968
1371
  //#endregion
969
- export { ATTACHMENT_SCHEMA, AttachmentBuilder, AttachmentNotFound, AttachmentService, DirectAttachmentUpload, DirectAttachmentUploadFactory, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SwitchboardAttachmentTransport, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
1372
+ export { ATTACHMENT_SCHEMA, AttachmentAlreadyExists, AttachmentBuilder, AttachmentNotFound, AttachmentPending, AttachmentService, DEFAULT_RESERVATION_TTL_MS, DirectAttachmentUpload, DirectAttachmentUploadFactory, HashMismatch, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SizeMismatch, SwitchboardAttachmentTransport, UploadTooLarge, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
970
1373
 
971
1374
  //# sourceMappingURL=index.js.map