@formancy/server-core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -606,13 +606,158 @@ interface WebhookHealth {
606
606
  }
607
607
  export declare function healthOf(webhook: WebhookRecord, now: Date): WebhookHealth;
608
608
  //#endregion
609
- //#region src/use-cases.d.ts
609
+ //#region src/uploads.d.ts
610
610
  /**
611
- * The backend's use-cases, framework-free.
611
+ * The file lifecycle: offered, stored, claimed, collected.
612
612
  *
613
- * Identity and time are injected like every other capability: the server pins
614
- * one clock per request, so a submission can be replayed later and produce the
615
- * same computed values byte for byte. Nothing here reads Date.now or crypto.
613
+ * **Bytes are not the submission's problem.** A submission stores what each
614
+ * file is and where it went, never the bytes themselves, so a submission read
615
+ * back years later is small, self-describing, and still says what was attached
616
+ * even if the object store has since been emptied.
617
+ *
618
+ * **The claim happens inside the submission's transaction.** A submission
619
+ * exists if and only if the files it names are claimed against it. Anything
620
+ * looser gives one of the two failures nobody can debug: a rolled-back
621
+ * submission leaving files claimed against nothing, or an accepted submission
622
+ * referencing files the collector is about to delete.
623
+ *
624
+ * **Nothing here touches a disk.** This package has no filesystem and no
625
+ * object store; it decides what is rubbish and the host deletes it. That is
626
+ * the same split every other port here makes, and it is why the whole
627
+ * lifecycle is testable without either.
628
+ */
629
+ interface OfferDeps {
630
+ storage: Storage;
631
+ now: () => Date;
632
+ newId: () => string;
633
+ }
634
+ interface OfferInput {
635
+ schema: FormSchema;
636
+ /** The form, for the access check — the same one a submission passes. */
637
+ form: FormRecord;
638
+ actor: 'anonymous' | 'authenticated';
639
+ origin: string | undefined;
640
+ /** The field the file is for, as a data path: `evidence`, `people[].passport`. */
641
+ field: string;
642
+ name: string;
643
+ size: number;
644
+ contentType: string;
645
+ }
646
+ type OfferOutcome = {
647
+ ok: true;
648
+ file: FileRecord;
649
+ } | {
650
+ ok: false;
651
+ reason: 'not_allowed' | 'unknown_field' | 'not_a_file_field' | 'too_large' | 'not_accepted';
652
+ };
653
+ /**
654
+ * Agree where a file will go, and refuse it before a byte is sent.
655
+ *
656
+ * Checking here rather than after the upload is the difference between
657
+ * refusing a 2 GB file and receiving one first. The browser's own `accept` and
658
+ * size limits are a convenience for the person filling the form in; these are
659
+ * the rule, because somebody posting to the endpoint directly has neither.
660
+ */
661
+ export declare function offerUpload(deps: OfferDeps, input: OfferInput): Promise<OfferOutcome>;
662
+ /** What a deployment's scanner says about an upload's bytes. */
663
+ type ScanVerdict = {
664
+ clean: true;
665
+ } | {
666
+ clean: false;
667
+ finding: string;
668
+ };
669
+ /**
670
+ * A deployment's virus scanner, asked about an upload's bytes before they are kept.
671
+ *
672
+ * A port because the deployment supplies it and there are two: the ClamAV adapter in
673
+ * `@formancy/server`, and the doubles that test this decision. **Throwing means it could
674
+ * not be asked** — a connection refused, a timeout — and never that the file is clean.
675
+ */
676
+ interface Scanner {
677
+ scan(file: {
678
+ name: string;
679
+ contentType: string;
680
+ size: number;
681
+ }, bytes: Uint8Array): Promise<ScanVerdict>;
682
+ }
683
+ type ScreenOutcome = {
684
+ ok: true;
685
+ } | {
686
+ ok: false;
687
+ reason: 'refused';
688
+ finding: string;
689
+ } |
690
+ /** `cause` is for the operator's log, never for the client: it can name an address. */
691
+ {
692
+ ok: false;
693
+ reason: 'scanner_unavailable';
694
+ cause: string;
695
+ };
696
+ /**
697
+ * Whether an upload's bytes may be kept.
698
+ *
699
+ * **Fail closed.** A scanner that cannot be asked refuses the file, the same as one that
700
+ * found something: a deployment that configured a scanner did so to keep unscanned files
701
+ * out, and its being down is not a reason to let one in. The person is told to try again,
702
+ * and the field offers exactly that. Without a scanner every upload is kept, as it was
703
+ * before there was one — a supported state, said in the documentation rather than here
704
+ * ([0131](../../../docs/decisions/0131-an-upload-is-scanned-before-it-is-kept.md)).
705
+ */
706
+ export declare function screenUpload(scanner: Scanner | undefined, file: {
707
+ name: string;
708
+ contentType: string;
709
+ size: number;
710
+ }, bytes: Uint8Array): Promise<ScreenOutcome>;
711
+ type ClaimOutcome = {
712
+ ok: true;
713
+ ids: readonly string[];
714
+ } | {
715
+ ok: false;
716
+ unknown: readonly string[];
717
+ };
718
+ /**
719
+ * Which files a submission may claim, checked but not yet claimed.
720
+ *
721
+ * The claiming itself is `insertSubmission`'s job, in the same call that
722
+ * stores the submission, so one COMMIT decides both — the same reasoning
723
+ * that put the webhook queue there. A submission exists if and only if the
724
+ * files it names belong to it. Claiming separately gives the two failures
725
+ * nobody can debug: a rolled-back submission leaving files claimed against
726
+ * nothing, or an accepted submission referencing bytes the collector is about
727
+ * to delete.
728
+ *
729
+ * Refuses rather than skips. A submission naming a file that does not exist,
730
+ * belongs to another form, or is already claimed is not a submission to fix up
731
+ * quietly: the first is a bug or somebody guessing ids; the second is how an
732
+ * id from a form you may submit to would let you attach a file from one you
733
+ * may not; the third would make deleting a submission either leak bytes or
734
+ * break a different one.
735
+ */
736
+ export declare function filesToClaim(storage: Storage, input: {
737
+ schema: FormSchema;
738
+ formId: string;
739
+ data: unknown;
740
+ }): Promise<ClaimOutcome>;
741
+ /**
742
+ * Files nobody claimed, old enough that nobody is going to.
743
+ *
744
+ * Returns the rows rather than deleting anything: the host removes the bytes
745
+ * and then the rows, in that order, because a row without bytes is a broken
746
+ * reference and bytes without a row are invisible rubbish. Of the two, the
747
+ * second is the one you never find out about.
748
+ */
749
+ export declare function collectAbandonedFiles(storage: Storage, now: Date, afterHours: number): Promise<FileRecord[]>;
750
+ /** Re-exported so a host can see which paths a schema's file fields live at. */
751
+ export declare function fileFieldPaths(schema: FormSchema): string[];
752
+ //#endregion
753
+ //#region src/deps.d.ts
754
+ /**
755
+ * What a deployment supplies to the use-cases: storage, identity, time, and the
756
+ * answers a document may only name — checks, lists and a scanner.
757
+ *
758
+ * Its own file because it is the one contract every use-case family shares, and
759
+ * `use-cases.ts` is where the families used to live; it moved when that file's size
760
+ * budget refused the scanner.
616
761
  */
617
762
  interface ServerDeps {
618
763
  storage: Storage;
@@ -654,7 +799,21 @@ interface ServerDeps {
654
799
  * legitimate configuration, and the reason the guarantee is written down.
655
800
  */
656
801
  optionsSources?: ServerOptionsSources;
802
+ /**
803
+ * The deployment's virus scanner, asked about every upload's bytes before they are
804
+ * kept. Absent means none is asked — a supported state, and the documented one.
805
+ */
806
+ scanner?: Scanner;
657
807
  }
808
+ //#endregion
809
+ //#region src/use-cases.d.ts
810
+ /**
811
+ * The backend's use-cases, framework-free.
812
+ *
813
+ * Identity and time are injected like every other capability: the server pins
814
+ * one clock per request, so a submission can be replayed later and produce the
815
+ * same computed values byte for byte. Nothing here reads Date.now or crypto.
816
+ */
658
817
  interface ResolvedForm {
659
818
  formId: string;
660
819
  versionId: string;
@@ -968,101 +1127,6 @@ export declare function retryDelayMs(attempt: number, random: () => number): num
968
1127
  /** Eight attempts reach roughly 24 hours with this schedule. */
969
1128
  export declare const MAX_ATTEMPTS = 8;
970
1129
  //#endregion
971
- //#region src/uploads.d.ts
972
- /**
973
- * The file lifecycle: offered, stored, claimed, collected.
974
- *
975
- * **Bytes are not the submission's problem.** A submission stores what each
976
- * file is and where it went, never the bytes themselves, so a submission read
977
- * back years later is small, self-describing, and still says what was attached
978
- * even if the object store has since been emptied.
979
- *
980
- * **The claim happens inside the submission's transaction.** A submission
981
- * exists if and only if the files it names are claimed against it. Anything
982
- * looser gives one of the two failures nobody can debug: a rolled-back
983
- * submission leaving files claimed against nothing, or an accepted submission
984
- * referencing files the collector is about to delete.
985
- *
986
- * **Nothing here touches a disk.** This package has no filesystem and no
987
- * object store; it decides what is rubbish and the host deletes it. That is
988
- * the same split every other port here makes, and it is why the whole
989
- * lifecycle is testable without either.
990
- */
991
- interface OfferDeps {
992
- storage: Storage;
993
- now: () => Date;
994
- newId: () => string;
995
- }
996
- interface OfferInput {
997
- schema: FormSchema;
998
- /** The form, for the access check — the same one a submission passes. */
999
- form: FormRecord;
1000
- actor: 'anonymous' | 'authenticated';
1001
- origin: string | undefined;
1002
- /** The field the file is for, as a data path: `evidence`, `people[].passport`. */
1003
- field: string;
1004
- name: string;
1005
- size: number;
1006
- contentType: string;
1007
- }
1008
- type OfferOutcome = {
1009
- ok: true;
1010
- file: FileRecord;
1011
- } | {
1012
- ok: false;
1013
- reason: 'not_allowed' | 'unknown_field' | 'not_a_file_field' | 'too_large' | 'not_accepted';
1014
- };
1015
- /**
1016
- * Agree where a file will go, and refuse it before a byte is sent.
1017
- *
1018
- * Checking here rather than after the upload is the difference between
1019
- * refusing a 2 GB file and receiving one first. The browser's own `accept` and
1020
- * size limits are a convenience for the person filling the form in; these are
1021
- * the rule, because somebody posting to the endpoint directly has neither.
1022
- */
1023
- export declare function offerUpload(deps: OfferDeps, input: OfferInput): Promise<OfferOutcome>;
1024
- type ClaimOutcome = {
1025
- ok: true;
1026
- ids: readonly string[];
1027
- } | {
1028
- ok: false;
1029
- unknown: readonly string[];
1030
- };
1031
- /**
1032
- * Which files a submission may claim, checked but not yet claimed.
1033
- *
1034
- * The claiming itself is `insertSubmission`'s job, in the same call that
1035
- * stores the submission, so one COMMIT decides both — the same reasoning
1036
- * that put the webhook queue there. A submission exists if and only if the
1037
- * files it names belong to it. Claiming separately gives the two failures
1038
- * nobody can debug: a rolled-back submission leaving files claimed against
1039
- * nothing, or an accepted submission referencing bytes the collector is about
1040
- * to delete.
1041
- *
1042
- * Refuses rather than skips. A submission naming a file that does not exist,
1043
- * belongs to another form, or is already claimed is not a submission to fix up
1044
- * quietly: the first is a bug or somebody guessing ids; the second is how an
1045
- * id from a form you may submit to would let you attach a file from one you
1046
- * may not; the third would make deleting a submission either leak bytes or
1047
- * break a different one.
1048
- */
1049
- export declare function filesToClaim(storage: Storage, input: {
1050
- schema: FormSchema;
1051
- formId: string;
1052
- data: unknown;
1053
- }): Promise<ClaimOutcome>;
1054
- /**
1055
- * Files nobody claimed, old enough that nobody is going to.
1056
- *
1057
- * Returns the rows rather than deleting anything: the host removes the bytes
1058
- * and then the rows, in that order, because a row without bytes is a broken
1059
- * reference and bytes without a row are invisible rubbish. Of the two, the
1060
- * second is the one you never find out about.
1061
- */
1062
- export declare function collectAbandonedFiles(storage: Storage, now: Date, afterHours: number): Promise<FileRecord[]>;
1063
- /** Re-exported so a host can see which paths a schema's file fields live at. */
1064
- export declare function fileFieldPaths(schema: FormSchema): string[];
1065
- //#endregion
1066
1130
  //#region src/publishing.d.ts
1067
1131
  /**
1068
1132
  * Publishing a form: the save gate, and who may overwrite whom.
@@ -1159,5 +1223,5 @@ export declare function publishForm(deps: ServerDeps, input: {
1159
1223
  basedOnSchemaHash?: string;
1160
1224
  }): Promise<PublishOutcome>;
1161
1225
  //#endregion
1162
- export { type Action, type Actor, type AttemptOutcome, type AuditAction, type AuditDraft, type AuditEntry, type AuthDeps, type AuthOutcome, type BreakerState, CHALLENGE_TTL_SECONDS, type Challenge, type ClaimOutcome, DEFAULT_MAX_NUMBER, type DeliveryHeaders, type DeliveryRecord, type DraftMigration, type DraftRecord, type FileRecord, type FormRecord, type FormVersionRecord, type ListedForm, type ListedSubmission, type ListedVersion, type MembershipOutcome, type OfferDeps, type OfferInput, type OfferOutcome, type OutboxDeps, type PublishOutcome, type ReplayOutcome, type ResolvedForm, type ResumeOutcome, type Role, type ServerDeps, type ServerOptionsSource, type ServerOptionsSources, type Solution, type Storage, type SubmissionOutcome, type SubmissionRecord, type UnsafePattern, type VerifyOutcome, type WebhookHealth, type WebhookRecord, decodeSolution, encodeSolution, hashOf, mintChallenge, solveChallenge, verifySolution };
1226
+ export { type Action, type Actor, type AttemptOutcome, type AuditAction, type AuditDraft, type AuditEntry, type AuthDeps, type AuthOutcome, type BreakerState, CHALLENGE_TTL_SECONDS, type Challenge, type ClaimOutcome, DEFAULT_MAX_NUMBER, type DeliveryHeaders, type DeliveryRecord, type DraftMigration, type DraftRecord, type FileRecord, type FormRecord, type FormVersionRecord, type ListedForm, type ListedSubmission, type ListedVersion, type MembershipOutcome, type OfferDeps, type OfferInput, type OfferOutcome, type OutboxDeps, type PublishOutcome, type ReplayOutcome, type ResolvedForm, type ResumeOutcome, type Role, type ScanVerdict, type Scanner, type ScreenOutcome, type ServerDeps, type ServerOptionsSource, type ServerOptionsSources, type Solution, type Storage, type SubmissionOutcome, type SubmissionRecord, type UnsafePattern, type VerifyOutcome, type WebhookHealth, type WebhookRecord, decodeSolution, encodeSolution, hashOf, mintChallenge, solveChallenge, verifySolution };
1163
1227
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/options-membership.ts","../src/ports.ts","../src/auth.ts","../src/audit.ts","../src/outbox.ts","../src/breaker.ts","../src/use-cases.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/address.ts","../src/webhook.ts","../src/uploads.ts","../src/publishing.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;UAoBiB;;;;;;;;;;;;EAYf,SAAS,4BAA4B;;KAG3B,uBAAuB,SAAS,eAAe;;;;;;;;;wBAU3C,eACd,QAAQ,YACR,iBACC;EAAgB;EAAc;EAAgB;;;KAiDrC;EACN;;;;EAEA;EAAW;EAAiB,QAAQ;;;;;;;;;;EAQpC;EAAW;EAA4B;;;;;;;;wBAQvB,gBACpB,QAAQ,YACR,gBACA,SAAS,mCACR,QAAQ;;;;;;wBA8CK,cAAc,QAAQ;;;;;;;;UC7JrB;EACf;EACA;EACA;;;;;;;;;;EAUA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA,QAAQ;EACR;;UAGe;EACf;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA,MAAM;EACN;;UAGe;EACf;EACA;;EAEA;EACA;EACA,MAAM;EACN;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;;EAEA;;;;;;;;;EASA;;EAEA;;;;;;;;;;;UAYe;EACf;EACA;EACA;;EAEA;EACA;EACA;;EAEA;;EAEA;EACA;;;;;;;;;;;;;;;;;;UAmBe;EACf;EACA;;EAEA;EACA;EACA;;EAEA;EACA;EACA;;EAEA;;UAGe;EACf,cAAc,eAAe,QAAQ;EACrC,aAAa,QAAQ;EACrB,WAAW,QAAQ,aAAa;EAChC,iBACE,gBACA,QAAQ,KAAK,iDACZ;EACH,kBAAkB,gBAAgB,oBAAoB;EACtD,cAAc,QAAQ,oBAAoB;;;;;;;;;;;;;;;;;;;EAoB1C,eAAe;IACb,OAAO;IACP,SAAS;IACT,QAAQ;MACN;EACJ,eAAe,aAAa,QAAQ;EACpC,kBAAkB,gBAAgB,qBAAqB,QAAQ;EAC/D,oBAAoB,iBAAiB;;;;;;;;;;;;;;;;;EAiBrC,iBACE,QAAQ,kBACR,sBAAsB,kBACtB,kCACA,QAAQ,aACP;;;;;;;;;;;;;;EAeH,YAAY,OAAO,aAAa;;EAGhC,UAAU,gBAAgB,QAAQ;;;;;;;;;;;;;;;EAgBlC,eAAe,mBAAmB,uBAAuB;;EAGzD,wBAAwB,oBAAoB;EAE5C,WAAW,QAAQ,aAAa;EAChC,QAAQ,aAAa,QAAQ;EAC7B,WAAW,QAAQ,aAAa;;EAEhC,eAAe,oBAAoB,QAAQ;EAC3C,YAAY,yBAAyB;EACrC,gBAAgB,iBAAiB,QAAQ;EACzC,cAAc,QAAQ,gBAAgB;;EAEtC,cAAc,QAAQ,gBAAgB;EACtC,gBAAgB,QAAQ;;;;;;;;EAQxB,eAAe,gBAAgB,QAAQ;EACvC,YAAY,aAAa,QAAQ;;EAEjC,mBAAmB,gBAAgB,gBAAgB,QAAQ;EAC3D,eAAe,QAAQ,iBAAiB;EACxC,mBAAmB,QAAQ;;EAE3B,sBAAsB,iBAAiB,QAAQ;EAC/C,mBAAmB,iBAAiB,QAAQ;EAC5C,YAAY,QAAQ,cAAc;EAClC,SAAS,gBAAgB,kBAAkB,QAAQ;EACnD,WAAW,QAAQ,aAAa;EAChC,eAAe,gBAAgB,QAAQ;EACvC,aAAa,QAAQ,eAAe;EACpC,eAAe,QAAQ;EACvB,oBAAoB,iBAAiB,QAAQ;EAC7C,aAAa,YAAY,gBAAgB;;;;;;;;;;;;KC9Q/B;UAEK;EACf;EACA;EACA,MAAM;;UAGS;EACf,SAAS;EACT;EACA;;EAEA;EACA,WAAW,iBAAiB;EAC5B,aAAa,gBAAgB,eAAe;;KAKlC;EACN;EAAU;;EACV;EAAW;;wBAEK,gBACpB,MAAM,UACN;EAAS;EAAe;EAAkB,MAAM;IAC/C,QAAQ;KAmBC;EAAgB;EAAU,OAAO;;EAAY;;;;;;;wBAOnC,kBACpB,MAAM,UACN;EAAS;EAAe;IACvB,QAAQ;KAYC;EACN;EAAU;EAAY;;EACtB;EAAW;;wBAEK,aACpB,MAAM,UACN;EAAS;EAAc,MAAM;IAC5B,QAAQ;wBAmBW,mBAAmB,MAAM,UAAU,oBAAoB,QAAQ;;;;;;;;KAkBzE;wBAwBI,IAAI,OAAO,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;qBCzH7B;KAiBD,sBAAsB;UAEjB;WACN;WACA;WACA,QAAQ;;;;;WAKR;WACA;;;;;;WAMA;;;;;WAKA;;WAEA,SAAS,SAAS;;;KAIjB,aAAa,KAAK;;;;;;;;wBASd,UAAU,OAAO,mBAAmB,OAAO,KAAK,uCAAuC;;;;;;;;;;UCnEtF;EACf;EACA;EACA;;wBAGc,aACd,UAAU,gBACV,SAAS,gBACT,KAAK,MACL,uBACC;UAyBc;EACf,SAAS;;EAET,OACE,UAAU,gBACV,SAAS,eACT,uBACG,QAAQ;EACb,WAAW;EACX;;;;;;;;;;;;;;;wBAgBoB,YAAY,MAAM,YAAY,iBAAa;;;;;;;;;;;;;;;;;;;KAqFrD;EACN;EAAU,UAAU;;EACpB;EAAW;EAA8B,QAAQ;;wBAEjC,eACpB;EAAQ,SAAS;EAAS,WAAW;GACrC,aACC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC1IE;;;;;;;;;qBAUA;;KAGD;wBAEI,aAAa,SAAS,eAAe,KAAK,OAAO;;wBAYjD,WAAW,SAAS,eAAe,KAAK;;;;;;;;wBAWxC,oBACd,SAAS,eACT,aACA,KAAK,OACJ;;;;;;;;UAuBc;EACf;EACA;EACA,OAAO;EACP;;EAEA;;wBAGc,SAAS,SAAS,eAAe,KAAK,OAAO;;;;;;;;;;UCjF5C;EACf,SAAS;EACT;EACA;;;;;;;;;EASA,SAAS,eAAe;;EAExB,cAAc;;;;;;;;;;EAUd;;;;;;;;;;;;;;;EAeA,iBAAiB;;UAGF;EACf;EACA;EACA;EACA,QAAQ;EACR;;wBAGoB,YAAY,MAAM,YAAY,eAAe,QAAQ;KAc/D;EACN;EAAU;EAAY;;EACtB;EAAW;;EACX;EAAW;EAAyB,UAAU;;EAC9C;EAAW;EAAiB,QAAQ;;;;EAEpC;EAAW;;;;;;;;;;EAQX;EAAW;EAA4B;;;;;;;;;;;;;;;;;;wBAkBvB,cACpB,MAAM,YACN;EAAS;EAAc;EAAoC;IAC1D;EAAU;;wBAYS,iBACpB,MAAM,YACN;EACE;EACA;EACA;;EAEA;EACA;IAED,QAAQ;UA4HM;EACf;EACA;EACA;EACA;;;;;wBAMoB,gBACpB,MAAM,YACN,eACC,QAAQ;;;;;;;;;;;wBA0BW,UAAU,MAAM,YAAY,eAAe;;;;;;;;wBA4G3C,WACpB,MAAM,YACN;EAAS;IACR;EAAU;EAAiB;;wBAQR,UACpB,MAAM,YACN;EAAS;EAAc;EAAiB;EAAe;IACtD;EAAU;EAAiB;;UAqBb;EACf;EACA,SAAS;;KAGC;EAEN;EACA;EACA,QAAQ;EACR;EACA;;EAEA,YAAY;;;;EAKZ;EACA;EACA,QAAQ;EACR;;;;;;;;;;;;;;wBAegB,YACpB,MAAM,YACN;EAAS;EAAc;EAAiB;IACvC,QAAQ;UAkIM;EACf;EACA;EACA;EACA;;;wBAIoB,UAAU,MAAM,aAAa,QAAQ;UAiB1C;EACf;EACA;EACA;;;;wBAKoB,aACpB,MAAM,YACN,eACC,QAAQ;;;;;;;;;;;;;;;;;;;UCjnBM;;EAEf;EACA;;EAEA;;;;;;;;;;;wBAqCoB,eAAe,QAAQ,aAAa,QAAQ;;;;;;;;wBC3DlD,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBCgCvB,iBAAiB;;;;;;;;;wBAkDjB,YAAY;;;;;;;;;;;qBC/Ef;qBACA;UAEI;GACd;;;;;;;;;;wBAWa,cAAc,0BAA0B;;wBAKxC,SAAS,gBAAgB,0BAA0B;;;;;;;;wBAWnD,gBAAgB;EAC9B;EACA;EACA;EACA;EACA;IACE;;;;;;;wBAiBY,gBAAgB;EAC9B;EACA;EACA;EACA;EACA;;;;;;;;;;wBA4Cc,aAAa,iBAAiB;;qBAQjC;;;;;;;;;;;;;;;;;;;;;;UCnGI;EACf,SAAS;EACT,WAAW;EACX;;UAGe;EACf,QAAQ;;EAER,MAAM;EACN;EACA;;EAEA;EACA;EACA;EACA;;KAGU;EACN;EAAU,MAAM;;EAEhB;EACA;;;;;;;;;;wBAWgB,YAAY,MAAM,WAAW,OAAO,aAAa,QAAQ;KAqCnE;EACN;EAAU;;EACV;EAAW;;;;;;;;;;;;;;;;;;;;wBAoBK,aACpB,SAAS,SACT;EAAS,QAAQ;EAAY;EAAgB;IAC5C,QAAQ;;;;;;;;;wBA6BW,sBACpB,SAAS,SACT,KAAK,MACL,qBACC,QAAQ;;wBAkGK,eAAe,QAAQ;;;;;;;;;;KCxO3B;EAEN;EACA;EACA;EACA;EACA;;;;;;;;;;EAUA;;EAEA;EAAW;EAAwB,SAAS;;EAC5C;EAAW;EAAuB;;EAClC;EAAW;EAAwB,UAAU;;;;EAE7C;EAAW;EAAgC;;;;;;;;;EAO3C;EAAW;EAAyB,UAAU;;;;;;;;;;;;;;EAY9C;EAAW;EAA2B;;;;;;;;;;;wBAWtB,YACpB,MAAM,YACN;EACE;EACA;EACA,QAAQ;;;;;;;;;;;EAWR;IAED,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/options-membership.ts","../src/ports.ts","../src/auth.ts","../src/audit.ts","../src/outbox.ts","../src/breaker.ts","../src/uploads.ts","../src/deps.ts","../src/use-cases.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/address.ts","../src/webhook.ts","../src/publishing.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -495,6 +495,38 @@ async function offerUpload(deps, input) {
495
495
  };
496
496
  }
497
497
  /**
498
+ * Whether an upload's bytes may be kept.
499
+ *
500
+ * **Fail closed.** A scanner that cannot be asked refuses the file, the same as one that
501
+ * found something: a deployment that configured a scanner did so to keep unscanned files
502
+ * out, and its being down is not a reason to let one in. The person is told to try again,
503
+ * and the field offers exactly that. Without a scanner every upload is kept, as it was
504
+ * before there was one — a supported state, said in the documentation rather than here
505
+ * ([0131](../../../docs/decisions/0131-an-upload-is-scanned-before-it-is-kept.md)).
506
+ */
507
+ async function screenUpload(scanner, file, bytes) {
508
+ if (scanner === void 0) return { ok: true };
509
+ let verdict;
510
+ try {
511
+ verdict = await scanner.scan({
512
+ name: file.name,
513
+ contentType: file.contentType,
514
+ size: file.size
515
+ }, bytes);
516
+ } catch (error) {
517
+ return {
518
+ ok: false,
519
+ reason: "scanner_unavailable",
520
+ cause: error instanceof Error ? error.message : String(error)
521
+ };
522
+ }
523
+ return verdict.clean ? { ok: true } : {
524
+ ok: false,
525
+ reason: "refused",
526
+ finding: verdict.finding
527
+ };
528
+ }
529
+ /**
498
530
  * Which files a submission may claim, checked but not yet claimed.
499
531
  *
500
532
  * The claiming itself is `insertSubmission`'s job, in the same call that
@@ -1576,6 +1608,6 @@ function publishAudit(deps, input, about) {
1576
1608
  };
1577
1609
  }
1578
1610
  //#endregion
1579
- export { AUDIT_ACTIONS, BREAKER_COOLDOWN_MS, BREAKER_THRESHOLD, CHALLENGE_TTL_SECONDS, DEFAULT_MAX_NUMBER, EVENT_ID_HEADER, MAX_ATTEMPTS, SIGNATURE_HEADER, afterAttempt, afterWebhookAttempt, auditedBy, authenticateApiKey, authenticateLocal, breakerState, can, checkMembership, collectAbandonedFiles, createApiKey, createLocalUser, createMemoryStorage, createSubmission, decodeSolution, deliveryHeaders, drainOutbox, encodeSolution, exportCsv, fileFieldPaths, filesToClaim, hashOf, healthOf, isIpLiteral, isPrivateAddress, listForms, listSubmissions, listVersions, mayAttempt, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, retryDelayMs, saveDraft, setFormAccess, signBody, signedPayload, solveChallenge, sourceNamesIn, sourcedAnswers, startDraft, unsafePatterns, verifySignature, verifySolution };
1611
+ export { AUDIT_ACTIONS, BREAKER_COOLDOWN_MS, BREAKER_THRESHOLD, CHALLENGE_TTL_SECONDS, DEFAULT_MAX_NUMBER, EVENT_ID_HEADER, MAX_ATTEMPTS, SIGNATURE_HEADER, afterAttempt, afterWebhookAttempt, auditedBy, authenticateApiKey, authenticateLocal, breakerState, can, checkMembership, collectAbandonedFiles, createApiKey, createLocalUser, createMemoryStorage, createSubmission, decodeSolution, deliveryHeaders, drainOutbox, encodeSolution, exportCsv, fileFieldPaths, filesToClaim, hashOf, healthOf, isIpLiteral, isPrivateAddress, listForms, listSubmissions, listVersions, mayAttempt, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, retryDelayMs, saveDraft, screenUpload, setFormAccess, signBody, signedPayload, solveChallenge, sourceNamesIn, sourcedAnswers, startDraft, unsafePatterns, verifySignature, verifySolution };
1580
1612
 
1581
1613
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/options-membership.ts","../src/audit.ts","../src/webhook.ts","../src/breaker.ts","../src/outbox.ts","../src/access.ts","../src/uploads.ts","../src/use-cases.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/auth.ts","../src/address.ts","../src/publishing.ts"],"sourcesContent":["import type { FieldDef, FormSchema } from '@formancy/spec'\n\n/**\n * Checking that a sourced answer is one the deployment's own source offers.\n *\n * The engine already refuses a value no *document* option offers\n * ([0076](../../../docs/decisions/0076-an-answer-is-one-of-the-options.md)), and\n * it runs on both sides. A `optionsSource` field has no document options, so that\n * check has nothing to compare against — deliberately, because a list living\n * outside the document cannot be checked against the document.\n *\n * So the server asks the deployment instead, and the deployment answers with a\n * database query rather than with a request to an address a form author typed.\n * That is why the port takes a function and this package ships no HTTP client:\n * `@formancy/server-core` imports no `URL` and no `fetch`, and adding an outbound\n * request here would add confused-deputy surface to a regulatory set that claims\n * none.\n */\n\n/** One named source, as the deployment configured it. */\nexport interface ServerOptionsSource {\n /**\n * Which of these values this source does NOT offer.\n *\n * Phrased as the rejects rather than the members so an adapter can answer from a\n * SQL `IN` clause without materialising a list — a source with two million rows\n * is exactly the case `optionsSource` exists for.\n *\n * Absent means: *this source exists, and I cannot check membership.* That is a\n * legitimate configuration and it weakens the guarantee, which is why the\n * guarantee is written down rather than implied.\n */\n members?(values: readonly string[]): Promise<readonly string[]>\n}\n\nexport type ServerOptionsSources = Readonly<Record<string, ServerOptionsSource>>\n\n/**\n * Every sourced answer in a submission, as `path → { source, value }`.\n *\n * Walked from the SCHEMA over the value, never from the value's own shape — the\n * same way `referencedFileIds` does it. A walk driven by what arrived would let a\n * client dodge the check by omitting a property, and hidden branches have already\n * been stripped by the engine before this runs.\n */\nexport function sourcedAnswers(\n schema: FormSchema,\n value: unknown,\n): ReadonlyArray<{ path: string; source: string; value: string }> {\n const found: Array<{ path: string; source: string; value: string }> = []\n\n const walk = (fields: readonly FieldDef[], data: unknown, prefix: string): void => {\n if (typeof data !== 'object' || data === null) return\n const record = data as Record<string, unknown>\n\n for (const field of fields) {\n const at = prefix === '' ? field.key : `${prefix}.${field.key}`\n const held = record[field.key]\n\n if (field.fields !== undefined) {\n // A repeater's rows are a list; a group's children are one object.\n if (Array.isArray(held)) {\n for (const [index, row] of held.entries()) {\n walk(field.fields, row, `${at}[${String(index)}]`)\n }\n } else {\n walk(field.fields, held, at)\n }\n continue\n }\n\n if (field.optionsSource === undefined) continue\n\n // A list answer is several answers to one question. This asked\n // `typeof held !== 'string'` and an array is not a string, so every value\n // in a sourced `selectboxes` was stored with nothing having looked at it —\n // hazard A7 wearing a different shape of answer.\n //\n // One entry per VALUE and the field's own path on each: the field is wrong\n // when any of its answers is, and naming an index would describe a payload\n // rather than the question.\n const answers = Array.isArray(held) ? held : [held]\n for (const answer of answers) {\n // An empty answer is `required`'s business, not this one's, and a\n // non-string is `modelViolations`' — a source asked about `{\"$gt\": \"\"}`\n // is a source handed a query it did not expect.\n if (typeof answer !== 'string' || answer === '') continue\n found.push({ path: at, source: field.optionsSource, value: answer })\n }\n }\n }\n\n walk(schema.model.fields, value, '')\n return found\n}\n\n/** What asking the deployment produced. */\nexport type MembershipOutcome =\n | { ok: true }\n /** Values the deployment's own source does not offer, by field path. */\n | { ok: false; kind: 'invalid'; errors: Record<string, string[]> }\n /**\n * A source could not answer.\n *\n * Failed CLOSED, always. An accepted bogus value is undetectable afterwards; a\n * refusal is retryable and the draft still holds the answers. It is the same rule\n * the engine applies to a validation rule it cannot evaluate.\n */\n | { ok: false; kind: 'source_unavailable'; source: string }\n\n/**\n * Ask each source about the values submitted for it.\n *\n * One call per source rather than per answer, so a deployment sees `IN (…)` and not\n * a query per row.\n */\nexport async function checkMembership(\n schema: FormSchema,\n value: unknown,\n sources: ServerOptionsSources | undefined,\n): Promise<MembershipOutcome> {\n const answers = sourcedAnswers(schema, value)\n if (answers.length === 0 || sources === undefined) return { ok: true }\n\n const bySource = new Map<string, Array<{ path: string; value: string }>>()\n for (const answer of answers) {\n const group = bySource.get(answer.source) ?? []\n group.push({ path: answer.path, value: answer.value })\n bySource.set(answer.source, group)\n }\n\n const errors: Record<string, string[]> = {}\n\n for (const [name, group] of bySource) {\n const source = sources[name]\n // A source the deployment has not configured cannot vouch for anything. Treated\n // as unavailable rather than as absent: the document says these answers come\n // from somewhere, and accepting them unchecked would be the silent version of\n // the same failure.\n if (source === undefined) return { ok: false, kind: 'source_unavailable', source: name }\n if (source.members === undefined) continue\n\n let rejected: readonly string[]\n try {\n rejected = await source.members(group.map((entry) => entry.value))\n } catch {\n return { ok: false, kind: 'source_unavailable', source: name }\n }\n\n const refused = new Set(rejected)\n for (const entry of group) {\n if (!refused.has(entry.value)) continue\n // The same code word a document option produces, so a message catalogue\n // learns one word rather than two for one idea.\n errors[entry.path] = [...(errors[entry.path] ?? []), 'option']\n }\n }\n\n return Object.keys(errors).length === 0 ? { ok: true } : { ok: false, kind: 'invalid', errors }\n}\n\n/**\n * The source names a document uses, for the publish-time vocabulary check.\n *\n * Walked over the schema rather than over any data, because publishing has none.\n */\nexport function sourceNamesIn(schema: FormSchema): readonly string[] {\n const names = new Set<string>()\n const walk = (fields: readonly FieldDef[]): void => {\n for (const field of fields) {\n if (field.optionsSource !== undefined) names.add(field.optionsSource)\n if (field.fields !== undefined) walk(field.fields)\n }\n }\n walk(schema.model.fields)\n return [...names]\n}\n","import type { Actor } from './auth.js'\n\n/**\n * Who did what, to which thing, and when.\n *\n * **Reads and exports are recorded as well as mutations** — `submission.read` and\n * `submission.exported` — which is what lets a self-hoster answer who downloaded four\n * thousand people's answers ([0057](../../../docs/decisions/0057-the-audit-log-records-reads.md)).\n *\n * **A row joins the mutation's transaction where there is one.** `insertSubmission` and\n * `publishVersion` both commit their audit row with the change it describes, so a\n * rollback leaves no trace saying it happened. A read has nothing to be atomic with, so\n * its row is appended after the rows are handed over and a failure to write it is logged\n * loudly rather than swallowed.\n *\n * **`detail` carries identifiers and counts, never submission content.** An audit log is\n * read by more people, kept longer and exported more freely than the data it describes.\n */\n\n/**\n * Every auditable action, as a closed list.\n *\n * Closed so a new route cannot invent its own spelling: an audit log where one event is\n * called two things is one nobody can query.\n */\nexport const AUDIT_ACTIONS = [\n 'auth.login',\n 'auth.login.failed',\n 'form.published',\n 'form.access.changed',\n 'submission.created',\n 'submission.read',\n 'submission.exported',\n 'file.downloaded',\n 'user.created',\n 'apiKey.created',\n 'apiKey.revoked',\n // Somebody decided a dead delivery should go after all. Recorded because it\n // sends data to a third party on a person's say-so.\n 'delivery.replayed',\n] as const\n\nexport type AuditAction = (typeof AUDIT_ACTIONS)[number]\n\nexport interface AuditEntry {\n readonly id: string\n readonly at: string\n readonly action: AuditAction\n /**\n * Who. Absent for an anonymous public submission, which is a real answer\n * rather than a gap: the form was open, and nobody was signed in.\n */\n readonly actorKind?: 'user' | 'apiKey'\n readonly actorId?: string\n /**\n * What it was done to, as a stable identifier: a form's path, a submission\n * id, a user's id. Absent only where the action has no object, which is\n * currently nothing.\n */\n readonly subject?: string\n /**\n * The request it happened in, so an export and the rows it returned can be\n * tied together later.\n */\n readonly requestId?: string\n /** Identifiers and counts. Never a submission's answers. */\n readonly detail?: Readonly<Record<string, string | number | boolean>>\n}\n\n/** What a caller has to supply; the id and the timestamp are minted for it. */\nexport type AuditDraft = Omit<AuditEntry, 'id' | 'at'>\n\n/**\n * An entry from an actor, with the fields spread the way the record stores\n * them.\n *\n * A helper rather than a convention, because `actorKind` and `actorId` being\n * set together is exactly the sort of thing eleven call sites get wrong once.\n */\nexport function auditedBy(actor: Actor | undefined, draft: Omit<AuditDraft, 'actorKind' | 'actorId'>): AuditDraft {\n return actor === undefined\n ? draft\n : { ...draft, actorKind: actor.kind, actorId: actor.id }\n}\n","import { hmac } from '@noble/hashes/hmac.js'\nimport { sha256 } from '@noble/hashes/sha2.js'\nimport { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js'\n\n/**\n * What a webhook delivery looks like on the wire.\n *\n * The signature scheme is Stripe's, deliberately: receivers already have code\n * for it, and a scheme somebody has to implement from scratch is a scheme most\n * receivers will skip. Adopting a familiar one is the difference between a\n * verified webhook and a verified-in-principle one.\n */\n\nexport const EVENT_ID_HEADER = 'x-formancy-event-id'\nexport const SIGNATURE_HEADER = 'x-formancy-signature'\n\nexport interface DeliveryHeaders {\n [header: string]: string\n}\n\n/**\n * The signed payload is `timestamp.body`, not `body`.\n *\n * Without the timestamp inside the signature, a signature captured once stays\n * valid forever and can be replayed. With it, a receiver can reject anything\n * older than its tolerance, and the signature covers the timestamp it is\n * checking — so the timestamp cannot be edited either.\n */\nexport function signedPayload(timestampSeconds: number, body: string): string {\n return `${String(timestampSeconds)}.${body}`\n}\n\n/** Hex HMAC-SHA256 over the signed payload. */\nexport function signBody(secret: string, timestampSeconds: number, body: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(signedPayload(timestampSeconds, body))))\n}\n\n/**\n * The headers for one delivery attempt.\n *\n * The event id is stable across retries, which is what makes the receiver able\n * to be idempotent. Generating a fresh one per attempt would turn our retry\n * into their duplicate.\n */\nexport function deliveryHeaders(input: {\n eventId: string\n secret: string\n body: string\n timestampSeconds: number\n attempt: number\n}): DeliveryHeaders {\n return {\n 'content-type': 'application/json',\n [EVENT_ID_HEADER]: input.eventId,\n [SIGNATURE_HEADER]: `t=${String(input.timestampSeconds)},v1=${signBody(input.secret, input.timestampSeconds, input.body)}`,\n // Useful in a receiver's logs, and never part of the signature: a retry\n // must produce the same signature as the first attempt.\n 'x-formancy-attempt': String(input.attempt),\n }\n}\n\n/**\n * Verify a delivery the way a receiver would.\n *\n * Exported because a scheme nobody can check is a scheme nobody does check —\n * this is the reference implementation, and the test for our own signing.\n */\nexport function verifySignature(input: {\n secret: string\n body: string\n header: string\n nowSeconds: number\n toleranceSeconds?: number\n}): boolean {\n const parts = new Map(\n input.header.split(',').map((part) => {\n const at = part.indexOf('=')\n return [part.slice(0, at).trim(), part.slice(at + 1).trim()] as const\n }),\n )\n\n const timestamp = Number(parts.get('t'))\n const provided = parts.get('v1')\n if (!Number.isFinite(timestamp) || provided === undefined) return false\n\n const tolerance = input.toleranceSeconds ?? 300\n if (Math.abs(input.nowSeconds - timestamp) > tolerance) return false\n\n const expected = signBody(input.secret, timestamp, input.body)\n return timingSafeEqual(expected, provided)\n}\n\n/**\n * Compare without leaking where the first difference is.\n *\n * `a === b` on a hex string returns as soon as two characters differ, and the\n * time it takes says how many leading characters were right — enough, over\n * many attempts, to recover a signature a byte at a time.\n */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let difference = 0\n for (let index = 0; index < a.length; index += 1) {\n difference |= a.charCodeAt(index) ^ b.charCodeAt(index)\n }\n return difference === 0\n}\n\n/**\n * How long to wait before attempt n, in milliseconds.\n *\n * Exponential, capped, with jitter. The jitter is the part that matters at\n * scale: without it every delivery queued by the same outage retries at the\n * same instant, and the receiver coming back up is knocked over by the\n * thundering herd of everyone's retries.\n */\nexport function retryDelayMs(attempt: number, random: () => number): number {\n const base = Math.min(30_000 * 2 ** (attempt - 1), 6 * 60 * 60 * 1000)\n // Full jitter: anywhere in [0, base). Better than base±10% precisely because\n // it spreads the herd across the whole window.\n return Math.floor(base * random())\n}\n\n/** Eight attempts reach roughly 24 hours with this schedule. */\nexport const MAX_ATTEMPTS = 8\n","import type { WebhookRecord } from './ports.js'\n\n/**\n * Stopping, when a destination has plainly stopped listening.\n *\n * Failure is counted per WEBHOOK as well as per delivery. Enough consecutive failures\n * and the breaker opens: nothing is attempted for that webhook until a cool-down passes,\n * then exactly one delivery goes through as a probe. It succeeds and the breaker closes;\n * it fails and the cool-down starts again.\n *\n * `consecutiveFailures` and `openedAt` live on the RECORD rather than in the worker's\n * memory, so the state survives a restart and can be shown on a screen — a self-hoster\n * has no operations team watching a dashboard\n * ([0058](../../../docs/decisions/0058-a-breaker-per-destination.md)).\n *\n * Everything here is a pure function of a record and a clock: decisions tangled into\n * network code can only be tested by making requests.\n */\n\n/**\n * How many consecutive failures open the breaker.\n *\n * Three, not one. A single failure is a bad minute — a deploy, a timeout, a\n * receiver restarting — and opening on it would make every routine blip a\n * visible outage. Three in a row is a destination that is not coming back on\n * its own.\n */\nexport const BREAKER_THRESHOLD = 3\n\n/**\n * How long the breaker stays shut before one probe is allowed through.\n *\n * Five minutes: long enough that a restarting receiver is finished, short\n * enough that nobody notices the delay once it recovers. The retry schedule\n * handles patience within a delivery; this handles patience about a\n * destination.\n */\nexport const BREAKER_COOLDOWN_MS = 5 * 60 * 1000\n\n/** Shut, letting one through, or open. */\nexport type BreakerState = 'closed' | 'half-open' | 'open'\n\nexport function breakerState(webhook: WebhookRecord, now: Date): BreakerState {\n if (webhook.consecutiveFailures < BREAKER_THRESHOLD) return 'closed'\n if (webhook.openedAt === null) return 'closed'\n\n const since = now.getTime() - new Date(webhook.openedAt).getTime()\n // Half-open rather than closed: the cool-down earns ONE attempt, not a\n // return to normal. Closing on the clock alone would send the whole backlog\n // at a destination that has not answered yet.\n return since >= BREAKER_COOLDOWN_MS ? 'half-open' : 'open'\n}\n\n/** Whether a delivery for this webhook may be attempted now. */\nexport function mayAttempt(webhook: WebhookRecord, now: Date): boolean {\n return breakerState(webhook, now) !== 'open'\n}\n\n/**\n * The webhook's health after one attempt against it.\n *\n * A success always closes the breaker completely. Decaying the count instead\n * would leave a destination that fails twice for every success permanently\n * one blip from opening, and that destination is working.\n */\nexport function afterWebhookAttempt(\n webhook: WebhookRecord,\n ok: boolean,\n now: Date,\n): WebhookRecord {\n if (ok) {\n return { ...webhook, consecutiveFailures: 0, openedAt: null }\n }\n\n const consecutiveFailures = webhook.consecutiveFailures + 1\n const opening = consecutiveFailures >= BREAKER_THRESHOLD\n return {\n ...webhook,\n consecutiveFailures,\n // Re-stamped on every failure once open, so a probe that fails starts the\n // cool-down again rather than letting the next one through immediately.\n openedAt: opening ? now.toISOString() : webhook.openedAt,\n }\n}\n\n/**\n * What a person is shown about a webhook's health.\n *\n * Shaped for a screen rather than for a log: the question being answered is\n * \"is this working, and if not since when\", which is what somebody asks when\n * the CRM has no leads this week.\n */\nexport interface WebhookHealth {\n id: string\n url: string\n state: BreakerState\n consecutiveFailures: number\n /** When it stopped working, or null while it is working. */\n failingSince: string | null\n}\n\nexport function healthOf(webhook: WebhookRecord, now: Date): WebhookHealth {\n return {\n id: webhook.id,\n url: webhook.url,\n state: breakerState(webhook, now),\n consecutiveFailures: webhook.consecutiveFailures,\n failingSince: webhook.openedAt,\n }\n}\n","import type { DeliveryRecord, Storage, WebhookRecord } from './ports.js'\nimport { MAX_ATTEMPTS, retryDelayMs } from './webhook.js'\nimport { BREAKER_COOLDOWN_MS, afterWebhookAttempt, mayAttempt } from './breaker.js'\n\n/**\n * What to do with a delivery once an attempt has finished.\n *\n * Pure, and separate from the thing that does the posting, because this is\n * where the decisions are: whether to retry, when, and when to stop. Tangled\n * into the network code those decisions are only testable by making requests.\n */\n\nexport interface AttemptOutcome {\n ok: boolean\n status?: number\n error?: string\n}\n\nexport function afterAttempt(\n delivery: DeliveryRecord,\n outcome: AttemptOutcome,\n now: Date,\n random: () => number,\n): DeliveryRecord {\n const attempt = delivery.attempt + 1\n\n if (outcome.ok) {\n return { ...delivery, attempt, state: 'delivered', lastError: null }\n }\n\n const reason = outcome.error ?? `Receiver answered ${String(outcome.status ?? 0)}.`\n\n if (attempt >= MAX_ATTEMPTS) {\n // Dead, not deleted. The row is the evidence that something was supposed\n // to be sent and never arrived, and a self-hoster with no operations team\n // needs to be able to find it and replay it.\n return { ...delivery, attempt, state: 'dead', lastError: reason }\n }\n\n return {\n ...delivery,\n attempt,\n state: 'pending',\n nextAttemptAt: new Date(now.getTime() + retryDelayMs(attempt, random)).toISOString(),\n lastError: reason,\n }\n}\n\nexport interface OutboxDeps {\n storage: Storage\n /** Posts one delivery. Injected so the loop is testable without a network. */\n send: (\n delivery: DeliveryRecord,\n webhook: WebhookRecord,\n nowSeconds: number,\n ) => Promise<AttemptOutcome>\n now: () => Date\n random: () => number\n}\n\n/**\n * Drain one batch of due deliveries.\n *\n * Returns how many were attempted, so a caller can decide whether to come back\n * immediately or wait. Deliberately one batch rather than a loop that runs\n * forever: the thing that decides *when* to run belongs to the host, and a\n * function that never returns cannot be tested.\n */\n/**\n * Returns how many were ATTEMPTED — deliveries skipped because their\n * destination's breaker is open do not count, so a caller polling until the\n * queue is quiet does not spin on a dead endpoint.\n */\nexport async function drainOutbox(deps: OutboxDeps, limit = 20): Promise<number> {\n const now = deps.now()\n const due = await deps.storage.claimDueDeliveries(now.toISOString(), limit)\n let skipped = 0\n\n for (const delivery of due) {\n // A webhook deleted since the row was queued leaves nothing to deliver to.\n // Marked dead rather than retried forever against a destination that no\n // longer exists.\n const webhook = await findWebhook(deps.storage, delivery.webhookId)\n if (webhook === undefined) {\n await deps.storage.updateDelivery({\n ...delivery,\n state: 'dead',\n lastError: 'The webhook it was queued for no longer exists.',\n })\n continue\n }\n\n // The breaker is per DESTINATION, where the retry schedule is per\n // delivery. A form taking a submission a minute otherwise produces a\n // minute's worth of deliveries all independently hammering an endpoint\n // that has been returning 502 since Tuesday.\n if (!mayAttempt(webhook, now)) {\n skipped += 1\n // Pushed out rather than left due, so the next batch is not the same\n // rows again. The delivery keeps its attempt count: being skipped is\n // not a failed attempt, and charging it one would use up a delivery's\n // eight tries on a destination it never reached.\n await deps.storage.updateDelivery({\n ...delivery,\n nextAttemptAt: new Date(now.getTime() + BREAKER_COOLDOWN_MS).toISOString(),\n })\n continue\n }\n\n let outcome: AttemptOutcome\n try {\n outcome = await deps.send(delivery, webhook, Math.floor(now.getTime() / 1000))\n } catch (error) {\n // A thrown send is a failed attempt, not a lost job. Letting it escape\n // would abandon every remaining delivery in the batch.\n outcome = { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n\n await deps.storage.updateDelivery(afterAttempt(delivery, outcome, now, deps.random))\n // The destination's health, which outlives this delivery. Written every\n // time rather than only on a change: a success has to clear the count,\n // and the write is one row.\n await deps.storage.updateWebhook(afterWebhookAttempt(webhook, outcome.ok, now))\n }\n\n return due.length - skipped\n}\n\nasync function findWebhook(storage: Storage, id: string): Promise<WebhookRecord | undefined> {\n // The port lists per form rather than by id, which is the shape every other\n // caller wants; this is the one place that pays for it.\n const forms = await storage.listForms()\n for (const form of forms) {\n const hooks = await storage.webhooksForForm(form.id)\n const found = hooks.find((hook) => hook.id === id)\n if (found !== undefined) return found\n }\n return undefined\n}\n\n/**\n * Put a dead delivery back in the queue.\n *\n * Dead is where a delivery goes when the attempts run out, and the row is the\n * evidence that something was supposed to be sent and never arrived. Replay is\n * what makes that evidence useful: the receiver was down for a morning, it is\n * back, and the twelve submissions it missed should go now.\n *\n * The attempt count resets. It is a new run against a destination somebody has\n * looked at and decided is fixed — carrying the old count over would give it\n * one try before dying again, which is not a replay so much as a formality.\n *\n * Refuses anything that is not dead. Replaying a pending delivery would\n * duplicate it, and replaying a delivered one would send it twice: the event\n * id is stable across retries so a well-behaved receiver would ignore the\n * second, but this service does not get to assume the receiver is\n * well-behaved.\n */\nexport type ReplayOutcome =\n | { ok: true; delivery: DeliveryRecord }\n | { ok: false; kind: 'unknown' | 'not-dead'; state?: DeliveryRecord['state'] }\n\nexport async function replayDelivery(\n deps: { storage: Storage; now: () => Date },\n id: string,\n): Promise<ReplayOutcome> {\n const delivery = await deps.storage.getDelivery(id)\n if (delivery === undefined) return { ok: false, kind: 'unknown' }\n if (delivery.state !== 'dead') return { ok: false, kind: 'not-dead', state: delivery.state }\n\n const revived: DeliveryRecord = {\n ...delivery,\n attempt: 0,\n state: 'pending',\n // Now, not on the retry schedule. Somebody pressed a button; the next\n // pass should pick it up.\n nextAttemptAt: deps.now().toISOString(),\n }\n await deps.storage.updateDelivery(revived)\n return { ok: true, delivery: revived }\n}\n","import type { FormRecord } from './ports.js'\n\n/**\n * Whether this caller may submit this form at all, before any of the work.\n *\n * Every branch that is not an explicit permission returns false. An allowlist\n * that exists but does not name the origin refuses; an origin header that is\n * absent refuses, because absent is not the same as allowed; an empty\n * allowlist refuses everything, because a list of no origins is a list.\n */\nexport function maySubmit(\n form: FormRecord,\n actor: 'anonymous' | 'authenticated',\n origin: string | undefined,\n): boolean {\n if (actor === 'authenticated') return true\n if (form.accessSubmit !== 'public') return false\n\n const allowed = form.allowedOrigins\n if (allowed === null) return true\n if (origin === undefined) return false\n\n // Exact match, never a prefix or suffix one: `https://evil-example.ch` ends\n // with the same characters as `example.ch` and must not pass, and\n // `https://example.ch:8443` is a different origin from `https://example.ch`.\n return allowed.includes(origin)\n}\n","import { modelDataPaths } from '@formancy/spec'\nimport type { FieldDef, FormSchema } from '@formancy/spec'\nimport { maySubmit } from './access.js'\nimport type { FileRecord, FormRecord, Storage } from './ports.js'\n\n/**\n * The file lifecycle: offered, stored, claimed, collected.\n *\n * **Bytes are not the submission's problem.** A submission stores what each\n * file is and where it went, never the bytes themselves, so a submission read\n * back years later is small, self-describing, and still says what was attached\n * even if the object store has since been emptied.\n *\n * **The claim happens inside the submission's transaction.** A submission\n * exists if and only if the files it names are claimed against it. Anything\n * looser gives one of the two failures nobody can debug: a rolled-back\n * submission leaving files claimed against nothing, or an accepted submission\n * referencing files the collector is about to delete.\n *\n * **Nothing here touches a disk.** This package has no filesystem and no\n * object store; it decides what is rubbish and the host deletes it. That is\n * the same split every other port here makes, and it is why the whole\n * lifecycle is testable without either.\n */\n\nexport interface OfferDeps {\n storage: Storage\n now: () => Date\n newId: () => string\n}\n\nexport interface OfferInput {\n schema: FormSchema\n /** The form, for the access check — the same one a submission passes. */\n form: FormRecord\n actor: 'anonymous' | 'authenticated'\n origin: string | undefined\n /** The field the file is for, as a data path: `evidence`, `people[].passport`. */\n field: string\n name: string\n size: number\n contentType: string\n}\n\nexport type OfferOutcome =\n | { ok: true; file: FileRecord }\n | {\n ok: false\n reason: 'not_allowed' | 'unknown_field' | 'not_a_file_field' | 'too_large' | 'not_accepted'\n }\n\n/**\n * Agree where a file will go, and refuse it before a byte is sent.\n *\n * Checking here rather than after the upload is the difference between\n * refusing a 2 GB file and receiving one first. The browser's own `accept` and\n * size limits are a convenience for the person filling the form in; these are\n * the rule, because somebody posting to the endpoint directly has neither.\n */\nexport async function offerUpload(deps: OfferDeps, input: OfferInput): Promise<OfferOutcome> {\n // The same gate the submission passes, and the same function — a form\n // nobody may submit to is not a form anybody may upload to either, or the\n // store becomes free disk for whoever finds the path. Two copies of that\n // rule is one copy that gets forgotten.\n if (!maySubmit(input.form, input.actor, input.origin)) return { ok: false, reason: 'not_allowed' }\n\n const def = fileFieldAt(input.schema, input.field)\n if (def === undefined) return { ok: false, reason: 'unknown_field' }\n if (def.type !== 'file') return { ok: false, reason: 'not_a_file_field' }\n\n if (def.maxFileSize !== undefined && input.size > def.maxFileSize) {\n return { ok: false, reason: 'too_large' }\n }\n if (!accepted(def.accept, input.name, input.contentType)) {\n return { ok: false, reason: 'not_accepted' }\n }\n\n const id = deps.newId()\n const file: FileRecord = {\n id,\n formId: input.form.id,\n name: input.name,\n size: input.size,\n contentType: input.contentType,\n // Built from the id and the form, never from the submitted name. The name\n // is display only and may be anything at all, including `../../etc/passwd`.\n storageKey: `${input.form.id}/${id}`,\n state: 'offered',\n createdAt: deps.now().toISOString(),\n submissionId: null,\n }\n\n await deps.storage.insertFile(file)\n return { ok: true, file }\n}\n\nexport type ClaimOutcome =\n | { ok: true; ids: readonly string[] }\n | { ok: false; unknown: readonly string[] }\n\n/**\n * Which files a submission may claim, checked but not yet claimed.\n *\n * The claiming itself is `insertSubmission`'s job, in the same call that\n * stores the submission, so one COMMIT decides both — the same reasoning\n * that put the webhook queue there. A submission exists if and only if the\n * files it names belong to it. Claiming separately gives the two failures\n * nobody can debug: a rolled-back submission leaving files claimed against\n * nothing, or an accepted submission referencing bytes the collector is about\n * to delete.\n *\n * Refuses rather than skips. A submission naming a file that does not exist,\n * belongs to another form, or is already claimed is not a submission to fix up\n * quietly: the first is a bug or somebody guessing ids; the second is how an\n * id from a form you may submit to would let you attach a file from one you\n * may not; the third would make deleting a submission either leak bytes or\n * break a different one.\n */\nexport async function filesToClaim(\n storage: Storage,\n input: { schema: FormSchema; formId: string; data: unknown },\n): Promise<ClaimOutcome> {\n const ids = referencedFileIds(input.schema, input.data)\n if (ids.length === 0) return { ok: true, ids: [] }\n\n const unknown: string[] = []\n const claimable: FileRecord[] = []\n\n for (const id of ids) {\n const file = await storage.getFile(id)\n if (file === undefined || file.formId !== input.formId || file.state !== 'stored') {\n unknown.push(id)\n continue\n }\n claimable.push(file)\n }\n\n if (unknown.length > 0) return { ok: false, unknown }\n\n return { ok: true, ids: claimable.map((file) => file.id) }\n}\n\n/**\n * Files nobody claimed, old enough that nobody is going to.\n *\n * Returns the rows rather than deleting anything: the host removes the bytes\n * and then the rows, in that order, because a row without bytes is a broken\n * reference and bytes without a row are invisible rubbish. Of the two, the\n * second is the one you never find out about.\n */\nexport async function collectAbandonedFiles(\n storage: Storage,\n now: Date,\n afterHours: number,\n): Promise<FileRecord[]> {\n const before = new Date(now.getTime() - afterHours * 3_600_000).toISOString()\n return storage.abandonedFiles(before)\n}\n\n// ------------------------------------------------------------------ helpers\n\n/** The field at a data path, with `[]` treated as \"inside this repeater\". */\nfunction fileFieldAt(schema: FormSchema, path: string): FieldDef | undefined {\n const wanted = path.replace(/\\[\\]/g, '')\n const segments = wanted.split('.').filter((segment) => segment !== '')\n\n let fields: readonly FieldDef[] = flattenPages(schema.model.fields)\n let found: FieldDef | undefined\n\n for (const segment of segments) {\n found = fields.find((field) => field.key === segment)\n if (found === undefined) return undefined\n fields = flattenPages(found.fields ?? [])\n }\n return found\n}\n\n/** A page contributes no segment to a data path. */\nfunction flattenPages(fields: readonly FieldDef[]): FieldDef[] {\n const out: FieldDef[] = []\n for (const field of fields) {\n if (field.type === 'page') out.push(...flattenPages(field.fields ?? []))\n else out.push(field)\n }\n return out\n}\n\n/** The HTML `accept` grammar, as the engine reads it. */\nfunction accepted(accept: readonly string[] | undefined, name: string, contentType: string): boolean {\n if (accept === undefined || accept.length === 0) return true\n const lowerName = name.toLowerCase()\n const lowerType = contentType.toLowerCase()\n\n return accept.some((raw) => {\n const rule = raw.trim().toLowerCase()\n if (rule === '') return false\n if (rule.startsWith('.')) return lowerName.endsWith(rule)\n if (rule.endsWith('/*')) return lowerType.startsWith(rule.slice(0, -1))\n return lowerType === rule\n })\n}\n\n/**\n * Every file id a submission references.\n *\n * Driven by the SCHEMA, not by the shape of the values. Recognising an\n * attachment by \"an object with an id\" would let a client omit a property and\n * have its reference quietly not claimed — the submission would be accepted\n * carrying a dangling file id, and the collector would delete the bytes a day\n * later. Reading only the paths the model says are file fields cannot be\n * fooled that way, and a text answer that happens to look like a file is not\n * one.\n *\n * A repeater contributes rows the schema cannot know about, so the walk\n * expands over whatever rows the data actually has.\n */\nfunction referencedFileIds(schema: FormSchema, data: unknown): string[] {\n const ids: string[] = []\n\n const walk = (fields: readonly FieldDef[], value: unknown): void => {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return\n const record = value as Record<string, unknown>\n\n for (const field of flattenPages(fields)) {\n const held = record[field.key]\n if (held === undefined) continue\n\n if (field.type === 'file') {\n if (!Array.isArray(held)) continue\n for (const entry of held) {\n const id = (entry as Record<string, unknown> | null)?.['id']\n if (typeof id === 'string') ids.push(id)\n }\n continue\n }\n\n if (field.type === 'group') {\n walk(field.fields ?? [], held)\n continue\n }\n\n if (field.type === 'repeater' && Array.isArray(held)) {\n for (const row of held) walk(field.fields ?? [], row)\n }\n }\n }\n\n walk(schema.model.fields, data)\n return ids\n}\n\n/** Re-exported so a host can see which paths a schema's file fields live at. */\nexport function fileFieldPaths(schema: FormSchema): string[] {\n return modelDataPaths(schema.model).filter(\n (path) => fileFieldAt(schema, path)?.type === 'file',\n )\n}\n","import { hmac } from '@noble/hashes/hmac.js'\nimport { sha256 } from '@noble/hashes/sha2.js'\nimport { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js'\nimport { diffSchemas, schemaHash } from '@formancy/spec'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { createFormEngine } from '@formancy/core'\nimport { checkMembership } from './options-membership.js'\nimport type { ServerOptionsSources } from './options-membership.js'\nimport type { CapabilitySource, Check } from '@formancy/core'\nimport { maySubmit } from './access.js'\nimport { filesToClaim } from './uploads.js'\nimport type { Storage } from './ports.js'\n\n/**\n * The backend's use-cases, framework-free.\n *\n * Identity and time are injected like every other capability: the server pins\n * one clock per request, so a submission can be replayed later and produce the\n * same computed values byte for byte. Nothing here reads Date.now or crypto.\n */\nexport interface ServerDeps {\n storage: Storage\n newId(): string\n nowIso(): string\n /**\n * Validators this deployment answers, by the name a `check` rule gives.\n *\n * The same shape `optionsSources` takes, and the same rule: the document names\n * a check, the deployment says how to answer it, and nothing in the engine\n * fetches anything. A check a document names and a deployment has not supplied\n * fails the field closed.\n */\n checks?: Record<string, Check>\n /** The clock/randomness the ENGINE sees during replay. */\n capabilities: CapabilitySource\n /**\n * Signs the token that proves somebody started a draft.\n *\n * The public plane is anonymous, so a draft has no account behind it — which\n * is exactly why it needs a secret of its own. The host's own signing key is\n * reused rather than a second one being configured: a draft token is a\n * server-signed bearer token, which is what that key is already for, and an\n * optional secret would mean drafts are unprotected whenever nobody set it.\n */\n draftSecret: string\n /**\n * The lists a form document may name with `optionsSource`, and how to check a\n * value against one.\n *\n * A function and never an address: for a real source this is a database query, not\n * a request to somewhere a form author typed. Shipping an HTTP adapter here would\n * add outbound-request and confused-deputy surface to a regulatory set that\n * currently claims neither — for a convenience nobody asked for.\n *\n * Absent entirely means this deployment has no vocabulary: publishing cannot be\n * judged against it, and no submission is checked. Present but without `members`\n * for a name means *this source exists and I cannot check membership* — a\n * legitimate configuration, and the reason the guarantee is written down.\n */\n optionsSources?: ServerOptionsSources\n}\n\nexport interface ResolvedForm {\n formId: string\n versionId: string\n version: number\n schema: FormSchema\n schemaHash: string\n}\n\nexport async function resolveForm(deps: ServerDeps, path: string): Promise<ResolvedForm | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined || form.currentVersionId === null) return undefined\n const current = await deps.storage.getVersionById(form.currentVersionId)\n if (current === undefined) return undefined\n return {\n formId: form.id,\n versionId: current.id,\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n }\n}\n\nexport type SubmissionOutcome =\n | { ok: true; id: string; canonicalData: unknown }\n | { ok: false; kind: 'unknown_form' }\n | { ok: false; kind: 'version_changed'; current?: ResolvedForm }\n | { ok: false; kind: 'invalid'; errors: Record<string, string[]> }\n /** The caller may not submit this form: not public, or not from this origin. */\n | { ok: false; kind: 'forbidden' }\n /**\n * A source that should have vouched for an answer could not.\n *\n * Failed closed and never accepted unchecked: a bogus value that is stored is\n * undetectable afterwards, while a refusal is retryable and the draft still holds\n * the answers.\n */\n | { ok: false; kind: 'source_unavailable'; source: string }\n\n/**\n * Accept one submission: resolve the version the client says it rendered,\n * replay the engine over the submitted data, and store the CANONICAL result.\n *\n * The replay is the whole point. Computed values are recomputed and overwrite\n * whatever arrived; visibility is evaluated server-side and hidden branches\n * are stripped, so a client cannot smuggle data by lying about what was shown.\n * Client validation is UX; this is truth.\n */\n/**\n * Change who may submit a form, and from where.\n *\n * Separate from publishing on purpose: access is a property of the deployment,\n * and changing it must not mint a new immutable form version or invalidate the\n * schema hash every client is holding.\n */\nexport async function setFormAccess(\n deps: ServerDeps,\n input: { path: string; submit: 'authenticated' | 'public'; allowedOrigins?: string[] },\n): Promise<{ ok: boolean }> {\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false }\n\n await deps.storage.updateFormAccess(form.id, {\n accessSubmit: input.submit,\n allowedOrigins: input.allowedOrigins ?? null,\n })\n return { ok: true }\n}\n\n\nexport async function createSubmission(\n deps: ServerDeps,\n input: {\n path: string\n declaredSchemaHash: string\n data: unknown\n /** Defaults to anonymous: the caller must prove otherwise, not the reverse. */\n actor?: 'anonymous' | 'authenticated'\n origin?: string\n },\n): Promise<SubmissionOutcome> {\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n if (!maySubmit(form, input.actor ?? 'anonymous', input.origin)) {\n return { ok: false, kind: 'forbidden' }\n }\n\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return { ok: false, kind: 'unknown_form' }\n\n if (input.declaredSchemaHash !== current.schemaHash) {\n // The thin slice's staleVersionPolicy is `reject`: the acceptCompatible\n // policy arrives with draft migration, driven by diffSchemas severity.\n return { ok: false, kind: 'version_changed', current }\n }\n\n const engine = createFormEngine({\n schema: current.schema,\n initialValue: input.data,\n capabilities: deps.capabilities,\n // The side this replay is, which was never said. Without it the server ran\n // as a CLIENT: a `runsOn: \"server\"` rule was skipped in the one place it was\n // meant to run and a `runsOn: \"client\"` rule ran in the one place it was\n // meant not to — both halves of [0043] backwards, in the product, with\n // nothing reporting it.\n mode: 'server',\n ...(deps.checks === undefined ? {} : { checks: deps.checks }),\n })\n // Checks are asked by the engine and answered later. Awaiting before `submit`\n // is what makes the server the authority on them rather than a second opinion\n // that happened to be quick.\n await engine.settle()\n const outcome = engine.submit()\n if (!outcome.ok) return { ok: false, kind: 'invalid', errors: outcome.errors }\n\n // Asked AFTER the engine and BEFORE anything is written, so a refusal stores\n // nothing. The engine cannot do this itself: a `optionsSource` field has no\n // document options to compare against, which is exactly what naming a source\n // means.\n const membership = await checkMembership(current.schema, engine.value(), deps.optionsSources)\n if (!membership.ok) {\n return membership.kind === 'invalid'\n ? { ok: false, kind: 'invalid', errors: membership.errors }\n : { ok: false, kind: 'source_unavailable', source: membership.source }\n }\n\n const id = deps.newId()\n\n // Queued in the SAME call that stores the submission, so one COMMIT decides\n // both. Posting after the insert returns gives the two failures a\n // self-hoster cannot debug: the webhook fired and the submission rolled\n // back, or the submission is stored and nothing was ever sent.\n const hooks = await deps.storage.webhooksForForm(current.formId)\n // The CANONICAL value, not the request body: the receiver sees what was\n // stored, with computed fields recomputed and hidden branches stripped.\n const body = JSON.stringify({ id, form: input.path, data: engine.value() })\n const queued = hooks.map((hook) => ({\n id: deps.newId(),\n webhookId: hook.id,\n submissionId: id,\n // Stable for every retry of this delivery, which is what lets a receiver\n // dedupe. A fresh id per attempt would turn our retry into their duplicate.\n eventId: deps.newId(),\n body,\n attempt: 0,\n nextAttemptAt: deps.nowIso(),\n state: 'pending' as const,\n lastError: null,\n }))\n\n // Checked before the insert and claimed inside it. A submission naming a\n // file that is not hers is not a submission.\n const claimable = await filesToClaim(deps.storage, {\n schema: current.schema,\n formId: current.formId,\n data: engine.value(),\n })\n if (!claimable.ok) {\n return {\n ok: false,\n kind: 'invalid',\n errors: {\n _files: [`unknown_file:${claimable.unknown.join(',')}`],\n },\n }\n }\n\n const at = deps.nowIso()\n await deps.storage.insertSubmission(\n {\n id,\n formId: current.formId,\n formVersionId: current.versionId,\n data: engine.value(),\n submittedAt: at,\n },\n queued,\n claimable.ids,\n // In the transaction, not after it: a submission that rolled back must\n // leave nothing behind saying it happened, and an audit row for a\n // submission nobody can find is worse than no row at all.\n //\n // No actor id — this route accepts anonymous submissions, and who\n // submitted is the form's own data rather than the audit log's business.\n // `actor` says only whether anybody was signed in.\n {\n id: deps.newId(),\n at,\n action: 'submission.created',\n subject: input.path,\n // The version, so a reader can tell which schema this answered without\n // joining; the count, so an unusual submission is visible. Never the\n // answers.\n detail: {\n submissionId: id,\n version: current.version,\n authenticated: input.actor === 'authenticated',\n },\n },\n )\n return { ok: true, id, canonicalData: engine.value() }\n}\n\nexport interface ListedSubmission {\n id: string\n version: number\n submittedAt: string\n data: unknown\n}\n\n/** The submissions of one form, newest first, each tagged with the schema\n * version that produced it. Undefined for an unknown form: the caller must\n * 404, and an empty list would lie about that. */\nexport async function listSubmissions(\n deps: ServerDeps,\n path: string,\n): Promise<ListedSubmission[] | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n\n const versions = await deps.storage.listVersionsByForm(form.id)\n const versionNumberById = new Map(versions.map((version) => [version.id, version.version]))\n\n const records = await deps.storage.listSubmissionsByForm(form.id)\n return records.map((record) => ({\n id: record.id,\n version: versionNumberById.get(record.formVersionId) ?? 0,\n submittedAt: record.submittedAt,\n data: record.data,\n }))\n}\n\n/**\n * Every submission of a form as CSV, columns UNIONED across schema versions:\n * the current version's columns lead in its own order, and columns that only\n * exist in older versions follow — data outlives the field that collected it,\n * and an export that silently dropped extinct columns would lose it.\n *\n * A repeater is one column carrying its rows as JSON: rows-per-cell beats\n * exploding one submission across several CSV lines, which breaks every\n * spreadsheet join a business user will try.\n */\nexport async function exportCsv(deps: ServerDeps, path: string): Promise<string | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n\n const versions = await deps.storage.listVersionsByForm(form.id)\n const versionNumberById = new Map(versions.map((version) => [version.id, version.version]))\n\n const columns: string[] = []\n const seen = new Set<string>()\n for (const version of versions) {\n for (const column of dataColumns(version.schema)) {\n if (!seen.has(column)) {\n seen.add(column)\n columns.push(column)\n }\n }\n }\n\n const records = await deps.storage.listSubmissionsByForm(form.id)\n const lines = [['id', 'submittedAt', 'version', ...columns].map(csvCell).join(',')]\n for (const record of records) {\n const cells = [\n record.id,\n record.submittedAt,\n String(versionNumberById.get(record.formVersionId) ?? 0),\n ...columns.map((column) => cellValue(record.data, column)),\n ]\n lines.push(cells.map(csvCell).join(','))\n }\n return lines.join('\\n') + '\\n'\n}\n\n/** One column per collected answer: leaves as dotted paths through groups,\n * a repeater as a single column, pages transparent as always. */\nfunction dataColumns(schema: FormSchema): string[] {\n const columns: string[] = []\n const walk = (fields: readonly FormSchema['model']['fields'][number][], prefix: string): void => {\n for (const field of fields) {\n if (field.type === 'page') walk(field.fields ?? [], prefix)\n else if (field.type === 'group') walk(field.fields ?? [], `${prefix}${field.key}.`)\n else columns.push(`${prefix}${field.key}`)\n }\n }\n walk(schema.model.fields, '')\n return columns\n}\n\nfunction cellValue(data: unknown, column: string): string {\n let current: unknown = data\n for (const segment of column.split('.')) {\n if (current === null || typeof current !== 'object') return ''\n current = (current as Record<string, unknown>)[segment]\n }\n if (current === undefined || current === null) return ''\n if (typeof current === 'object') return JSON.stringify(current)\n if (typeof current === 'string' && FORMULA_STARTERS.has(current.charAt(0))) {\n // Spreadsheets execute a cell that starts like a formula. A submission is\n // attacker-controlled and this file is opened by exactly the person worth\n // attacking, so neutralise with the apostrophe convention. Only strings:\n // a number cannot smuggle a formula, and a quoted -5 would break every\n // numeric column.\n return \"'\" + current\n }\n return String(current)\n}\n\nfunction csvCell(value: string): string {\n return /[\",\\n\\r]/.test(value) ? `\"${value.replaceAll('\"', '\"\"')}\"` : value\n}\n\n/** Save (or re-save) a partial form, bound to the CURRENT version. Undefined\n * for an unknown form. */\n/**\n * The token that proves the bearer started this draft.\n *\n * HMAC over the form and the draft id, the same stateless shape the\n * proof-of-work challenge uses\n * ([0059](../../../docs/decisions/0059-proof-of-work-not-a-captcha.md)): no\n * second table, and no lookup before the check. Bound to the FORM as well as the\n * id so a token cannot be carried to a draft of another form that happens to\n * share an id.\n */\nfunction draftToken(secret: string, formId: string, draftId: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(`${formId}:${draftId}`)))\n}\n\n/**\n * Compared in constant time.\n *\n * A byte-at-a-time comparison that returns early leaks how much of a guess was\n * right, and a token is guessed one byte at a time by exactly that signal.\n */\nfunction sameToken(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let differences = 0\n for (let at = 0; at < a.length; at += 1) {\n differences |= a.charCodeAt(at) ^ b.charCodeAt(at)\n }\n return differences === 0\n}\n\n/**\n * Start a draft: the server picks the id, and signs it.\n *\n * The id is the server's because an id a caller supplies is an id a caller can\n * enumerate — and enumerating them was enough to read other people's\n * part-filled answers.\n */\nexport async function startDraft(\n deps: ServerDeps,\n input: { path: string },\n): Promise<{ draftId: string; token: string } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n const draftId = deps.newId()\n return { draftId, token: draftToken(deps.draftSecret, current.formId, draftId) }\n}\n\nexport async function saveDraft(\n deps: ServerDeps,\n input: { path: string; draftId: string; token: string; data: unknown },\n): Promise<{ version: number; saved: boolean } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n // Refused before the write, not after it. Overwriting is the worse half of\n // this: the person whose draft it is then submits the substituted content\n // under their own name, and nothing anywhere says otherwise.\n if (!sameToken(input.token, draftToken(deps.draftSecret, current.formId, input.draftId))) {\n return { version: current.version, saved: false }\n }\n\n await deps.storage.upsertDraft({\n id: input.draftId,\n formId: current.formId,\n formVersionId: current.versionId,\n data: input.data,\n updatedAt: deps.nowIso(),\n })\n return { version: current.version, saved: true }\n}\n\nexport interface DraftMigration {\n severity: 'lossy'\n changes: Change[]\n}\n\nexport type ResumeOutcome =\n | {\n outcome: 'resumed'\n version: number\n schema: FormSchema\n schemaHash: string\n data: unknown\n /** Present when the rebind lost something; absent for a silent rebind. */\n migration?: DraftMigration\n }\n | {\n /** The schema changed in a way no automatic rebind survives: the draft\n * comes back against ITS OWN version, and the caller offers a restart. */\n outcome: 'readOnly'\n version: number\n schema: FormSchema\n data: unknown\n }\n\n/**\n * Resume a draft — and migrate it LAZILY if the form was republished since,\n * which is versioning rule five: never migrate eagerly on publish, because\n * most drafts are abandoned and eager migration multiplies every publish by\n * every draft.\n *\n * diffSchemas drives the decision: `compatible` rebinds silently, `lossy`\n * rebinds with a report — declared renames carry their value to the new key,\n * removed fields move their value into `__orphaned` rather than deleting it —\n * and `breaking` refuses to rebind at all. A successful rebind is persisted,\n * so the migration runs once, not on every resume.\n */\nexport async function resumeDraft(\n deps: ServerDeps,\n input: { path: string; draftId: string; token: string },\n): Promise<ResumeOutcome | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n // Checked before the lookup, and answered identically to a draft that is not\n // there: replying differently would confirm which ids exist.\n if (!sameToken(input.token, draftToken(deps.draftSecret, current.formId, input.draftId))) {\n return undefined\n }\n\n const draft = await deps.storage.getDraft(current.formId, input.draftId)\n if (draft === undefined) return undefined\n\n if (draft.formVersionId === current.versionId) {\n return {\n outcome: 'resumed',\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n data: draft.data,\n }\n }\n\n const draftVersion = await deps.storage.getVersionById(draft.formVersionId)\n if (draftVersion === undefined) return undefined\n\n const changes = diffSchemas(draftVersion.schema, current.schema)\n const severity = changes.reduce<'compatible' | 'lossy' | 'breaking'>(\n (worst, change) =>\n SEVERITY_RANK[change.severity] > SEVERITY_RANK[worst] ? change.severity : worst,\n 'compatible',\n )\n\n if (severity === 'breaking') {\n return {\n outcome: 'readOnly',\n version: draftVersion.version,\n schema: draftVersion.schema,\n data: draft.data,\n }\n }\n\n const migrated = migrateDraftData(draft.data, current.schema, changes)\n\n await deps.storage.upsertDraft({\n id: draft.id,\n formId: draft.formId,\n formVersionId: current.versionId,\n data: migrated,\n updatedAt: deps.nowIso(),\n })\n\n return {\n outcome: 'resumed',\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n data: migrated,\n ...(severity === 'lossy' ? { migration: { severity, changes } } : {}),\n }\n}\n\nconst SEVERITY_RANK = { compatible: 0, lossy: 1, breaking: 2 } as const\n\nconst FORMULA_STARTERS = new Set(['=', '+', '-', '@', String.fromCharCode(9), String.fromCharCode(13)])\n\nfunction migrateDraftData(data: unknown, currentSchema: FormSchema, changes: Change[]): unknown {\n if (data === null || typeof data !== 'object' || Array.isArray(data)) return data\n const migrated: Record<string, unknown> = structuredCloneJson(data as Record<string, unknown>)\n\n // Declared renames first: the value follows the field to its new key.\n applyRenames(currentSchema.model.fields, migrated)\n\n // Then removals: the value moves aside instead of vanishing. Only top-level\n // and group paths move; row members stay inside their (possibly orphaned)\n // repeater value, which travels as a whole.\n const orphaned: Record<string, unknown> = {}\n for (const change of changes) {\n if (change.kind !== 'field.removed') continue\n const dataPath = change.path.replace(/^model\\.fields\\./, '')\n if (dataPath.includes('[]')) continue\n const value = takeAtDottedPath(migrated, dataPath)\n if (value !== undefined) orphaned[dataPath] = value\n }\n if (Object.keys(orphaned).length > 0) {\n migrated['__orphaned'] = {\n ...(migrated['__orphaned'] as Record<string, unknown> | undefined),\n ...orphaned,\n }\n }\n return migrated\n}\n\nfunction applyRenames(fields: FormSchema['model']['fields'], data: Record<string, unknown>): void {\n for (const field of fields) {\n if (field.type === 'page') {\n applyRenames(field.fields ?? [], data)\n continue\n }\n if (field.renamedFrom !== undefined && !(field.key in data) && field.renamedFrom in data) {\n data[field.key] = data[field.renamedFrom]\n delete data[field.renamedFrom]\n }\n if (field.type === 'group' && typeof data[field.key] === 'object' && data[field.key] !== null) {\n applyRenames(field.fields ?? [], data[field.key] as Record<string, unknown>)\n }\n }\n}\n\n/** Remove and return the value at a dotted path, pruning nothing else. */\nfunction takeAtDottedPath(data: Record<string, unknown>, dottedPath: string): unknown {\n const segments = dottedPath.split('.')\n let parent: Record<string, unknown> = data\n for (const segment of segments.slice(0, -1)) {\n const next = parent[segment]\n if (next === null || typeof next !== 'object' || Array.isArray(next)) return undefined\n parent = next as Record<string, unknown>\n }\n const last = segments[segments.length - 1]!\n const value = parent[last]\n delete parent[last]\n return value\n}\n\n/** JSON-safe deep clone: draft data is JSON by construction, and this package\n * runs without structuredClone in its lib on purpose. */\nfunction structuredCloneJson<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport interface ListedForm {\n path: string\n title: string\n version: number\n schemaHash: string\n}\n\n/** Every form with its current version — the admin's landing view. */\nexport async function listForms(deps: ServerDeps): Promise<ListedForm[]> {\n const forms = await deps.storage.listForms()\n const listed: ListedForm[] = []\n for (const form of forms) {\n if (form.currentVersionId === null) continue\n const current = await deps.storage.getVersionById(form.currentVersionId)\n if (current === undefined) continue\n listed.push({\n path: form.path,\n title: current.schema.title,\n version: current.version,\n schemaHash: current.schemaHash,\n })\n }\n return listed\n}\n\nexport interface ListedVersion {\n version: number\n schemaHash: string\n title: string\n}\n\n/** A form's version history, newest first — metadata only, because a history\n * view rendering fifty full schemas would be its own denial of service. */\nexport async function listVersions(\n deps: ServerDeps,\n path: string,\n): Promise<ListedVersion[] | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n const versions = await deps.storage.listVersionsByForm(form.id)\n return versions.map((version) => ({\n version: version.version,\n schemaHash: version.schemaHash,\n title: version.schema.title,\n }))\n}\n","import { check } from 'recheck'\nimport type { FieldDef, FormSchema } from '@formancy/spec'\n\n/**\n * Refuse a form whose `pattern` values can be made to backtrack.\n *\n * A form author supplies these, and the server runs them against whatever a\n * submitter typed. That makes a regular expression an author-side denial of\n * service vector: one bad pattern, published once, and every submission after\n * it can be turned into unbounded CPU by anyone who can reach the form.\n *\n * Checked at PUBLISH time rather than at match time, because there is no way\n * to time out a JavaScript regular expression once it has started. The only\n * moment the cost can be refused is before the pattern is stored.\n *\n * The check is static — `recheck` analyses the expression rather than running\n * it — so it costs nothing per submission, and it found a polynomial case in\n * formancy's own built-in email format before it was ever pointed at a user's.\n */\n\n// Which engine recheck uses is chosen by the HOST, not here: this package has\n// no @types/node by design, so it cannot read an environment variable, and\n// that boundary is worth more than the convenience of setting one.\n// `@formancy/server` pins `RECHECK_BACKEND=pure` at startup and says why.\n// Absent that, recheck picks for itself and falls back to the pure engine,\n// which reaches the same verdicts on every case in the test suite.\n\nexport interface UnsafePattern {\n /** Data path of the field carrying it, e.g. `contacts[].code`. */\n path: string\n pattern: string\n /** `exponential` or `polynomial`, with the degree where there is one. */\n complexity: string\n}\n\n/** Every `pattern` in the model, with the data path of the field it sits on. */\nfunction patternsIn(fields: readonly FieldDef[], prefix: string): Array<{ path: string; pattern: string }> {\n const found: Array<{ path: string; pattern: string }> = []\n\n for (const field of fields) {\n // Pages are transparent to data paths; groups nest; a repeater's template\n // fields are addressed with `[]`. Same walk as everything else.\n const path =\n field.type === 'page'\n ? prefix\n : field.type === 'repeater'\n ? `${prefix}${field.key}[].`\n : field.type === 'group'\n ? `${prefix}${field.key}.`\n : `${prefix}${field.key}`\n\n if (typeof field.pattern === 'string' && field.pattern !== '') {\n found.push({ path, pattern: field.pattern })\n }\n if (field.fields !== undefined) found.push(...patternsIn(field.fields, path))\n }\n\n return found\n}\n\n/**\n * The patterns in this schema that an attacker could exploit, or an empty array\n * when there are none.\n *\n * A pattern `recheck` cannot decide about is ACCEPTED. Refusing on `unknown`\n * would make publishing depend on an analysis timeout, which is a worse failure\n * than the one being prevented — an author whose legitimate form stops\n * publishing intermittently has no way to act on that.\n */\nexport async function unsafePatterns(schema: FormSchema): Promise<UnsafePattern[]> {\n const unsafe: UnsafePattern[] = []\n\n for (const { path, pattern } of patternsIn(schema.model.fields, '')) {\n // Anchored the same way the engine anchors it, so what is analysed is what\n // will run. An unanchored analysis can miss a case the anchors create.\n const diagnostics = await check(`^(?:${pattern})$`, '')\n if (diagnostics.status !== 'vulnerable') continue\n\n const complexity = diagnostics.complexity\n unsafe.push({\n path,\n pattern,\n complexity:\n complexity === undefined\n ? 'unknown'\n : complexity.type === 'polynomial'\n ? `polynomial degree ${String(complexity.degree)}`\n : complexity.type,\n })\n }\n\n return unsafe\n}\n","import type { AuditEntry } from '../audit.js'\nimport type {\n DeliveryRecord,\n WebhookRecord, ApiKeyRecord, DraftRecord, FileRecord, FormRecord, FormVersionRecord, Storage, SubmissionRecord, UserRecord } from '../ports.js'\n\n/**\n * The in-memory Storage — the second implementation that keeps the port\n * honest, and the one unit tests run against. Deliberately dumb: Maps and\n * scans, no cleverness to accidentally diverge from SQL semantics.\n */\nexport function createMemoryStorage(): Storage {\n const forms = new Map<string, FormRecord>()\n const webhooks = new Map<string, WebhookRecord>()\n const deliveries = new Map<string, DeliveryRecord>()\n const versions = new Map<string, FormVersionRecord>()\n const submissions: SubmissionRecord[] = []\n const audits: AuditEntry[] = []\n const spentChallenges = new Map<string, string>()\n const drafts = new Map<string, DraftRecord>()\n const users = new Map<string, UserRecord>()\n const apiKeys = new Map<string, ApiKeyRecord>()\n const files = new Map<string, FileRecord>()\n\n return {\n getFormByPath: async (path) => [...forms.values()].find((form) => form.path === path),\n\n listForms: async () => [...forms.values()].map((form) => ({ ...form })),\n\n updateFormAccess: async (formId, access) => {\n const form = forms.get(formId)\n if (form !== undefined) forms.set(formId, { ...form, ...access })\n },\n\n createForm: async (record) => {\n forms.set(record.id, { ...record })\n },\n\n setCurrentVersion: async (formId, versionId) => {\n const form = forms.get(formId)\n if (form === undefined) throw new Error(`No form \"${formId}\"`)\n form.currentVersionId = versionId\n },\n\n insertVersion: async (record) => {\n versions.set(record.id, { ...record })\n },\n\n publishVersion: async ({ form, version, audit }) => {\n // One step, like the transaction it stands in for. A form whose pointer\n // was never set resolves to nothing, so all of this lands or none does.\n if (form !== undefined) forms.set(form.id, { ...form })\n const target = forms.get(version.formId)\n if (target === undefined) throw new Error(`No form \"${version.formId}\"`)\n versions.set(version.id, { ...version })\n target.currentVersionId = version.id\n if (audit !== undefined) audits.push({ ...audit })\n },\n\n getVersionById: async (id) => versions.get(id),\n\n findVersionByHash: async (formId, hash) =>\n [...versions.values()].find(\n (version) => version.formId === formId && version.schemaHash === hash,\n ),\n\n latestVersionNumber: async (formId) =>\n Math.max(\n 0,\n ...[...versions.values()]\n .filter((version) => version.formId === formId)\n .map((version) => version.version),\n ),\n\n webhooksForForm: async (formId) =>\n [...webhooks.values()].filter((hook) => hook.formId === formId).map((hook) => ({ ...hook })),\n\n insertWebhook: async (record) => {\n webhooks.set(record.id, { ...record })\n },\n\n updateWebhook: async (record) => {\n webhooks.set(record.id, { ...record })\n },\n\n listWebhooks: async () => [...webhooks.values()].map((w) => ({ ...w })),\n\n deadDeliveries: async (limit) =>\n [...deliveries.values()]\n .filter((delivery) => delivery.state === 'dead')\n .slice(0, limit)\n .map((delivery) => ({ ...delivery })),\n\n getDelivery: async (id) => {\n const found = deliveries.get(id)\n return found === undefined ? undefined : { ...found }\n },\n\n claimDueDeliveries: async (nowIso, limit) =>\n [...deliveries.values()]\n .filter((entry) => entry.state === 'pending' && entry.nextAttemptAt <= nowIso)\n .sort((a, b) => a.nextAttemptAt.localeCompare(b.nextAttemptAt))\n .slice(0, limit)\n .map((entry) => ({ ...entry })),\n\n updateDelivery: async (record) => {\n deliveries.set(record.id, { ...record })\n },\n\n insertFile: async (record) => {\n files.set(record.id, { ...record })\n },\n\n getFile: async (id) => {\n const found = files.get(id)\n return found === undefined ? undefined : { ...found }\n },\n\n updateFile: async (record) => {\n files.set(record.id, { ...record })\n },\n\n // A claimed file belongs to a submission and is never rubbish, however\n // old. Age is a reason to collect an unclaimed one, never a claimed one.\n abandonedFiles: async (beforeIso) =>\n [...files.values()]\n .filter((file) => file.state !== 'claimed' && file.createdAt < beforeIso)\n .map((file) => ({ ...file })),\n\n deleteFiles: async (ids) => {\n for (const id of ids) files.delete(id)\n },\n\n insertSubmission: async (record, queued, claimFileIds, audit) => {\n // One step, like the real transaction it stands in for: both land or\n // neither does.\n submissions.push({ ...record })\n for (const delivery of queued ?? []) deliveries.set(delivery.id, { ...delivery })\n for (const id of claimFileIds ?? []) {\n const file = files.get(id)\n // Same call, same commit: a submission exists if and only if the\n // files it names belong to it.\n if (file !== undefined) {\n files.set(id, { ...file, state: 'claimed', submissionId: record.id })\n }\n }\n // Same commit again: a submission that rolled back must leave no trace\n // saying it happened.\n if (audit !== undefined) audits.push({ ...audit })\n },\n\n spendChallenge: async (challenge, expiresAtIso) => {\n // One map operation, standing in for the unique constraint the real\n // storage relies on: both callers cannot win.\n if (spentChallenges.has(challenge)) return false\n spentChallenges.set(challenge, expiresAtIso)\n return true\n },\n\n forgetExpiredChallenges: async (beforeIso) => {\n let dropped = 0\n for (const [challenge, expires] of [...spentChallenges]) {\n if (expires < beforeIso) {\n spentChallenges.delete(challenge)\n dropped += 1\n }\n }\n return dropped\n },\n\n recordAudit: async (entry) => {\n audits.push({ ...entry })\n },\n\n listAudit: async (limit) => [...audits].reverse().slice(0, limit),\n\n listSubmissions: async () => [...submissions],\n\n listSubmissionsByForm: async (formId) =>\n submissions.filter((record) => record.formId === formId).reverse(),\n\n listVersionsByForm: async (formId) =>\n [...versions.values()]\n .filter((version) => version.formId === formId)\n .sort((a, b) => b.version - a.version),\n\n upsertDraft: async (record) => {\n drafts.set(record.formId + ':' + record.id, { ...record })\n },\n\n getDraft: async (formId, draftId) => drafts.get(formId + ':' + draftId),\n\n insertUser: async (record) => {\n users.set(record.id, { ...record })\n },\n\n getUserByEmail: async (email) =>\n [...users.values()].find((user) => user.email === email),\n\n insertApiKey: async (record) => {\n apiKeys.set(record.id, { ...record })\n },\n\n listApiKeys: async () => [...apiKeys.values()].map((key) => ({ ...key })),\n\n findApiKeysByPrefix: async (prefix) =>\n [...apiKeys.values()].filter((key) => key.prefix === prefix),\n\n revokeApiKey: async (id, atIso) => {\n const key = apiKeys.get(id)\n if (key !== undefined) key.revokedAt = atIso\n },\n }\n}\n","import type { Storage } from './ports.js'\n\n/**\n * The management plane's identity and authorization, framework-free.\n *\n * Hashing and randomness are injected — this package cannot reach a CSPRNG or\n * argon2 on its own, the composition root hands them in — which keeps every\n * behaviour here testable with deterministic fakes while the real server gets\n * real cryptography.\n */\nexport type Role = 'admin' | 'editor' | 'viewer'\n\nexport interface Actor {\n kind: 'user' | 'apiKey'\n id: string\n role: Role\n}\n\nexport interface AuthDeps {\n storage: Storage\n newId(): string\n nowIso(): string\n /** At least 160 bits of entropy, URL-safe. Injected, never Math.random. */\n randomToken(): string\n hashSecret(secret: string): Promise<string>\n verifySecret(secret: string, hash: string): Promise<boolean>\n}\n\nconst MIN_PASSWORD_LENGTH = 10\n\nexport type CreateUserOutcome =\n | { ok: true; id: string }\n | { ok: false; message: string }\n\nexport async function createLocalUser(\n deps: AuthDeps,\n input: { email: string; password: string; role: Role },\n): Promise<CreateUserOutcome> {\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n return { ok: false, message: `A password needs at least ${MIN_PASSWORD_LENGTH} characters.` }\n }\n if ((await deps.storage.getUserByEmail(input.email)) !== undefined) {\n return { ok: false, message: 'That email is already in use.' }\n }\n\n const id = deps.newId()\n await deps.storage.insertUser({\n id,\n email: input.email,\n passwordHash: await deps.hashSecret(input.password),\n role: input.role,\n createdAt: deps.nowIso(),\n })\n return { ok: true, id }\n}\n\nexport type AuthOutcome = { ok: true; actor: Actor } | { ok: false }\n\n/**\n * A wrong password and an unknown email answer identically, and BOTH run a\n * hash verification — the difference in shape or timing would be a\n * user-enumeration oracle.\n */\nexport async function authenticateLocal(\n deps: AuthDeps,\n input: { email: string; password: string },\n): Promise<AuthOutcome> {\n const user = await deps.storage.getUserByEmail(input.email)\n const hash = user?.passwordHash ?? (await deps.hashSecret(`decoy:${deps.newId()}`))\n const verified = await deps.verifySecret(input.password, hash)\n if (user === undefined || !verified) return { ok: false }\n return { ok: true, actor: { kind: 'user', id: user.id, role: user.role } }\n}\n\n/** The visible prefix of an API key: enough to index and to name in a UI\n * (\"fmc_tok_a2c…\"), never enough to authenticate. */\nconst KEY_PREFIX_LENGTH = 12\n\nexport type CreateApiKeyOutcome =\n | { ok: true; id: string; secret: string }\n | { ok: false; message: string }\n\nexport async function createApiKey(\n deps: AuthDeps,\n input: { name: string; role: Role },\n): Promise<CreateApiKeyOutcome> {\n if (input.name.trim() === '') return { ok: false, message: 'An API key needs a name.' }\n\n // The secret exists in full exactly once — in this return value. Storage\n // holds the prefix (for lookup and display) and a hash (for verification).\n const secret = `fmc_${deps.randomToken()}`\n const id = deps.newId()\n await deps.storage.insertApiKey({\n id,\n name: input.name,\n prefix: secret.slice(0, KEY_PREFIX_LENGTH),\n secretHash: await deps.hashSecret(secret),\n role: input.role,\n createdAt: deps.nowIso(),\n revokedAt: null,\n })\n return { ok: true, id, secret }\n}\n\nexport async function authenticateApiKey(deps: AuthDeps, presented: string): Promise<AuthOutcome> {\n const candidates = await deps.storage.findApiKeysByPrefix(presented.slice(0, KEY_PREFIX_LENGTH))\n for (const candidate of candidates) {\n if (candidate.revokedAt !== null) continue\n if (await deps.verifySecret(presented, candidate.secretHash)) {\n return { ok: true, actor: { kind: 'apiKey', id: candidate.id, role: candidate.role } }\n }\n }\n return { ok: false }\n}\n\n/**\n * The whole authorization model, in one table a reviewer can read top to\n * bottom. Deliberately NOT Postgres RLS in this phase: RLS couples the app to\n * per-request database roles and hides authorization from the test suite.\n * An unknown action is denied — permissions are granted by presence, never by\n * omission.\n */\nexport type Action =\n | 'form.read'\n | 'form.publish'\n | 'submission.read'\n | 'submission.export'\n | 'draft.write'\n | 'user.create'\n | 'apiKey.create'\n\nconst VIEWER_ACTIONS: readonly Action[] = ['form.read', 'submission.read']\nconst EDITOR_ACTIONS: readonly Action[] = [\n ...VIEWER_ACTIONS,\n 'form.publish',\n 'submission.export',\n 'draft.write',\n]\nconst ADMIN_ACTIONS: readonly Action[] = [...EDITOR_ACTIONS, 'user.create', 'apiKey.create']\n\nconst PERMISSIONS: Record<Role, ReadonlySet<Action>> = {\n viewer: new Set(VIEWER_ACTIONS),\n editor: new Set(EDITOR_ACTIONS),\n admin: new Set(ADMIN_ACTIONS),\n}\n\nexport function can(actor: Actor, action: Action): boolean {\n return PERMISSIONS[actor.role].has(action)\n}\n","/**\n * Deciding whether an address is somewhere this server may fetch from.\n *\n * A self-hosted formancy sits inside somebody's private network and webhook\n * URLs are written by whoever can edit a form, which makes the server a\n * confused deputy: it can reach the database, the metadata service and every\n * internal admin panel, and it will fetch whatever it is told to.\n *\n * This half is pure — given an address, is it private — so it lives here and is\n * tested exhaustively. The rest lives in @formancy/server, because it cannot be\n * pure: parsing a URL needs `URL`, and resolving a hostname then connecting to\n * the address that was checked, rather than re-resolving, needs Node's DNS and\n * a custom agent. This package has no @types/node by design, and hand-rolling\n * a URL parser for a security check is how such a check gets quietly wrong.\n * All three parts are necessary. Validating a URL and then handing it to fetch is defeated by DNS\n * rebinding — the name resolves to a public address for the check and a private\n * one for the request — and that is the difference between a mitigation and\n * theatre.\n */\n\n/** Everything before the first dot, as four octets, or undefined. */\nfunction ipv4Octets(address: string): [number, number, number, number] | undefined {\n const parts = address.split('.')\n if (parts.length !== 4) return undefined\n\n const octets = parts.map((part) => {\n // Reject '', '1e2', '0x7f', ' 1' and leading zeros that some parsers read\n // as octal. Only plain decimal counts.\n if (!/^\\d{1,3}$/.test(part)) return Number.NaN\n return Number(part)\n })\n\n if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) return undefined\n return octets as [number, number, number, number]\n}\n\n/**\n * Whether this address is one the server must not be pointed at.\n *\n * Unparseable input counts as private. An address this cannot classify is one\n * it cannot vouch for, and the safe direction is refusing to fetch.\n */\nexport function isPrivateAddress(address: string): boolean {\n const trimmed = address.trim().toLowerCase()\n if (trimmed === '') return true\n\n // Strip a zone index (`fe80::1%eth0`) and brackets (`[::1]`).\n const bare = trimmed.replace(/^\\[|\\]$/g, '').split('%')[0] ?? ''\n\n // ::ffff:127.0.0.1 is loopback wearing a hat. Unwrap before classifying, or\n // every private range walks straight through the IPv6 branch.\n const mapped = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(bare)\n if (mapped?.[1] !== undefined) return isPrivateAddress(mapped[1])\n\n const octets = ipv4Octets(bare)\n if (octets !== undefined) {\n const [a, b] = octets\n if (a === 0) return true // \"this network\" — 0.0.0.0 is a local wildcard\n if (a === 10) return true // RFC1918\n if (a === 127) return true // loopback\n if (a === 169 && b === 254) return true // link-local, including 169.254.169.254\n if (a === 172 && b >= 16 && b <= 31) return true // RFC1918\n if (a === 192 && b === 168) return true // RFC1918\n if (a === 100 && b >= 64 && b <= 127) return true // carrier-grade NAT\n if (a === 192 && b === 0) return true // IETF protocol assignments / test nets\n if (a === 198 && (b === 18 || b === 19)) return true // benchmarking\n if (a >= 224) return true // multicast and reserved, up to 255.255.255.255\n return false\n }\n\n if (bare.includes(':')) {\n if (bare === '::' || bare === '::1') return true\n // fc00::/7 unique-local, fe80::/10 link-local.\n if (/^f[cd]/.test(bare)) return true\n if (/^fe[89ab]/.test(bare)) return true\n if (/^ff/.test(bare)) return true // multicast\n return false\n }\n\n // Not an address at all — a hostname, or nonsense. The caller resolves\n // hostnames and checks what comes back; anything else fails closed.\n return true\n}\n\n/**\n * Whether this is a literal IP address rather than a hostname.\n *\n * The two need different treatment and conflating them breaks the check in\n * both directions: `isPrivateAddress` fails closed on anything it cannot\n * parse, which is correct for an address that came back from DNS and wrong for\n * `example.ch`, where it would refuse every hostname in the world.\n */\nexport function isIpLiteral(host: string): boolean {\n const bare = host.trim().replace(/^\\[|\\]$/g, '')\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(bare)) return true\n // Anything with a colon is IPv6; a hostname cannot contain one.\n return bare.includes(':')\n}\n","import { schemaHash } from '@formancy/spec'\nimport type { FormSchema } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { SchemaError } from '@formancy/spec/validate'\nimport type { ResolvedForm, ServerDeps } from './use-cases.js'\nimport { createFormEngine, expressionProblems, unknownReferences } from '@formancy/core'\nimport { sourceNamesIn } from './options-membership.js'\nimport type { AuditEntry } from './audit.js'\nimport type { Actor } from './auth.js'\nimport { unsafePatterns } from './redos.js'\nimport type { UnsafePattern } from './redos.js'\n\n\n/**\n * Publishing a form: the save gate, and who may overwrite whom.\n *\n * Its own file because `use-cases.ts` had become the place every family went,\n * and the size budget refused the next thing added to it — the entry already\n * said the seam was one use-case family per file, and this is the family.\n */\nexport type PublishOutcome =\n | {\n ok: true\n formId: string\n versionId: string\n version: number\n schemaHash: string\n /**\n * Things worth telling the publisher that are not grounds to refuse.\n *\n * Always present, empty when there is nothing to say, so a caller reads a\n * list rather than branching on whether there is one. Sentences rather\n * than codes: the only consumers are a person reading a CLI, an admin\n * screen or a log, and a code would need a catalogue nobody would write\n * ([0097](../../../docs/decisions/0097-a-publish-may-warn.md)).\n */\n warnings: readonly string[]\n }\n | { ok: false; kind: 'invalid_schema'; errors?: SchemaError[] }\n | { ok: false; kind: 'invalid_logic'; message: string }\n | { ok: false; kind: 'unsafe_pattern'; patterns: UnsafePattern[] }\n /** The document names a list this deployment has never heard of. */\n | { ok: false; kind: 'unknown_options_source'; sources: string[] }\n /**\n * Somebody else published while this editor was editing.\n *\n * The same kind and the same body the submission path uses, deliberately: a\n * client that already knows how to handle one stale version handles both.\n */\n | { ok: false; kind: 'version_changed'; current?: ResolvedForm }\n /**\n * This exact document is already a version of this form, and not the current\n * one — somebody republishing an older version to undo a bad publish.\n *\n * `UNIQUE (form_id, schema_hash)` is what makes republishing the CURRENT\n * document idempotent rather than a version factory, and it refuses this too:\n * without this branch the insert raised and the route answered **500**. A\n * published version is immutable and cannot be published twice\n * ([0025](../../../docs/decisions/0025-immutability-in-the-database.md)), so\n * the honest answer names the version it already is.\n */\n | { ok: false; kind: 'already_published'; version: number }\n\n/**\n * Publish a schema as a form's next version.\n *\n * This is the save gate: structural validation, then the ENGINE's own compile\n * — type-checked expressions, kind policies and the cycle graph — so a form\n * that can loop or read a ghost field is refused here and never persisted.\n * Republishing the identical document is idempotent, because \"deploy again\"\n * must never manufacture a version.\n */\nexport async function publishForm(\n deps: ServerDeps,\n input: {\n path: string\n schema: unknown\n actor?: Actor\n /**\n * The hash of the version this edit started from, when there was one.\n *\n * **Optional, and that is the decision.** A script, the CLI and an agent\n * publish a document they composed rather than one they opened, and have\n * nothing to declare; requiring it would make every one of them read the\n * current version first to satisfy a rule about editors. What declares is\n * the thing that OPENED a version — the builder — and declaring is how it\n * asks to be told it has been overtaken.\n */\n basedOnSchemaHash?: string\n },\n): Promise<PublishOutcome> {\n const validated = validateSchema(input.schema)\n if (!validated.valid) return { ok: false, kind: 'invalid_schema', errors: validated.errors }\n const schema: FormSchema = validated.schema\n\n try {\n createFormEngine({ schema, capabilities: deps.capabilities })\n } catch (error) {\n return {\n ok: false,\n kind: 'invalid_logic',\n message: error instanceof Error ? error.message : String(error),\n }\n }\n\n // Expressions that compile and then fail for every value anybody enters.\n // The engine cannot refuse these — it declares leaves as `dyn` so that a\n // half-typed answer is not a type error, which also makes `seats * 4` look\n // fine until it runs. Refused here rather than at render, so a form already\n // published with the mistake keeps opening for whoever is filling it in.\n const problems = expressionProblems(schema)\n if (problems.length > 0) {\n return {\n ok: false,\n kind: 'invalid_logic',\n message: problems.map((problem) => problem.message).join('\\n'),\n }\n }\n\n /*\n * And the ones that are worth saying rather than refusing.\n *\n * A rule reading a path no field provides is a real defect — it evaluates to\n * nothing for the life of an immutable version — and refusing it is not\n * available: documents valid today would become invalid tomorrow, and what a\n * reader accepts is the frozen version contract. Computed before the storage\n * branches below so that every successful return carries the same answer,\n * including the idempotent one.\n */\n const warnings = unknownReferences(schema).map((found) => found.message)\n\n // Before the schema is persisted, because after it is there is no way to\n // time out a regular expression that has already started matching.\n const unsafe = await unsafePatterns(schema)\n if (unsafe.length > 0) {\n return {\n ok: false,\n kind: 'unsafe_pattern',\n patterns: unsafe,\n }\n }\n\n // The source names a document uses, checked against the vocabulary this deployment\n // actually has — but only when it HAS one. With no `optionsSources` configured the\n // server cannot judge, so publishing is allowed and the audit detail records the\n // names, which is what lets an operator compare them with their configuration.\n //\n // Caught here rather than at the first submission, because a published version is\n // frozen forever: a form naming a list nobody can resolve is a form whose select\n // renders a message instead of a chooser, and nothing would have said so.\n if (deps.optionsSources !== undefined) {\n const unknown = sourceNamesIn(schema).filter((name) => deps.optionsSources?.[name] === undefined)\n if (unknown.length > 0) return { ok: false, kind: 'unknown_options_source', sources: unknown }\n }\n\n const hash = schemaHash(schema)\n const existing = await deps.storage.getFormByPath(input.path)\n\n if (existing !== undefined) {\n const current =\n existing.currentVersionId === null\n ? undefined\n : await deps.storage.getVersionById(existing.currentVersionId)\n if (current !== undefined && current.schemaHash === hash) {\n return {\n ok: true,\n formId: existing.id,\n versionId: current.id,\n version: current.version,\n schemaHash: hash,\n warnings,\n }\n }\n\n // AFTER the idempotent case on purpose. An editor whose document already\n // matches what is published has nothing to merge — somebody else wrote\n // exactly what they were going to write — and telling them to resolve a\n // conflict with themselves would be a worse answer than silence.\n if (input.basedOnSchemaHash !== undefined && input.basedOnSchemaHash !== current?.schemaHash) {\n return {\n ok: false,\n kind: 'version_changed',\n ...(current === undefined\n ? {}\n : {\n current: {\n formId: existing.id,\n versionId: current.id,\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n },\n }),\n }\n }\n\n // An older version of this form already holds this exact document. The\n // unique index would raise on the insert and the route answered 500 —\n // reverting by republishing yesterday's document is a reasonable thing to\n // try, and it crashed.\n const already = (await deps.storage.listVersionsByForm(existing.id)).find(\n (candidate) => candidate.schemaHash === hash,\n )\n if (already !== undefined) {\n return { ok: false, kind: 'already_published', version: already.version }\n }\n\n const version = (await deps.storage.latestVersionNumber(existing.id)) + 1\n const versionId = deps.newId()\n await deps.storage.publishVersion({\n version: { id: versionId, formId: existing.id, version, schema, schemaHash: hash },\n audit: publishAudit(deps, input, {\n path: input.path,\n version,\n hash,\n sources: sourceNamesIn(schema),\n }),\n })\n return { ok: true, formId: existing.id, versionId, version, schemaHash: hash, warnings }\n }\n\n // A base declared for a form that has none is an editor working from something\n // this deployment never published — a path that was deleted, or the wrong\n // instance. Publishing anyway would make their document version 1 of a form\n // they believe already has a history.\n if (input.basedOnSchemaHash !== undefined) {\n return { ok: false, kind: 'version_changed' }\n }\n\n const formId = deps.newId()\n const versionId = deps.newId()\n await deps.storage.publishVersion({\n form: {\n id: formId,\n path: input.path,\n currentVersionId: null,\n // A newly published form is private. Opening it is a separate,\n // deliberate act through setFormAccess.\n accessSubmit: 'authenticated',\n allowedOrigins: null,\n },\n version: { id: versionId, formId, version: 1, schema, schemaHash: hash },\n audit: publishAudit(deps, input, {\n path: input.path,\n version: 1,\n hash,\n sources: sourceNamesIn(schema),\n }),\n })\n return { ok: true, formId, versionId, version: 1, schemaHash: hash, warnings }\n}\n\n/**\n * The audit row for a publish, built here so it can travel INSIDE the\n * transaction rather than being appended after it.\n *\n * Before this it was written by the route once all three storage calls had\n * returned, which meant a publish could half-apply and still be recorded as\n * having happened. Now it rolls back with everything else.\n */\nfunction publishAudit(\n deps: ServerDeps,\n input: { actor?: Actor },\n about: { path: string; version: number; hash: string; sources: readonly string[] },\n): AuditEntry {\n return {\n id: deps.newId(),\n at: deps.nowIso(),\n action: 'form.published',\n subject: about.path,\n ...(input.actor === undefined\n ? {}\n : { actorKind: input.actor.kind, actorId: input.actor.id }),\n detail: {\n version: about.version,\n schemaHash: about.hash,\n /*\n * The lists this version needs the deployment to resolve, named in the record that\n * freezes it -- and only when there are any, so an ordinary form's audit row does\n * not grow a field saying \"none\".\n *\n * `SAFETY-ANALYSIS.md` A7 tells a manufacturer to treat `optionsSource` as a\n * per-field weakening of A6's guarantee, \"enumerable from the publish audit\n * detail\". It was not: this recorded the version and the hash and nothing else, so\n * the document described a record that did not exist. Written here rather than the\n * sentence being softened, because an operator comparing a form's sources with\n * their own configuration is exactly the use the sentence names.\n *\n * Joined into a string because an audit detail is flat by design -- one level, no\n * nesting, so a reader never has to walk it.\n */\n ...(about.sources.length === 0 ? {} : { optionsSources: [...about.sources].sort().join(', ') }),\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/options-membership.ts","../src/audit.ts","../src/webhook.ts","../src/breaker.ts","../src/outbox.ts","../src/access.ts","../src/uploads.ts","../src/use-cases.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/auth.ts","../src/address.ts","../src/publishing.ts"],"sourcesContent":["import type { FieldDef, FormSchema } from '@formancy/spec'\n\n/**\n * Checking that a sourced answer is one the deployment's own source offers.\n *\n * The engine already refuses a value no *document* option offers\n * ([0076](../../../docs/decisions/0076-an-answer-is-one-of-the-options.md)), and\n * it runs on both sides. A `optionsSource` field has no document options, so that\n * check has nothing to compare against — deliberately, because a list living\n * outside the document cannot be checked against the document.\n *\n * So the server asks the deployment instead, and the deployment answers with a\n * database query rather than with a request to an address a form author typed.\n * That is why the port takes a function and this package ships no HTTP client:\n * `@formancy/server-core` imports no `URL` and no `fetch`, and adding an outbound\n * request here would add confused-deputy surface to a regulatory set that claims\n * none.\n */\n\n/** One named source, as the deployment configured it. */\nexport interface ServerOptionsSource {\n /**\n * Which of these values this source does NOT offer.\n *\n * Phrased as the rejects rather than the members so an adapter can answer from a\n * SQL `IN` clause without materialising a list — a source with two million rows\n * is exactly the case `optionsSource` exists for.\n *\n * Absent means: *this source exists, and I cannot check membership.* That is a\n * legitimate configuration and it weakens the guarantee, which is why the\n * guarantee is written down rather than implied.\n */\n members?(values: readonly string[]): Promise<readonly string[]>\n}\n\nexport type ServerOptionsSources = Readonly<Record<string, ServerOptionsSource>>\n\n/**\n * Every sourced answer in a submission, as `path → { source, value }`.\n *\n * Walked from the SCHEMA over the value, never from the value's own shape — the\n * same way `referencedFileIds` does it. A walk driven by what arrived would let a\n * client dodge the check by omitting a property, and hidden branches have already\n * been stripped by the engine before this runs.\n */\nexport function sourcedAnswers(\n schema: FormSchema,\n value: unknown,\n): ReadonlyArray<{ path: string; source: string; value: string }> {\n const found: Array<{ path: string; source: string; value: string }> = []\n\n const walk = (fields: readonly FieldDef[], data: unknown, prefix: string): void => {\n if (typeof data !== 'object' || data === null) return\n const record = data as Record<string, unknown>\n\n for (const field of fields) {\n const at = prefix === '' ? field.key : `${prefix}.${field.key}`\n const held = record[field.key]\n\n if (field.fields !== undefined) {\n // A repeater's rows are a list; a group's children are one object.\n if (Array.isArray(held)) {\n for (const [index, row] of held.entries()) {\n walk(field.fields, row, `${at}[${String(index)}]`)\n }\n } else {\n walk(field.fields, held, at)\n }\n continue\n }\n\n if (field.optionsSource === undefined) continue\n\n // A list answer is several answers to one question. This asked\n // `typeof held !== 'string'` and an array is not a string, so every value\n // in a sourced `selectboxes` was stored with nothing having looked at it —\n // hazard A7 wearing a different shape of answer.\n //\n // One entry per VALUE and the field's own path on each: the field is wrong\n // when any of its answers is, and naming an index would describe a payload\n // rather than the question.\n const answers = Array.isArray(held) ? held : [held]\n for (const answer of answers) {\n // An empty answer is `required`'s business, not this one's, and a\n // non-string is `modelViolations`' — a source asked about `{\"$gt\": \"\"}`\n // is a source handed a query it did not expect.\n if (typeof answer !== 'string' || answer === '') continue\n found.push({ path: at, source: field.optionsSource, value: answer })\n }\n }\n }\n\n walk(schema.model.fields, value, '')\n return found\n}\n\n/** What asking the deployment produced. */\nexport type MembershipOutcome =\n | { ok: true }\n /** Values the deployment's own source does not offer, by field path. */\n | { ok: false; kind: 'invalid'; errors: Record<string, string[]> }\n /**\n * A source could not answer.\n *\n * Failed CLOSED, always. An accepted bogus value is undetectable afterwards; a\n * refusal is retryable and the draft still holds the answers. It is the same rule\n * the engine applies to a validation rule it cannot evaluate.\n */\n | { ok: false; kind: 'source_unavailable'; source: string }\n\n/**\n * Ask each source about the values submitted for it.\n *\n * One call per source rather than per answer, so a deployment sees `IN (…)` and not\n * a query per row.\n */\nexport async function checkMembership(\n schema: FormSchema,\n value: unknown,\n sources: ServerOptionsSources | undefined,\n): Promise<MembershipOutcome> {\n const answers = sourcedAnswers(schema, value)\n if (answers.length === 0 || sources === undefined) return { ok: true }\n\n const bySource = new Map<string, Array<{ path: string; value: string }>>()\n for (const answer of answers) {\n const group = bySource.get(answer.source) ?? []\n group.push({ path: answer.path, value: answer.value })\n bySource.set(answer.source, group)\n }\n\n const errors: Record<string, string[]> = {}\n\n for (const [name, group] of bySource) {\n const source = sources[name]\n // A source the deployment has not configured cannot vouch for anything. Treated\n // as unavailable rather than as absent: the document says these answers come\n // from somewhere, and accepting them unchecked would be the silent version of\n // the same failure.\n if (source === undefined) return { ok: false, kind: 'source_unavailable', source: name }\n if (source.members === undefined) continue\n\n let rejected: readonly string[]\n try {\n rejected = await source.members(group.map((entry) => entry.value))\n } catch {\n return { ok: false, kind: 'source_unavailable', source: name }\n }\n\n const refused = new Set(rejected)\n for (const entry of group) {\n if (!refused.has(entry.value)) continue\n // The same code word a document option produces, so a message catalogue\n // learns one word rather than two for one idea.\n errors[entry.path] = [...(errors[entry.path] ?? []), 'option']\n }\n }\n\n return Object.keys(errors).length === 0 ? { ok: true } : { ok: false, kind: 'invalid', errors }\n}\n\n/**\n * The source names a document uses, for the publish-time vocabulary check.\n *\n * Walked over the schema rather than over any data, because publishing has none.\n */\nexport function sourceNamesIn(schema: FormSchema): readonly string[] {\n const names = new Set<string>()\n const walk = (fields: readonly FieldDef[]): void => {\n for (const field of fields) {\n if (field.optionsSource !== undefined) names.add(field.optionsSource)\n if (field.fields !== undefined) walk(field.fields)\n }\n }\n walk(schema.model.fields)\n return [...names]\n}\n","import type { Actor } from './auth.js'\n\n/**\n * Who did what, to which thing, and when.\n *\n * **Reads and exports are recorded as well as mutations** — `submission.read` and\n * `submission.exported` — which is what lets a self-hoster answer who downloaded four\n * thousand people's answers ([0057](../../../docs/decisions/0057-the-audit-log-records-reads.md)).\n *\n * **A row joins the mutation's transaction where there is one.** `insertSubmission` and\n * `publishVersion` both commit their audit row with the change it describes, so a\n * rollback leaves no trace saying it happened. A read has nothing to be atomic with, so\n * its row is appended after the rows are handed over and a failure to write it is logged\n * loudly rather than swallowed.\n *\n * **`detail` carries identifiers and counts, never submission content.** An audit log is\n * read by more people, kept longer and exported more freely than the data it describes.\n */\n\n/**\n * Every auditable action, as a closed list.\n *\n * Closed so a new route cannot invent its own spelling: an audit log where one event is\n * called two things is one nobody can query.\n */\nexport const AUDIT_ACTIONS = [\n 'auth.login',\n 'auth.login.failed',\n 'form.published',\n 'form.access.changed',\n 'submission.created',\n 'submission.read',\n 'submission.exported',\n 'file.downloaded',\n 'user.created',\n 'apiKey.created',\n 'apiKey.revoked',\n // Somebody decided a dead delivery should go after all. Recorded because it\n // sends data to a third party on a person's say-so.\n 'delivery.replayed',\n] as const\n\nexport type AuditAction = (typeof AUDIT_ACTIONS)[number]\n\nexport interface AuditEntry {\n readonly id: string\n readonly at: string\n readonly action: AuditAction\n /**\n * Who. Absent for an anonymous public submission, which is a real answer\n * rather than a gap: the form was open, and nobody was signed in.\n */\n readonly actorKind?: 'user' | 'apiKey'\n readonly actorId?: string\n /**\n * What it was done to, as a stable identifier: a form's path, a submission\n * id, a user's id. Absent only where the action has no object, which is\n * currently nothing.\n */\n readonly subject?: string\n /**\n * The request it happened in, so an export and the rows it returned can be\n * tied together later.\n */\n readonly requestId?: string\n /** Identifiers and counts. Never a submission's answers. */\n readonly detail?: Readonly<Record<string, string | number | boolean>>\n}\n\n/** What a caller has to supply; the id and the timestamp are minted for it. */\nexport type AuditDraft = Omit<AuditEntry, 'id' | 'at'>\n\n/**\n * An entry from an actor, with the fields spread the way the record stores\n * them.\n *\n * A helper rather than a convention, because `actorKind` and `actorId` being\n * set together is exactly the sort of thing eleven call sites get wrong once.\n */\nexport function auditedBy(actor: Actor | undefined, draft: Omit<AuditDraft, 'actorKind' | 'actorId'>): AuditDraft {\n return actor === undefined\n ? draft\n : { ...draft, actorKind: actor.kind, actorId: actor.id }\n}\n","import { hmac } from '@noble/hashes/hmac.js'\nimport { sha256 } from '@noble/hashes/sha2.js'\nimport { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js'\n\n/**\n * What a webhook delivery looks like on the wire.\n *\n * The signature scheme is Stripe's, deliberately: receivers already have code\n * for it, and a scheme somebody has to implement from scratch is a scheme most\n * receivers will skip. Adopting a familiar one is the difference between a\n * verified webhook and a verified-in-principle one.\n */\n\nexport const EVENT_ID_HEADER = 'x-formancy-event-id'\nexport const SIGNATURE_HEADER = 'x-formancy-signature'\n\nexport interface DeliveryHeaders {\n [header: string]: string\n}\n\n/**\n * The signed payload is `timestamp.body`, not `body`.\n *\n * Without the timestamp inside the signature, a signature captured once stays\n * valid forever and can be replayed. With it, a receiver can reject anything\n * older than its tolerance, and the signature covers the timestamp it is\n * checking — so the timestamp cannot be edited either.\n */\nexport function signedPayload(timestampSeconds: number, body: string): string {\n return `${String(timestampSeconds)}.${body}`\n}\n\n/** Hex HMAC-SHA256 over the signed payload. */\nexport function signBody(secret: string, timestampSeconds: number, body: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(signedPayload(timestampSeconds, body))))\n}\n\n/**\n * The headers for one delivery attempt.\n *\n * The event id is stable across retries, which is what makes the receiver able\n * to be idempotent. Generating a fresh one per attempt would turn our retry\n * into their duplicate.\n */\nexport function deliveryHeaders(input: {\n eventId: string\n secret: string\n body: string\n timestampSeconds: number\n attempt: number\n}): DeliveryHeaders {\n return {\n 'content-type': 'application/json',\n [EVENT_ID_HEADER]: input.eventId,\n [SIGNATURE_HEADER]: `t=${String(input.timestampSeconds)},v1=${signBody(input.secret, input.timestampSeconds, input.body)}`,\n // Useful in a receiver's logs, and never part of the signature: a retry\n // must produce the same signature as the first attempt.\n 'x-formancy-attempt': String(input.attempt),\n }\n}\n\n/**\n * Verify a delivery the way a receiver would.\n *\n * Exported because a scheme nobody can check is a scheme nobody does check —\n * this is the reference implementation, and the test for our own signing.\n */\nexport function verifySignature(input: {\n secret: string\n body: string\n header: string\n nowSeconds: number\n toleranceSeconds?: number\n}): boolean {\n const parts = new Map(\n input.header.split(',').map((part) => {\n const at = part.indexOf('=')\n return [part.slice(0, at).trim(), part.slice(at + 1).trim()] as const\n }),\n )\n\n const timestamp = Number(parts.get('t'))\n const provided = parts.get('v1')\n if (!Number.isFinite(timestamp) || provided === undefined) return false\n\n const tolerance = input.toleranceSeconds ?? 300\n if (Math.abs(input.nowSeconds - timestamp) > tolerance) return false\n\n const expected = signBody(input.secret, timestamp, input.body)\n return timingSafeEqual(expected, provided)\n}\n\n/**\n * Compare without leaking where the first difference is.\n *\n * `a === b` on a hex string returns as soon as two characters differ, and the\n * time it takes says how many leading characters were right — enough, over\n * many attempts, to recover a signature a byte at a time.\n */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let difference = 0\n for (let index = 0; index < a.length; index += 1) {\n difference |= a.charCodeAt(index) ^ b.charCodeAt(index)\n }\n return difference === 0\n}\n\n/**\n * How long to wait before attempt n, in milliseconds.\n *\n * Exponential, capped, with jitter. The jitter is the part that matters at\n * scale: without it every delivery queued by the same outage retries at the\n * same instant, and the receiver coming back up is knocked over by the\n * thundering herd of everyone's retries.\n */\nexport function retryDelayMs(attempt: number, random: () => number): number {\n const base = Math.min(30_000 * 2 ** (attempt - 1), 6 * 60 * 60 * 1000)\n // Full jitter: anywhere in [0, base). Better than base±10% precisely because\n // it spreads the herd across the whole window.\n return Math.floor(base * random())\n}\n\n/** Eight attempts reach roughly 24 hours with this schedule. */\nexport const MAX_ATTEMPTS = 8\n","import type { WebhookRecord } from './ports.js'\n\n/**\n * Stopping, when a destination has plainly stopped listening.\n *\n * Failure is counted per WEBHOOK as well as per delivery. Enough consecutive failures\n * and the breaker opens: nothing is attempted for that webhook until a cool-down passes,\n * then exactly one delivery goes through as a probe. It succeeds and the breaker closes;\n * it fails and the cool-down starts again.\n *\n * `consecutiveFailures` and `openedAt` live on the RECORD rather than in the worker's\n * memory, so the state survives a restart and can be shown on a screen — a self-hoster\n * has no operations team watching a dashboard\n * ([0058](../../../docs/decisions/0058-a-breaker-per-destination.md)).\n *\n * Everything here is a pure function of a record and a clock: decisions tangled into\n * network code can only be tested by making requests.\n */\n\n/**\n * How many consecutive failures open the breaker.\n *\n * Three, not one. A single failure is a bad minute — a deploy, a timeout, a\n * receiver restarting — and opening on it would make every routine blip a\n * visible outage. Three in a row is a destination that is not coming back on\n * its own.\n */\nexport const BREAKER_THRESHOLD = 3\n\n/**\n * How long the breaker stays shut before one probe is allowed through.\n *\n * Five minutes: long enough that a restarting receiver is finished, short\n * enough that nobody notices the delay once it recovers. The retry schedule\n * handles patience within a delivery; this handles patience about a\n * destination.\n */\nexport const BREAKER_COOLDOWN_MS = 5 * 60 * 1000\n\n/** Shut, letting one through, or open. */\nexport type BreakerState = 'closed' | 'half-open' | 'open'\n\nexport function breakerState(webhook: WebhookRecord, now: Date): BreakerState {\n if (webhook.consecutiveFailures < BREAKER_THRESHOLD) return 'closed'\n if (webhook.openedAt === null) return 'closed'\n\n const since = now.getTime() - new Date(webhook.openedAt).getTime()\n // Half-open rather than closed: the cool-down earns ONE attempt, not a\n // return to normal. Closing on the clock alone would send the whole backlog\n // at a destination that has not answered yet.\n return since >= BREAKER_COOLDOWN_MS ? 'half-open' : 'open'\n}\n\n/** Whether a delivery for this webhook may be attempted now. */\nexport function mayAttempt(webhook: WebhookRecord, now: Date): boolean {\n return breakerState(webhook, now) !== 'open'\n}\n\n/**\n * The webhook's health after one attempt against it.\n *\n * A success always closes the breaker completely. Decaying the count instead\n * would leave a destination that fails twice for every success permanently\n * one blip from opening, and that destination is working.\n */\nexport function afterWebhookAttempt(\n webhook: WebhookRecord,\n ok: boolean,\n now: Date,\n): WebhookRecord {\n if (ok) {\n return { ...webhook, consecutiveFailures: 0, openedAt: null }\n }\n\n const consecutiveFailures = webhook.consecutiveFailures + 1\n const opening = consecutiveFailures >= BREAKER_THRESHOLD\n return {\n ...webhook,\n consecutiveFailures,\n // Re-stamped on every failure once open, so a probe that fails starts the\n // cool-down again rather than letting the next one through immediately.\n openedAt: opening ? now.toISOString() : webhook.openedAt,\n }\n}\n\n/**\n * What a person is shown about a webhook's health.\n *\n * Shaped for a screen rather than for a log: the question being answered is\n * \"is this working, and if not since when\", which is what somebody asks when\n * the CRM has no leads this week.\n */\nexport interface WebhookHealth {\n id: string\n url: string\n state: BreakerState\n consecutiveFailures: number\n /** When it stopped working, or null while it is working. */\n failingSince: string | null\n}\n\nexport function healthOf(webhook: WebhookRecord, now: Date): WebhookHealth {\n return {\n id: webhook.id,\n url: webhook.url,\n state: breakerState(webhook, now),\n consecutiveFailures: webhook.consecutiveFailures,\n failingSince: webhook.openedAt,\n }\n}\n","import type { DeliveryRecord, Storage, WebhookRecord } from './ports.js'\nimport { MAX_ATTEMPTS, retryDelayMs } from './webhook.js'\nimport { BREAKER_COOLDOWN_MS, afterWebhookAttempt, mayAttempt } from './breaker.js'\n\n/**\n * What to do with a delivery once an attempt has finished.\n *\n * Pure, and separate from the thing that does the posting, because this is\n * where the decisions are: whether to retry, when, and when to stop. Tangled\n * into the network code those decisions are only testable by making requests.\n */\n\nexport interface AttemptOutcome {\n ok: boolean\n status?: number\n error?: string\n}\n\nexport function afterAttempt(\n delivery: DeliveryRecord,\n outcome: AttemptOutcome,\n now: Date,\n random: () => number,\n): DeliveryRecord {\n const attempt = delivery.attempt + 1\n\n if (outcome.ok) {\n return { ...delivery, attempt, state: 'delivered', lastError: null }\n }\n\n const reason = outcome.error ?? `Receiver answered ${String(outcome.status ?? 0)}.`\n\n if (attempt >= MAX_ATTEMPTS) {\n // Dead, not deleted. The row is the evidence that something was supposed\n // to be sent and never arrived, and a self-hoster with no operations team\n // needs to be able to find it and replay it.\n return { ...delivery, attempt, state: 'dead', lastError: reason }\n }\n\n return {\n ...delivery,\n attempt,\n state: 'pending',\n nextAttemptAt: new Date(now.getTime() + retryDelayMs(attempt, random)).toISOString(),\n lastError: reason,\n }\n}\n\nexport interface OutboxDeps {\n storage: Storage\n /** Posts one delivery. Injected so the loop is testable without a network. */\n send: (\n delivery: DeliveryRecord,\n webhook: WebhookRecord,\n nowSeconds: number,\n ) => Promise<AttemptOutcome>\n now: () => Date\n random: () => number\n}\n\n/**\n * Drain one batch of due deliveries.\n *\n * Returns how many were attempted, so a caller can decide whether to come back\n * immediately or wait. Deliberately one batch rather than a loop that runs\n * forever: the thing that decides *when* to run belongs to the host, and a\n * function that never returns cannot be tested.\n */\n/**\n * Returns how many were ATTEMPTED — deliveries skipped because their\n * destination's breaker is open do not count, so a caller polling until the\n * queue is quiet does not spin on a dead endpoint.\n */\nexport async function drainOutbox(deps: OutboxDeps, limit = 20): Promise<number> {\n const now = deps.now()\n const due = await deps.storage.claimDueDeliveries(now.toISOString(), limit)\n let skipped = 0\n\n for (const delivery of due) {\n // A webhook deleted since the row was queued leaves nothing to deliver to.\n // Marked dead rather than retried forever against a destination that no\n // longer exists.\n const webhook = await findWebhook(deps.storage, delivery.webhookId)\n if (webhook === undefined) {\n await deps.storage.updateDelivery({\n ...delivery,\n state: 'dead',\n lastError: 'The webhook it was queued for no longer exists.',\n })\n continue\n }\n\n // The breaker is per DESTINATION, where the retry schedule is per\n // delivery. A form taking a submission a minute otherwise produces a\n // minute's worth of deliveries all independently hammering an endpoint\n // that has been returning 502 since Tuesday.\n if (!mayAttempt(webhook, now)) {\n skipped += 1\n // Pushed out rather than left due, so the next batch is not the same\n // rows again. The delivery keeps its attempt count: being skipped is\n // not a failed attempt, and charging it one would use up a delivery's\n // eight tries on a destination it never reached.\n await deps.storage.updateDelivery({\n ...delivery,\n nextAttemptAt: new Date(now.getTime() + BREAKER_COOLDOWN_MS).toISOString(),\n })\n continue\n }\n\n let outcome: AttemptOutcome\n try {\n outcome = await deps.send(delivery, webhook, Math.floor(now.getTime() / 1000))\n } catch (error) {\n // A thrown send is a failed attempt, not a lost job. Letting it escape\n // would abandon every remaining delivery in the batch.\n outcome = { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n\n await deps.storage.updateDelivery(afterAttempt(delivery, outcome, now, deps.random))\n // The destination's health, which outlives this delivery. Written every\n // time rather than only on a change: a success has to clear the count,\n // and the write is one row.\n await deps.storage.updateWebhook(afterWebhookAttempt(webhook, outcome.ok, now))\n }\n\n return due.length - skipped\n}\n\nasync function findWebhook(storage: Storage, id: string): Promise<WebhookRecord | undefined> {\n // The port lists per form rather than by id, which is the shape every other\n // caller wants; this is the one place that pays for it.\n const forms = await storage.listForms()\n for (const form of forms) {\n const hooks = await storage.webhooksForForm(form.id)\n const found = hooks.find((hook) => hook.id === id)\n if (found !== undefined) return found\n }\n return undefined\n}\n\n/**\n * Put a dead delivery back in the queue.\n *\n * Dead is where a delivery goes when the attempts run out, and the row is the\n * evidence that something was supposed to be sent and never arrived. Replay is\n * what makes that evidence useful: the receiver was down for a morning, it is\n * back, and the twelve submissions it missed should go now.\n *\n * The attempt count resets. It is a new run against a destination somebody has\n * looked at and decided is fixed — carrying the old count over would give it\n * one try before dying again, which is not a replay so much as a formality.\n *\n * Refuses anything that is not dead. Replaying a pending delivery would\n * duplicate it, and replaying a delivered one would send it twice: the event\n * id is stable across retries so a well-behaved receiver would ignore the\n * second, but this service does not get to assume the receiver is\n * well-behaved.\n */\nexport type ReplayOutcome =\n | { ok: true; delivery: DeliveryRecord }\n | { ok: false; kind: 'unknown' | 'not-dead'; state?: DeliveryRecord['state'] }\n\nexport async function replayDelivery(\n deps: { storage: Storage; now: () => Date },\n id: string,\n): Promise<ReplayOutcome> {\n const delivery = await deps.storage.getDelivery(id)\n if (delivery === undefined) return { ok: false, kind: 'unknown' }\n if (delivery.state !== 'dead') return { ok: false, kind: 'not-dead', state: delivery.state }\n\n const revived: DeliveryRecord = {\n ...delivery,\n attempt: 0,\n state: 'pending',\n // Now, not on the retry schedule. Somebody pressed a button; the next\n // pass should pick it up.\n nextAttemptAt: deps.now().toISOString(),\n }\n await deps.storage.updateDelivery(revived)\n return { ok: true, delivery: revived }\n}\n","import type { FormRecord } from './ports.js'\n\n/**\n * Whether this caller may submit this form at all, before any of the work.\n *\n * Every branch that is not an explicit permission returns false. An allowlist\n * that exists but does not name the origin refuses; an origin header that is\n * absent refuses, because absent is not the same as allowed; an empty\n * allowlist refuses everything, because a list of no origins is a list.\n */\nexport function maySubmit(\n form: FormRecord,\n actor: 'anonymous' | 'authenticated',\n origin: string | undefined,\n): boolean {\n if (actor === 'authenticated') return true\n if (form.accessSubmit !== 'public') return false\n\n const allowed = form.allowedOrigins\n if (allowed === null) return true\n if (origin === undefined) return false\n\n // Exact match, never a prefix or suffix one: `https://evil-example.ch` ends\n // with the same characters as `example.ch` and must not pass, and\n // `https://example.ch:8443` is a different origin from `https://example.ch`.\n return allowed.includes(origin)\n}\n","import { modelDataPaths } from '@formancy/spec'\nimport type { FieldDef, FormSchema } from '@formancy/spec'\nimport { maySubmit } from './access.js'\nimport type { FileRecord, FormRecord, Storage } from './ports.js'\n\n/**\n * The file lifecycle: offered, stored, claimed, collected.\n *\n * **Bytes are not the submission's problem.** A submission stores what each\n * file is and where it went, never the bytes themselves, so a submission read\n * back years later is small, self-describing, and still says what was attached\n * even if the object store has since been emptied.\n *\n * **The claim happens inside the submission's transaction.** A submission\n * exists if and only if the files it names are claimed against it. Anything\n * looser gives one of the two failures nobody can debug: a rolled-back\n * submission leaving files claimed against nothing, or an accepted submission\n * referencing files the collector is about to delete.\n *\n * **Nothing here touches a disk.** This package has no filesystem and no\n * object store; it decides what is rubbish and the host deletes it. That is\n * the same split every other port here makes, and it is why the whole\n * lifecycle is testable without either.\n */\n\nexport interface OfferDeps {\n storage: Storage\n now: () => Date\n newId: () => string\n}\n\nexport interface OfferInput {\n schema: FormSchema\n /** The form, for the access check — the same one a submission passes. */\n form: FormRecord\n actor: 'anonymous' | 'authenticated'\n origin: string | undefined\n /** The field the file is for, as a data path: `evidence`, `people[].passport`. */\n field: string\n name: string\n size: number\n contentType: string\n}\n\nexport type OfferOutcome =\n | { ok: true; file: FileRecord }\n | {\n ok: false\n reason: 'not_allowed' | 'unknown_field' | 'not_a_file_field' | 'too_large' | 'not_accepted'\n }\n\n/**\n * Agree where a file will go, and refuse it before a byte is sent.\n *\n * Checking here rather than after the upload is the difference between\n * refusing a 2 GB file and receiving one first. The browser's own `accept` and\n * size limits are a convenience for the person filling the form in; these are\n * the rule, because somebody posting to the endpoint directly has neither.\n */\nexport async function offerUpload(deps: OfferDeps, input: OfferInput): Promise<OfferOutcome> {\n // The same gate the submission passes, and the same function — a form\n // nobody may submit to is not a form anybody may upload to either, or the\n // store becomes free disk for whoever finds the path. Two copies of that\n // rule is one copy that gets forgotten.\n if (!maySubmit(input.form, input.actor, input.origin)) return { ok: false, reason: 'not_allowed' }\n\n const def = fileFieldAt(input.schema, input.field)\n if (def === undefined) return { ok: false, reason: 'unknown_field' }\n if (def.type !== 'file') return { ok: false, reason: 'not_a_file_field' }\n\n if (def.maxFileSize !== undefined && input.size > def.maxFileSize) {\n return { ok: false, reason: 'too_large' }\n }\n if (!accepted(def.accept, input.name, input.contentType)) {\n return { ok: false, reason: 'not_accepted' }\n }\n\n const id = deps.newId()\n const file: FileRecord = {\n id,\n formId: input.form.id,\n name: input.name,\n size: input.size,\n contentType: input.contentType,\n // Built from the id and the form, never from the submitted name. The name\n // is display only and may be anything at all, including `../../etc/passwd`.\n storageKey: `${input.form.id}/${id}`,\n state: 'offered',\n createdAt: deps.now().toISOString(),\n submissionId: null,\n }\n\n await deps.storage.insertFile(file)\n return { ok: true, file }\n}\n\n/** What a deployment's scanner says about an upload's bytes. */\nexport type ScanVerdict = { clean: true } | { clean: false; finding: string }\n\n/**\n * A deployment's virus scanner, asked about an upload's bytes before they are kept.\n *\n * A port because the deployment supplies it and there are two: the ClamAV adapter in\n * `@formancy/server`, and the doubles that test this decision. **Throwing means it could\n * not be asked** — a connection refused, a timeout — and never that the file is clean.\n */\nexport interface Scanner {\n scan(\n file: { name: string; contentType: string; size: number },\n bytes: Uint8Array,\n ): Promise<ScanVerdict>\n}\n\nexport type ScreenOutcome =\n | { ok: true }\n | { ok: false; reason: 'refused'; finding: string }\n /** `cause` is for the operator's log, never for the client: it can name an address. */\n | { ok: false; reason: 'scanner_unavailable'; cause: string }\n\n/**\n * Whether an upload's bytes may be kept.\n *\n * **Fail closed.** A scanner that cannot be asked refuses the file, the same as one that\n * found something: a deployment that configured a scanner did so to keep unscanned files\n * out, and its being down is not a reason to let one in. The person is told to try again,\n * and the field offers exactly that. Without a scanner every upload is kept, as it was\n * before there was one — a supported state, said in the documentation rather than here\n * ([0131](../../../docs/decisions/0131-an-upload-is-scanned-before-it-is-kept.md)).\n */\nexport async function screenUpload(\n scanner: Scanner | undefined,\n file: { name: string; contentType: string; size: number },\n bytes: Uint8Array,\n): Promise<ScreenOutcome> {\n if (scanner === undefined) return { ok: true }\n let verdict: ScanVerdict\n try {\n verdict = await scanner.scan(\n { name: file.name, contentType: file.contentType, size: file.size },\n bytes,\n )\n } catch (error) {\n return {\n ok: false,\n reason: 'scanner_unavailable',\n cause: error instanceof Error ? error.message : String(error),\n }\n }\n return verdict.clean ? { ok: true } : { ok: false, reason: 'refused', finding: verdict.finding }\n}\n\nexport type ClaimOutcome =\n | { ok: true; ids: readonly string[] }\n | { ok: false; unknown: readonly string[] }\n\n/**\n * Which files a submission may claim, checked but not yet claimed.\n *\n * The claiming itself is `insertSubmission`'s job, in the same call that\n * stores the submission, so one COMMIT decides both — the same reasoning\n * that put the webhook queue there. A submission exists if and only if the\n * files it names belong to it. Claiming separately gives the two failures\n * nobody can debug: a rolled-back submission leaving files claimed against\n * nothing, or an accepted submission referencing bytes the collector is about\n * to delete.\n *\n * Refuses rather than skips. A submission naming a file that does not exist,\n * belongs to another form, or is already claimed is not a submission to fix up\n * quietly: the first is a bug or somebody guessing ids; the second is how an\n * id from a form you may submit to would let you attach a file from one you\n * may not; the third would make deleting a submission either leak bytes or\n * break a different one.\n */\nexport async function filesToClaim(\n storage: Storage,\n input: { schema: FormSchema; formId: string; data: unknown },\n): Promise<ClaimOutcome> {\n const ids = referencedFileIds(input.schema, input.data)\n if (ids.length === 0) return { ok: true, ids: [] }\n\n const unknown: string[] = []\n const claimable: FileRecord[] = []\n\n for (const id of ids) {\n const file = await storage.getFile(id)\n if (file === undefined || file.formId !== input.formId || file.state !== 'stored') {\n unknown.push(id)\n continue\n }\n claimable.push(file)\n }\n\n if (unknown.length > 0) return { ok: false, unknown }\n\n return { ok: true, ids: claimable.map((file) => file.id) }\n}\n\n/**\n * Files nobody claimed, old enough that nobody is going to.\n *\n * Returns the rows rather than deleting anything: the host removes the bytes\n * and then the rows, in that order, because a row without bytes is a broken\n * reference and bytes without a row are invisible rubbish. Of the two, the\n * second is the one you never find out about.\n */\nexport async function collectAbandonedFiles(\n storage: Storage,\n now: Date,\n afterHours: number,\n): Promise<FileRecord[]> {\n const before = new Date(now.getTime() - afterHours * 3_600_000).toISOString()\n return storage.abandonedFiles(before)\n}\n\n// ------------------------------------------------------------------ helpers\n\n/** The field at a data path, with `[]` treated as \"inside this repeater\". */\nfunction fileFieldAt(schema: FormSchema, path: string): FieldDef | undefined {\n const wanted = path.replace(/\\[\\]/g, '')\n const segments = wanted.split('.').filter((segment) => segment !== '')\n\n let fields: readonly FieldDef[] = flattenPages(schema.model.fields)\n let found: FieldDef | undefined\n\n for (const segment of segments) {\n found = fields.find((field) => field.key === segment)\n if (found === undefined) return undefined\n fields = flattenPages(found.fields ?? [])\n }\n return found\n}\n\n/** A page contributes no segment to a data path. */\nfunction flattenPages(fields: readonly FieldDef[]): FieldDef[] {\n const out: FieldDef[] = []\n for (const field of fields) {\n if (field.type === 'page') out.push(...flattenPages(field.fields ?? []))\n else out.push(field)\n }\n return out\n}\n\n/** The HTML `accept` grammar, as the engine reads it. */\nfunction accepted(accept: readonly string[] | undefined, name: string, contentType: string): boolean {\n if (accept === undefined || accept.length === 0) return true\n const lowerName = name.toLowerCase()\n const lowerType = contentType.toLowerCase()\n\n return accept.some((raw) => {\n const rule = raw.trim().toLowerCase()\n if (rule === '') return false\n if (rule.startsWith('.')) return lowerName.endsWith(rule)\n if (rule.endsWith('/*')) return lowerType.startsWith(rule.slice(0, -1))\n return lowerType === rule\n })\n}\n\n/**\n * Every file id a submission references.\n *\n * Driven by the SCHEMA, not by the shape of the values. Recognising an\n * attachment by \"an object with an id\" would let a client omit a property and\n * have its reference quietly not claimed — the submission would be accepted\n * carrying a dangling file id, and the collector would delete the bytes a day\n * later. Reading only the paths the model says are file fields cannot be\n * fooled that way, and a text answer that happens to look like a file is not\n * one.\n *\n * A repeater contributes rows the schema cannot know about, so the walk\n * expands over whatever rows the data actually has.\n */\nfunction referencedFileIds(schema: FormSchema, data: unknown): string[] {\n const ids: string[] = []\n\n const walk = (fields: readonly FieldDef[], value: unknown): void => {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return\n const record = value as Record<string, unknown>\n\n for (const field of flattenPages(fields)) {\n const held = record[field.key]\n if (held === undefined) continue\n\n if (field.type === 'file') {\n if (!Array.isArray(held)) continue\n for (const entry of held) {\n const id = (entry as Record<string, unknown> | null)?.['id']\n if (typeof id === 'string') ids.push(id)\n }\n continue\n }\n\n if (field.type === 'group') {\n walk(field.fields ?? [], held)\n continue\n }\n\n if (field.type === 'repeater' && Array.isArray(held)) {\n for (const row of held) walk(field.fields ?? [], row)\n }\n }\n }\n\n walk(schema.model.fields, data)\n return ids\n}\n\n/** Re-exported so a host can see which paths a schema's file fields live at. */\nexport function fileFieldPaths(schema: FormSchema): string[] {\n return modelDataPaths(schema.model).filter(\n (path) => fileFieldAt(schema, path)?.type === 'file',\n )\n}\n","import { hmac } from '@noble/hashes/hmac.js'\nimport { sha256 } from '@noble/hashes/sha2.js'\nimport { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js'\nimport { diffSchemas, schemaHash } from '@formancy/spec'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { createFormEngine } from '@formancy/core'\nimport { checkMembership } from './options-membership.js'\nimport { maySubmit } from './access.js'\nimport { filesToClaim } from './uploads.js'\nimport type { ServerDeps } from './deps.js'\n\n/**\n * The backend's use-cases, framework-free.\n *\n * Identity and time are injected like every other capability: the server pins\n * one clock per request, so a submission can be replayed later and produce the\n * same computed values byte for byte. Nothing here reads Date.now or crypto.\n */\n\nexport interface ResolvedForm {\n formId: string\n versionId: string\n version: number\n schema: FormSchema\n schemaHash: string\n}\n\nexport async function resolveForm(deps: ServerDeps, path: string): Promise<ResolvedForm | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined || form.currentVersionId === null) return undefined\n const current = await deps.storage.getVersionById(form.currentVersionId)\n if (current === undefined) return undefined\n return {\n formId: form.id,\n versionId: current.id,\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n }\n}\n\nexport type SubmissionOutcome =\n | { ok: true; id: string; canonicalData: unknown }\n | { ok: false; kind: 'unknown_form' }\n | { ok: false; kind: 'version_changed'; current?: ResolvedForm }\n | { ok: false; kind: 'invalid'; errors: Record<string, string[]> }\n /** The caller may not submit this form: not public, or not from this origin. */\n | { ok: false; kind: 'forbidden' }\n /**\n * A source that should have vouched for an answer could not.\n *\n * Failed closed and never accepted unchecked: a bogus value that is stored is\n * undetectable afterwards, while a refusal is retryable and the draft still holds\n * the answers.\n */\n | { ok: false; kind: 'source_unavailable'; source: string }\n\n/**\n * Accept one submission: resolve the version the client says it rendered,\n * replay the engine over the submitted data, and store the CANONICAL result.\n *\n * The replay is the whole point. Computed values are recomputed and overwrite\n * whatever arrived; visibility is evaluated server-side and hidden branches\n * are stripped, so a client cannot smuggle data by lying about what was shown.\n * Client validation is UX; this is truth.\n */\n/**\n * Change who may submit a form, and from where.\n *\n * Separate from publishing on purpose: access is a property of the deployment,\n * and changing it must not mint a new immutable form version or invalidate the\n * schema hash every client is holding.\n */\nexport async function setFormAccess(\n deps: ServerDeps,\n input: { path: string; submit: 'authenticated' | 'public'; allowedOrigins?: string[] },\n): Promise<{ ok: boolean }> {\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false }\n\n await deps.storage.updateFormAccess(form.id, {\n accessSubmit: input.submit,\n allowedOrigins: input.allowedOrigins ?? null,\n })\n return { ok: true }\n}\n\n\nexport async function createSubmission(\n deps: ServerDeps,\n input: {\n path: string\n declaredSchemaHash: string\n data: unknown\n /** Defaults to anonymous: the caller must prove otherwise, not the reverse. */\n actor?: 'anonymous' | 'authenticated'\n origin?: string\n },\n): Promise<SubmissionOutcome> {\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n if (!maySubmit(form, input.actor ?? 'anonymous', input.origin)) {\n return { ok: false, kind: 'forbidden' }\n }\n\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return { ok: false, kind: 'unknown_form' }\n\n if (input.declaredSchemaHash !== current.schemaHash) {\n // The thin slice's staleVersionPolicy is `reject`: the acceptCompatible\n // policy arrives with draft migration, driven by diffSchemas severity.\n return { ok: false, kind: 'version_changed', current }\n }\n\n const engine = createFormEngine({\n schema: current.schema,\n initialValue: input.data,\n capabilities: deps.capabilities,\n // The side this replay is, which was never said. Without it the server ran\n // as a CLIENT: a `runsOn: \"server\"` rule was skipped in the one place it was\n // meant to run and a `runsOn: \"client\"` rule ran in the one place it was\n // meant not to — both halves of [0043] backwards, in the product, with\n // nothing reporting it.\n mode: 'server',\n ...(deps.checks === undefined ? {} : { checks: deps.checks }),\n })\n // Checks are asked by the engine and answered later. Awaiting before `submit`\n // is what makes the server the authority on them rather than a second opinion\n // that happened to be quick.\n await engine.settle()\n const outcome = engine.submit()\n if (!outcome.ok) return { ok: false, kind: 'invalid', errors: outcome.errors }\n\n // Asked AFTER the engine and BEFORE anything is written, so a refusal stores\n // nothing. The engine cannot do this itself: a `optionsSource` field has no\n // document options to compare against, which is exactly what naming a source\n // means.\n const membership = await checkMembership(current.schema, engine.value(), deps.optionsSources)\n if (!membership.ok) {\n return membership.kind === 'invalid'\n ? { ok: false, kind: 'invalid', errors: membership.errors }\n : { ok: false, kind: 'source_unavailable', source: membership.source }\n }\n\n const id = deps.newId()\n\n // Queued in the SAME call that stores the submission, so one COMMIT decides\n // both. Posting after the insert returns gives the two failures a\n // self-hoster cannot debug: the webhook fired and the submission rolled\n // back, or the submission is stored and nothing was ever sent.\n const hooks = await deps.storage.webhooksForForm(current.formId)\n // The CANONICAL value, not the request body: the receiver sees what was\n // stored, with computed fields recomputed and hidden branches stripped.\n const body = JSON.stringify({ id, form: input.path, data: engine.value() })\n const queued = hooks.map((hook) => ({\n id: deps.newId(),\n webhookId: hook.id,\n submissionId: id,\n // Stable for every retry of this delivery, which is what lets a receiver\n // dedupe. A fresh id per attempt would turn our retry into their duplicate.\n eventId: deps.newId(),\n body,\n attempt: 0,\n nextAttemptAt: deps.nowIso(),\n state: 'pending' as const,\n lastError: null,\n }))\n\n // Checked before the insert and claimed inside it. A submission naming a\n // file that is not hers is not a submission.\n const claimable = await filesToClaim(deps.storage, {\n schema: current.schema,\n formId: current.formId,\n data: engine.value(),\n })\n if (!claimable.ok) {\n return {\n ok: false,\n kind: 'invalid',\n errors: {\n _files: [`unknown_file:${claimable.unknown.join(',')}`],\n },\n }\n }\n\n const at = deps.nowIso()\n await deps.storage.insertSubmission(\n {\n id,\n formId: current.formId,\n formVersionId: current.versionId,\n data: engine.value(),\n submittedAt: at,\n },\n queued,\n claimable.ids,\n // In the transaction, not after it: a submission that rolled back must\n // leave nothing behind saying it happened, and an audit row for a\n // submission nobody can find is worse than no row at all.\n //\n // No actor id — this route accepts anonymous submissions, and who\n // submitted is the form's own data rather than the audit log's business.\n // `actor` says only whether anybody was signed in.\n {\n id: deps.newId(),\n at,\n action: 'submission.created',\n subject: input.path,\n // The version, so a reader can tell which schema this answered without\n // joining; the count, so an unusual submission is visible. Never the\n // answers.\n detail: {\n submissionId: id,\n version: current.version,\n authenticated: input.actor === 'authenticated',\n },\n },\n )\n return { ok: true, id, canonicalData: engine.value() }\n}\n\nexport interface ListedSubmission {\n id: string\n version: number\n submittedAt: string\n data: unknown\n}\n\n/** The submissions of one form, newest first, each tagged with the schema\n * version that produced it. Undefined for an unknown form: the caller must\n * 404, and an empty list would lie about that. */\nexport async function listSubmissions(\n deps: ServerDeps,\n path: string,\n): Promise<ListedSubmission[] | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n\n const versions = await deps.storage.listVersionsByForm(form.id)\n const versionNumberById = new Map(versions.map((version) => [version.id, version.version]))\n\n const records = await deps.storage.listSubmissionsByForm(form.id)\n return records.map((record) => ({\n id: record.id,\n version: versionNumberById.get(record.formVersionId) ?? 0,\n submittedAt: record.submittedAt,\n data: record.data,\n }))\n}\n\n/**\n * Every submission of a form as CSV, columns UNIONED across schema versions:\n * the current version's columns lead in its own order, and columns that only\n * exist in older versions follow — data outlives the field that collected it,\n * and an export that silently dropped extinct columns would lose it.\n *\n * A repeater is one column carrying its rows as JSON: rows-per-cell beats\n * exploding one submission across several CSV lines, which breaks every\n * spreadsheet join a business user will try.\n */\nexport async function exportCsv(deps: ServerDeps, path: string): Promise<string | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n\n const versions = await deps.storage.listVersionsByForm(form.id)\n const versionNumberById = new Map(versions.map((version) => [version.id, version.version]))\n\n const columns: string[] = []\n const seen = new Set<string>()\n for (const version of versions) {\n for (const column of dataColumns(version.schema)) {\n if (!seen.has(column)) {\n seen.add(column)\n columns.push(column)\n }\n }\n }\n\n const records = await deps.storage.listSubmissionsByForm(form.id)\n const lines = [['id', 'submittedAt', 'version', ...columns].map(csvCell).join(',')]\n for (const record of records) {\n const cells = [\n record.id,\n record.submittedAt,\n String(versionNumberById.get(record.formVersionId) ?? 0),\n ...columns.map((column) => cellValue(record.data, column)),\n ]\n lines.push(cells.map(csvCell).join(','))\n }\n return lines.join('\\n') + '\\n'\n}\n\n/** One column per collected answer: leaves as dotted paths through groups,\n * a repeater as a single column, pages transparent as always. */\nfunction dataColumns(schema: FormSchema): string[] {\n const columns: string[] = []\n const walk = (fields: readonly FormSchema['model']['fields'][number][], prefix: string): void => {\n for (const field of fields) {\n if (field.type === 'page') walk(field.fields ?? [], prefix)\n else if (field.type === 'group') walk(field.fields ?? [], `${prefix}${field.key}.`)\n else columns.push(`${prefix}${field.key}`)\n }\n }\n walk(schema.model.fields, '')\n return columns\n}\n\nfunction cellValue(data: unknown, column: string): string {\n let current: unknown = data\n for (const segment of column.split('.')) {\n if (current === null || typeof current !== 'object') return ''\n current = (current as Record<string, unknown>)[segment]\n }\n if (current === undefined || current === null) return ''\n if (typeof current === 'object') return JSON.stringify(current)\n if (typeof current === 'string' && FORMULA_STARTERS.has(current.charAt(0))) {\n // Spreadsheets execute a cell that starts like a formula. A submission is\n // attacker-controlled and this file is opened by exactly the person worth\n // attacking, so neutralise with the apostrophe convention. Only strings:\n // a number cannot smuggle a formula, and a quoted -5 would break every\n // numeric column.\n return \"'\" + current\n }\n return String(current)\n}\n\nfunction csvCell(value: string): string {\n return /[\",\\n\\r]/.test(value) ? `\"${value.replaceAll('\"', '\"\"')}\"` : value\n}\n\n/** Save (or re-save) a partial form, bound to the CURRENT version. Undefined\n * for an unknown form. */\n/**\n * The token that proves the bearer started this draft.\n *\n * HMAC over the form and the draft id, the same stateless shape the\n * proof-of-work challenge uses\n * ([0059](../../../docs/decisions/0059-proof-of-work-not-a-captcha.md)): no\n * second table, and no lookup before the check. Bound to the FORM as well as the\n * id so a token cannot be carried to a draft of another form that happens to\n * share an id.\n */\nfunction draftToken(secret: string, formId: string, draftId: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(`${formId}:${draftId}`)))\n}\n\n/**\n * Compared in constant time.\n *\n * A byte-at-a-time comparison that returns early leaks how much of a guess was\n * right, and a token is guessed one byte at a time by exactly that signal.\n */\nfunction sameToken(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let differences = 0\n for (let at = 0; at < a.length; at += 1) {\n differences |= a.charCodeAt(at) ^ b.charCodeAt(at)\n }\n return differences === 0\n}\n\n/**\n * Start a draft: the server picks the id, and signs it.\n *\n * The id is the server's because an id a caller supplies is an id a caller can\n * enumerate — and enumerating them was enough to read other people's\n * part-filled answers.\n */\nexport async function startDraft(\n deps: ServerDeps,\n input: { path: string },\n): Promise<{ draftId: string; token: string } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n const draftId = deps.newId()\n return { draftId, token: draftToken(deps.draftSecret, current.formId, draftId) }\n}\n\nexport async function saveDraft(\n deps: ServerDeps,\n input: { path: string; draftId: string; token: string; data: unknown },\n): Promise<{ version: number; saved: boolean } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n // Refused before the write, not after it. Overwriting is the worse half of\n // this: the person whose draft it is then submits the substituted content\n // under their own name, and nothing anywhere says otherwise.\n if (!sameToken(input.token, draftToken(deps.draftSecret, current.formId, input.draftId))) {\n return { version: current.version, saved: false }\n }\n\n await deps.storage.upsertDraft({\n id: input.draftId,\n formId: current.formId,\n formVersionId: current.versionId,\n data: input.data,\n updatedAt: deps.nowIso(),\n })\n return { version: current.version, saved: true }\n}\n\nexport interface DraftMigration {\n severity: 'lossy'\n changes: Change[]\n}\n\nexport type ResumeOutcome =\n | {\n outcome: 'resumed'\n version: number\n schema: FormSchema\n schemaHash: string\n data: unknown\n /** Present when the rebind lost something; absent for a silent rebind. */\n migration?: DraftMigration\n }\n | {\n /** The schema changed in a way no automatic rebind survives: the draft\n * comes back against ITS OWN version, and the caller offers a restart. */\n outcome: 'readOnly'\n version: number\n schema: FormSchema\n data: unknown\n }\n\n/**\n * Resume a draft — and migrate it LAZILY if the form was republished since,\n * which is versioning rule five: never migrate eagerly on publish, because\n * most drafts are abandoned and eager migration multiplies every publish by\n * every draft.\n *\n * diffSchemas drives the decision: `compatible` rebinds silently, `lossy`\n * rebinds with a report — declared renames carry their value to the new key,\n * removed fields move their value into `__orphaned` rather than deleting it —\n * and `breaking` refuses to rebind at all. A successful rebind is persisted,\n * so the migration runs once, not on every resume.\n */\nexport async function resumeDraft(\n deps: ServerDeps,\n input: { path: string; draftId: string; token: string },\n): Promise<ResumeOutcome | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n // Checked before the lookup, and answered identically to a draft that is not\n // there: replying differently would confirm which ids exist.\n if (!sameToken(input.token, draftToken(deps.draftSecret, current.formId, input.draftId))) {\n return undefined\n }\n\n const draft = await deps.storage.getDraft(current.formId, input.draftId)\n if (draft === undefined) return undefined\n\n if (draft.formVersionId === current.versionId) {\n return {\n outcome: 'resumed',\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n data: draft.data,\n }\n }\n\n const draftVersion = await deps.storage.getVersionById(draft.formVersionId)\n if (draftVersion === undefined) return undefined\n\n const changes = diffSchemas(draftVersion.schema, current.schema)\n const severity = changes.reduce<'compatible' | 'lossy' | 'breaking'>(\n (worst, change) =>\n SEVERITY_RANK[change.severity] > SEVERITY_RANK[worst] ? change.severity : worst,\n 'compatible',\n )\n\n if (severity === 'breaking') {\n return {\n outcome: 'readOnly',\n version: draftVersion.version,\n schema: draftVersion.schema,\n data: draft.data,\n }\n }\n\n const migrated = migrateDraftData(draft.data, current.schema, changes)\n\n await deps.storage.upsertDraft({\n id: draft.id,\n formId: draft.formId,\n formVersionId: current.versionId,\n data: migrated,\n updatedAt: deps.nowIso(),\n })\n\n return {\n outcome: 'resumed',\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n data: migrated,\n ...(severity === 'lossy' ? { migration: { severity, changes } } : {}),\n }\n}\n\nconst SEVERITY_RANK = { compatible: 0, lossy: 1, breaking: 2 } as const\n\nconst FORMULA_STARTERS = new Set(['=', '+', '-', '@', String.fromCharCode(9), String.fromCharCode(13)])\n\nfunction migrateDraftData(data: unknown, currentSchema: FormSchema, changes: Change[]): unknown {\n if (data === null || typeof data !== 'object' || Array.isArray(data)) return data\n const migrated: Record<string, unknown> = structuredCloneJson(data as Record<string, unknown>)\n\n // Declared renames first: the value follows the field to its new key.\n applyRenames(currentSchema.model.fields, migrated)\n\n // Then removals: the value moves aside instead of vanishing. Only top-level\n // and group paths move; row members stay inside their (possibly orphaned)\n // repeater value, which travels as a whole.\n const orphaned: Record<string, unknown> = {}\n for (const change of changes) {\n if (change.kind !== 'field.removed') continue\n const dataPath = change.path.replace(/^model\\.fields\\./, '')\n if (dataPath.includes('[]')) continue\n const value = takeAtDottedPath(migrated, dataPath)\n if (value !== undefined) orphaned[dataPath] = value\n }\n if (Object.keys(orphaned).length > 0) {\n migrated['__orphaned'] = {\n ...(migrated['__orphaned'] as Record<string, unknown> | undefined),\n ...orphaned,\n }\n }\n return migrated\n}\n\nfunction applyRenames(fields: FormSchema['model']['fields'], data: Record<string, unknown>): void {\n for (const field of fields) {\n if (field.type === 'page') {\n applyRenames(field.fields ?? [], data)\n continue\n }\n if (field.renamedFrom !== undefined && !(field.key in data) && field.renamedFrom in data) {\n data[field.key] = data[field.renamedFrom]\n delete data[field.renamedFrom]\n }\n if (field.type === 'group' && typeof data[field.key] === 'object' && data[field.key] !== null) {\n applyRenames(field.fields ?? [], data[field.key] as Record<string, unknown>)\n }\n }\n}\n\n/** Remove and return the value at a dotted path, pruning nothing else. */\nfunction takeAtDottedPath(data: Record<string, unknown>, dottedPath: string): unknown {\n const segments = dottedPath.split('.')\n let parent: Record<string, unknown> = data\n for (const segment of segments.slice(0, -1)) {\n const next = parent[segment]\n if (next === null || typeof next !== 'object' || Array.isArray(next)) return undefined\n parent = next as Record<string, unknown>\n }\n const last = segments[segments.length - 1]!\n const value = parent[last]\n delete parent[last]\n return value\n}\n\n/** JSON-safe deep clone: draft data is JSON by construction, and this package\n * runs without structuredClone in its lib on purpose. */\nfunction structuredCloneJson<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport interface ListedForm {\n path: string\n title: string\n version: number\n schemaHash: string\n}\n\n/** Every form with its current version — the admin's landing view. */\nexport async function listForms(deps: ServerDeps): Promise<ListedForm[]> {\n const forms = await deps.storage.listForms()\n const listed: ListedForm[] = []\n for (const form of forms) {\n if (form.currentVersionId === null) continue\n const current = await deps.storage.getVersionById(form.currentVersionId)\n if (current === undefined) continue\n listed.push({\n path: form.path,\n title: current.schema.title,\n version: current.version,\n schemaHash: current.schemaHash,\n })\n }\n return listed\n}\n\nexport interface ListedVersion {\n version: number\n schemaHash: string\n title: string\n}\n\n/** A form's version history, newest first — metadata only, because a history\n * view rendering fifty full schemas would be its own denial of service. */\nexport async function listVersions(\n deps: ServerDeps,\n path: string,\n): Promise<ListedVersion[] | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n const versions = await deps.storage.listVersionsByForm(form.id)\n return versions.map((version) => ({\n version: version.version,\n schemaHash: version.schemaHash,\n title: version.schema.title,\n }))\n}\n","import { check } from 'recheck'\nimport type { FieldDef, FormSchema } from '@formancy/spec'\n\n/**\n * Refuse a form whose `pattern` values can be made to backtrack.\n *\n * A form author supplies these, and the server runs them against whatever a\n * submitter typed. That makes a regular expression an author-side denial of\n * service vector: one bad pattern, published once, and every submission after\n * it can be turned into unbounded CPU by anyone who can reach the form.\n *\n * Checked at PUBLISH time rather than at match time, because there is no way\n * to time out a JavaScript regular expression once it has started. The only\n * moment the cost can be refused is before the pattern is stored.\n *\n * The check is static — `recheck` analyses the expression rather than running\n * it — so it costs nothing per submission, and it found a polynomial case in\n * formancy's own built-in email format before it was ever pointed at a user's.\n */\n\n// Which engine recheck uses is chosen by the HOST, not here: this package has\n// no @types/node by design, so it cannot read an environment variable, and\n// that boundary is worth more than the convenience of setting one.\n// `@formancy/server` pins `RECHECK_BACKEND=pure` at startup and says why.\n// Absent that, recheck picks for itself and falls back to the pure engine,\n// which reaches the same verdicts on every case in the test suite.\n\nexport interface UnsafePattern {\n /** Data path of the field carrying it, e.g. `contacts[].code`. */\n path: string\n pattern: string\n /** `exponential` or `polynomial`, with the degree where there is one. */\n complexity: string\n}\n\n/** Every `pattern` in the model, with the data path of the field it sits on. */\nfunction patternsIn(fields: readonly FieldDef[], prefix: string): Array<{ path: string; pattern: string }> {\n const found: Array<{ path: string; pattern: string }> = []\n\n for (const field of fields) {\n // Pages are transparent to data paths; groups nest; a repeater's template\n // fields are addressed with `[]`. Same walk as everything else.\n const path =\n field.type === 'page'\n ? prefix\n : field.type === 'repeater'\n ? `${prefix}${field.key}[].`\n : field.type === 'group'\n ? `${prefix}${field.key}.`\n : `${prefix}${field.key}`\n\n if (typeof field.pattern === 'string' && field.pattern !== '') {\n found.push({ path, pattern: field.pattern })\n }\n if (field.fields !== undefined) found.push(...patternsIn(field.fields, path))\n }\n\n return found\n}\n\n/**\n * The patterns in this schema that an attacker could exploit, or an empty array\n * when there are none.\n *\n * A pattern `recheck` cannot decide about is ACCEPTED. Refusing on `unknown`\n * would make publishing depend on an analysis timeout, which is a worse failure\n * than the one being prevented — an author whose legitimate form stops\n * publishing intermittently has no way to act on that.\n */\nexport async function unsafePatterns(schema: FormSchema): Promise<UnsafePattern[]> {\n const unsafe: UnsafePattern[] = []\n\n for (const { path, pattern } of patternsIn(schema.model.fields, '')) {\n // Anchored the same way the engine anchors it, so what is analysed is what\n // will run. An unanchored analysis can miss a case the anchors create.\n const diagnostics = await check(`^(?:${pattern})$`, '')\n if (diagnostics.status !== 'vulnerable') continue\n\n const complexity = diagnostics.complexity\n unsafe.push({\n path,\n pattern,\n complexity:\n complexity === undefined\n ? 'unknown'\n : complexity.type === 'polynomial'\n ? `polynomial degree ${String(complexity.degree)}`\n : complexity.type,\n })\n }\n\n return unsafe\n}\n","import type { AuditEntry } from '../audit.js'\nimport type {\n DeliveryRecord,\n WebhookRecord, ApiKeyRecord, DraftRecord, FileRecord, FormRecord, FormVersionRecord, Storage, SubmissionRecord, UserRecord } from '../ports.js'\n\n/**\n * The in-memory Storage — the second implementation that keeps the port\n * honest, and the one unit tests run against. Deliberately dumb: Maps and\n * scans, no cleverness to accidentally diverge from SQL semantics.\n */\nexport function createMemoryStorage(): Storage {\n const forms = new Map<string, FormRecord>()\n const webhooks = new Map<string, WebhookRecord>()\n const deliveries = new Map<string, DeliveryRecord>()\n const versions = new Map<string, FormVersionRecord>()\n const submissions: SubmissionRecord[] = []\n const audits: AuditEntry[] = []\n const spentChallenges = new Map<string, string>()\n const drafts = new Map<string, DraftRecord>()\n const users = new Map<string, UserRecord>()\n const apiKeys = new Map<string, ApiKeyRecord>()\n const files = new Map<string, FileRecord>()\n\n return {\n getFormByPath: async (path) => [...forms.values()].find((form) => form.path === path),\n\n listForms: async () => [...forms.values()].map((form) => ({ ...form })),\n\n updateFormAccess: async (formId, access) => {\n const form = forms.get(formId)\n if (form !== undefined) forms.set(formId, { ...form, ...access })\n },\n\n createForm: async (record) => {\n forms.set(record.id, { ...record })\n },\n\n setCurrentVersion: async (formId, versionId) => {\n const form = forms.get(formId)\n if (form === undefined) throw new Error(`No form \"${formId}\"`)\n form.currentVersionId = versionId\n },\n\n insertVersion: async (record) => {\n versions.set(record.id, { ...record })\n },\n\n publishVersion: async ({ form, version, audit }) => {\n // One step, like the transaction it stands in for. A form whose pointer\n // was never set resolves to nothing, so all of this lands or none does.\n if (form !== undefined) forms.set(form.id, { ...form })\n const target = forms.get(version.formId)\n if (target === undefined) throw new Error(`No form \"${version.formId}\"`)\n versions.set(version.id, { ...version })\n target.currentVersionId = version.id\n if (audit !== undefined) audits.push({ ...audit })\n },\n\n getVersionById: async (id) => versions.get(id),\n\n findVersionByHash: async (formId, hash) =>\n [...versions.values()].find(\n (version) => version.formId === formId && version.schemaHash === hash,\n ),\n\n latestVersionNumber: async (formId) =>\n Math.max(\n 0,\n ...[...versions.values()]\n .filter((version) => version.formId === formId)\n .map((version) => version.version),\n ),\n\n webhooksForForm: async (formId) =>\n [...webhooks.values()].filter((hook) => hook.formId === formId).map((hook) => ({ ...hook })),\n\n insertWebhook: async (record) => {\n webhooks.set(record.id, { ...record })\n },\n\n updateWebhook: async (record) => {\n webhooks.set(record.id, { ...record })\n },\n\n listWebhooks: async () => [...webhooks.values()].map((w) => ({ ...w })),\n\n deadDeliveries: async (limit) =>\n [...deliveries.values()]\n .filter((delivery) => delivery.state === 'dead')\n .slice(0, limit)\n .map((delivery) => ({ ...delivery })),\n\n getDelivery: async (id) => {\n const found = deliveries.get(id)\n return found === undefined ? undefined : { ...found }\n },\n\n claimDueDeliveries: async (nowIso, limit) =>\n [...deliveries.values()]\n .filter((entry) => entry.state === 'pending' && entry.nextAttemptAt <= nowIso)\n .sort((a, b) => a.nextAttemptAt.localeCompare(b.nextAttemptAt))\n .slice(0, limit)\n .map((entry) => ({ ...entry })),\n\n updateDelivery: async (record) => {\n deliveries.set(record.id, { ...record })\n },\n\n insertFile: async (record) => {\n files.set(record.id, { ...record })\n },\n\n getFile: async (id) => {\n const found = files.get(id)\n return found === undefined ? undefined : { ...found }\n },\n\n updateFile: async (record) => {\n files.set(record.id, { ...record })\n },\n\n // A claimed file belongs to a submission and is never rubbish, however\n // old. Age is a reason to collect an unclaimed one, never a claimed one.\n abandonedFiles: async (beforeIso) =>\n [...files.values()]\n .filter((file) => file.state !== 'claimed' && file.createdAt < beforeIso)\n .map((file) => ({ ...file })),\n\n deleteFiles: async (ids) => {\n for (const id of ids) files.delete(id)\n },\n\n insertSubmission: async (record, queued, claimFileIds, audit) => {\n // One step, like the real transaction it stands in for: both land or\n // neither does.\n submissions.push({ ...record })\n for (const delivery of queued ?? []) deliveries.set(delivery.id, { ...delivery })\n for (const id of claimFileIds ?? []) {\n const file = files.get(id)\n // Same call, same commit: a submission exists if and only if the\n // files it names belong to it.\n if (file !== undefined) {\n files.set(id, { ...file, state: 'claimed', submissionId: record.id })\n }\n }\n // Same commit again: a submission that rolled back must leave no trace\n // saying it happened.\n if (audit !== undefined) audits.push({ ...audit })\n },\n\n spendChallenge: async (challenge, expiresAtIso) => {\n // One map operation, standing in for the unique constraint the real\n // storage relies on: both callers cannot win.\n if (spentChallenges.has(challenge)) return false\n spentChallenges.set(challenge, expiresAtIso)\n return true\n },\n\n forgetExpiredChallenges: async (beforeIso) => {\n let dropped = 0\n for (const [challenge, expires] of [...spentChallenges]) {\n if (expires < beforeIso) {\n spentChallenges.delete(challenge)\n dropped += 1\n }\n }\n return dropped\n },\n\n recordAudit: async (entry) => {\n audits.push({ ...entry })\n },\n\n listAudit: async (limit) => [...audits].reverse().slice(0, limit),\n\n listSubmissions: async () => [...submissions],\n\n listSubmissionsByForm: async (formId) =>\n submissions.filter((record) => record.formId === formId).reverse(),\n\n listVersionsByForm: async (formId) =>\n [...versions.values()]\n .filter((version) => version.formId === formId)\n .sort((a, b) => b.version - a.version),\n\n upsertDraft: async (record) => {\n drafts.set(record.formId + ':' + record.id, { ...record })\n },\n\n getDraft: async (formId, draftId) => drafts.get(formId + ':' + draftId),\n\n insertUser: async (record) => {\n users.set(record.id, { ...record })\n },\n\n getUserByEmail: async (email) =>\n [...users.values()].find((user) => user.email === email),\n\n insertApiKey: async (record) => {\n apiKeys.set(record.id, { ...record })\n },\n\n listApiKeys: async () => [...apiKeys.values()].map((key) => ({ ...key })),\n\n findApiKeysByPrefix: async (prefix) =>\n [...apiKeys.values()].filter((key) => key.prefix === prefix),\n\n revokeApiKey: async (id, atIso) => {\n const key = apiKeys.get(id)\n if (key !== undefined) key.revokedAt = atIso\n },\n }\n}\n","import type { Storage } from './ports.js'\n\n/**\n * The management plane's identity and authorization, framework-free.\n *\n * Hashing and randomness are injected — this package cannot reach a CSPRNG or\n * argon2 on its own, the composition root hands them in — which keeps every\n * behaviour here testable with deterministic fakes while the real server gets\n * real cryptography.\n */\nexport type Role = 'admin' | 'editor' | 'viewer'\n\nexport interface Actor {\n kind: 'user' | 'apiKey'\n id: string\n role: Role\n}\n\nexport interface AuthDeps {\n storage: Storage\n newId(): string\n nowIso(): string\n /** At least 160 bits of entropy, URL-safe. Injected, never Math.random. */\n randomToken(): string\n hashSecret(secret: string): Promise<string>\n verifySecret(secret: string, hash: string): Promise<boolean>\n}\n\nconst MIN_PASSWORD_LENGTH = 10\n\nexport type CreateUserOutcome =\n | { ok: true; id: string }\n | { ok: false; message: string }\n\nexport async function createLocalUser(\n deps: AuthDeps,\n input: { email: string; password: string; role: Role },\n): Promise<CreateUserOutcome> {\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n return { ok: false, message: `A password needs at least ${MIN_PASSWORD_LENGTH} characters.` }\n }\n if ((await deps.storage.getUserByEmail(input.email)) !== undefined) {\n return { ok: false, message: 'That email is already in use.' }\n }\n\n const id = deps.newId()\n await deps.storage.insertUser({\n id,\n email: input.email,\n passwordHash: await deps.hashSecret(input.password),\n role: input.role,\n createdAt: deps.nowIso(),\n })\n return { ok: true, id }\n}\n\nexport type AuthOutcome = { ok: true; actor: Actor } | { ok: false }\n\n/**\n * A wrong password and an unknown email answer identically, and BOTH run a\n * hash verification — the difference in shape or timing would be a\n * user-enumeration oracle.\n */\nexport async function authenticateLocal(\n deps: AuthDeps,\n input: { email: string; password: string },\n): Promise<AuthOutcome> {\n const user = await deps.storage.getUserByEmail(input.email)\n const hash = user?.passwordHash ?? (await deps.hashSecret(`decoy:${deps.newId()}`))\n const verified = await deps.verifySecret(input.password, hash)\n if (user === undefined || !verified) return { ok: false }\n return { ok: true, actor: { kind: 'user', id: user.id, role: user.role } }\n}\n\n/** The visible prefix of an API key: enough to index and to name in a UI\n * (\"fmc_tok_a2c…\"), never enough to authenticate. */\nconst KEY_PREFIX_LENGTH = 12\n\nexport type CreateApiKeyOutcome =\n | { ok: true; id: string; secret: string }\n | { ok: false; message: string }\n\nexport async function createApiKey(\n deps: AuthDeps,\n input: { name: string; role: Role },\n): Promise<CreateApiKeyOutcome> {\n if (input.name.trim() === '') return { ok: false, message: 'An API key needs a name.' }\n\n // The secret exists in full exactly once — in this return value. Storage\n // holds the prefix (for lookup and display) and a hash (for verification).\n const secret = `fmc_${deps.randomToken()}`\n const id = deps.newId()\n await deps.storage.insertApiKey({\n id,\n name: input.name,\n prefix: secret.slice(0, KEY_PREFIX_LENGTH),\n secretHash: await deps.hashSecret(secret),\n role: input.role,\n createdAt: deps.nowIso(),\n revokedAt: null,\n })\n return { ok: true, id, secret }\n}\n\nexport async function authenticateApiKey(deps: AuthDeps, presented: string): Promise<AuthOutcome> {\n const candidates = await deps.storage.findApiKeysByPrefix(presented.slice(0, KEY_PREFIX_LENGTH))\n for (const candidate of candidates) {\n if (candidate.revokedAt !== null) continue\n if (await deps.verifySecret(presented, candidate.secretHash)) {\n return { ok: true, actor: { kind: 'apiKey', id: candidate.id, role: candidate.role } }\n }\n }\n return { ok: false }\n}\n\n/**\n * The whole authorization model, in one table a reviewer can read top to\n * bottom. Deliberately NOT Postgres RLS in this phase: RLS couples the app to\n * per-request database roles and hides authorization from the test suite.\n * An unknown action is denied — permissions are granted by presence, never by\n * omission.\n */\nexport type Action =\n | 'form.read'\n | 'form.publish'\n | 'submission.read'\n | 'submission.export'\n | 'draft.write'\n | 'user.create'\n | 'apiKey.create'\n\nconst VIEWER_ACTIONS: readonly Action[] = ['form.read', 'submission.read']\nconst EDITOR_ACTIONS: readonly Action[] = [\n ...VIEWER_ACTIONS,\n 'form.publish',\n 'submission.export',\n 'draft.write',\n]\nconst ADMIN_ACTIONS: readonly Action[] = [...EDITOR_ACTIONS, 'user.create', 'apiKey.create']\n\nconst PERMISSIONS: Record<Role, ReadonlySet<Action>> = {\n viewer: new Set(VIEWER_ACTIONS),\n editor: new Set(EDITOR_ACTIONS),\n admin: new Set(ADMIN_ACTIONS),\n}\n\nexport function can(actor: Actor, action: Action): boolean {\n return PERMISSIONS[actor.role].has(action)\n}\n","/**\n * Deciding whether an address is somewhere this server may fetch from.\n *\n * A self-hosted formancy sits inside somebody's private network and webhook\n * URLs are written by whoever can edit a form, which makes the server a\n * confused deputy: it can reach the database, the metadata service and every\n * internal admin panel, and it will fetch whatever it is told to.\n *\n * This half is pure — given an address, is it private — so it lives here and is\n * tested exhaustively. The rest lives in @formancy/server, because it cannot be\n * pure: parsing a URL needs `URL`, and resolving a hostname then connecting to\n * the address that was checked, rather than re-resolving, needs Node's DNS and\n * a custom agent. This package has no @types/node by design, and hand-rolling\n * a URL parser for a security check is how such a check gets quietly wrong.\n * All three parts are necessary. Validating a URL and then handing it to fetch is defeated by DNS\n * rebinding — the name resolves to a public address for the check and a private\n * one for the request — and that is the difference between a mitigation and\n * theatre.\n */\n\n/** Everything before the first dot, as four octets, or undefined. */\nfunction ipv4Octets(address: string): [number, number, number, number] | undefined {\n const parts = address.split('.')\n if (parts.length !== 4) return undefined\n\n const octets = parts.map((part) => {\n // Reject '', '1e2', '0x7f', ' 1' and leading zeros that some parsers read\n // as octal. Only plain decimal counts.\n if (!/^\\d{1,3}$/.test(part)) return Number.NaN\n return Number(part)\n })\n\n if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) return undefined\n return octets as [number, number, number, number]\n}\n\n/**\n * Whether this address is one the server must not be pointed at.\n *\n * Unparseable input counts as private. An address this cannot classify is one\n * it cannot vouch for, and the safe direction is refusing to fetch.\n */\nexport function isPrivateAddress(address: string): boolean {\n const trimmed = address.trim().toLowerCase()\n if (trimmed === '') return true\n\n // Strip a zone index (`fe80::1%eth0`) and brackets (`[::1]`).\n const bare = trimmed.replace(/^\\[|\\]$/g, '').split('%')[0] ?? ''\n\n // ::ffff:127.0.0.1 is loopback wearing a hat. Unwrap before classifying, or\n // every private range walks straight through the IPv6 branch.\n const mapped = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(bare)\n if (mapped?.[1] !== undefined) return isPrivateAddress(mapped[1])\n\n const octets = ipv4Octets(bare)\n if (octets !== undefined) {\n const [a, b] = octets\n if (a === 0) return true // \"this network\" — 0.0.0.0 is a local wildcard\n if (a === 10) return true // RFC1918\n if (a === 127) return true // loopback\n if (a === 169 && b === 254) return true // link-local, including 169.254.169.254\n if (a === 172 && b >= 16 && b <= 31) return true // RFC1918\n if (a === 192 && b === 168) return true // RFC1918\n if (a === 100 && b >= 64 && b <= 127) return true // carrier-grade NAT\n if (a === 192 && b === 0) return true // IETF protocol assignments / test nets\n if (a === 198 && (b === 18 || b === 19)) return true // benchmarking\n if (a >= 224) return true // multicast and reserved, up to 255.255.255.255\n return false\n }\n\n if (bare.includes(':')) {\n if (bare === '::' || bare === '::1') return true\n // fc00::/7 unique-local, fe80::/10 link-local.\n if (/^f[cd]/.test(bare)) return true\n if (/^fe[89ab]/.test(bare)) return true\n if (/^ff/.test(bare)) return true // multicast\n return false\n }\n\n // Not an address at all — a hostname, or nonsense. The caller resolves\n // hostnames and checks what comes back; anything else fails closed.\n return true\n}\n\n/**\n * Whether this is a literal IP address rather than a hostname.\n *\n * The two need different treatment and conflating them breaks the check in\n * both directions: `isPrivateAddress` fails closed on anything it cannot\n * parse, which is correct for an address that came back from DNS and wrong for\n * `example.ch`, where it would refuse every hostname in the world.\n */\nexport function isIpLiteral(host: string): boolean {\n const bare = host.trim().replace(/^\\[|\\]$/g, '')\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(bare)) return true\n // Anything with a colon is IPv6; a hostname cannot contain one.\n return bare.includes(':')\n}\n","import { schemaHash } from '@formancy/spec'\nimport type { FormSchema } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { SchemaError } from '@formancy/spec/validate'\nimport type { ServerDeps } from './deps.js'\nimport type { ResolvedForm } from './use-cases.js'\nimport { createFormEngine, expressionProblems, unknownReferences } from '@formancy/core'\nimport { sourceNamesIn } from './options-membership.js'\nimport type { AuditEntry } from './audit.js'\nimport type { Actor } from './auth.js'\nimport { unsafePatterns } from './redos.js'\nimport type { UnsafePattern } from './redos.js'\n\n\n/**\n * Publishing a form: the save gate, and who may overwrite whom.\n *\n * Its own file because `use-cases.ts` had become the place every family went,\n * and the size budget refused the next thing added to it — the entry already\n * said the seam was one use-case family per file, and this is the family.\n */\nexport type PublishOutcome =\n | {\n ok: true\n formId: string\n versionId: string\n version: number\n schemaHash: string\n /**\n * Things worth telling the publisher that are not grounds to refuse.\n *\n * Always present, empty when there is nothing to say, so a caller reads a\n * list rather than branching on whether there is one. Sentences rather\n * than codes: the only consumers are a person reading a CLI, an admin\n * screen or a log, and a code would need a catalogue nobody would write\n * ([0097](../../../docs/decisions/0097-a-publish-may-warn.md)).\n */\n warnings: readonly string[]\n }\n | { ok: false; kind: 'invalid_schema'; errors?: SchemaError[] }\n | { ok: false; kind: 'invalid_logic'; message: string }\n | { ok: false; kind: 'unsafe_pattern'; patterns: UnsafePattern[] }\n /** The document names a list this deployment has never heard of. */\n | { ok: false; kind: 'unknown_options_source'; sources: string[] }\n /**\n * Somebody else published while this editor was editing.\n *\n * The same kind and the same body the submission path uses, deliberately: a\n * client that already knows how to handle one stale version handles both.\n */\n | { ok: false; kind: 'version_changed'; current?: ResolvedForm }\n /**\n * This exact document is already a version of this form, and not the current\n * one — somebody republishing an older version to undo a bad publish.\n *\n * `UNIQUE (form_id, schema_hash)` is what makes republishing the CURRENT\n * document idempotent rather than a version factory, and it refuses this too:\n * without this branch the insert raised and the route answered **500**. A\n * published version is immutable and cannot be published twice\n * ([0025](../../../docs/decisions/0025-immutability-in-the-database.md)), so\n * the honest answer names the version it already is.\n */\n | { ok: false; kind: 'already_published'; version: number }\n\n/**\n * Publish a schema as a form's next version.\n *\n * This is the save gate: structural validation, then the ENGINE's own compile\n * — type-checked expressions, kind policies and the cycle graph — so a form\n * that can loop or read a ghost field is refused here and never persisted.\n * Republishing the identical document is idempotent, because \"deploy again\"\n * must never manufacture a version.\n */\nexport async function publishForm(\n deps: ServerDeps,\n input: {\n path: string\n schema: unknown\n actor?: Actor\n /**\n * The hash of the version this edit started from, when there was one.\n *\n * **Optional, and that is the decision.** A script, the CLI and an agent\n * publish a document they composed rather than one they opened, and have\n * nothing to declare; requiring it would make every one of them read the\n * current version first to satisfy a rule about editors. What declares is\n * the thing that OPENED a version — the builder — and declaring is how it\n * asks to be told it has been overtaken.\n */\n basedOnSchemaHash?: string\n },\n): Promise<PublishOutcome> {\n const validated = validateSchema(input.schema)\n if (!validated.valid) return { ok: false, kind: 'invalid_schema', errors: validated.errors }\n const schema: FormSchema = validated.schema\n\n try {\n createFormEngine({ schema, capabilities: deps.capabilities })\n } catch (error) {\n return {\n ok: false,\n kind: 'invalid_logic',\n message: error instanceof Error ? error.message : String(error),\n }\n }\n\n // Expressions that compile and then fail for every value anybody enters.\n // The engine cannot refuse these — it declares leaves as `dyn` so that a\n // half-typed answer is not a type error, which also makes `seats * 4` look\n // fine until it runs. Refused here rather than at render, so a form already\n // published with the mistake keeps opening for whoever is filling it in.\n const problems = expressionProblems(schema)\n if (problems.length > 0) {\n return {\n ok: false,\n kind: 'invalid_logic',\n message: problems.map((problem) => problem.message).join('\\n'),\n }\n }\n\n /*\n * And the ones that are worth saying rather than refusing.\n *\n * A rule reading a path no field provides is a real defect — it evaluates to\n * nothing for the life of an immutable version — and refusing it is not\n * available: documents valid today would become invalid tomorrow, and what a\n * reader accepts is the frozen version contract. Computed before the storage\n * branches below so that every successful return carries the same answer,\n * including the idempotent one.\n */\n const warnings = unknownReferences(schema).map((found) => found.message)\n\n // Before the schema is persisted, because after it is there is no way to\n // time out a regular expression that has already started matching.\n const unsafe = await unsafePatterns(schema)\n if (unsafe.length > 0) {\n return {\n ok: false,\n kind: 'unsafe_pattern',\n patterns: unsafe,\n }\n }\n\n // The source names a document uses, checked against the vocabulary this deployment\n // actually has — but only when it HAS one. With no `optionsSources` configured the\n // server cannot judge, so publishing is allowed and the audit detail records the\n // names, which is what lets an operator compare them with their configuration.\n //\n // Caught here rather than at the first submission, because a published version is\n // frozen forever: a form naming a list nobody can resolve is a form whose select\n // renders a message instead of a chooser, and nothing would have said so.\n if (deps.optionsSources !== undefined) {\n const unknown = sourceNamesIn(schema).filter((name) => deps.optionsSources?.[name] === undefined)\n if (unknown.length > 0) return { ok: false, kind: 'unknown_options_source', sources: unknown }\n }\n\n const hash = schemaHash(schema)\n const existing = await deps.storage.getFormByPath(input.path)\n\n if (existing !== undefined) {\n const current =\n existing.currentVersionId === null\n ? undefined\n : await deps.storage.getVersionById(existing.currentVersionId)\n if (current !== undefined && current.schemaHash === hash) {\n return {\n ok: true,\n formId: existing.id,\n versionId: current.id,\n version: current.version,\n schemaHash: hash,\n warnings,\n }\n }\n\n // AFTER the idempotent case on purpose. An editor whose document already\n // matches what is published has nothing to merge — somebody else wrote\n // exactly what they were going to write — and telling them to resolve a\n // conflict with themselves would be a worse answer than silence.\n if (input.basedOnSchemaHash !== undefined && input.basedOnSchemaHash !== current?.schemaHash) {\n return {\n ok: false,\n kind: 'version_changed',\n ...(current === undefined\n ? {}\n : {\n current: {\n formId: existing.id,\n versionId: current.id,\n version: current.version,\n schema: current.schema,\n schemaHash: current.schemaHash,\n },\n }),\n }\n }\n\n // An older version of this form already holds this exact document. The\n // unique index would raise on the insert and the route answered 500 —\n // reverting by republishing yesterday's document is a reasonable thing to\n // try, and it crashed.\n const already = (await deps.storage.listVersionsByForm(existing.id)).find(\n (candidate) => candidate.schemaHash === hash,\n )\n if (already !== undefined) {\n return { ok: false, kind: 'already_published', version: already.version }\n }\n\n const version = (await deps.storage.latestVersionNumber(existing.id)) + 1\n const versionId = deps.newId()\n await deps.storage.publishVersion({\n version: { id: versionId, formId: existing.id, version, schema, schemaHash: hash },\n audit: publishAudit(deps, input, {\n path: input.path,\n version,\n hash,\n sources: sourceNamesIn(schema),\n }),\n })\n return { ok: true, formId: existing.id, versionId, version, schemaHash: hash, warnings }\n }\n\n // A base declared for a form that has none is an editor working from something\n // this deployment never published — a path that was deleted, or the wrong\n // instance. Publishing anyway would make their document version 1 of a form\n // they believe already has a history.\n if (input.basedOnSchemaHash !== undefined) {\n return { ok: false, kind: 'version_changed' }\n }\n\n const formId = deps.newId()\n const versionId = deps.newId()\n await deps.storage.publishVersion({\n form: {\n id: formId,\n path: input.path,\n currentVersionId: null,\n // A newly published form is private. Opening it is a separate,\n // deliberate act through setFormAccess.\n accessSubmit: 'authenticated',\n allowedOrigins: null,\n },\n version: { id: versionId, formId, version: 1, schema, schemaHash: hash },\n audit: publishAudit(deps, input, {\n path: input.path,\n version: 1,\n hash,\n sources: sourceNamesIn(schema),\n }),\n })\n return { ok: true, formId, versionId, version: 1, schemaHash: hash, warnings }\n}\n\n/**\n * The audit row for a publish, built here so it can travel INSIDE the\n * transaction rather than being appended after it.\n *\n * Before this it was written by the route once all three storage calls had\n * returned, which meant a publish could half-apply and still be recorded as\n * having happened. Now it rolls back with everything else.\n */\nfunction publishAudit(\n deps: ServerDeps,\n input: { actor?: Actor },\n about: { path: string; version: number; hash: string; sources: readonly string[] },\n): AuditEntry {\n return {\n id: deps.newId(),\n at: deps.nowIso(),\n action: 'form.published',\n subject: about.path,\n ...(input.actor === undefined\n ? {}\n : { actorKind: input.actor.kind, actorId: input.actor.id }),\n detail: {\n version: about.version,\n schemaHash: about.hash,\n /*\n * The lists this version needs the deployment to resolve, named in the record that\n * freezes it -- and only when there are any, so an ordinary form's audit row does\n * not grow a field saying \"none\".\n *\n * `SAFETY-ANALYSIS.md` A7 tells a manufacturer to treat `optionsSource` as a\n * per-field weakening of A6's guarantee, \"enumerable from the publish audit\n * detail\". It was not: this recorded the version and the hash and nothing else, so\n * the document described a record that did not exist. Written here rather than the\n * sentence being softened, because an operator comparing a form's sources with\n * their own configuration is exactly the use the sentence names.\n *\n * Joined into a string because an audit detail is flat by design -- one level, no\n * nesting, so a reader never has to walk it.\n */\n ...(about.sources.length === 0 ? {} : { optionsSources: [...about.sources].sort().join(', ') }),\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formancy/server-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The formancy backend's use-cases: publish, resolve, revalidate, store — framework-free, against repository ports.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,9 +17,9 @@
17
17
  "NOTICE"
18
18
  ],
19
19
  "dependencies": {
20
- "@formancy/challenge": "0.3.0",
21
- "@formancy/core": "0.3.0",
22
- "@formancy/spec": "0.3.0",
20
+ "@formancy/challenge": "0.4.0",
21
+ "@formancy/core": "0.4.0",
22
+ "@formancy/spec": "0.4.0",
23
23
  "@noble/hashes": "^2.4.0",
24
24
  "recheck": "^4.5.0"
25
25
  },