@powerhousedao/reactor-attachments 6.2.2-dev.5 → 6.2.2-dev.50

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
@@ -1,10 +1,17 @@
1
- import { _ as SizeMismatch, a as RemoteAttachmentUpload, c as AttachmentService, d as AttachmentAlreadyExists, f as AttachmentNotFound, g as ReservationNotFound, h as InvalidAttachmentRef, i as RemoteAttachmentUploadFactory, l as createRef, m as HashMismatch, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentPending, r as RemoteAttachmentStore, s as SwitchboardAttachmentTransport, t as NullAttachmentTransport, u as parseRef, v as UploadTooLarge } from "./null-attachment-transport-Drx03s02.js";
1
+ import { _ as HashMismatch, a as RemoteAttachmentUpload, b as SizeMismatch, c as AttachmentService, d as parseAttachmentDownloadTarget, f as parseAttachmentUploadTarget, g as AttachmentTransferError, h as AttachmentPending, i as RemoteAttachmentUploadFactory, l as createRef, m as AttachmentNotFound, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentAlreadyExists, r as RemoteAttachmentStore, s as SwitchboardAttachmentTransport, t as NullAttachmentTransport, u as parseRef, v as InvalidAttachmentRef, x as UploadTooLarge, y as ReservationNotFound } from "./null-attachment-transport-CTBrRCDe.js";
2
2
  import { dirname, join } from "node:path";
3
3
  import { Migrator, sql } from "kysely";
4
4
  import { mkdir, rename, rm } from "node:fs/promises";
5
5
  import { createReadStream, createWriteStream } from "node:fs";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { Readable } from "node:stream";
8
+ import { generatorTypeDefs } from "@powerhousedao/document-engineering/graphql";
9
+ import { constantCase, pascalCase } from "change-case";
10
+ import { Kind, buildASTSchema, getNamedType, isInputObjectType, isListType, isNonNullType, parse } from "graphql";
11
+ import { BaseReadModel } from "@powerhousedao/reactor";
12
+ import { Buffer as Buffer$1 } from "node:buffer";
13
+ import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
14
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
8
15
  //#region \0rolldown/runtime.js
9
16
  var __defProp = Object.defineProperty;
10
17
  var __exportAll = (all, no_symbols) => {
@@ -391,84 +398,84 @@ var KyselyReservationStore = class {
391
398
  //#endregion
392
399
  //#region src/storage/migrations/001_create_attachment_table.ts
393
400
  var _001_create_attachment_table_exports = /* @__PURE__ */ __exportAll({
394
- down: () => down$5,
395
- up: () => up$5
401
+ down: () => down$6,
402
+ up: () => up$6
396
403
  });
397
- async function up$5(db) {
404
+ async function up$6(db) {
398
405
  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();
399
406
  await db.schema.createIndex("idx_attachment_status").on("attachment").column("status").execute();
400
407
  await db.schema.createIndex("idx_attachment_lru").on("attachment").columns(["status", "last_accessed_at_utc"]).execute();
401
408
  }
402
- async function down$5(db) {
409
+ async function down$6(db) {
403
410
  await db.schema.dropTable("attachment").ifExists().execute();
404
411
  }
405
412
  //#endregion
406
413
  //#region src/storage/migrations/002_create_reservation_table.ts
407
414
  var _002_create_reservation_table_exports = /* @__PURE__ */ __exportAll({
408
- down: () => down$4,
409
- up: () => up$4
415
+ down: () => down$5,
416
+ up: () => up$5
410
417
  });
411
- async function up$4(db) {
418
+ async function up$5(db) {
412
419
  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();
413
420
  }
414
- async function down$4(db) {
421
+ async function down$5(db) {
415
422
  await db.schema.dropTable("attachment_reservation").ifExists().execute();
416
423
  }
417
424
  //#endregion
418
425
  //#region src/storage/migrations/003_add_reservation_expires_at.ts
419
426
  var _003_add_reservation_expires_at_exports = /* @__PURE__ */ __exportAll({
420
- down: () => down$3,
421
- up: () => up$3
427
+ down: () => down$4,
428
+ up: () => up$4
422
429
  });
423
- async function up$3(db) {
430
+ async function up$4(db) {
424
431
  await db.schema.alterTable("attachment_reservation").addColumn("expires_at_utc", "text").execute();
425
432
  await db.updateTable("attachment_reservation").set({ expires_at_utc: sql`created_at_utc` }).where("expires_at_utc", "is", null).execute();
426
433
  await db.schema.alterTable("attachment_reservation").alterColumn("expires_at_utc", (col) => col.setNotNull()).execute();
427
434
  await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
428
435
  }
429
- async function down$3(db) {
436
+ async function down$4(db) {
430
437
  await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
431
438
  await db.schema.alterTable("attachment_reservation").dropColumn("expires_at_utc").execute();
432
439
  }
433
440
  //#endregion
434
441
  //#region src/storage/migrations/004_add_reservation_soft_delete.ts
435
442
  var _004_add_reservation_soft_delete_exports = /* @__PURE__ */ __exportAll({
436
- down: () => down$2,
437
- up: () => up$2
443
+ down: () => down$3,
444
+ up: () => up$3
438
445
  });
439
- async function up$2(db) {
446
+ async function up$3(db) {
440
447
  await db.schema.alterTable("attachment_reservation").addColumn("deleted_at_utc", "text").execute();
441
448
  }
442
- async function down$2(db) {
449
+ async function down$3(db) {
443
450
  await db.schema.alterTable("attachment_reservation").dropColumn("deleted_at_utc").execute();
444
451
  }
445
452
  //#endregion
446
453
  //#region src/storage/migrations/005_add_reservation_active_index.ts
447
454
  var _005_add_reservation_active_index_exports = /* @__PURE__ */ __exportAll({
448
- down: () => down$1,
449
- up: () => up$1
455
+ down: () => down$2,
456
+ up: () => up$2
450
457
  });
451
- async function up$1(db) {
458
+ async function up$2(db) {
452
459
  await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
453
460
  await db.schema.createIndex("idx_reservation_expires_at_active").on("attachment_reservation").column("expires_at_utc").where(sql`deleted_at_utc IS NULL`).execute();
454
461
  }
455
- async function down$1(db) {
462
+ async function down$2(db) {
456
463
  await db.schema.dropIndex("idx_reservation_expires_at_active").ifExists().execute();
457
464
  await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
458
465
  }
459
466
  //#endregion
460
467
  //#region src/storage/migrations/006_add_reservation_client_hash.ts
461
468
  var _006_add_reservation_client_hash_exports = /* @__PURE__ */ __exportAll({
462
- down: () => down,
463
- up: () => up
469
+ down: () => down$1,
470
+ up: () => up$1
464
471
  });
465
- async function up(db) {
472
+ async function up$1(db) {
466
473
  await db.schema.alterTable("attachment_reservation").addColumn("client_hash", "text").execute();
467
474
  await db.schema.alterTable("attachment_reservation").addColumn("size_bytes", "bigint").execute();
468
475
  await db.schema.alterTable("attachment_reservation").addCheckConstraint("attachment_reservation_hash_size_check", sql`client_hash is null or size_bytes is not null`).execute();
469
476
  await db.schema.createIndex("idx_reservation_client_hash").on("attachment_reservation").column("client_hash").execute();
470
477
  }
471
- async function down(db) {
478
+ async function down$1(db) {
472
479
  await db.schema.dropIndex("idx_reservation_client_hash").ifExists().execute();
473
480
  await db.schema.alterTable("attachment_reservation").dropColumn("size_bytes").execute();
474
481
  await db.schema.alterTable("attachment_reservation").dropColumn("client_hash").execute();
@@ -476,7 +483,7 @@ async function down(db) {
476
483
  //#endregion
477
484
  //#region src/storage/migrations/migrator.ts
478
485
  const ATTACHMENT_SCHEMA = "attachments";
479
- const migrations = {
486
+ const migrations$1 = {
480
487
  "001_create_attachment_table": _001_create_attachment_table_exports,
481
488
  "002_create_reservation_table": _002_create_reservation_table_exports,
482
489
  "003_add_reservation_expires_at": _003_add_reservation_expires_at_exports,
@@ -484,9 +491,9 @@ const migrations = {
484
491
  "005_add_reservation_active_index": _005_add_reservation_active_index_exports,
485
492
  "006_add_reservation_client_hash": _006_add_reservation_client_hash_exports
486
493
  };
487
- var ProgrammaticMigrationProvider = class {
494
+ var ProgrammaticMigrationProvider$1 = class {
488
495
  getMigrations() {
489
- return Promise.resolve(migrations);
496
+ return Promise.resolve(migrations$1);
490
497
  }
491
498
  };
492
499
  async function runAttachmentMigrations(db, schema = ATTACHMENT_SCHEMA) {
@@ -501,7 +508,7 @@ async function runAttachmentMigrations(db, schema = ATTACHMENT_SCHEMA) {
501
508
  }
502
509
  const migrator = new Migrator({
503
510
  db: db.withSchema(schema),
504
- provider: new ProgrammaticMigrationProvider(),
511
+ provider: new ProgrammaticMigrationProvider$1(),
505
512
  migrationTableSchema: schema
506
513
  });
507
514
  let error;
@@ -621,12 +628,69 @@ var DirectAttachmentUploadFactory = class {
621
628
  }
622
629
  };
623
630
  //#endregion
631
+ //#region src/direct/filesystem-attachment-backend.ts
632
+ /**
633
+ * Filesystem keeps byte transfer behind Switchboard. URL construction stays
634
+ * at the server edge, while this adapter validates that a filesystem backend
635
+ * can never accidentally return a direct-provider target.
636
+ */
637
+ var FilesystemAttachmentBackend = class {
638
+ kind = "filesystem";
639
+ constructor(store, config) {
640
+ this.store = store;
641
+ this.config = config;
642
+ }
643
+ async prepareUploadTarget(reservation) {
644
+ const target = parseAttachmentUploadTarget(await this.config.uploadTarget(reservation));
645
+ if (target.kind !== "switchboard") throw new Error("Filesystem upload target must use Switchboard");
646
+ return target;
647
+ }
648
+ async prepareDownloadTarget(hash) {
649
+ const target = parseAttachmentDownloadTarget(await this.config.downloadTarget(hash));
650
+ if (target.kind !== "switchboard") throw new Error("Filesystem download target must use Switchboard");
651
+ return target;
652
+ }
653
+ exists(hash) {
654
+ return this.store.has(hash);
655
+ }
656
+ async health() {
657
+ return {
658
+ kind: this.kind,
659
+ ready: await (this.config.readiness?.() ?? true)
660
+ };
661
+ }
662
+ };
663
+ //#endregion
664
+ //#region src/storage/s3/upload-factory.ts
665
+ var S3AttachmentUpload = class {
666
+ reservationId;
667
+ ref;
668
+ expiresAtUtc;
669
+ uploadTarget;
670
+ constructor(reservation) {
671
+ this.reservationId = reservation.reservationId;
672
+ this.ref = reservation.clientHash === null ? null : createRef(reservation.clientHash);
673
+ this.expiresAtUtc = reservation.expiresAtUtc;
674
+ this.uploadTarget = reservation.uploadTarget;
675
+ }
676
+ async send(data) {
677
+ await data.cancel().catch(() => {});
678
+ throw new Error("S3 attachment upload must use the presigned target");
679
+ }
680
+ };
681
+ var S3AttachmentUploadFactory = class {
682
+ createUpload(reservation) {
683
+ return new S3AttachmentUpload(reservation);
684
+ }
685
+ };
686
+ //#endregion
624
687
  //#region src/attachment-builder.ts
625
688
  var AttachmentBuilder = class {
626
689
  transport = new NullAttachmentTransport();
627
690
  customUploadFactory;
628
691
  maxUploadBytes;
629
692
  reservationSweepMs;
693
+ backend;
630
694
  constructor(db, storagePath) {
631
695
  this.db = db;
632
696
  this.storagePath = storagePath;
@@ -639,6 +703,10 @@ var AttachmentBuilder = class {
639
703
  this.customUploadFactory = factory;
640
704
  return this;
641
705
  }
706
+ withBackend(backend) {
707
+ this.backend = backend;
708
+ return this;
709
+ }
642
710
  withMaxUploadBytes(maxBytes) {
643
711
  this.maxUploadBytes = maxBytes;
644
712
  return this;
@@ -661,8 +729,8 @@ var AttachmentBuilder = class {
661
729
  const scopedDb = this.db.withSchema(ATTACHMENT_SCHEMA);
662
730
  const store = new KyselyAttachmentStore(scopedDb, this.transport, this.storagePath);
663
731
  const reservations = new KyselyReservationStore(scopedDb);
664
- const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations, this.maxUploadBytes);
665
- const service = new AttachmentService(store, reservations, uploadFactory);
732
+ const uploadFactory = this.customUploadFactory ?? (this.backend?.kind === "s3" ? new S3AttachmentUploadFactory() : new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations, this.maxUploadBytes));
733
+ const service = new AttachmentService(store, reservations, uploadFactory, this.backend);
666
734
  let sweepTimer;
667
735
  if (this.reservationSweepMs !== void 0) {
668
736
  const intervalMs = this.reservationSweepMs;
@@ -682,11 +750,728 @@ var AttachmentBuilder = class {
682
750
  store,
683
751
  reservations,
684
752
  uploadFactory,
753
+ ...this.backend ? { backend: this.backend } : {},
685
754
  destroy
686
755
  };
687
756
  }
688
757
  };
689
758
  //#endregion
690
- 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 };
759
+ //#region src/reference-index/attachment-schema-compiler.ts
760
+ const ATTACHMENT_REF_TYPE = "AttachmentRef";
761
+ const CODEGEN_SCALAR_NAMES = new Set([
762
+ "Unknown",
763
+ "DateTime",
764
+ "Address",
765
+ ATTACHMENT_REF_TYPE,
766
+ ...Object.keys(generatorTypeDefs)
767
+ ]);
768
+ function describeContext(context) {
769
+ return `document type "${context.documentType}", version ${context.version}, action "${context.actionType}"`;
770
+ }
771
+ function compilationError(context, reason) {
772
+ return /* @__PURE__ */ new Error(`Attachment schema compilation failed for ${describeContext(context)}: ${reason}`);
773
+ }
774
+ function extractionError(context, path, reason) {
775
+ return /* @__PURE__ */ new Error(`Attachment extraction failed for ${describeContext(context)} at ${path}: ${reason}`);
776
+ }
777
+ function applicableSpecification(module, context) {
778
+ const matches = module.documentModel.global.specifications.filter((specification) => specification.version === context.version);
779
+ if (matches.length !== 1) throw compilationError(context, matches.length === 0 ? "the module has no matching specification" : "the module has multiple matching specifications");
780
+ return matches[0];
781
+ }
782
+ function parseOperations(specification, context) {
783
+ return specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.map((operation) => {
784
+ if (operation.schema === null) return {
785
+ document: null,
786
+ operation
787
+ };
788
+ try {
789
+ return {
790
+ document: parse(operation.schema),
791
+ operation
792
+ };
793
+ } catch {
794
+ throw compilationError(context, "an operation has invalid GraphQL SDL");
795
+ }
796
+ }));
797
+ }
798
+ function selectOperation(operations, context) {
799
+ const matches = operations.filter(({ operation }) => operation.name !== null && constantCase(operation.name) === context.actionType);
800
+ if (matches.length > 1) throw compilationError(context, "multiple operations map to the action");
801
+ if (matches.length === 0) return null;
802
+ return matches[0];
803
+ }
804
+ function buildEffectiveSchema(specification, context) {
805
+ const scalarSchemas = Array.from(CODEGEN_SCALAR_NAMES, (name) => `scalar ${name}`);
806
+ const stateSchemas = Object.values(specification.state).map((state) => state.schema);
807
+ const operationSchemas = specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.flatMap((operation) => operation.schema === null ? [] : [operation.schema]));
808
+ try {
809
+ return buildASTSchema(parse([
810
+ ...scalarSchemas,
811
+ ...stateSchemas,
812
+ ...operationSchemas
813
+ ].filter(Boolean).join("\n\n")));
814
+ } catch {
815
+ throw compilationError(context, "the effective GraphQL schema is invalid");
816
+ }
817
+ }
818
+ function attachmentReachableTypes(definitions) {
819
+ const reachable = /* @__PURE__ */ new Set();
820
+ let changed = true;
821
+ while (changed) {
822
+ changed = false;
823
+ for (const [name, definition] of definitions) {
824
+ if (reachable.has(name)) continue;
825
+ if (Object.values(definition.getFields()).some((field) => {
826
+ const typeName = getNamedType(field.type).name;
827
+ return typeName === ATTACHMENT_REF_TYPE || definitions.has(typeName) && reachable.has(typeName);
828
+ })) {
829
+ reachable.add(name);
830
+ changed = true;
831
+ }
832
+ }
833
+ }
834
+ return reachable;
835
+ }
836
+ function compileValuePlan(type, objectPlans) {
837
+ if (isNonNullType(type)) return {
838
+ ...compileValuePlan(type.ofType, objectPlans),
839
+ required: true
840
+ };
841
+ if (isListType(type)) return {
842
+ item: compileValuePlan(type.ofType, objectPlans),
843
+ kind: "list",
844
+ required: false
845
+ };
846
+ const typeName = type.name;
847
+ if (typeName === ATTACHMENT_REF_TYPE) return {
848
+ kind: "attachment",
849
+ required: false
850
+ };
851
+ const body = objectPlans.get(typeName);
852
+ if (!body) throw new Error(`Internal attachment schema plan error for ${typeName}`);
853
+ return {
854
+ body,
855
+ kind: "object",
856
+ required: false
857
+ };
858
+ }
859
+ function compileRootPlan(rootName, definitions) {
860
+ const reachable = attachmentReachableTypes(definitions);
861
+ if (!reachable.has(rootName)) return null;
862
+ const objectPlans = /* @__PURE__ */ new Map();
863
+ for (const name of reachable) objectPlans.set(name, { fields: [] });
864
+ for (const name of reachable) {
865
+ const definition = definitions.get(name);
866
+ const body = objectPlans.get(name);
867
+ if (!definition || !body) continue;
868
+ for (const field of Object.values(definition.getFields())) {
869
+ const typeName = getNamedType(field.type).name;
870
+ if (typeName !== ATTACHMENT_REF_TYPE && !reachable.has(typeName)) continue;
871
+ body.fields.push({
872
+ hasDefault: field.defaultValue !== void 0,
873
+ name: field.name,
874
+ value: compileValuePlan(field.type, objectPlans)
875
+ });
876
+ }
877
+ }
878
+ return objectPlans.get(rootName) ?? null;
879
+ }
880
+ function readOwnField(value, fieldName, context, path) {
881
+ try {
882
+ if (!Object.prototype.hasOwnProperty.call(value, fieldName)) return {
883
+ present: false,
884
+ value: void 0
885
+ };
886
+ return {
887
+ present: true,
888
+ value: value[fieldName]
889
+ };
890
+ } catch {
891
+ throw extractionError(context, path, "the declared field cannot be read");
892
+ }
893
+ }
894
+ function extractValue(plan, value, path, context, refs, seenRefs, activeObjects) {
895
+ if (value === null || value === void 0) {
896
+ if (plan.required) throw extractionError(context, path, "a required value is missing or null");
897
+ return;
898
+ }
899
+ if (plan.kind === "attachment") {
900
+ if (typeof value !== "string") throw extractionError(context, path, "expected an AttachmentRef string");
901
+ try {
902
+ parseRef(value);
903
+ } catch {
904
+ throw extractionError(context, path, "the AttachmentRef is malformed");
905
+ }
906
+ if (!seenRefs.has(value)) {
907
+ seenRefs.add(value);
908
+ refs.push(value);
909
+ }
910
+ return;
911
+ }
912
+ if (plan.kind === "list") {
913
+ if (!Array.isArray(value)) throw extractionError(context, path, "expected a list");
914
+ for (let index = 0; index < value.length; index += 1) extractValue(plan.item, value[index], `${path}[${index}]`, context, refs, seenRefs, activeObjects);
915
+ return;
916
+ }
917
+ if (typeof value !== "object" || Array.isArray(value)) throw extractionError(context, path, "expected an input object");
918
+ if (activeObjects.has(value)) throw extractionError(context, path, "the input value contains a cycle");
919
+ activeObjects.add(value);
920
+ try {
921
+ const record = value;
922
+ for (const field of plan.body.fields) {
923
+ const fieldPath = `${path}.${field.name}`;
924
+ const result = readOwnField(record, field.name, context, fieldPath);
925
+ if ((!result.present || result.value === void 0) && field.hasDefault) continue;
926
+ extractValue(field.value, result.value, fieldPath, context, refs, seenRefs, activeObjects);
927
+ }
928
+ } finally {
929
+ activeObjects.delete(value);
930
+ }
931
+ }
932
+ var SchemaCompiledAttachmentExtractor = class {
933
+ constructor(context, rootPlan) {
934
+ this.context = context;
935
+ this.rootPlan = rootPlan;
936
+ }
937
+ extract(action) {
938
+ if (action.type !== this.context.actionType) throw extractionError(this.context, "input", "the action type does not match the compiled schema");
939
+ if (!this.rootPlan) return [];
940
+ if (action.input === null || typeof action.input !== "object" || Array.isArray(action.input)) throw extractionError(this.context, "input", "expected an input object");
941
+ const refs = [];
942
+ const seenRefs = /* @__PURE__ */ new Set();
943
+ const activeObjects = /* @__PURE__ */ new WeakSet();
944
+ activeObjects.add(action.input);
945
+ try {
946
+ const input = action.input;
947
+ for (const field of this.rootPlan.fields) {
948
+ const path = `input.${field.name}`;
949
+ const result = readOwnField(input, field.name, this.context, path);
950
+ if ((!result.present || result.value === void 0) && field.hasDefault) continue;
951
+ extractValue(field.value, result.value, path, this.context, refs, seenRefs, activeObjects);
952
+ }
953
+ } finally {
954
+ activeObjects.delete(action.input);
955
+ }
956
+ return refs;
957
+ }
958
+ };
959
+ function compileExtractor(module, actionType) {
960
+ const context = {
961
+ actionType,
962
+ documentType: module.documentModel.global.id,
963
+ version: module.version ?? 1
964
+ };
965
+ const specification = applicableSpecification(module, context);
966
+ const selected = selectOperation(parseOperations(specification, context), context);
967
+ if (selected === null || selected.operation.schema === null) return new SchemaCompiledAttachmentExtractor(context, null);
968
+ const effectiveSchema = buildEffectiveSchema(specification, context);
969
+ const operationName = selected.operation.name;
970
+ if (operationName === null) throw compilationError(context, "the operation has no name");
971
+ const rootName = `${pascalCase(operationName)}Input`;
972
+ const definitions = /* @__PURE__ */ new Map();
973
+ for (const type of Object.values(effectiveSchema.getTypeMap())) if (isInputObjectType(type) && !type.name.startsWith("__")) definitions.set(type.name, type);
974
+ if (!(selected.document?.definitions.filter((definition) => (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) && definition.name.value === rootName))?.length || !definitions.has(rootName)) throw compilationError(context, `the operation does not declare its expected root input "${rootName}"`);
975
+ return new SchemaCompiledAttachmentExtractor(context, compileRootPlan(rootName, definitions));
976
+ }
977
+ var AttachmentSchemaCompiler = class {
978
+ cache = /* @__PURE__ */ new WeakMap();
979
+ forModuleAction(module, actionType) {
980
+ let moduleCache = this.cache.get(module);
981
+ if (!moduleCache) {
982
+ moduleCache = /* @__PURE__ */ new Map();
983
+ this.cache.set(module, moduleCache);
984
+ }
985
+ const cached = moduleCache.get(actionType);
986
+ if (cached) return cached;
987
+ const compiled = compileExtractor(module, actionType);
988
+ moduleCache.set(actionType, compiled);
989
+ return compiled;
990
+ }
991
+ };
992
+ //#endregion
993
+ //#region src/read-models/attachment-reference/kysely-attachment-reference-store.ts
994
+ var KyselyAttachmentReferenceStore = class {
995
+ constructor(db) {
996
+ this.db = db;
997
+ }
998
+ async hasReference(documentId, ref) {
999
+ return await this.db.selectFrom("attachment_reference").select("document_id").where("document_id", "=", documentId).where("attachment_ref", "=", ref).executeTakeFirst() !== void 0;
1000
+ }
1001
+ async addReferences(references) {
1002
+ if (references.length === 0) return;
1003
+ await this.db.insertInto("attachment_reference").values(references.map((reference) => ({
1004
+ document_id: reference.documentId,
1005
+ attachment_ref: reference.ref,
1006
+ attachment_hash: parseRef(reference.ref).hash,
1007
+ first_operation_id: reference.operationId,
1008
+ branch: reference.branch,
1009
+ scope: reference.scope,
1010
+ first_seen_ordinal: reference.ordinal,
1011
+ created_at_utc: (/* @__PURE__ */ new Date()).toISOString()
1012
+ }))).onConflict((oc) => oc.columns(["document_id", "attachment_ref"]).doNothing()).execute();
1013
+ }
1014
+ };
1015
+ //#endregion
1016
+ //#region src/read-models/attachment-reference/storage/migrations/001_create_attachment_reference_table.ts
1017
+ var _001_create_attachment_reference_table_exports = /* @__PURE__ */ __exportAll({
1018
+ down: () => down,
1019
+ up: () => up
1020
+ });
1021
+ async function up(db) {
1022
+ await db.schema.createTable("attachment_reference").addColumn("document_id", "text", (col) => col.notNull()).addColumn("attachment_ref", "text", (col) => col.notNull()).addColumn("attachment_hash", "text", (col) => col.notNull()).addColumn("first_operation_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("first_seen_ordinal", "integer", (col) => col.notNull()).addColumn("created_at_utc", "text", (col) => col.notNull()).addUniqueConstraint("unique_attachment_reference_document_ref", ["document_id", "attachment_ref"]).execute();
1023
+ await db.schema.createIndex("idx_attachment_reference_ref").on("attachment_reference").column("attachment_ref").execute();
1024
+ await db.schema.createIndex("idx_attachment_reference_hash").on("attachment_reference").column("attachment_hash").execute();
1025
+ }
1026
+ async function down(db) {
1027
+ await db.schema.dropTable("attachment_reference").ifExists().execute();
1028
+ }
1029
+ //#endregion
1030
+ //#region src/read-models/attachment-reference/storage/migrations/migrator.ts
1031
+ const ATTACHMENT_REFERENCE_SCHEMA = "attachment_reference_read_model";
1032
+ const ATTACHMENT_REFERENCE_MIGRATION_TABLE = "kysely_migration_attachment_reference_read_model";
1033
+ const ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE = "kysely_migration_attachment_reference_read_model_lock";
1034
+ const migrations = { "001_create_attachment_reference_table": _001_create_attachment_reference_table_exports };
1035
+ var ProgrammaticMigrationProvider = class {
1036
+ getMigrations() {
1037
+ return Promise.resolve(migrations);
1038
+ }
1039
+ };
1040
+ function createMigrator(db, schema) {
1041
+ return new Migrator({
1042
+ db: db.withSchema(schema),
1043
+ provider: new ProgrammaticMigrationProvider(),
1044
+ migrationTableSchema: schema,
1045
+ migrationTableName: ATTACHMENT_REFERENCE_MIGRATION_TABLE,
1046
+ migrationLockTableName: ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE
1047
+ });
1048
+ }
1049
+ function toResult(error, results) {
1050
+ const migrationsExecuted = results?.map((result) => result.migrationName) ?? [];
1051
+ if (error) return {
1052
+ success: false,
1053
+ migrationsExecuted,
1054
+ error: error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown migration error")
1055
+ };
1056
+ return {
1057
+ success: true,
1058
+ migrationsExecuted
1059
+ };
1060
+ }
1061
+ async function runAttachmentReferenceMigrations(db, schema = ATTACHMENT_REFERENCE_SCHEMA) {
1062
+ try {
1063
+ await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);
1064
+ } catch (error) {
1065
+ return {
1066
+ success: false,
1067
+ migrationsExecuted: [],
1068
+ error: error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create schema")
1069
+ };
1070
+ }
1071
+ try {
1072
+ const { error, results } = await createMigrator(db, schema).migrateToLatest();
1073
+ return toResult(error, results);
1074
+ } catch (error) {
1075
+ return toResult(error, []);
1076
+ }
1077
+ }
1078
+ async function rollbackAttachmentReferenceMigration(db, schema = ATTACHMENT_REFERENCE_SCHEMA) {
1079
+ try {
1080
+ const { error, results } = await createMigrator(db, schema).migrateDown();
1081
+ return toResult(error, results);
1082
+ } catch (error) {
1083
+ return toResult(error, []);
1084
+ }
1085
+ }
1086
+ async function getAttachmentReferenceMigrationStatus(db, schema = ATTACHMENT_REFERENCE_SCHEMA) {
1087
+ return await createMigrator(db, schema).getMigrations();
1088
+ }
1089
+ //#endregion
1090
+ //#region src/read-models/attachment-reference/index-builder.ts
1091
+ var AttachmentReferenceIndexBuilder = class {
1092
+ constructor(db) {
1093
+ this.db = db;
1094
+ }
1095
+ async build() {
1096
+ const result = await runAttachmentReferenceMigrations(this.db);
1097
+ if (!result.success && result.error) throw result.error;
1098
+ return { store: new KyselyAttachmentReferenceStore(this.db.withSchema(ATTACHMENT_REFERENCE_SCHEMA)) };
1099
+ }
1100
+ };
1101
+ //#endregion
1102
+ //#region src/read-models/attachment-reference/attachment-reference-read-model.ts
1103
+ const ATTACHMENT_REFERENCE_READ_MODEL_ID = "attachment-reference-read-model";
1104
+ var AttachmentReferenceReadModel = class extends BaseReadModel {
1105
+ indexingQueue = Promise.resolve();
1106
+ constructor(db, operationIndex, writeCache, consistencyTracker, documentModelRegistry, schemaCompiler, referenceWriter) {
1107
+ super(db, operationIndex, writeCache, consistencyTracker, {
1108
+ readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,
1109
+ rebuildStateOnInit: false
1110
+ });
1111
+ this.documentModelRegistry = documentModelRegistry;
1112
+ this.schemaCompiler = schemaCompiler;
1113
+ this.referenceWriter = referenceWriter;
1114
+ }
1115
+ indexOperations(items) {
1116
+ return this.enqueue(() => this.indexOperationsInOrdinalOrder(items));
1117
+ }
1118
+ init() {
1119
+ return this.enqueue(async () => {
1120
+ const viewState = await this.loadState();
1121
+ if (viewState !== void 0) this.lastOrdinal = viewState;
1122
+ else await this.initializeState();
1123
+ let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
1124
+ while (page.results.length > 0) {
1125
+ await this.indexOperationsInOrdinalOrder(page.results);
1126
+ if (!page.next) break;
1127
+ page = await page.next();
1128
+ }
1129
+ });
1130
+ }
1131
+ enqueue(work) {
1132
+ const result = this.indexingQueue.then(work);
1133
+ this.indexingQueue = result.catch(() => void 0);
1134
+ return result;
1135
+ }
1136
+ async commitOperations(items) {
1137
+ const references = [];
1138
+ for (const { operation, context } of items) {
1139
+ if (operation.error !== void 0) continue;
1140
+ const module = this.documentModelRegistry.getModule(context.documentType);
1141
+ const refs = this.schemaCompiler.forModuleAction(module, operation.action.type).extract(operation.action);
1142
+ for (const ref of refs) references.push({
1143
+ documentId: context.documentId,
1144
+ ref,
1145
+ operationId: operation.id,
1146
+ branch: context.branch,
1147
+ scope: context.scope,
1148
+ ordinal: context.ordinal
1149
+ });
1150
+ }
1151
+ if (references.length > 0) await this.referenceWriter.addReferences(references);
1152
+ }
1153
+ async indexOperationsInOrdinalOrder(incoming) {
1154
+ const pending = this.sortAndDedupe(incoming.filter(({ context }) => context.ordinal > this.lastOrdinal));
1155
+ if (pending.length === 0) return;
1156
+ const incomingMax = pending[pending.length - 1].context.ordinal;
1157
+ let candidates = pending;
1158
+ if (!this.isContiguousThrough(candidates, incomingMax)) {
1159
+ const replayed = await this.loadThroughOrdinal(incomingMax);
1160
+ candidates = this.sortAndDedupe([...replayed, ...pending]);
1161
+ }
1162
+ const contiguous = [];
1163
+ let expectedOrdinal = this.lastOrdinal + 1;
1164
+ for (const item of candidates) {
1165
+ const ordinal = item.context.ordinal;
1166
+ if (ordinal < expectedOrdinal) continue;
1167
+ if (ordinal > expectedOrdinal || ordinal > incomingMax) break;
1168
+ contiguous.push(item);
1169
+ expectedOrdinal++;
1170
+ }
1171
+ if (expectedOrdinal <= incomingMax) throw new Error(`Attachment reference read model cannot advance past missing ordinal ${expectedOrdinal}`);
1172
+ const previousOrdinal = this.lastOrdinal;
1173
+ try {
1174
+ await super.indexOperations(contiguous);
1175
+ } catch (error) {
1176
+ this.lastOrdinal = previousOrdinal;
1177
+ throw error;
1178
+ }
1179
+ }
1180
+ isContiguousThrough(items, maxOrdinal) {
1181
+ let expectedOrdinal = this.lastOrdinal + 1;
1182
+ for (const item of items) {
1183
+ if (item.context.ordinal !== expectedOrdinal) return false;
1184
+ expectedOrdinal++;
1185
+ }
1186
+ return expectedOrdinal > maxOrdinal;
1187
+ }
1188
+ async loadThroughOrdinal(maxOrdinal) {
1189
+ const operations = [];
1190
+ let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
1191
+ for (;;) {
1192
+ for (const item of page.results) if (item.context.ordinal <= maxOrdinal) operations.push(item);
1193
+ if (page.results.some(({ context }) => context.ordinal >= maxOrdinal) || !page.next) break;
1194
+ page = await page.next();
1195
+ }
1196
+ return operations;
1197
+ }
1198
+ sortAndDedupe(items) {
1199
+ const byOrdinal = /* @__PURE__ */ new Map();
1200
+ for (const item of items) byOrdinal.set(item.context.ordinal, item);
1201
+ return [...byOrdinal.values()].sort((left, right) => left.context.ordinal - right.context.ordinal);
1202
+ }
1203
+ };
1204
+ //#endregion
1205
+ //#region src/storage/s3/config.ts
1206
+ const DEFAULT_S3_ATTACHMENT_PREFIX = "attachments";
1207
+ const DEFAULT_S3_UPLOAD_TTL_SECONDS = 900;
1208
+ const DEFAULT_S3_DOWNLOAD_TTL_SECONDS = 300;
1209
+ const MAX_S3_PRESIGN_TTL_SECONDS = 604800;
1210
+ function required(env, name) {
1211
+ const value = env[name];
1212
+ if (value === void 0 || value.trim().length === 0) throw new Error(`${name} is required and must not be blank`);
1213
+ if (value.trim() !== value) throw new Error(`${name} must not have leading or trailing whitespace`);
1214
+ return value;
1215
+ }
1216
+ const LOOPBACK_HOSTNAMES = new Set([
1217
+ "127.0.0.1",
1218
+ "localhost",
1219
+ "[::1]"
1220
+ ]);
1221
+ /** Plain HTTP is allowed only for loopback hosts (local S3 emulators). */
1222
+ function isLoopbackHost(endpoint) {
1223
+ return LOOPBACK_HOSTNAMES.has(endpoint.hostname.toLowerCase());
1224
+ }
1225
+ function parseEndpoint(value) {
1226
+ const message = "PH_ATTACHMENT_S3_ENDPOINT must be a valid HTTPS URL (HTTP is allowed only for loopback hosts)";
1227
+ let endpoint;
1228
+ try {
1229
+ endpoint = new URL(value);
1230
+ } catch {
1231
+ throw new Error(message);
1232
+ }
1233
+ const protocolAllowed = endpoint.protocol === "https:" || endpoint.protocol === "http:" && isLoopbackHost(endpoint);
1234
+ if (value.trim() !== value || !protocolAllowed || endpoint.username !== "" || endpoint.password !== "" || endpoint.search !== "" || endpoint.hash !== "") throw new Error(message);
1235
+ return endpoint.toString().replace(/\/$/, "");
1236
+ }
1237
+ function parseBoolean(env, name, defaultValue) {
1238
+ const value = env[name];
1239
+ if (value === void 0) return defaultValue;
1240
+ if (value === "true") return true;
1241
+ if (value === "false") return false;
1242
+ throw new Error(`${name} must be either true or false`);
1243
+ }
1244
+ function parseTtl(env, name, defaultValue) {
1245
+ const value = env[name];
1246
+ if (value === void 0) return defaultValue;
1247
+ if (!/^[1-9]\d*$/.test(value)) throw new Error(`${name} must be a positive integer`);
1248
+ const seconds = Number(value);
1249
+ if (!Number.isSafeInteger(seconds) || seconds > 604800) throw new Error(`${name} must be between 1 and ${MAX_S3_PRESIGN_TTL_SECONDS}`);
1250
+ return seconds;
1251
+ }
1252
+ function normalizeS3AttachmentPrefix(prefix) {
1253
+ if (prefix.trim() !== prefix || prefix.startsWith("/") || prefix.includes("\\")) throw new Error("S3_ATTACHMENT_PREFIX is unsafe");
1254
+ const normalized = prefix.replace(/\/+$/, "");
1255
+ if (normalized.length === 0) throw new Error("S3_ATTACHMENT_PREFIX must not be blank");
1256
+ const segments = normalized.split("/");
1257
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment))) throw new Error("S3_ATTACHMENT_PREFIX contains an unsafe segment");
1258
+ return segments.join("/");
1259
+ }
1260
+ function parseAttachmentStorageConfig(env = process.env) {
1261
+ const selector = env.PH_ATTACHMENT_STORAGE;
1262
+ if (selector === void 0 || selector === "filesystem") return { kind: "filesystem" };
1263
+ if (selector !== "s3") throw new Error("PH_ATTACHMENT_STORAGE must be either filesystem or s3");
1264
+ return {
1265
+ kind: "s3",
1266
+ s3: {
1267
+ endpoint: parseEndpoint(required(env, "PH_ATTACHMENT_S3_ENDPOINT")),
1268
+ region: required(env, "PH_ATTACHMENT_S3_REGION"),
1269
+ bucket: required(env, "PH_ATTACHMENT_S3_BUCKET"),
1270
+ accessKeyId: required(env, "PH_ATTACHMENT_S3_ACCESS_KEY_ID"),
1271
+ secretAccessKey: required(env, "PH_ATTACHMENT_S3_SECRET_ACCESS_KEY"),
1272
+ prefix: normalizeS3AttachmentPrefix(env.S3_ATTACHMENT_PREFIX ?? "attachments"),
1273
+ forcePathStyle: parseBoolean(env, "PH_ATTACHMENT_S3_FORCE_PATH_STYLE", false),
1274
+ uploadTtlSeconds: parseTtl(env, "PH_ATTACHMENT_S3_UPLOAD_TTL_SECONDS", 900),
1275
+ downloadTtlSeconds: parseTtl(env, "PH_ATTACHMENT_S3_DOWNLOAD_TTL_SECONDS", 300)
1276
+ }
1277
+ };
1278
+ }
1279
+ //#endregion
1280
+ //#region src/storage/s3/keying.ts
1281
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
1282
+ function validateHash(hash) {
1283
+ if (!SHA256_HEX.test(hash)) throw new Error("Attachment hash must be 64 lowercase hexadecimal characters");
1284
+ }
1285
+ function deriveS3AttachmentKey(hash, prefix) {
1286
+ validateHash(hash);
1287
+ if (prefix.startsWith("/") || prefix.endsWith("/") || prefix.includes("\\") || prefix.split("/").some((segment) => segment.length === 0 || segment === "." || segment === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment))) throw new Error("S3 attachment prefix must be normalized and safe");
1288
+ return `${prefix}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;
1289
+ }
1290
+ function sha256HexToBase64(hash) {
1291
+ validateHash(hash);
1292
+ return Buffer$1.from(hash, "hex").toString("base64");
1293
+ }
1294
+ //#endregion
1295
+ //#region src/storage/s3/primitives.ts
1296
+ const defaultPresigner = (client, command, expiresInSeconds) => getSignedUrl(client, command, {
1297
+ expiresIn: expiresInSeconds,
1298
+ unhoistableHeaders: new Set(["x-amz-checksum-sha256"])
1299
+ });
1300
+ var S3AttachmentPrimitives = class {
1301
+ client;
1302
+ presign;
1303
+ now;
1304
+ constructor(config, dependencies = {}) {
1305
+ this.config = config;
1306
+ this.client = dependencies.client ?? new S3Client({
1307
+ endpoint: config.endpoint,
1308
+ region: config.region,
1309
+ forcePathStyle: config.forcePathStyle,
1310
+ credentials: {
1311
+ accessKeyId: config.accessKeyId,
1312
+ secretAccessKey: config.secretAccessKey
1313
+ }
1314
+ });
1315
+ this.presign = dependencies.presign ?? defaultPresigner;
1316
+ this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
1317
+ }
1318
+ buildHeadObjectCommand(hash) {
1319
+ return new HeadObjectCommand({
1320
+ Bucket: this.config.bucket,
1321
+ Key: deriveS3AttachmentKey(hash, this.config.prefix)
1322
+ });
1323
+ }
1324
+ buildPutObjectCommand(hash, mimeType) {
1325
+ if (mimeType.trim().length === 0 || mimeType.includes("\r") || mimeType.includes("\n")) throw new Error("Attachment MIME type must not be blank or contain newlines");
1326
+ return new PutObjectCommand({
1327
+ Bucket: this.config.bucket,
1328
+ Key: deriveS3AttachmentKey(hash, this.config.prefix),
1329
+ ContentType: mimeType,
1330
+ ChecksumSHA256: sha256HexToBase64(hash)
1331
+ });
1332
+ }
1333
+ buildGetObjectCommand(hash) {
1334
+ return new GetObjectCommand({
1335
+ Bucket: this.config.bucket,
1336
+ Key: deriveS3AttachmentKey(hash, this.config.prefix)
1337
+ });
1338
+ }
1339
+ headObject(hash) {
1340
+ return this.client.send(this.buildHeadObjectCommand(hash));
1341
+ }
1342
+ async createUploadTarget(hash, mimeType, ttlSeconds = this.config.uploadTtlSeconds) {
1343
+ const checksum = sha256HexToBase64(hash);
1344
+ return {
1345
+ kind: "presigned-put",
1346
+ method: "PUT",
1347
+ url: await this.presign(this.client, this.buildPutObjectCommand(hash, mimeType), ttlSeconds),
1348
+ headers: {
1349
+ "content-type": mimeType,
1350
+ "x-amz-checksum-sha256": checksum
1351
+ },
1352
+ expiresAtUtc: new Date(this.now().getTime() + ttlSeconds * 1e3).toISOString()
1353
+ };
1354
+ }
1355
+ async createDownloadTarget(hash, ttlSeconds = this.config.downloadTtlSeconds) {
1356
+ return {
1357
+ kind: "presigned-get",
1358
+ method: "GET",
1359
+ url: await this.presign(this.client, this.buildGetObjectCommand(hash), ttlSeconds),
1360
+ headers: {},
1361
+ expiresAtUtc: new Date(this.now().getTime() + ttlSeconds * 1e3).toISOString()
1362
+ };
1363
+ }
1364
+ };
1365
+ function createS3AttachmentPrimitives(config, dependencies = {}) {
1366
+ return new S3AttachmentPrimitives(config, dependencies);
1367
+ }
1368
+ //#endregion
1369
+ //#region src/storage/s3/backend.ts
1370
+ const READINESS_PROBE_HASH = "0".repeat(64);
1371
+ const OBJECT_NOT_FOUND_NAMES = new Set([
1372
+ "NotFound",
1373
+ "NoSuchKey",
1374
+ "NoSuchObject"
1375
+ ]);
1376
+ function isObjectNotFound(error) {
1377
+ if (typeof error !== "object" || error === null) return false;
1378
+ const providerError = error;
1379
+ if (providerError.$metadata?.httpStatusCode !== 404) return false;
1380
+ return [providerError.name, providerError.code].some((code) => typeof code === "string" && OBJECT_NOT_FOUND_NAMES.has(code));
1381
+ }
1382
+ function requireHashFirstReservation(reservation) {
1383
+ if (reservation.clientHash === null) throw new Error("S3 attachment reservations require a client hash");
1384
+ deriveS3AttachmentKey(reservation.clientHash, "validation");
1385
+ if (reservation.sizeBytes === null || !Number.isSafeInteger(reservation.sizeBytes) || reservation.sizeBytes <= 0) throw new Error("S3 attachment reservation sizeBytes must be a positive safe integer");
1386
+ }
1387
+ /** Server-only S3 capability. Callers complete authorization before download. */
1388
+ var S3AttachmentBackend = class {
1389
+ kind = "s3";
1390
+ primitives;
1391
+ now;
1392
+ uploadTtlSeconds;
1393
+ downloadTtlSeconds;
1394
+ constructor(db, config, dependencies = {}) {
1395
+ this.db = db;
1396
+ this.config = config;
1397
+ this.primitives = new S3AttachmentPrimitives(config, dependencies);
1398
+ this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
1399
+ this.uploadTtlSeconds = dependencies.uploadTtlSeconds ?? config.uploadTtlSeconds;
1400
+ this.downloadTtlSeconds = dependencies.downloadTtlSeconds ?? config.downloadTtlSeconds;
1401
+ }
1402
+ async prepareUploadTarget(reservation) {
1403
+ requireHashFirstReservation(reservation);
1404
+ const hash = reservation.clientHash;
1405
+ const now = this.now().toISOString();
1406
+ const storagePath = deriveS3AttachmentKey(hash, this.config.prefix);
1407
+ try {
1408
+ await this.db.insertInto("attachment").values({
1409
+ hash,
1410
+ mime_type: reservation.mimeType,
1411
+ file_name: reservation.fileName,
1412
+ size_bytes: reservation.sizeBytes,
1413
+ extension: reservation.extension,
1414
+ status: "available",
1415
+ storage_path: storagePath,
1416
+ source: "local",
1417
+ created_at_utc: reservation.createdAtUtc,
1418
+ last_accessed_at_utc: now
1419
+ }).onConflict((conflict) => conflict.column("hash").doUpdateSet({
1420
+ mime_type: reservation.mimeType,
1421
+ file_name: reservation.fileName,
1422
+ size_bytes: reservation.sizeBytes,
1423
+ extension: reservation.extension,
1424
+ status: "available",
1425
+ storage_path: storagePath,
1426
+ last_accessed_at_utc: now
1427
+ })).execute();
1428
+ } catch {
1429
+ throw new Error("S3 attachment metadata registration failed");
1430
+ }
1431
+ let target;
1432
+ try {
1433
+ target = await this.primitives.createUploadTarget(hash, reservation.mimeType, this.uploadTtlSeconds);
1434
+ } catch {
1435
+ throw new Error("S3 attachment upload target preparation failed");
1436
+ }
1437
+ return target;
1438
+ }
1439
+ async prepareDownloadTarget(hash, ttlSeconds) {
1440
+ try {
1441
+ return await this.primitives.createDownloadTarget(hash, ttlSeconds ?? this.downloadTtlSeconds);
1442
+ } catch {
1443
+ throw new Error("S3 attachment download target preparation failed");
1444
+ }
1445
+ }
1446
+ async exists(hash) {
1447
+ if (!await this.db.selectFrom("attachment").select("hash").where("hash", "=", hash).executeTakeFirst()) return false;
1448
+ try {
1449
+ await this.primitives.headObject(hash);
1450
+ return true;
1451
+ } catch (error) {
1452
+ if (isObjectNotFound(error)) return false;
1453
+ throw new Error("S3 attachment existence check failed");
1454
+ }
1455
+ }
1456
+ async health() {
1457
+ try {
1458
+ await this.primitives.headObject(READINESS_PROBE_HASH);
1459
+ return {
1460
+ kind: this.kind,
1461
+ ready: true
1462
+ };
1463
+ } catch (error) {
1464
+ return {
1465
+ kind: this.kind,
1466
+ ready: isObjectNotFound(error)
1467
+ };
1468
+ }
1469
+ }
1470
+ };
1471
+ function createS3AttachmentBackend(db, config, dependencies = {}) {
1472
+ return new S3AttachmentBackend(db, config, dependencies);
1473
+ }
1474
+ //#endregion
1475
+ export { ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE, ATTACHMENT_REFERENCE_MIGRATION_TABLE, ATTACHMENT_REFERENCE_READ_MODEL_ID, ATTACHMENT_REFERENCE_SCHEMA, ATTACHMENT_SCHEMA, AttachmentAlreadyExists, AttachmentBuilder, AttachmentNotFound, AttachmentPending, AttachmentReferenceIndexBuilder, AttachmentReferenceReadModel, AttachmentSchemaCompiler, AttachmentService, AttachmentTransferError, DEFAULT_RESERVATION_TTL_MS, DEFAULT_S3_ATTACHMENT_PREFIX, DEFAULT_S3_DOWNLOAD_TTL_SECONDS, DEFAULT_S3_UPLOAD_TTL_SECONDS, DirectAttachmentUpload, DirectAttachmentUploadFactory, FilesystemAttachmentBackend, HashMismatch, InvalidAttachmentRef, KyselyAttachmentReferenceStore, KyselyAttachmentStore, KyselyReservationStore, MAX_S3_PRESIGN_TTL_SECONDS, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, S3AttachmentBackend, S3AttachmentPrimitives, S3AttachmentUploadFactory, SizeMismatch, SwitchboardAttachmentTransport, UploadTooLarge, createRef, createRemoteAttachmentService, createS3AttachmentBackend, createS3AttachmentPrimitives, deriveS3AttachmentKey, getAttachmentReferenceMigrationStatus, normalizeS3AttachmentPrefix, parseAttachmentDownloadTarget, parseAttachmentStorageConfig, parseAttachmentUploadTarget, parseRef, rollbackAttachmentReferenceMigration, runAttachmentMigrations, runAttachmentReferenceMigrations, sha256HexToBase64 };
691
1476
 
692
1477
  //# sourceMappingURL=index.js.map