@formancy/server-core 0.10.0 → 0.12.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
@@ -1,6 +1,7 @@
1
1
  import { CapabilitySource, Check } from "@formancy/core";
2
2
  import { CHALLENGE_TTL_SECONDS, Challenge, DEFAULT_MAX_NUMBER, Solution, VerifyOutcome, decodeSolution, encodeSolution, hashOf, mintChallenge, solveChallenge, verifySolution } from "@formancy/challenge";
3
3
  import { Change, FormSchema } from "@formancy/spec";
4
+ import { QuestionSummary } from "@formancy/core/summary";
4
5
  import { SchemaError } from "@formancy/spec/validate";
5
6
  import { Scenario } from "@formancy/core/authoring";
6
7
  //#region src/options-membership.d.ts
@@ -126,6 +127,11 @@ interface SubmissionRecord {
126
127
  data: unknown;
127
128
  submittedAt: string;
128
129
  }
130
+ /** Where a page of submissions ended: the last one's time and id, the order they are listed in. */
131
+ interface SubmissionPlace {
132
+ submittedAt: string;
133
+ id: string;
134
+ }
129
135
  interface UserRecord {
130
136
  id: string;
131
137
  email: string;
@@ -421,6 +427,12 @@ interface Storage {
421
427
  listSubmissions(): Promise<SubmissionRecord[]>;
422
428
  /** Newest first. */
423
429
  listSubmissionsByForm(formId: string): Promise<SubmissionRecord[]>;
430
+ /**
431
+ * At most `limit` of a form's submissions, newest first by time and then by id, and only those
432
+ * listed after `before` in that order (0222). Bounded by an index rather than a count of rows
433
+ * to skip, so a page costs the same at the end of a long list as at its start.
434
+ */
435
+ pageSubmissionsByForm(formId: string, limit: number, before?: SubmissionPlace): Promise<SubmissionRecord[]>;
424
436
  listVersionsByForm(formId: string): Promise<FormVersionRecord[]>;
425
437
  upsertDraft(record: DraftRecord): Promise<void>;
426
438
  getDraft(formId: string, draftId: string): Promise<DraftRecord | undefined>;
@@ -449,8 +461,8 @@ interface Actor {
449
461
  }
450
462
  interface AuthDeps {
451
463
  storage: Storage;
452
- newId(): string;
453
- nowIso(): string;
464
+ newId(this: void): string;
465
+ nowIso(this: void): string;
454
466
  /** At least 160 bits of entropy, URL-safe. Injected, never Math.random. */
455
467
  randomToken(): string;
456
468
  hashSecret(secret: string): Promise<string>;
@@ -854,8 +866,8 @@ export declare function fileFieldPaths(schema: FormSchema): string[];
854
866
  */
855
867
  interface ServerDeps {
856
868
  storage: Storage;
857
- newId(): string;
858
- nowIso(): string;
869
+ newId(this: void): string;
870
+ nowIso(this: void): string;
859
871
  /**
860
872
  * Validators this deployment answers, by the name a `check` rule gives.
861
873
  *
@@ -1166,6 +1178,60 @@ export declare function createSubmission(deps: ServerDeps, input: {
1166
1178
  token?: string;
1167
1179
  }): Promise<SubmissionOutcome>;
1168
1180
  //#endregion
1181
+ //#region src/submission-pages.d.ts
1182
+ type SubmissionPage =
1183
+ /** `next` is what asks for the page after this one, or `null` when this is the last. */
1184
+ {
1185
+ kind: 'page';
1186
+ submissions: ListedSubmission[];
1187
+ next: string | null;
1188
+ } |
1189
+ /** A cursor that names no place in the list. */
1190
+ {
1191
+ kind: 'bad_cursor';
1192
+ };
1193
+ /**
1194
+ * A form's submissions a page at a time, newest first (0222). Undefined for an unknown form, as
1195
+ * `listSubmissions` answers.
1196
+ *
1197
+ * **Keyset, not offset.** The next page is asked for from where the last one ended, a response's
1198
+ * time and id, so a response stored while somebody pages comes before the first page and moves
1199
+ * nothing after it. An offset would shift every row by one, and the next page would list again
1200
+ * the last row of the one before.
1201
+ *
1202
+ * **The cursor is opaque by contract and readable by design**, the time and the id joined by `_`,
1203
+ * because this package has no base64 to hide them with and there is nothing in them to hide: a
1204
+ * caller who may read the page may read both. One that names no place in the list is refused,
1205
+ * since read as the start it would list again every response the caller already has.
1206
+ */
1207
+ export declare function listSubmissionPage(deps: ServerDeps, path: string, { limit, after }: {
1208
+ limit: number;
1209
+ after?: string | undefined;
1210
+ }): Promise<SubmissionPage | undefined>;
1211
+ //#endregion
1212
+ //#region src/response-summaries.d.ts
1213
+ /** One version's responses, summarised by question against that version's own document. */
1214
+ interface VersionSummary {
1215
+ version: number;
1216
+ /** How many responses this version has. */
1217
+ responses: number;
1218
+ questions: QuestionSummary[];
1219
+ }
1220
+ /**
1221
+ * A form's responses summarised by question, one summary per version that has any, newest first
1222
+ * (0223). Undefined for an unknown form, as the listing answers.
1223
+ *
1224
+ * Counted here with `createSummary`, what `summariseAnswers` counts with, the function the admin
1225
+ * called when it counted in the browser, so the counting rules are written once (0206): each
1226
+ * version against its own document, whose questions may not be another's (0207), and in that
1227
+ * document's language, as the admin read it.
1228
+ *
1229
+ * **A page at a time** (0224): the responses are read from the storage as the listing pages them,
1230
+ * from where the last page ended, and each is counted as it is read, so the server holds a page of
1231
+ * them and the counts, never the form's every response.
1232
+ */
1233
+ export declare function summariseResponses(deps: ServerDeps, path: string): Promise<VersionSummary[] | undefined>;
1234
+ //#endregion
1169
1235
  //#region src/redos.d.ts
1170
1236
  /**
1171
1237
  * Refuse a form whose `pattern` values can be made to backtrack.
@@ -1588,5 +1654,5 @@ type BuilderRequestOutcome = Completion |
1588
1654
  */
1589
1655
  export declare function completeBuilderRequest(completer: Completer, request: BuilderRequest, cancellation: Cancellation): Promise<BuilderRequestOutcome>;
1590
1656
  //#endregion
1591
- export { type Action, type Actor, type AttemptOutcome, type AuditAction, type AuditDraft, type AuditEntry, type AuthDeps, type AuthOutcome, type BreakerState, type BuilderRequest, type BuilderRequestOutcome, CHALLENGE_TTL_SECONDS, type Cancellation, type Challenge, type ClaimOutcome, type Completer, type Completion, type CompletionPrompt, DEFAULT_MAX_NUMBER, type DeliveryHeaders, type DeliveryRecord, type DraftMigration, type DraftRecord, type ExamplesOutcome, type ExamplesRecord, type FileRecord, type FormExamples, type FormRecord, type FormToFill, type FormVersionOutcome, type FormVersionRecord, type KeepExamplesOutcome, 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 };
1657
+ export { type Action, type Actor, type AttemptOutcome, type AuditAction, type AuditDraft, type AuditEntry, type AuthDeps, type AuthOutcome, type BreakerState, type BuilderRequest, type BuilderRequestOutcome, CHALLENGE_TTL_SECONDS, type Cancellation, type Challenge, type ClaimOutcome, type Completer, type Completion, type CompletionPrompt, DEFAULT_MAX_NUMBER, type DeliveryHeaders, type DeliveryRecord, type DraftMigration, type DraftRecord, type ExamplesOutcome, type ExamplesRecord, type FileRecord, type FormExamples, type FormRecord, type FormToFill, type FormVersionOutcome, type FormVersionRecord, type KeepExamplesOutcome, 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 SubmissionPage, type SubmissionPlace, type SubmissionRecord, type UnsafePattern, type VerifyOutcome, type VersionSummary, type WebhookHealth, type WebhookRecord, decodeSolution, encodeSolution, hashOf, mintChallenge, solveChallenge, verifySolution };
1592
1658
  //# 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/uploads.ts","../src/deps.ts","../src/use-cases.ts","../src/submitting.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/address.ts","../src/webhook.ts","../src/publishing.ts","../src/examples.ts","../src/model.ts"],"mappings":"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"}
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/submitting.ts","../src/submission-pages.ts","../src/response-summaries.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/address.ts","../src/webhook.ts","../src/publishing.ts","../src/examples.ts","../src/model.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -3,7 +3,9 @@ import { CHALLENGE_TTL_SECONDS, DEFAULT_MAX_NUMBER, decodeSolution, encodeSoluti
3
3
  import { hmac } from "@noble/hashes/hmac.js";
4
4
  import { sha256 } from "@noble/hashes/sha2.js";
5
5
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
6
- import { diffSchemas, modelDataPaths, schemaHash } from "@formancy/spec";
6
+ import { PATTERN_FLAGS, anchoredPattern, diffSchemas, modelDataPaths, schemaHash } from "@formancy/spec";
7
+ import { createSummary } from "@formancy/core/summary";
8
+ import { createFormText } from "@formancy/core/words";
7
9
  import { check } from "recheck";
8
10
  import { validateSchema } from "@formancy/spec/validate";
9
11
  import { expressionProblems, runScenarios, unknownReferences } from "@formancy/core/authoring";
@@ -546,7 +548,7 @@ function cellValue(data, column) {
546
548
  if (current === void 0 || current === null) return "";
547
549
  if (typeof current === "object") return JSON.stringify(current);
548
550
  if (typeof current === "string" && FORMULA_STARTERS.has(current.charAt(0))) return "'" + current;
549
- return String(current);
551
+ return typeof current === "string" ? current : typeof current === "number" || typeof current === "boolean" ? String(current) : "";
550
552
  }
551
553
  function csvCell(value) {
552
554
  return /[",\n\r]/.test(value) ? `"${value.replaceAll("\"", "\"\"")}"` : value;
@@ -862,6 +864,80 @@ async function createSubmission(deps, input) {
862
864
  };
863
865
  }
864
866
  //#endregion
867
+ //#region src/submission-pages.ts
868
+ const MOST = 1e3;
869
+ async function listSubmissionPage(deps, path, { limit, after }) {
870
+ const form = await deps.storage.getFormByPath(path);
871
+ if (form === void 0) return void 0;
872
+ const before = after === void 0 ? void 0 : placeOf(after);
873
+ if (before === null) return { kind: "bad_cursor" };
874
+ const size = Math.min(Math.max(Number.isFinite(limit) ? Math.trunc(limit) : 1, 1), MOST);
875
+ const versions = await deps.storage.listVersionsByForm(form.id);
876
+ const versionNumberById = new Map(versions.map((version) => [version.id, version.version]));
877
+ const records = await deps.storage.pageSubmissionsByForm(form.id, size + 1, before);
878
+ const shown = records.slice(0, size);
879
+ const last = shown.at(-1);
880
+ return {
881
+ kind: "page",
882
+ submissions: shown.map((record) => ({
883
+ id: record.id,
884
+ version: versionNumberById.get(record.formVersionId) ?? 0,
885
+ submittedAt: record.submittedAt,
886
+ data: record.data
887
+ })),
888
+ next: records.length > size && last !== void 0 ? `${last.submittedAt}_${last.id}` : null
889
+ };
890
+ }
891
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
892
+ function placeOf(cursor) {
893
+ const at = cursor.lastIndexOf("_");
894
+ if (at === -1) return null;
895
+ const time = Date.parse(cursor.slice(0, at));
896
+ const id = cursor.slice(at + 1);
897
+ if (!Number.isFinite(time) || !UUID.test(id)) return null;
898
+ return {
899
+ submittedAt: new Date(time).toISOString(),
900
+ id
901
+ };
902
+ }
903
+ //#endregion
904
+ //#region src/response-summaries.ts
905
+ const PAGE = 1e3;
906
+ async function summariseResponses(deps, path) {
907
+ const form = await deps.storage.getFormByPath(path);
908
+ if (form === void 0) return void 0;
909
+ const versions = await deps.storage.listVersionsByForm(form.id);
910
+ const counting = new Map(versions.map((version) => [version.id, {
911
+ responses: 0,
912
+ summary: createSummary(version.schema, createFormText({ locale: version.schema.i18n?.defaultLocale ?? "" }))
913
+ }]));
914
+ let before;
915
+ for (;;) {
916
+ const page = await deps.storage.pageSubmissionsByForm(form.id, PAGE, before);
917
+ for (const record of page) {
918
+ const counted = counting.get(record.formVersionId);
919
+ if (counted === void 0) continue;
920
+ counted.responses += 1;
921
+ counted.summary.add(record.data);
922
+ }
923
+ const last = page.at(-1);
924
+ if (page.length < PAGE || last === void 0) break;
925
+ before = {
926
+ submittedAt: last.submittedAt,
927
+ id: last.id
928
+ };
929
+ }
930
+ return versions.flatMap((version) => {
931
+ const counted = counting.get(version.id);
932
+ if (counted === void 0 || counted.responses === 0) return [];
933
+ return [{
934
+ version: version.version,
935
+ responses: counted.responses,
936
+ questions: counted.summary.questions()
937
+ }];
938
+ });
939
+ }
940
+ //#endregion
865
941
  //#region src/redos.ts
866
942
  function patternsIn(fields, prefix) {
867
943
  const found = [];
@@ -878,7 +954,7 @@ function patternsIn(fields, prefix) {
878
954
  async function unsafePatterns(schema) {
879
955
  const unsafe = [];
880
956
  for (const { path, pattern } of patternsIn(schema.model.fields, "")) {
881
- const diagnostics = await check(`^(?:${pattern})$`, "");
957
+ const diagnostics = await check(anchoredPattern(pattern), PATTERN_FLAGS);
882
958
  if (diagnostics.status !== "vulnerable") continue;
883
959
  const complexity = diagnostics.complexity;
884
960
  unsafe.push({
@@ -1037,6 +1113,11 @@ function createMemoryStorage() {
1037
1113
  listAudit: async (limit) => [...audits].reverse().slice(0, limit),
1038
1114
  listSubmissions: async () => [...submissions],
1039
1115
  listSubmissionsByForm: async (formId) => submissions.filter((record) => record.formId === formId).reverse(),
1116
+ pageSubmissionsByForm: async (formId, limit, before) => {
1117
+ const placed = ({ submittedAt, id }) => [Date.parse(submittedAt), id];
1118
+ const newer = (a, b) => a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
1119
+ return submissions.filter((record) => record.formId === formId && (before === void 0 || newer(placed(before), placed(record)))).sort((a, b) => newer(placed(a), placed(b)) ? -1 : 1).slice(0, limit);
1120
+ },
1040
1121
  listVersionsByForm: async (formId) => [...versions.values()].filter((version) => version.formId === formId).sort((a, b) => b.version - a.version),
1041
1122
  upsertDraft: async (record) => {
1042
1123
  drafts.set(record.formId + ":" + record.id, { ...record });
@@ -1529,6 +1610,6 @@ async function completeBuilderRequest(completer, request, cancellation) {
1529
1610
  }
1530
1611
  }
1531
1612
  //#endregion
1532
- 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, completeBuilderRequest, createApiKey, createLocalUser, createMemoryStorage, createSubmission, decodeSolution, deliveryHeaders, drainOutbox, encodeSolution, exportCsv, fileFieldPaths, filesToClaim, formToFill, formVersion, hashOf, healthOf, isIpLiteral, isPrivateAddress, keepExamples, listForms, listSubmissions, listVersions, mayAttempt, mintChallenge, offerUpload, publishForm, readExamples, replayDelivery, resolveForm, resumeDraft, retryDelayMs, saveDraft, screenUpload, setFormAccess, signBody, signedPayload, solveChallenge, sourceNamesIn, sourcedAnswers, startDraft, unsafePatterns, verifySignature, verifySolution };
1613
+ 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, completeBuilderRequest, createApiKey, createLocalUser, createMemoryStorage, createSubmission, decodeSolution, deliveryHeaders, drainOutbox, encodeSolution, exportCsv, fileFieldPaths, filesToClaim, formToFill, formVersion, hashOf, healthOf, isIpLiteral, isPrivateAddress, keepExamples, listForms, listSubmissionPage, listSubmissions, listVersions, mayAttempt, mintChallenge, offerUpload, publishForm, readExamples, replayDelivery, resolveForm, resumeDraft, retryDelayMs, saveDraft, screenUpload, setFormAccess, signBody, signedPayload, solveChallenge, sourceNamesIn, sourcedAnswers, startDraft, summariseResponses, unsafePatterns, verifySignature, verifySolution };
1533
1614
 
1534
1615
  //# 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/signing.ts","../src/use-cases.ts","../src/submitting.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/auth.ts","../src/address.ts","../src/examples.ts","../src/publishing.ts","../src/model.ts"],"sourcesContent":["import { getAt } from '@formancy/core'\nimport 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\n for (const field of fields) {\n const at = prefix === '' ? field.key : `${prefix}.${field.key}`\n // What the submission holds, as the engine reads it: never a member of every object (0185).\n const held = getAt(data, [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 // Its own, never inherited: `constructor` names no list this deployment has (0185).\n const source = Object.hasOwn(sources, name) ? sources[name] : undefined\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 // What a form is checked against changed: its examples, or the sample they start from.\n // With counts, never the examples (0166).\n 'form.examples.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 // A form's words or its document sent to the operator's model, on a person's say-so and\n // at the operator's cost. Recorded with the kind and the size, never the text (0165).\n 'model.asked',\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 — and a row to the line a server's request log wrote\n * for that request. A use-case that writes its row inside its transaction\n * takes the id in its input, and names it when it is given one.\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 { getAt } from '@formancy/core'\nimport { 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 receivingUntil: 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\n for (const field of flattenPages(fields)) {\n // What the submission holds, as the engine reads it: never a member of every object (0185).\n const held = getAt(value, [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'\n\n/**\n * The keys the public plane hands out: a draft's and a response's, each an HMAC under the\n * deployment's signing key over what it opens.\n *\n * Stateless, the shape the proof-of-work challenge uses\n * ([0059](../../../docs/decisions/0059-proof-of-work-not-a-captcha.md)): handing one out is\n * public and unauthenticated, so it must not be a write — a key kept in a table is a table an\n * attacker fills by asking for keys they never use. Both in one file, so what each is signed\n * over can be read side by side, and nobody has to check two files to see that one cannot\n * stand in for the other.\n */\nfunction signed(secret: string, message: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(message)))\n}\n\n/**\n * The key to a draft: HMAC over the form and the draft id\n * ([0062](../../../docs/decisions/0062-a-draft-carries-its-own-key.md)).\n *\n * Bound to the FORM as well as the id so a key cannot be carried to a draft of another form\n * that happens to share an id. The message is unchanged since 0.2.0, which is what keeps every\n * key handed out since then opening its draft.\n */\nexport function draftKey(secret: string, formId: string, draftId: string): string {\n return signed(secret, `${formId}:${draftId}`)\n}\n\n/**\n * The token a response is sent with: the id it will be stored under, and an HMAC over the form\n * and that id ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\n *\n * The id travels in the clear because the server has to read it back, and the signature is\n * what makes it the server's choice rather than the sender's. The message begins with a word a\n * form id never is, so a draft's key — signed over `form:draft` — is never also a valid\n * signature for a response with the same id, and the reverse.\n */\nexport function submissionToken(secret: string, formId: string, id: string): string {\n return `${id}.${signed(secret, `submission:${formId}:${id}`)}`\n}\n\n/**\n * The id a token names, when this server signed it for this form; otherwise undefined.\n *\n * Read from the LAST dot, since the signature is hex and the id is whatever `newId` minted. A\n * forged token, another form's, one signed under a key this server no longer has, and one that\n * is not a token at all are the same answer: no id.\n */\nexport function submissionTokenId(secret: string, formId: string, token: string): string | undefined {\n const dot = token.lastIndexOf('.')\n if (dot <= 0) return undefined\n const id = token.slice(0, dot)\n return sameToken(token, submissionToken(secret, formId, id)) ? id : undefined\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 */\nexport function 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","import { getAt } from '@formancy/core'\nimport { diffSchemas } from '@formancy/spec'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { draftKey, sameToken, submissionToken } from './signing.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\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 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\n/** The answer a submission holds at a column, read as the engine reads one: its own, so an\n * unanswered `constructor` is an empty cell and not `function Object` (0185). */\nfunction cellValue(data: unknown, column: string): string {\n const current = getAt(data, column.split('.'))\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/**\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 *\n * A draft is one response, so it comes with the token that response is sent with, naming the\n * draft's own id ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)). Resuming\n * hands back the same one, so a response whose answer was lost and is sent again after a reload\n * is refused as already sent rather than stored twice.\n */\nexport async function startDraft(\n deps: ServerDeps,\n input: { path: string },\n): Promise<{ draftId: string; token: string; submissionToken: string } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n const draftId = deps.newId()\n return {\n draftId,\n token: draftKey(deps.draftSecret, current.formId, draftId),\n submissionToken: submissionToken(deps.draftSecret, current.formId, draftId),\n }\n}\n\n/** Save (or re-save) a partial form, bound to the CURRENT version. Undefined\n * for an unknown form. */\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, draftKey(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 /** What the response is sent with: the same on every resume, naming the draft's id. */\n submissionToken: string\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, draftKey(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 const sendWith = submissionToken(deps.draftSecret, current.formId, draft.id)\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 submissionToken: sendWith,\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 submissionToken: sendWith,\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 // What the draft holds as its own, not `in`: every object answers `constructor`, so a\n // rename to it was never applied and one from it moved `function Object` (0185).\n if (field.renamedFrom !== undefined && !Object.hasOwn(data, field.key) && Object.hasOwn(data, field.renamedFrom)) {\n data[field.key] = data[field.renamedFrom]\n delete data[field.renamedFrom]\n }\n const held = getAt(data, [field.key])\n if (field.type === 'group' && typeof held === 'object' && held !== null) {\n applyRenames(field.fields ?? [], held as Record<string, unknown>)\n }\n }\n}\n\n/** Remove and return the value at a dotted path, pruning nothing else — what the draft\n * holds there as its own, never what every object inherits. */\nfunction takeAtDottedPath(data: Record<string, unknown>, dottedPath: string): unknown {\n const segments = dottedPath.split('.')\n const parent = getAt(data, segments.slice(0, -1))\n const last = segments[segments.length - 1]!\n if (parent === null || typeof parent !== 'object' || Array.isArray(parent) || !Object.hasOwn(parent, last)) {\n return undefined\n }\n const value = (parent as Record<string, unknown>)[last]\n delete (parent as Record<string, unknown>)[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\n/** One version's document, or why there is none to give. */\nexport type FormVersionOutcome =\n | { ok: true; version: number; schemaHash: string; schema: FormSchema }\n | { ok: false; kind: 'unknown_form' | 'unknown_version' }\n\n/**\n * One published version's document, by its number (0207): what an older version's submissions are\n * read against, since they answered its questions and not the current one's. The history above\n * leaves documents out on purpose; this is one, asked for by name.\n *\n * **Looked up among this form's own versions**, so reading one form never reads another's: a\n * version of another form, by its id or by its number, is as unknown as one never made, and says\n * nothing about where else it exists.\n *\n * **One spelling per version** — the number's own decimal digits, no sign, no leading zero, no\n * fraction or exponent — so one document is not reachable under several addresses.\n */\nexport async function formVersion(deps: ServerDeps, path: string, version: string): Promise<FormVersionOutcome> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n if (!/^[1-9]\\d{0,8}$/.test(version)) return { ok: false, kind: 'unknown_version' }\n const found = (await deps.storage.listVersionsByForm(form.id)).find((candidate) => candidate.version === Number(version))\n if (found === undefined) return { ok: false, kind: 'unknown_version' }\n return { ok: true, version: found.version, schemaHash: found.schemaHash, schema: found.schema }\n}\n","import { createFormEngine } from '@formancy/core'\nimport { checkMembership } from './options-membership.js'\nimport { maySubmit } from './access.js'\nimport { filesToClaim } from './uploads.js'\nimport { resolveForm } from './use-cases.js'\nimport type { ResolvedForm } from './use-cases.js'\nimport { submissionToken, submissionTokenId } from './signing.js'\nimport type { ServerDeps } from './deps.js'\n\n/**\n * Submitting a form: what a respondent is handed, the replay, and what is stored.\n *\n * Its own file because `use-cases.ts` named it as the next family to leave when its size\n * budget refused growth, and the submission is what grew.\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 /** An anonymous response arrived without the token its form was handed out with. */\n | { ok: false; kind: 'token_required' }\n /**\n * A token this server did not sign for this form: forged, another form's, or signed under\n * a key the deployment no longer has. Loading the form again hands out a good one.\n */\n | { ok: false; kind: 'token_invalid' }\n /** The response this token names is already stored, as `id`; nothing was stored again. */\n | { ok: false; kind: 'token_spent'; id: string }\n\n/** A published form as a respondent is handed it: its current version, and what to send it with. */\nexport interface FormToFill extends ResolvedForm {\n /** Names the id the response will be stored under, signed for this form (0169). */\n submissionToken: string\n}\n\n/**\n * The form a respondent fills in, with the token their response is sent with.\n *\n * A fresh id on every call, because every reading is a response that might be sent: two\n * people handed one token would find the second refused as already sent. Minting writes\n * nothing — an HMAC over the form and the id — so handing one to everybody who reads the form\n * costs this server a hash and fills no table\n * ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\n */\nexport async function formToFill(deps: ServerDeps, path: string): Promise<FormToFill | undefined> {\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return undefined\n return { ...resolved, submissionToken: submissionToken(deps.draftSecret, resolved.formId, deps.newId()) }\n}\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 * And it is stored once. A response is stored under the id its token names, and the storage\n * refuses that id twice, so the same response sent again — a retry after an answer that never\n * arrived, a replay — is told it was sent rather than stored as somebody else\n * ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\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 requestId?: string\n /**\n * The token the form was handed out with. Required of an anonymous response. A signed-in\n * one may leave it out and is stored under a fresh id, as before; one it sends is checked\n * and spent like any other, because a token that is ignored when it is wrong is decoration.\n */\n token?: 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 const actor = input.actor ?? 'anonymous'\n if (!maySubmit(form, actor, input.origin)) {\n return { ok: false, kind: 'forbidden' }\n }\n\n // Before the version and the replay: once a response is stored, nothing else about a second\n // send of it matters, and a refusal for something else would invite a third.\n let id: string\n if (input.token === undefined) {\n if (actor === 'anonymous') return { ok: false, kind: 'token_required' }\n id = deps.newId()\n } else {\n const named = submissionTokenId(deps.draftSecret, form.id, input.token)\n if (named === undefined) return { ok: false, kind: 'token_invalid' }\n if (await deps.storage.hasSubmission(named)) return { ok: false, kind: 'token_spent', id: named }\n id = named\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 // Never undefined: an engine handed nothing starts a new form, with the author's defaults,\n // and a replay of nothing would store answers nobody sent (0197).\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 // 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 const stored = 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 ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\n // The version, so a reader can tell which schema this answered without\n // joining. Never the answers.\n detail: {\n submissionId: id,\n version: current.version,\n authenticated: input.actor === 'authenticated',\n },\n },\n )\n // Two sends of one response at once both found nothing stored above; the storage decided\n // which of them it is, and wrote nothing at all for the other.\n if (!stored) return { ok: false, kind: 'token_spent', id }\n return { ok: true, id, canonicalData: engine.value() }\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, ExamplesRecord, 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 const examples = new Map<string, ExamplesRecord>()\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 getExamples: async (formId) => {\n const found = examples.get(formId)\n return found === undefined ? undefined : { ...found }\n },\n\n keepExamples: async (record, audit) => {\n // The foreign key the real table has: examples are kept beside a form, so a record\n // for a form that is not here is refused rather than kept for whatever comes next.\n if (!forms.has(record.formId)) throw new Error(`No form \"${record.formId}\"`)\n examples.set(record.formId, { ...record })\n if (audit !== undefined) audits.push({ ...audit })\n },\n\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 // The three below are each one read and one write with no await between them, which\n // is what stands in for the conditional UPDATE: nothing else runs in the middle.\n leaseFile: async (id, nowIso, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.state !== 'offered') return false\n if (file.receivingUntil !== null && file.receivingUntil > nowIso) return false\n files.set(id, { ...file, receivingUntil: untilIso })\n return true\n },\n\n settleFile: async (id, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.state !== 'offered' || file.receivingUntil !== untilIso) {\n return false\n }\n files.set(id, { ...file, state: 'stored', receivingUntil: null })\n return true\n },\n\n releaseFile: async (id, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.receivingUntil !== untilIso) return false\n files.set(id, { ...file, receivingUntil: null })\n return true\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 // The primary key the real table has: a response sent twice is stored once, and the\n // second writes nothing at all.\n if (submissions.some((stored) => stored.id === record.id)) return false\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 return true\n },\n\n hasSubmission: async (id) => submissions.some((stored) => stored.id === id),\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 { comparedToLastRun, readScenario } from '@formancy/builder-core'\nimport type { ScenarioShapeProblem } from '@formancy/builder-core'\nimport { runScenarios } from '@formancy/core/authoring'\nimport type { Scenario } from '@formancy/core/authoring'\nimport type { FormSchema } from '@formancy/spec'\nimport { can } from './auth.js'\nimport type { Actor } from './auth.js'\nimport type { ServerDeps } from './deps.js'\nimport type { ExamplesRecord, Storage } from './ports.js'\n\n/**\n * A form's examples, kept by the deployment beside the form, and run when it is published\n * ([0166](../../../docs/decisions/0166-a-deployment-keeps-a-forms-examples-and-runs-them-at-publish.md)).\n *\n * An example with its answer written down is the one check that tells a condition that\n * compiles from the condition that was asked for\n * ([0110](../../../docs/decisions/0110-a-form-is-checked-against-examples.md)). The builders\n * run them, and the scenarios are the host's to keep\n * ([0111](../../../docs/decisions/0111-a-scenario-panel-names-what-stopped-holding.md)); on a\n * deployment the host is this server, so it keeps them, with the fictional sample they start\n * from, and publishing — the last moment before a version is frozen — runs them.\n *\n * **The permission is decided here, not in the route.** Reading and changing a form's\n * examples take what editing the form takes, `form.publish`, and a host composing its own\n * HTTP over this package gets that rule with the use-case rather than having to remember it.\n *\n * **What is kept is read as what is sent is read**, by `readList`, on the way in and on the\n * way out. A list sent to be kept with anything in it that is not an example keeps nothing; a\n * row already kept that is not one — edited around `keepExamples` — is left out and named\n * wherever the examples are read back, so the admin that draws them and the publish that runs\n * them are only ever handed examples.\n */\n\n/** A form's examples as a builder holds them: the list, and where every one starts. */\nexport interface FormExamples {\n readonly scenarios: readonly Scenario[]\n /** Absent when none was kept: every example then starts from an empty form. */\n readonly sample?: Readonly<Record<string, unknown>>\n /**\n * When they are read back: what is kept and is not an example, or not a sample, one sentence\n * each. Left out of the above, so nothing draws or runs it, and gone at the next save, which\n * keeps only what it is sent. Absent when everything kept could be read.\n */\n readonly unreadable?: readonly string[]\n}\n\nexport type ExamplesOutcome =\n | { readonly ok: true; readonly examples: FormExamples }\n /** Not with this actor's role: a viewer may read the form, and not what it is checked against. */\n | { readonly ok: false; readonly kind: 'forbidden' }\n /** Examples are kept beside a form, so there must be one at this path. */\n | { readonly ok: false; readonly kind: 'unknown_form' }\n\nexport type KeepExamplesOutcome =\n | ExamplesOutcome\n /** Something sent is not an example, or not a sample: one sentence each, and nothing kept. */\n | { readonly ok: false; readonly kind: 'invalid_examples'; readonly problems: readonly string[] }\n\n/** The examples kept for the form at `path`; none, for a form nobody wrote any for. */\nexport async function readExamples(\n deps: ServerDeps,\n input: { path: string; actor: Actor },\n): Promise<ExamplesOutcome> {\n if (!can(input.actor, 'form.publish')) return { ok: false, kind: 'forbidden' }\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n const kept = readKept(await deps.storage.getExamples(form.id))\n return {\n ok: true,\n examples: {\n ...asExamples(kept.scenarios, kept.sample),\n ...(kept.problems.length === 0 ? {} : { unreadable: kept.problems }),\n },\n }\n}\n\n/**\n * Replace the examples of the form at `path`, and the sample they start from.\n *\n * Each is read as an example the way a builder reads a model's draft — `readScenario`, one\n * decision for both — and a list with any item that is not one keeps nothing. Paths are not\n * checked against the published form: an example is written for the form being edited, which\n * may name a field no version has yet, and the runner says so when it runs.\n */\nexport async function keepExamples(\n deps: ServerDeps,\n /** `requestId` is the request this change answers, which its audit row names. */\n input: { path: string; actor: Actor; examples: unknown; requestId?: string },\n): Promise<KeepExamplesOutcome> {\n if (!can(input.actor, 'form.publish')) return { ok: false, kind: 'forbidden' }\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n const read = readExamplesBody(input.examples)\n if ('problems' in read) return { ok: false, kind: 'invalid_examples', problems: read.problems }\n\n await deps.storage.keepExamples(\n { formId: form.id, scenarios: read.scenarios, sample: read.sample, updatedAt: deps.nowIso() },\n {\n id: deps.newId(),\n at: deps.nowIso(),\n action: 'form.examples.changed',\n subject: input.path,\n actorKind: input.actor.kind,\n actorId: input.actor.id,\n ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\n // How many, and whether there is a sample: what changed, never what it says.\n detail: { examples: read.scenarios.length, sample: read.sample !== null },\n },\n )\n return { ok: true, examples: asExamples(read.scenarios, read.sample) }\n}\n\n/**\n * What publishing says about the form's examples: one sentence for each that held against the\n * version the form has and does not hold against the one being published.\n *\n * **Run as the server replays a submission**, in `server` mode, from the kept sample, and\n * compared by `comparedToLastRun` — the scenario panel's own verdict, so the publish cannot\n * call something a regression the panel would not\n * ([0091](../../../docs/decisions/0091-a-second-builder-is-a-binding.md)). An example that did\n * not hold before either is not this publish's doing, and one that holds again is good news;\n * neither is a warning.\n *\n * **It never refuses.** A publish may warn and never refuses over its examples\n * ([0097](../../../docs/decisions/0097-a-publish-may-warn.md)): a rule changed on purpose stops\n * its old example holding, and the person decides. A stored row that is not an example — one\n * edited around `keepExamples` — is read out before the run, as the admin reads it out, and\n * named; run as it was, the runner throws on it and every publish of the form is a 500.\n */\nexport async function examplesThatStopHolding(\n storage: Storage,\n formId: string,\n before: { readonly schema: FormSchema; readonly version: number } | undefined,\n after: { readonly schema: FormSchema; readonly version: number },\n): Promise<string[]> {\n if (before === undefined) return []\n const kept = readKept(await storage.getExamples(formId))\n const leftOut =\n kept.problems.length === 0\n ? []\n : [`Not everything kept with this form's examples could be read, and what could not was not run: ${kept.problems.join(' ')}`]\n if (kept.scenarios.length === 0) return leftOut\n\n const options = { mode: 'server' as const, ...(kept.sample === null ? {} : { initialValue: kept.sample }) }\n const now = runScenarios(after.schema, kept.scenarios, options)\n const { regressions } = comparedToLastRun(runScenarios(before.schema, kept.scenarios, options), now)\n const stopped = regressions.map((name) => {\n const failures = now.find((result) => result.name === name)?.failures ?? []\n return (\n `The example \"${name}\" held against version ${String(before.version)} and does not hold against ` +\n `version ${String(after.version)}: ${failures.map((failure) => failure.detail).join(' ')}`\n )\n })\n return [...stopped, ...leftOut]\n}\n\nfunction asExamples(scenarios: readonly Scenario[], sample: Readonly<Record<string, unknown>> | null): FormExamples {\n return { scenarios, ...(sample === null ? {} : { sample }) }\n}\n\ntype ReadBody =\n | { readonly scenarios: readonly Scenario[]; readonly sample: Readonly<Record<string, unknown>> | null }\n | { readonly problems: readonly string[] }\n\n/** `{ scenarios, sample? }` from a request body, or a sentence for everything that is not. */\nfunction readExamplesBody(body: unknown): ReadBody {\n if (!isRecord(body) || !Array.isArray(body['scenarios'])) {\n return { problems: ['Send { scenarios, sample? }, where scenarios is a list of examples.'] }\n }\n const read = readList(body['scenarios'], body['sample'])\n return read.problems.length > 0 ? { problems: read.problems } : { scenarios: read.scenarios, sample: read.sample }\n}\n\n/**\n * What a storage keeps, read as a list sent to be kept is read. The storage checks nothing\n * (`Storage.keepExamples`), so a row written around the use-case comes back here as it was\n * written, and only what reads as an example is handed on.\n */\nfunction readKept(kept: ExamplesRecord | undefined): ReadList {\n if (kept === undefined) return { scenarios: [], sample: null, problems: [] }\n const items: unknown = kept.scenarios\n if (Array.isArray(items)) return readList(items, kept.sample)\n const read = readList([], kept.sample)\n return { ...read, problems: ['What is kept as the examples is not a list of them.', ...read.problems] }\n}\n\ninterface ReadList {\n /** The items that are examples, in their order. */\n readonly scenarios: readonly Scenario[]\n readonly sample: Readonly<Record<string, unknown>> | null\n /** One sentence for each item that is not an example, and for a sample that is not answers. */\n readonly problems: readonly string[]\n}\n\n/** Each item through `readScenario`, names once each, and the sample as answers by field. */\nfunction readList(items: readonly unknown[], sample: unknown): ReadList {\n const problems: string[] = []\n if (sample !== undefined && sample !== null && !isRecord(sample)) {\n problems.push('The sample is the answers every example starts from, by field, and this is not that.')\n }\n\n const scenarios: Scenario[] = []\n const first = new Map<string, number>()\n items.forEach((item: unknown, index) => {\n const position = index + 1\n const read = readScenario(item)\n if (!read.ok) {\n problems.push(`${which(position, read.problem.name)} ${SHAPE_PROBLEMS[read.problem.reason](read.problem)}`)\n return\n }\n const earlier = first.get(read.scenario.name)\n if (earlier !== undefined) {\n problems.push(\n `${which(position, read.scenario.name)} has the name of example ${String(earlier)}; a name is how an example's result is found.`,\n )\n return\n }\n first.set(read.scenario.name, position)\n scenarios.push(read.scenario)\n })\n\n return { scenarios, sample: isRecord(sample) ? sample : null, problems }\n}\n\nfunction which(position: number, name: string | undefined): string {\n return `Example ${String(position)}${name === undefined ? '' : ` (\"${name}\")`}`\n}\n\n/** What each reason `readScenario` gives means, said for whoever sent the list. */\nconst SHAPE_PROBLEMS: Readonly<Record<ScenarioShapeProblem['reason'], (problem: ScenarioShapeProblem) => string>> = {\n 'not-an-object': () => 'is not an object.',\n 'unknown-key': ({ part }) => `has \"${part ?? ''}\", which an example does not have.`,\n 'no-name': () => 'has no name.',\n 'no-changes': () => 'has no \"changes\": the answers it sets, by field.',\n 'no-verdict': () => 'does not say whether the form is \"valid\" afterwards, as true or false.',\n malformed: ({ part }) => `has a \"${part ?? ''}\" of the wrong shape.`,\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\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 } from '@formancy/core'\nimport { expressionProblems, unknownReferences } from '@formancy/core/authoring'\nimport { sourceNamesIn } from './options-membership.js'\nimport { examplesThatStopHolding } from './examples.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 /** The request this publish is answering, which its audit row names (`AuditEntry.requestId`). */\n requestId?: 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 // Its own names: `constructor` is not a list because every object answers to it (0185).\n const sources = deps.optionsSources\n if (sources !== undefined) {\n const unknown = sourceNamesIn(schema).filter((name) => !Object.hasOwn(sources, name) || sources[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 // The form's examples, against the version it has and this one: each that stops holding\n // is said, and nothing is refused for it (0166). A first publish has none to run, since\n // examples are kept beside a form the deployment already has.\n const stopped = await examplesThatStopHolding(deps.storage, existing.id, current, { schema, version })\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: [...warnings, ...stopped] }\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; requestId?: string },\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 ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\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","import { isModelRequestKind, modelBriefing } from '@formancy/builder-core'\n\n/**\n * The deployment's own model, asked on a builder's behalf.\n *\n * The builders ask a model through an `AskModel` the host supplies, and a key in a browser\n * is a key anybody who opens the page can read. So a deployment that has a model keeps it\n * here, behind its own sessions, and the admin's `AskModel` is a request to this server\n * ([0165](../../../docs/decisions/0165-a-deployments-model-is-asked-through-its-server.md)).\n *\n * **The server writes the briefing.** The browser names the kind of request — one of the\n * three formancy makes — and sends the user part; the system part is the one\n * `@formancy/builder-core` briefs that kind with, and one in the request is never read, so\n * every request the operator's key pays for is asked under one of formancy's briefings.\n * That narrows the endpoint; it does not close it. The user part is still the person's\n * text, and nothing here stops it asking the model for something else under formancy's\n * briefing: the permission, the rate limit, the size cap and the output limit bound what\n * that costs, and the audit log says who asked.\n */\n\n/** Everything a completion is asked: one system part and one user part, no conversation. */\nexport interface CompletionPrompt {\n readonly system: string\n readonly user: string\n}\n\n/**\n * Told when whoever asked has gone, so the request is abandoned at the provider rather than\n * paid for to the end.\n *\n * A callback rather than an `AbortSignal`, which this package's types do not have\n * ([0008](../../../docs/decisions/0008-layered-packages.md)) — the shape builder-core's\n * `AskTurn` has for the same reason. An adapter wires it in one line:\n * `const c = new AbortController(); cancellation.onCancel(() => c.abort())`.\n */\nexport interface Cancellation {\n /** Called at most once: when the asker goes, or at once if it already has. */\n onCancel(listener: () => void): void\n}\n\n/**\n * What one completion came to. Only the first is an answer.\n *\n * - `refused`: the provider declined in its response rather than in its text. `reason` is\n * for the person, and ends their run as a decline\n * ([0158](../../../docs/decisions/0158-a-model-may-decline.md)).\n * - `truncated`: the answer reached the output limit. Cut off, it is not an answer, and\n * is never handed over as half a document.\n * - `unavailable`: not reached, or it answered with an error. `status` is the provider's\n * HTTP status when it gave one; `cause` is for the operator's log, never the client.\n * - `cancelled`: the asker went away and the request was abandoned.\n */\nexport type Completion =\n | { readonly ok: true; readonly text: string }\n | { readonly ok: false; readonly failure: 'refused'; readonly reason: string }\n | { readonly ok: false; readonly failure: 'truncated' }\n | { readonly ok: false; readonly failure: 'unavailable'; readonly status?: number; readonly cause: string }\n | { readonly ok: false; readonly failure: 'cancelled' }\n\n/**\n * A model, asked to complete one prompt.\n *\n * A port because what is behind it is genuinely swappable: the deployment chooses Anthropic,\n * OpenAI or xAI, `@formancy/server` has an adapter for each, and the tests drive the route\n * through doubles. **It does not throw for a provider's failure**; it says which failure.\n */\nexport interface Completer {\n complete(prompt: CompletionPrompt, cancellation: Cancellation): Promise<Completion>\n}\n\n/** A builder's request as the browser sends it, unread. */\nexport interface BuilderRequest {\n readonly kind?: unknown\n readonly user?: unknown\n}\n\nexport type BuilderRequestOutcome =\n | Completion\n /** Not one of the requests formancy makes, so there is no briefing to ask it under. */\n | { readonly ok: false; readonly failure: 'unknown_kind' }\n /** No text to ask about: the briefing alone, which no run sends. */\n | { readonly ok: false; readonly failure: 'no_user' }\n\n/**\n * Ask the deployment's model a builder's request, under the briefing for its kind.\n *\n * Whatever else the request carries is not read — a `system` above all. Resolves on every\n * ending: an adapter that throws broke its contract, and is still a model that could not\n * be asked.\n */\nexport async function completeBuilderRequest(\n completer: Completer,\n request: BuilderRequest,\n cancellation: Cancellation,\n): Promise<BuilderRequestOutcome> {\n const { kind, user } = request\n if (!isModelRequestKind(kind)) return { ok: false, failure: 'unknown_kind' }\n if (typeof user !== 'string' || user.trim() === '') return { ok: false, failure: 'no_user' }\n\n try {\n return await completer.complete({ system: modelBriefing(kind), user }, cancellation)\n } catch (error) {\n return { ok: false, failure: 'unavailable', cause: error instanceof Error ? error.message : String(error) }\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/signing.ts","../src/use-cases.ts","../src/submitting.ts","../src/submission-pages.ts","../src/response-summaries.ts","../src/redos.ts","../src/testing/memory-storage.ts","../src/auth.ts","../src/address.ts","../src/examples.ts","../src/publishing.ts","../src/model.ts"],"sourcesContent":["import { getAt } from '@formancy/core'\nimport 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(schema: FormSchema, value: unknown): 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\n for (const field of fields) {\n const at = prefix === '' ? field.key : `${prefix}.${field.key}`\n // What the submission holds, as the engine reads it: never a member of every object (0185).\n const held = getAt(data, [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(schema: FormSchema, value: unknown, sources: ServerOptionsSources | undefined): 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 // Its own, never inherited: `constructor` names no list this deployment has (0185).\n const source = Object.hasOwn(sources, name) ? sources[name] : undefined\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 // What a form is checked against changed: its examples, or the sample they start from.\n // With counts, never the examples (0166).\n 'form.examples.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 // A form's words or its document sent to the operator's model, on a person's say-so and\n // at the operator's cost. Recorded with the kind and the size, never the text (0165).\n 'model.asked',\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 — and a row to the line a server's request log wrote\n * for that request. A use-case that writes its row inside its transaction\n * takes the id in its input, and names it when it is given one.\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 ? draft : { ...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: { eventId: string; secret: string; body: string; timestampSeconds: number; attempt: number }): 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: { secret: string; body: string; header: string; nowSeconds: number; toleranceSeconds?: number }): 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(webhook: WebhookRecord, ok: boolean, now: Date): 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(delivery: DeliveryRecord, outcome: AttemptOutcome, now: Date, random: () => number): 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: (delivery: DeliveryRecord, webhook: WebhookRecord, nowSeconds: number) => 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 = { ok: true; delivery: DeliveryRecord } | { ok: false; kind: 'unknown' | 'not-dead'; state?: DeliveryRecord['state'] }\n\nexport async function replayDelivery(deps: { storage: Storage; now: () => Date }, id: string): 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(form: FormRecord, actor: 'anonymous' | 'authenticated', origin: string | undefined): 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 { getAt } from '@formancy/core'\nimport { 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 receivingUntil: 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(file: { name: string; contentType: string; size: number }, bytes: Uint8Array): 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({ name: file.name, contentType: file.contentType, size: file.size }, bytes)\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 = { ok: true; ids: readonly string[] } | { 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(storage: Storage, input: { schema: FormSchema; formId: string; data: unknown }): 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(storage: Storage, now: Date, afterHours: number): 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\n for (const field of flattenPages(fields)) {\n // What the submission holds, as the engine reads it: never a member of every object (0185).\n const held = getAt(value, [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((path) => fileFieldAt(schema, path)?.type === 'file')\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 * The keys the public plane hands out: a draft's and a response's, each an HMAC under the\n * deployment's signing key over what it opens.\n *\n * Stateless, the shape the proof-of-work challenge uses\n * ([0059](../../../docs/decisions/0059-proof-of-work-not-a-captcha.md)): handing one out is\n * public and unauthenticated, so it must not be a write — a key kept in a table is a table an\n * attacker fills by asking for keys they never use. Both in one file, so what each is signed\n * over can be read side by side, and nobody has to check two files to see that one cannot\n * stand in for the other.\n */\nfunction signed(secret: string, message: string): string {\n return bytesToHex(hmac(sha256, utf8ToBytes(secret), utf8ToBytes(message)))\n}\n\n/**\n * The key to a draft: HMAC over the form and the draft id\n * ([0062](../../../docs/decisions/0062-a-draft-carries-its-own-key.md)).\n *\n * Bound to the FORM as well as the id so a key cannot be carried to a draft of another form\n * that happens to share an id. The message is unchanged since 0.2.0, which is what keeps every\n * key handed out since then opening its draft.\n */\nexport function draftKey(secret: string, formId: string, draftId: string): string {\n return signed(secret, `${formId}:${draftId}`)\n}\n\n/**\n * The token a response is sent with: the id it will be stored under, and an HMAC over the form\n * and that id ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\n *\n * The id travels in the clear because the server has to read it back, and the signature is\n * what makes it the server's choice rather than the sender's. The message begins with a word a\n * form id never is, so a draft's key — signed over `form:draft` — is never also a valid\n * signature for a response with the same id, and the reverse.\n */\nexport function submissionToken(secret: string, formId: string, id: string): string {\n return `${id}.${signed(secret, `submission:${formId}:${id}`)}`\n}\n\n/**\n * The id a token names, when this server signed it for this form; otherwise undefined.\n *\n * Read from the LAST dot, since the signature is hex and the id is whatever `newId` minted. A\n * forged token, another form's, one signed under a key this server no longer has, and one that\n * is not a token at all are the same answer: no id.\n */\nexport function submissionTokenId(secret: string, formId: string, token: string): string | undefined {\n const dot = token.lastIndexOf('.')\n if (dot <= 0) return undefined\n const id = token.slice(0, dot)\n return sameToken(token, submissionToken(secret, formId, id)) ? id : undefined\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 */\nexport function 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","import { getAt } from '@formancy/core'\nimport { diffSchemas } from '@formancy/spec'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { draftKey, sameToken, submissionToken } from './signing.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\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\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(deps: ServerDeps, path: string): 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\n/** The answer a submission holds at a column, read as the engine reads one: its own, so an\n * unanswered `constructor` is an empty cell and not `function Object` (0185). */\nfunction cellValue(data: unknown, column: string): string {\n const current = getAt(data, column.split('.'))\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 typeof current === 'string' ? current : typeof current === 'number' || typeof current === 'boolean' ? String(current) : ''\n}\n\nfunction csvCell(value: string): string {\n return /[\",\\n\\r]/.test(value) ? `\"${value.replaceAll('\"', '\"\"')}\"` : value\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 *\n * A draft is one response, so it comes with the token that response is sent with, naming the\n * draft's own id ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)). Resuming\n * hands back the same one, so a response whose answer was lost and is sent again after a reload\n * is refused as already sent rather than stored twice.\n */\nexport async function startDraft(deps: ServerDeps, input: { path: string }): Promise<{ draftId: string; token: string; submissionToken: string } | undefined> {\n const current = await resolveForm(deps, input.path)\n if (current === undefined) return undefined\n\n const draftId = deps.newId()\n return {\n draftId,\n token: draftKey(deps.draftSecret, current.formId, draftId),\n submissionToken: submissionToken(deps.draftSecret, current.formId, draftId),\n }\n}\n\n/** Save (or re-save) a partial form, bound to the CURRENT version. Undefined\n * for an unknown form. */\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, draftKey(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 /** What the response is sent with: the same on every resume, naming the draft's id. */\n submissionToken: string\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(deps: ServerDeps, input: { path: string; draftId: string; token: string }): 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, draftKey(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 const sendWith = submissionToken(deps.draftSecret, current.formId, draft.id)\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 submissionToken: sendWith,\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) => (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 submissionToken: sendWith,\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 // What the draft holds as its own, not `in`: every object answers `constructor`, so a\n // rename to it was never applied and one from it moved `function Object` (0185).\n if (field.renamedFrom !== undefined && !Object.hasOwn(data, field.key) && Object.hasOwn(data, field.renamedFrom)) {\n data[field.key] = data[field.renamedFrom]\n delete data[field.renamedFrom]\n }\n const held = getAt(data, [field.key])\n if (field.type === 'group' && typeof held === 'object' && held !== null) {\n applyRenames(field.fields ?? [], held as Record<string, unknown>)\n }\n }\n}\n\n/** Remove and return the value at a dotted path, pruning nothing else — what the draft\n * holds there as its own, never what every object inherits. */\nfunction takeAtDottedPath(data: Record<string, unknown>, dottedPath: string): unknown {\n const segments = dottedPath.split('.')\n const parent = getAt(data, segments.slice(0, -1))\n const last = segments[segments.length - 1]!\n if (parent === null || typeof parent !== 'object' || Array.isArray(parent) || !Object.hasOwn(parent, last)) {\n return undefined\n }\n const value = (parent as Record<string, unknown>)[last]\n delete (parent as Record<string, unknown>)[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(deps: ServerDeps, path: string): 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\n/** One version's document, or why there is none to give. */\nexport type FormVersionOutcome = { ok: true; version: number; schemaHash: string; schema: FormSchema } | { ok: false; kind: 'unknown_form' | 'unknown_version' }\n\n/**\n * One published version's document, by its number (0207): what an older version's submissions are\n * read against, since they answered its questions and not the current one's. The history above\n * leaves documents out on purpose; this is one, asked for by name.\n *\n * **Looked up among this form's own versions**, so reading one form never reads another's: a\n * version of another form, by its id or by its number, is as unknown as one never made, and says\n * nothing about where else it exists.\n *\n * **One spelling per version** — the number's own decimal digits, no sign, no leading zero, no\n * fraction or exponent — so one document is not reachable under several addresses.\n */\nexport async function formVersion(deps: ServerDeps, path: string, version: string): Promise<FormVersionOutcome> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n if (!/^[1-9]\\d{0,8}$/.test(version)) return { ok: false, kind: 'unknown_version' }\n const found = (await deps.storage.listVersionsByForm(form.id)).find((candidate) => candidate.version === Number(version))\n if (found === undefined) return { ok: false, kind: 'unknown_version' }\n return { ok: true, version: found.version, schemaHash: found.schemaHash, schema: found.schema }\n}\n","import { createFormEngine } from '@formancy/core'\nimport { checkMembership } from './options-membership.js'\nimport { maySubmit } from './access.js'\nimport { filesToClaim } from './uploads.js'\nimport { resolveForm } from './use-cases.js'\nimport type { ResolvedForm } from './use-cases.js'\nimport { submissionToken, submissionTokenId } from './signing.js'\nimport type { ServerDeps } from './deps.js'\n\n/**\n * Submitting a form: what a respondent is handed, the replay, and what is stored.\n *\n * Its own file because `use-cases.ts` named it as the next family to leave when its size\n * budget refused growth, and the submission is what grew.\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 /** An anonymous response arrived without the token its form was handed out with. */\n | { ok: false; kind: 'token_required' }\n /**\n * A token this server did not sign for this form: forged, another form's, or signed under\n * a key the deployment no longer has. Loading the form again hands out a good one.\n */\n | { ok: false; kind: 'token_invalid' }\n /** The response this token names is already stored, as `id`; nothing was stored again. */\n | { ok: false; kind: 'token_spent'; id: string }\n\n/** A published form as a respondent is handed it: its current version, and what to send it with. */\nexport interface FormToFill extends ResolvedForm {\n /** Names the id the response will be stored under, signed for this form (0169). */\n submissionToken: string\n}\n\n/**\n * The form a respondent fills in, with the token their response is sent with.\n *\n * A fresh id on every call, because every reading is a response that might be sent: two\n * people handed one token would find the second refused as already sent. Minting writes\n * nothing — an HMAC over the form and the id — so handing one to everybody who reads the form\n * costs this server a hash and fills no table\n * ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\n */\nexport async function formToFill(deps: ServerDeps, path: string): Promise<FormToFill | undefined> {\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return undefined\n return { ...resolved, submissionToken: submissionToken(deps.draftSecret, resolved.formId, deps.newId()) }\n}\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 * And it is stored once. A response is stored under the id its token names, and the storage\n * refuses that id twice, so the same response sent again — a retry after an answer that never\n * arrived, a replay — is told it was sent rather than stored as somebody else\n * ([0169](../../../docs/decisions/0169-a-response-is-stored-once.md)).\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 requestId?: string\n /**\n * The token the form was handed out with. Required of an anonymous response. A signed-in\n * one may leave it out and is stored under a fresh id, as before; one it sends is checked\n * and spent like any other, because a token that is ignored when it is wrong is decoration.\n */\n token?: 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 const actor = input.actor ?? 'anonymous'\n if (!maySubmit(form, actor, input.origin)) {\n return { ok: false, kind: 'forbidden' }\n }\n\n // Before the version and the replay: once a response is stored, nothing else about a second\n // send of it matters, and a refusal for something else would invite a third.\n let id: string\n if (input.token === undefined) {\n if (actor === 'anonymous') return { ok: false, kind: 'token_required' }\n id = deps.newId()\n } else {\n const named = submissionTokenId(deps.draftSecret, form.id, input.token)\n if (named === undefined) return { ok: false, kind: 'token_invalid' }\n if (await deps.storage.hasSubmission(named)) return { ok: false, kind: 'token_spent', id: named }\n id = named\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 // Never undefined: an engine handed nothing starts a new form, with the author's defaults,\n // and a replay of nothing would store answers nobody sent (0197).\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 // 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 const stored = 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 ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\n // The version, so a reader can tell which schema this answered without\n // joining. Never the answers.\n detail: {\n submissionId: id,\n version: current.version,\n authenticated: input.actor === 'authenticated',\n },\n },\n )\n // Two sends of one response at once both found nothing stored above; the storage decided\n // which of them it is, and wrote nothing at all for the other.\n if (!stored) return { ok: false, kind: 'token_spent', id }\n return { ok: true, id, canonicalData: engine.value() }\n}\n","import type { ServerDeps } from './deps.js'\nimport type { SubmissionPlace } from './ports.js'\nimport type { ListedSubmission } from './use-cases.js'\n\n/** The most submissions one page holds, whatever is asked: a page is the point of paging. */\nconst MOST = 1000\n\nexport type SubmissionPage =\n /** `next` is what asks for the page after this one, or `null` when this is the last. */\n | { kind: 'page'; submissions: ListedSubmission[]; next: string | null }\n /** A cursor that names no place in the list. */\n | { kind: 'bad_cursor' }\n\n/**\n * A form's submissions a page at a time, newest first (0222). Undefined for an unknown form, as\n * `listSubmissions` answers.\n *\n * **Keyset, not offset.** The next page is asked for from where the last one ended, a response's\n * time and id, so a response stored while somebody pages comes before the first page and moves\n * nothing after it. An offset would shift every row by one, and the next page would list again\n * the last row of the one before.\n *\n * **The cursor is opaque by contract and readable by design**, the time and the id joined by `_`,\n * because this package has no base64 to hide them with and there is nothing in them to hide: a\n * caller who may read the page may read both. One that names no place in the list is refused,\n * since read as the start it would list again every response the caller already has.\n */\nexport async function listSubmissionPage(\n deps: ServerDeps,\n path: string,\n { limit, after }: { limit: number; after?: string | undefined },\n): Promise<SubmissionPage | undefined> {\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return undefined\n const before = after === undefined ? undefined : placeOf(after)\n if (before === null) return { kind: 'bad_cursor' }\n const size = Math.min(Math.max(Number.isFinite(limit) ? Math.trunc(limit) : 1, 1), MOST)\n\n const versions = await deps.storage.listVersionsByForm(form.id)\n const versionNumberById = new Map(versions.map((version) => [version.id, version.version]))\n // One more than the page, so whether another follows is known without asking twice.\n const records = await deps.storage.pageSubmissionsByForm(form.id, size + 1, before)\n const shown = records.slice(0, size)\n const last = shown.at(-1)\n return {\n kind: 'page',\n submissions: shown.map((record) => ({\n id: record.id,\n version: versionNumberById.get(record.formVersionId) ?? 0,\n submittedAt: record.submittedAt,\n data: record.data,\n })),\n next: records.length > size && last !== undefined ? `${last.submittedAt}_${last.id}` : null,\n }\n}\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** The place a cursor names, or `null` when it names none: a time and a uuid, joined by `_`. */\nfunction placeOf(cursor: string): SubmissionPlace | null {\n const at = cursor.lastIndexOf('_')\n if (at === -1) return null\n const time = Date.parse(cursor.slice(0, at))\n const id = cursor.slice(at + 1)\n if (!Number.isFinite(time) || !UUID.test(id)) return null\n return { submittedAt: new Date(time).toISOString(), id }\n}\n","import { createSummary } from '@formancy/core/summary'\nimport type { QuestionSummary, Summary } from '@formancy/core/summary'\nimport { createFormText } from '@formancy/core/words'\nimport type { ServerDeps } from './deps.js'\nimport type { SubmissionPlace } from './ports.js'\n\n/** How many responses are read into memory at a time to be counted. */\nconst PAGE = 1000\n\n/** One version's responses, summarised by question against that version's own document. */\nexport interface VersionSummary {\n version: number\n /** How many responses this version has. */\n responses: number\n questions: QuestionSummary[]\n}\n\n/**\n * A form's responses summarised by question, one summary per version that has any, newest first\n * (0223). Undefined for an unknown form, as the listing answers.\n *\n * Counted here with `createSummary`, what `summariseAnswers` counts with, the function the admin\n * called when it counted in the browser, so the counting rules are written once (0206): each\n * version against its own document, whose questions may not be another's (0207), and in that\n * document's language, as the admin read it.\n *\n * **A page at a time** (0224): the responses are read from the storage as the listing pages them,\n * from where the last page ended, and each is counted as it is read, so the server holds a page of\n * them and the counts, never the form's every response.\n */\nexport async function summariseResponses(deps: ServerDeps, path: string): Promise<VersionSummary[] | 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 counting = new Map<string, { responses: number; summary: Summary }>(\n versions.map((version) => [\n version.id,\n { responses: 0, summary: createSummary(version.schema, createFormText({ locale: version.schema.i18n?.defaultLocale ?? '' })) },\n ]),\n )\n let before: SubmissionPlace | undefined\n for (;;) {\n const page = await deps.storage.pageSubmissionsByForm(form.id, PAGE, before)\n for (const record of page) {\n const counted = counting.get(record.formVersionId)\n if (counted === undefined) continue\n counted.responses += 1\n counted.summary.add(record.data)\n }\n const last = page.at(-1)\n if (page.length < PAGE || last === undefined) break\n before = { submittedAt: last.submittedAt, id: last.id }\n }\n return versions.flatMap((version) => {\n const counted = counting.get(version.id)\n if (counted === undefined || counted.responses === 0) return []\n return [{ version: version.version, responses: counted.responses, questions: counted.summary.questions() }]\n })\n}\n","import { check } from 'recheck'\nimport { PATTERN_FLAGS, anchoredPattern } from '@formancy/spec'\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 and flagged as the engine runs it, from the definition the engine reads, so\n // what is analysed is what will run. An unanchored analysis can miss a case the anchors\n // create, and one without `u` read `\\p{L}` as a p and passed `(\\p{L}+)+` (0226).\n const diagnostics = await check(anchoredPattern(pattern), PATTERN_FLAGS)\n if (diagnostics.status !== 'vulnerable') continue\n\n const complexity = diagnostics.complexity\n unsafe.push({\n path,\n pattern,\n complexity: complexity === undefined ? 'unknown' : complexity.type === 'polynomial' ? `polynomial degree ${String(complexity.degree)}` : complexity.type,\n })\n }\n\n return unsafe\n}\n","import type { AuditEntry } from '../audit.js'\nimport type {\n DeliveryRecord,\n WebhookRecord,\n ApiKeyRecord,\n DraftRecord,\n ExamplesRecord,\n FileRecord,\n FormRecord,\n FormVersionRecord,\n Storage,\n SubmissionRecord,\n UserRecord,\n} 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 const examples = new Map<string, ExamplesRecord>()\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 getExamples: async (formId) => {\n const found = examples.get(formId)\n return found === undefined ? undefined : { ...found }\n },\n\n keepExamples: async (record, audit) => {\n // The foreign key the real table has: examples are kept beside a form, so a record\n // for a form that is not here is refused rather than kept for whatever comes next.\n if (!forms.has(record.formId)) throw new Error(`No form \"${record.formId}\"`)\n examples.set(record.formId, { ...record })\n if (audit !== undefined) audits.push({ ...audit })\n },\n\n findVersionByHash: async (formId, hash) => [...versions.values()].find((version) => version.formId === formId && version.schemaHash === hash),\n\n latestVersionNumber: async (formId) =>\n Math.max(0, ...[...versions.values()].filter((version) => version.formId === formId).map((version) => version.version)),\n\n webhooksForForm: async (formId) => [...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 // The three below are each one read and one write with no await between them, which\n // is what stands in for the conditional UPDATE: nothing else runs in the middle.\n leaseFile: async (id, nowIso, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.state !== 'offered') return false\n if (file.receivingUntil !== null && file.receivingUntil > nowIso) return false\n files.set(id, { ...file, receivingUntil: untilIso })\n return true\n },\n\n settleFile: async (id, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.state !== 'offered' || file.receivingUntil !== untilIso) {\n return false\n }\n files.set(id, { ...file, state: 'stored', receivingUntil: null })\n return true\n },\n\n releaseFile: async (id, untilIso) => {\n const file = files.get(id)\n if (file === undefined || file.receivingUntil !== untilIso) return false\n files.set(id, { ...file, receivingUntil: null })\n return true\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()].filter((file) => file.state !== 'claimed' && file.createdAt < beforeIso).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 // The primary key the real table has: a response sent twice is stored once, and the\n // second writes nothing at all.\n if (submissions.some((stored) => stored.id === record.id)) return false\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 return true\n },\n\n hasSubmission: async (id) => submissions.some((stored) => stored.id === id),\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) => submissions.filter((record) => record.formId === formId).reverse(),\n\n // Compared as PostgreSQL compares the columns, a time and then an id, so the two storages page\n // one list the same way.\n pageSubmissionsByForm: async (formId, limit, before) => {\n const placed = ({ submittedAt, id }: { submittedAt: string; id: string }): [number, string] => [Date.parse(submittedAt), id]\n const newer = (a: [number, string], b: [number, string]): boolean => a[0] > b[0] || (a[0] === b[0] && a[1] > b[1])\n return submissions\n .filter((record) => record.formId === formId && (before === undefined || newer(placed(before), placed(record))))\n .sort((a, b) => (newer(placed(a), placed(b)) ? -1 : 1))\n .slice(0, limit)\n },\n\n listVersionsByForm: async (formId) => [...versions.values()].filter((version) => version.formId === formId).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) => [...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) => [...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(this: void): string\n nowIso(this: void): 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 = { ok: true; id: string } | { ok: false; message: string }\n\nexport async function createLocalUser(deps: AuthDeps, input: { email: string; password: string; role: Role }): 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(deps: AuthDeps, input: { email: string; password: string }): 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 = { ok: true; id: string; secret: string } | { ok: false; message: string }\n\nexport async function createApiKey(deps: AuthDeps, input: { name: string; role: Role }): 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 = 'form.read' | 'form.publish' | 'submission.read' | 'submission.export' | 'draft.write' | 'user.create' | 'apiKey.create'\n\nconst VIEWER_ACTIONS: readonly Action[] = ['form.read', 'submission.read']\nconst EDITOR_ACTIONS: readonly Action[] = [...VIEWER_ACTIONS, 'form.publish', 'submission.export', 'draft.write']\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 { comparedToLastRun, readScenario } from '@formancy/builder-core'\nimport type { ScenarioShapeProblem } from '@formancy/builder-core'\nimport { runScenarios } from '@formancy/core/authoring'\nimport type { Scenario } from '@formancy/core/authoring'\nimport type { FormSchema } from '@formancy/spec'\nimport { can } from './auth.js'\nimport type { Actor } from './auth.js'\nimport type { ServerDeps } from './deps.js'\nimport type { ExamplesRecord, Storage } from './ports.js'\n\n/**\n * A form's examples, kept by the deployment beside the form, and run when it is published\n * ([0166](../../../docs/decisions/0166-a-deployment-keeps-a-forms-examples-and-runs-them-at-publish.md)).\n *\n * An example with its answer written down is the one check that tells a condition that\n * compiles from the condition that was asked for\n * ([0110](../../../docs/decisions/0110-a-form-is-checked-against-examples.md)). The builders\n * run them, and the scenarios are the host's to keep\n * ([0111](../../../docs/decisions/0111-a-scenario-panel-names-what-stopped-holding.md)); on a\n * deployment the host is this server, so it keeps them, with the fictional sample they start\n * from, and publishing — the last moment before a version is frozen — runs them.\n *\n * **The permission is decided here, not in the route.** Reading and changing a form's\n * examples take what editing the form takes, `form.publish`, and a host composing its own\n * HTTP over this package gets that rule with the use-case rather than having to remember it.\n *\n * **What is kept is read as what is sent is read**, by `readList`, on the way in and on the\n * way out. A list sent to be kept with anything in it that is not an example keeps nothing; a\n * row already kept that is not one — edited around `keepExamples` — is left out and named\n * wherever the examples are read back, so the admin that draws them and the publish that runs\n * them are only ever handed examples.\n */\n\n/** A form's examples as a builder holds them: the list, and where every one starts. */\nexport interface FormExamples {\n readonly scenarios: readonly Scenario[]\n /** Absent when none was kept: every example then starts from an empty form. */\n readonly sample?: Readonly<Record<string, unknown>>\n /**\n * When they are read back: what is kept and is not an example, or not a sample, one sentence\n * each. Left out of the above, so nothing draws or runs it, and gone at the next save, which\n * keeps only what it is sent. Absent when everything kept could be read.\n */\n readonly unreadable?: readonly string[]\n}\n\nexport type ExamplesOutcome =\n | { readonly ok: true; readonly examples: FormExamples }\n /** Not with this actor's role: a viewer may read the form, and not what it is checked against. */\n | { readonly ok: false; readonly kind: 'forbidden' }\n /** Examples are kept beside a form, so there must be one at this path. */\n | { readonly ok: false; readonly kind: 'unknown_form' }\n\nexport type KeepExamplesOutcome =\n | ExamplesOutcome\n /** Something sent is not an example, or not a sample: one sentence each, and nothing kept. */\n | { readonly ok: false; readonly kind: 'invalid_examples'; readonly problems: readonly string[] }\n\n/** The examples kept for the form at `path`; none, for a form nobody wrote any for. */\nexport async function readExamples(deps: ServerDeps, input: { path: string; actor: Actor }): Promise<ExamplesOutcome> {\n if (!can(input.actor, 'form.publish')) return { ok: false, kind: 'forbidden' }\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n const kept = readKept(await deps.storage.getExamples(form.id))\n return {\n ok: true,\n examples: {\n ...asExamples(kept.scenarios, kept.sample),\n ...(kept.problems.length === 0 ? {} : { unreadable: kept.problems }),\n },\n }\n}\n\n/**\n * Replace the examples of the form at `path`, and the sample they start from.\n *\n * Each is read as an example the way a builder reads a model's draft — `readScenario`, one\n * decision for both — and a list with any item that is not one keeps nothing. Paths are not\n * checked against the published form: an example is written for the form being edited, which\n * may name a field no version has yet, and the runner says so when it runs.\n */\nexport async function keepExamples(\n deps: ServerDeps,\n /** `requestId` is the request this change answers, which its audit row names. */\n input: { path: string; actor: Actor; examples: unknown; requestId?: string },\n): Promise<KeepExamplesOutcome> {\n if (!can(input.actor, 'form.publish')) return { ok: false, kind: 'forbidden' }\n const form = await deps.storage.getFormByPath(input.path)\n if (form === undefined) return { ok: false, kind: 'unknown_form' }\n\n const read = readExamplesBody(input.examples)\n if ('problems' in read) return { ok: false, kind: 'invalid_examples', problems: read.problems }\n\n await deps.storage.keepExamples(\n { formId: form.id, scenarios: read.scenarios, sample: read.sample, updatedAt: deps.nowIso() },\n {\n id: deps.newId(),\n at: deps.nowIso(),\n action: 'form.examples.changed',\n subject: input.path,\n actorKind: input.actor.kind,\n actorId: input.actor.id,\n ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\n // How many, and whether there is a sample: what changed, never what it says.\n detail: { examples: read.scenarios.length, sample: read.sample !== null },\n },\n )\n return { ok: true, examples: asExamples(read.scenarios, read.sample) }\n}\n\n/**\n * What publishing says about the form's examples: one sentence for each that held against the\n * version the form has and does not hold against the one being published.\n *\n * **Run as the server replays a submission**, in `server` mode, from the kept sample, and\n * compared by `comparedToLastRun` — the scenario panel's own verdict, so the publish cannot\n * call something a regression the panel would not\n * ([0091](../../../docs/decisions/0091-a-second-builder-is-a-binding.md)). An example that did\n * not hold before either is not this publish's doing, and one that holds again is good news;\n * neither is a warning.\n *\n * **It never refuses.** A publish may warn and never refuses over its examples\n * ([0097](../../../docs/decisions/0097-a-publish-may-warn.md)): a rule changed on purpose stops\n * its old example holding, and the person decides. A stored row that is not an example — one\n * edited around `keepExamples` — is read out before the run, as the admin reads it out, and\n * named; run as it was, the runner throws on it and every publish of the form is a 500.\n */\nexport async function examplesThatStopHolding(\n storage: Storage,\n formId: string,\n before: { readonly schema: FormSchema; readonly version: number } | undefined,\n after: { readonly schema: FormSchema; readonly version: number },\n): Promise<string[]> {\n if (before === undefined) return []\n const kept = readKept(await storage.getExamples(formId))\n const leftOut =\n kept.problems.length === 0\n ? []\n : [`Not everything kept with this form's examples could be read, and what could not was not run: ${kept.problems.join(' ')}`]\n if (kept.scenarios.length === 0) return leftOut\n\n const options = { mode: 'server' as const, ...(kept.sample === null ? {} : { initialValue: kept.sample }) }\n const now = runScenarios(after.schema, kept.scenarios, options)\n const { regressions } = comparedToLastRun(runScenarios(before.schema, kept.scenarios, options), now)\n const stopped = regressions.map((name) => {\n const failures = now.find((result) => result.name === name)?.failures ?? []\n return (\n `The example \"${name}\" held against version ${String(before.version)} and does not hold against ` +\n `version ${String(after.version)}: ${failures.map((failure) => failure.detail).join(' ')}`\n )\n })\n return [...stopped, ...leftOut]\n}\n\nfunction asExamples(scenarios: readonly Scenario[], sample: Readonly<Record<string, unknown>> | null): FormExamples {\n return { scenarios, ...(sample === null ? {} : { sample }) }\n}\n\ntype ReadBody =\n { readonly scenarios: readonly Scenario[]; readonly sample: Readonly<Record<string, unknown>> | null } | { readonly problems: readonly string[] }\n\n/** `{ scenarios, sample? }` from a request body, or a sentence for everything that is not. */\nfunction readExamplesBody(body: unknown): ReadBody {\n if (!isRecord(body) || !Array.isArray(body['scenarios'])) {\n return { problems: ['Send { scenarios, sample? }, where scenarios is a list of examples.'] }\n }\n const read = readList(body['scenarios'], body['sample'])\n return read.problems.length > 0 ? { problems: read.problems } : { scenarios: read.scenarios, sample: read.sample }\n}\n\n/**\n * What a storage keeps, read as a list sent to be kept is read. The storage checks nothing\n * (`Storage.keepExamples`), so a row written around the use-case comes back here as it was\n * written, and only what reads as an example is handed on.\n */\nfunction readKept(kept: ExamplesRecord | undefined): ReadList {\n if (kept === undefined) return { scenarios: [], sample: null, problems: [] }\n const items: unknown = kept.scenarios\n if (Array.isArray(items)) return readList(items, kept.sample)\n const read = readList([], kept.sample)\n return { ...read, problems: ['What is kept as the examples is not a list of them.', ...read.problems] }\n}\n\ninterface ReadList {\n /** The items that are examples, in their order. */\n readonly scenarios: readonly Scenario[]\n readonly sample: Readonly<Record<string, unknown>> | null\n /** One sentence for each item that is not an example, and for a sample that is not answers. */\n readonly problems: readonly string[]\n}\n\n/** Each item through `readScenario`, names once each, and the sample as answers by field. */\nfunction readList(items: readonly unknown[], sample: unknown): ReadList {\n const problems: string[] = []\n if (sample !== undefined && sample !== null && !isRecord(sample)) {\n problems.push('The sample is the answers every example starts from, by field, and this is not that.')\n }\n\n const scenarios: Scenario[] = []\n const first = new Map<string, number>()\n items.forEach((item: unknown, index) => {\n const position = index + 1\n const read = readScenario(item)\n if (!read.ok) {\n problems.push(`${which(position, read.problem.name)} ${SHAPE_PROBLEMS[read.problem.reason](read.problem)}`)\n return\n }\n const earlier = first.get(read.scenario.name)\n if (earlier !== undefined) {\n problems.push(`${which(position, read.scenario.name)} has the name of example ${String(earlier)}; a name is how an example's result is found.`)\n return\n }\n first.set(read.scenario.name, position)\n scenarios.push(read.scenario)\n })\n\n return { scenarios, sample: isRecord(sample) ? sample : null, problems }\n}\n\nfunction which(position: number, name: string | undefined): string {\n return `Example ${String(position)}${name === undefined ? '' : ` (\"${name}\")`}`\n}\n\n/** What each reason `readScenario` gives means, said for whoever sent the list. */\nconst SHAPE_PROBLEMS: Readonly<Record<ScenarioShapeProblem['reason'], (problem: ScenarioShapeProblem) => string>> = {\n 'not-an-object': () => 'is not an object.',\n 'unknown-key': ({ part }) => `has \"${part ?? ''}\", which an example does not have.`,\n 'no-name': () => 'has no name.',\n 'no-changes': () => 'has no \"changes\": the answers it sets, by field.',\n 'no-verdict': () => 'does not say whether the form is \"valid\" afterwards, as true or false.',\n malformed: ({ part }) => `has a \"${part ?? ''}\" of the wrong shape.`,\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\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 } from '@formancy/core'\nimport { expressionProblems, unknownReferences } from '@formancy/core/authoring'\nimport { sourceNamesIn } from './options-membership.js'\nimport { examplesThatStopHolding } from './examples.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 * 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 /** The request this publish is answering, which its audit row names (`AuditEntry.requestId`). */\n requestId?: 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 // Its own names: `constructor` is not a list because every object answers to it (0185).\n const sources = deps.optionsSources\n if (sources !== undefined) {\n const unknown = sourceNamesIn(schema).filter((name) => !Object.hasOwn(sources, name) || sources[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 = existing.currentVersionId === null ? undefined : 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((candidate) => candidate.schemaHash === hash)\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 // The form's examples, against the version it has and this one: each that stops holding\n // is said, and nothing is refused for it (0166). A first publish has none to run, since\n // examples are kept beside a form the deployment already has.\n const stopped = await examplesThatStopHolding(deps.storage, existing.id, current, { schema, version })\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: [...warnings, ...stopped] }\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; requestId?: string },\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 ? {} : { actorKind: input.actor.kind, actorId: input.actor.id }),\n ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\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","import { isModelRequestKind, modelBriefing } from '@formancy/builder-core'\n\n/**\n * The deployment's own model, asked on a builder's behalf.\n *\n * The builders ask a model through an `AskModel` the host supplies, and a key in a browser\n * is a key anybody who opens the page can read. So a deployment that has a model keeps it\n * here, behind its own sessions, and the admin's `AskModel` is a request to this server\n * ([0165](../../../docs/decisions/0165-a-deployments-model-is-asked-through-its-server.md)).\n *\n * **The server writes the briefing.** The browser names the kind of request — one of the\n * three formancy makes — and sends the user part; the system part is the one\n * `@formancy/builder-core` briefs that kind with, and one in the request is never read, so\n * every request the operator's key pays for is asked under one of formancy's briefings.\n * That narrows the endpoint; it does not close it. The user part is still the person's\n * text, and nothing here stops it asking the model for something else under formancy's\n * briefing: the permission, the rate limit, the size cap and the output limit bound what\n * that costs, and the audit log says who asked.\n */\n\n/** Everything a completion is asked: one system part and one user part, no conversation. */\nexport interface CompletionPrompt {\n readonly system: string\n readonly user: string\n}\n\n/**\n * Told when whoever asked has gone, so the request is abandoned at the provider rather than\n * paid for to the end.\n *\n * A callback rather than an `AbortSignal`, which this package's types do not have\n * ([0008](../../../docs/decisions/0008-layered-packages.md)) — the shape builder-core's\n * `AskTurn` has for the same reason. An adapter wires it in one line:\n * `const c = new AbortController(); cancellation.onCancel(() => c.abort())`.\n */\nexport interface Cancellation {\n /** Called at most once: when the asker goes, or at once if it already has. */\n onCancel(listener: () => void): void\n}\n\n/**\n * What one completion came to. Only the first is an answer.\n *\n * - `refused`: the provider declined in its response rather than in its text. `reason` is\n * for the person, and ends their run as a decline\n * ([0158](../../../docs/decisions/0158-a-model-may-decline.md)).\n * - `truncated`: the answer reached the output limit. Cut off, it is not an answer, and\n * is never handed over as half a document.\n * - `unavailable`: not reached, or it answered with an error. `status` is the provider's\n * HTTP status when it gave one; `cause` is for the operator's log, never the client.\n * - `cancelled`: the asker went away and the request was abandoned.\n */\nexport type Completion =\n | { readonly ok: true; readonly text: string }\n | { readonly ok: false; readonly failure: 'refused'; readonly reason: string }\n | { readonly ok: false; readonly failure: 'truncated' }\n | { readonly ok: false; readonly failure: 'unavailable'; readonly status?: number; readonly cause: string }\n | { readonly ok: false; readonly failure: 'cancelled' }\n\n/**\n * A model, asked to complete one prompt.\n *\n * A port because what is behind it is genuinely swappable: the deployment chooses Anthropic,\n * OpenAI or xAI, `@formancy/server` has an adapter for each, and the tests drive the route\n * through doubles. **It does not throw for a provider's failure**; it says which failure.\n */\nexport interface Completer {\n complete(prompt: CompletionPrompt, cancellation: Cancellation): Promise<Completion>\n}\n\n/** A builder's request as the browser sends it, unread. */\nexport interface BuilderRequest {\n readonly kind?: unknown\n readonly user?: unknown\n}\n\nexport type BuilderRequestOutcome =\n | Completion\n /** Not one of the requests formancy makes, so there is no briefing to ask it under. */\n | { readonly ok: false; readonly failure: 'unknown_kind' }\n /** No text to ask about: the briefing alone, which no run sends. */\n | { readonly ok: false; readonly failure: 'no_user' }\n\n/**\n * Ask the deployment's model a builder's request, under the briefing for its kind.\n *\n * Whatever else the request carries is not read — a `system` above all. Resolves on every\n * ending: an adapter that throws broke its contract, and is still a model that could not\n * be asked.\n */\nexport async function completeBuilderRequest(completer: Completer, request: BuilderRequest, cancellation: Cancellation): Promise<BuilderRequestOutcome> {\n const { kind, user } = request\n if (!isModelRequestKind(kind)) return { ok: false, failure: 'unknown_kind' }\n if (typeof user !== 'string' || user.trim() === '') return { ok: false, failure: 'no_user' }\n\n try {\n return await completer.complete({ system: modelBriefing(kind), user }, cancellation)\n } catch (error) {\n return { ok: false, failure: 'unavailable', cause: error instanceof Error ? error.message : String(error) }\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.10.0",
3
+ "version": "0.12.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,10 +17,10 @@
17
17
  "NOTICE"
18
18
  ],
19
19
  "dependencies": {
20
- "@formancy/builder-core": "0.10.0",
21
- "@formancy/challenge": "0.10.0",
22
- "@formancy/core": "0.10.0",
23
- "@formancy/spec": "0.10.0",
20
+ "@formancy/builder-core": "0.12.0",
21
+ "@formancy/challenge": "0.12.0",
22
+ "@formancy/core": "0.12.0",
23
+ "@formancy/spec": "0.12.0",
24
24
  "@noble/hashes": "^2.4.0",
25
25
  "recheck": "^4.5.0"
26
26
  },