@formancy/server 0.2.0 → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, healthOf, listForms, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, saveDraft, setFormAccess, startDraft, verifySolution } from "@formancy/server-core";
2
3
  import Fastify from "fastify";
3
4
  import rateLimit from "@fastify/rate-limit";
4
- import { auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, healthOf, listForms, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, saveDraft, setFormAccess, startDraft, verifySolution } from "@formancy/server-core";
5
5
  import { hash, verify } from "@node-rs/argon2";
6
6
  import { SignJWT, jwtVerify } from "jose";
7
7
  import { and, desc, eq, inArray, lt, lte, max, ne } from "drizzle-orm";
@@ -10,6 +10,77 @@ import { integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-c
10
10
  import { createReadStream } from "node:fs";
11
11
  import { mkdir, rm, stat, writeFile } from "node:fs/promises";
12
12
  import { dirname, join, resolve, sep } from "node:path";
13
+ //#region src/headers.ts
14
+ /**
15
+ * The header a client declares what it was looking at with.
16
+ *
17
+ * One name, read by the submission route and by the publish plugin: a
18
+ * submission says which version it rendered, and a publish says which version it
19
+ * opened. Two spellings of one header is a bug nobody sees until a client sends
20
+ * the other one.
21
+ */
22
+ const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
23
+ //#endregion
24
+ //#region src/routes/publish.ts
25
+ /**
26
+ * Publishing a form, as a plugin of its own.
27
+ *
28
+ * One route family per plugin, which is Fastify's own unit and what `app.ts`'s
29
+ * size budget named as the seam. It moved when the budget refused the next thing
30
+ * added to it rather than letting the number drift.
31
+ */
32
+ async function publishRoutes(app, { deps, requires }) {
33
+ app.post("/forms", { preHandler: requires("form.publish") }, async (request, reply) => {
34
+ const body = request.body;
35
+ if (body === null || typeof body.path !== "string" || body.path === "" || body.schema === void 0) return reply.code(400).send({
36
+ error: "bad_request",
37
+ message: "Body needs { path, schema }."
38
+ });
39
+ const basedOn = request.headers[SCHEMA_HASH_HEADER];
40
+ const outcome = await publishForm(deps, {
41
+ path: body.path,
42
+ schema: body.schema,
43
+ actor: request.actor,
44
+ ...typeof basedOn === "string" && basedOn !== "" ? { basedOnSchemaHash: basedOn } : {}
45
+ });
46
+ if (!outcome.ok) switch (outcome.kind) {
47
+ case "invalid_schema": return reply.code(422).send({
48
+ error: outcome.kind,
49
+ errors: outcome.errors
50
+ });
51
+ case "unsafe_pattern": return reply.code(422).send({
52
+ error: outcome.kind,
53
+ patterns: outcome.patterns
54
+ });
55
+ case "invalid_logic": return reply.code(422).send({
56
+ error: outcome.kind,
57
+ message: outcome.message
58
+ });
59
+ case "unknown_options_source": return reply.code(422).send({
60
+ error: outcome.kind,
61
+ sources: outcome.sources
62
+ });
63
+ case "already_published": return reply.code(409).send({
64
+ error: outcome.kind,
65
+ version: outcome.version
66
+ });
67
+ case "version_changed": return reply.code(409).send({
68
+ error: "FORM_VERSION_CHANGED",
69
+ current: outcome.current === void 0 ? void 0 : {
70
+ version: outcome.current.version,
71
+ schemaHash: outcome.current.schemaHash,
72
+ schema: outcome.current.schema
73
+ }
74
+ });
75
+ }
76
+ return reply.code(201).send({
77
+ version: outcome.version,
78
+ schemaHash: outcome.schemaHash,
79
+ ...outcome.warnings.length > 0 ? { warnings: outcome.warnings } : {}
80
+ });
81
+ });
82
+ }
83
+ //#endregion
13
84
  //#region src/auth-runtime.ts
14
85
  /**
15
86
  * The real cryptography, confined to the composition root: argon2id for
@@ -67,7 +138,6 @@ function createSessionTokens(secret) {
67
138
  }
68
139
  //#endregion
69
140
  //#region src/app.ts
70
- const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
71
141
  /** Base64 JSON, the shape an ALTCHA client already produces. */
72
142
  const CHALLENGE_HEADER = "x-formancy-challenge";
73
143
  /** Proves the bearer started this draft. Lower-case: Fastify normalises. */
@@ -261,39 +331,9 @@ async function createApp(storage, options) {
261
331
  secret: outcome.secret
262
332
  });
263
333
  });
264
- app.post("/forms", { preHandler: requires("form.publish") }, async (request, reply) => {
265
- const body = request.body;
266
- if (body === null || typeof body.path !== "string" || body.path === "" || body.schema === void 0) return reply.code(400).send({
267
- error: "bad_request",
268
- message: "Body needs { path, schema }."
269
- });
270
- const outcome = await publishForm(deps, {
271
- path: body.path,
272
- schema: body.schema,
273
- actor: request.actor
274
- });
275
- if (!outcome.ok) switch (outcome.kind) {
276
- case "invalid_schema": return reply.code(422).send({
277
- error: outcome.kind,
278
- errors: outcome.errors
279
- });
280
- case "unsafe_pattern": return reply.code(422).send({
281
- error: outcome.kind,
282
- patterns: outcome.patterns
283
- });
284
- case "invalid_logic": return reply.code(422).send({
285
- error: outcome.kind,
286
- message: outcome.message
287
- });
288
- case "unknown_options_source": return reply.code(422).send({
289
- error: outcome.kind,
290
- sources: outcome.sources
291
- });
292
- }
293
- return reply.code(201).send({
294
- version: outcome.version,
295
- schemaHash: outcome.schemaHash
296
- });
334
+ await app.register(publishRoutes, {
335
+ deps,
336
+ requires
297
337
  });
298
338
  app.put("/f/:path/access", { preHandler: requires("form.publish") }, async (request, reply) => {
299
339
  const { path } = request.params;
@@ -1459,6 +1499,6 @@ function startFileCollector(storage, store, options = {}) {
1459
1499
  } };
1460
1500
  }
1461
1501
  //#endregion
1462
- export { bootstrapSchema as a, createPostgresStorage as i, createLocalFileStore as n, SCHEMA_HASH_HEADER as o, sha256 as r, createApp as s, startFileCollector as t };
1502
+ export { bootstrapSchema as a, createPostgresStorage as i, createLocalFileStore as n, createApp as o, sha256 as r, SCHEMA_HASH_HEADER as s, startFileCollector as t };
1463
1503
 
1464
- //# sourceMappingURL=file-collector-WWc-eo8h.mjs.map
1504
+ //# sourceMappingURL=file-collector-LmfD8Y_d.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-collector-LmfD8Y_d.mjs","names":["argon2Hash","argon2Verify"],"sources":["../src/headers.ts","../src/routes/publish.ts","../src/auth-runtime.ts","../src/app.ts","../src/db.ts","../src/postgres-storage.ts","../src/file-store.ts","../src/file-collector.ts"],"sourcesContent":["/**\n * The header a client declares what it was looking at with.\n *\n * One name, read by the submission route and by the publish plugin: a\n * submission says which version it rendered, and a publish says which version it\n * opened. Two spellings of one header is a bug nobody sees until a client sends\n * the other one.\n */\nexport const SCHEMA_HASH_HEADER = 'x-formancy-schema-hash'\n","import type { FastifyInstance, FastifyRequest, preHandlerHookHandler } from 'fastify'\nimport { publishForm } from '@formancy/server-core'\nimport type { Actor, ServerDeps } from '@formancy/server-core'\nimport { SCHEMA_HASH_HEADER } from '../headers.js'\n\n/**\n * Publishing a form, as a plugin of its own.\n *\n * One route family per plugin, which is Fastify's own unit and what `app.ts`'s\n * size budget named as the seam. It moved when the budget refused the next thing\n * added to it rather than letting the number drift.\n */\nexport async function publishRoutes(\n app: FastifyInstance,\n { deps, requires }: { deps: ServerDeps; requires: (action: 'form.publish') => preHandlerHookHandler },\n): Promise<void> {\napp.post('/forms', { preHandler: requires('form.publish') }, async (request, reply) => {\n const body = request.body as { path?: unknown; schema?: unknown } | null\n if (body === null || typeof body.path !== 'string' || body.path === '' || body.schema === undefined) {\n return reply.code(400).send({ error: 'bad_request', message: 'Body needs { path, schema }.' })\n }\n\n // The same header the submission route reads, and for the same reason: the\n // client declares what it was looking at. Absent is a real answer — a script\n // or an agent composes a document rather than opening one — so this is not\n // required the way it is on a submission.\n const basedOn = request.headers[SCHEMA_HASH_HEADER]\n\n const outcome = await publishForm(deps, {\n path: body.path,\n schema: body.schema,\n // Passed in so the audit row can be written INSIDE the publish's\n // transaction. Appended here afterwards, it would record a publish that\n // half-applied as having happened.\n actor: (request as FastifyRequest & { actor: Actor }).actor,\n ...(typeof basedOn === 'string' && basedOn !== '' ? { basedOnSchemaHash: basedOn } : {}),\n })\n if (!outcome.ok) {\n // A switch rather than a ternary chain, so adding a refusal kind to\n // PublishOutcome fails to compile here until it is given a shape.\n switch (outcome.kind) {\n case 'invalid_schema':\n return reply.code(422).send({ error: outcome.kind, errors: outcome.errors })\n case 'unsafe_pattern':\n // The pattern and its complexity, so the author can see which field\n // and why rather than being told no.\n return reply.code(422).send({ error: outcome.kind, patterns: outcome.patterns })\n case 'invalid_logic':\n return reply.code(422).send({ error: outcome.kind, message: outcome.message })\n case 'unknown_options_source':\n // The names, so an author can see which list this deployment has never\n // heard of rather than being told no. A published version is frozen\n // forever, so a form naming an unresolvable list would render a message\n // instead of a chooser with nothing ever having said so.\n return reply.code(422).send({ error: outcome.kind, sources: outcome.sources })\n case 'already_published':\n // 409 as well, because it is the same kind of answer: the state on the\n // server is not what this request assumed. Named differently, and\n // carrying the version, because the fix is different — there is\n // nothing to merge, the document is already there.\n return reply.code(409).send({ error: outcome.kind, version: outcome.version })\n case 'version_changed':\n // The same status, the same error name and the same body the\n // submission route sends, so a client that already handles one stale\n // version handles both — and an editor can show what changed rather\n // than fetching and diffing to find out why it was refused.\n return reply.code(409).send({\n error: 'FORM_VERSION_CHANGED',\n current:\n outcome.current === undefined\n ? undefined\n : {\n version: outcome.current.version,\n schemaHash: outcome.current.schemaHash,\n schema: outcome.current.schema,\n },\n })\n }\n }\n // Warnings ride on the 201 rather than changing the status, because the\n // publish succeeded and a 2xx that means \"partly\" is a status nobody can\n // handle. Omitted when there are none, so a client that has never heard of\n // them sees the body it always saw.\n return reply.code(201).send({\n version: outcome.version,\n schemaHash: outcome.schemaHash,\n ...(outcome.warnings.length > 0 ? { warnings: outcome.warnings } : {}),\n })\n})\n}\n","import { randomBytes } from 'node:crypto'\nimport { hash as argon2Hash, verify as argon2Verify } from '@node-rs/argon2'\nimport { SignJWT, jwtVerify } from 'jose'\nimport type { Actor, Role } from '@formancy/server-core'\n\n/**\n * The real cryptography, confined to the composition root: argon2id for\n * secrets at rest, an HMAC-signed short-lived access token for sessions, and\n * a CSPRNG for API keys. server-core never sees any of this — it is handed\n * hash/verify/random functions and stays deterministic under test.\n *\n * HS256 rather than EdDSA for the session token, deliberately for this phase:\n * one secret in one env var is the self-hosting story a single-container\n * deployment can operate, and the signing scheme is an implementation detail\n * of a token that lives fifteen minutes. Revisit with key rotation.\n */\nexport function realSecretHashing(): {\n hashSecret(secret: string): Promise<string>\n verifySecret(secret: string, hash: string): Promise<boolean>\n} {\n return {\n hashSecret: (secret) => argon2Hash(secret),\n verifySecret: async (secret, digest) => {\n try {\n return await argon2Verify(digest, secret)\n } catch {\n // A malformed digest is a failed verification, not a crash: this runs\n // on the login path with attacker-influenced timing.\n return false\n }\n },\n }\n}\n\nexport function realRandomToken(): string {\n return randomBytes(30).toString('base64url')\n}\n\nconst TOKEN_LIFETIME = '15m'\n\nexport interface SessionTokens {\n issue(actor: Actor): Promise<string>\n verify(token: string): Promise<Actor | undefined>\n}\n\nexport function createSessionTokens(secret: string): SessionTokens {\n if (secret.length < 32) {\n throw new Error(\n 'FORMANCY_AUTH_SECRET must be at least 32 characters. Generate one: openssl rand -base64 33',\n )\n }\n const key = new TextEncoder().encode(secret)\n\n return {\n async issue(actor) {\n return new SignJWT({ role: actor.role, kind: actor.kind })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(actor.id)\n .setIssuedAt()\n .setExpirationTime(TOKEN_LIFETIME)\n .sign(key)\n },\n\n async verify(token) {\n try {\n const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] })\n const role = payload['role']\n const kind = payload['kind']\n if (\n typeof payload.sub !== 'string' ||\n (role !== 'admin' && role !== 'editor' && role !== 'viewer') ||\n (kind !== 'user' && kind !== 'apiKey')\n ) {\n return undefined\n }\n return { id: payload.sub, role: role as Role, kind }\n } catch {\n return undefined\n }\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport { publishRoutes } from './routes/publish.js'\nimport { SCHEMA_HASH_HEADER } from './headers.js'\nimport Fastify from 'fastify'\nimport type { FileStore } from './file-store.js'\nimport rateLimit from '@fastify/rate-limit'\nimport type { FastifyInstance, FastifyRequest, preHandlerHookHandler } from 'fastify'\nimport {\n auditedBy,\n authenticateApiKey,\n decodeSolution,\n mintChallenge,\n verifySolution,\n healthOf,\n replayDelivery,\n authenticateLocal,\n can,\n createApiKey,\n createLocalUser,\n createSubmission,\n exportCsv,\n listForms,\n listSubmissions,\n listVersions,\n offerUpload,\n publishForm,\n resolveForm,\n resumeDraft,\n saveDraft,\n startDraft,\n setFormAccess,\n} from '@formancy/server-core'\nimport type {\n Action,\n Actor,\n AuditDraft,\n AuthDeps,\n Role,\n ServerDeps,\n ServerOptionsSources,\n Storage,\n} from '@formancy/server-core'\nimport { createSessionTokens, realRandomToken, realSecretHashing } from './auth-runtime.js'\n\nexport { SCHEMA_HASH_HEADER } from './headers.js'\n/** Base64 JSON, the shape an ALTCHA client already produces. */\nexport const CHALLENGE_HEADER = 'x-formancy-challenge'\n\n/** Proves the bearer started this draft. Lower-case: Fastify normalises. */\nconst DRAFT_TOKEN_HEADER = 'x-formancy-draft-token'\nexport const API_KEY_HEADER = 'x-formancy-api-key'\n\nexport interface AppOptions {\n /** Signs the short-lived session tokens. At least 32 characters. */\n authSecret: string\n /**\n * Created at startup IF no user exists yet — the docker-compose path to a\n * first login. Ignored once any user exists, so it cannot re-seed an admin\n * into a running installation.\n */\n bootstrapAdmin?: { email: string; password: string }\n /**\n * Turns the proof-of-work challenge on, and signs the challenges it mints.\n *\n * Absent means anonymous submissions are defended by the rate limits, the\n * origin allowlist and the body cap alone. That is a supported state rather\n * than a broken one — a deployment whose forms all require a session has\n * no anonymous surface to protect — and the challenge route says so with a\n * 404 rather than failing.\n */\n challengeSecret?: string\n /**\n * The lists a form document may name with `optionsSource`, and how to check a value\n * against one.\n *\n * Without this the membership check in `server-core` is unreachable, which is how it\n * shipped: the port existed, the use-case called it, and the HTTP server never passed\n * anything — so `SAFETY-ANALYSIS.md`'s A7 stated a constraint that could not run.\n * Found by review before the beta.\n *\n * Absent is still a supported state, and it means what A7 says it means: the server\n * has no vocabulary, so it cannot refuse a publish for naming an unknown list and\n * cannot check a submitted value. A deployment with no sourced field never needs one.\n */\n optionsSources?: ServerOptionsSources\n /**\n * Anonymous submissions allowed per IP per minute. Off in tests by setting\n * it high; a real deployment should leave the default.\n *\n * NOTE: @fastify/rate-limit's default store is in-memory and therefore\n * PER PROCESS. Behind more than one replica this counts a fraction of the\n * traffic and silently permits N times the limit. A multi-replica\n * deployment must supply a shared store.\n */\n submissionRateLimit?: { max: number; timeWindowMs: number }\n /** Login attempts per IP per minute. Defaults to 10. */\n loginRateLimit?: { max: number; timeWindowMs: number }\n /**\n * Largest accepted request body, in bytes. A structural cap BEFORE parsing:\n * the JSON parser should never be handed something enormous in the first\n * place, whatever the schema says afterwards.\n */\n bodyLimitBytes?: number\n /**\n * Where uploaded bytes go. Absent means this deployment accepts no files:\n * a form with a file field still renders and still submits, and the field\n * says plainly that there is nowhere to put one\n * ([0055](../../../docs/decisions/0055-files-are-claimed.md)).\n */\n fileStore?: FileStore\n /**\n * Largest file this deployment will accept, whatever a form says. A form\n * author sets the per-field limit; this is the operator's ceiling over all\n * of them, because the disk is theirs. Defaults to 10 MB.\n */\n maxFileBytes?: number\n}\n\n/**\n * The HTTP surface, split into two planes.\n *\n * PUBLIC — what a person filling a form needs: resolve, submit, drafts. These\n * stay unauthenticated by design; anonymous-submission hardening is its own\n * upcoming step and is opt-in per form.\n *\n * MANAGEMENT — everything an operator does: publish, catalog, submissions,\n * export, users, keys. Authenticated via a session token (login) or an API\n * key header, authorized through server-core's one-table can().\n *\n * This file is the composition root, so THIS is where ambient reality enters:\n * randomUUID, the real clock, argon2 and the CSPRNG. Everything below stays\n * injected and replayable.\n */\nexport async function createApp(storage: Storage, options: AppOptions): Promise<FastifyInstance> {\n // 256 kB by default. Large enough for a long form with a repeater, small\n // enough that a request cannot cost meaningful memory before it is rejected.\n const app = Fastify({ logger: false, bodyLimit: options.bodyLimitBytes ?? 256 * 1024 })\n\n const maxFileBytes = options.maxFileBytes ?? 10 * 1024 * 1024\n const fileStore = options.fileStore\n\n // Bytes arrive as a body, not as JSON, so the parser is told to hand them\n // over untouched. Registered for every content type because a file is\n // whatever the reader had — the form's `accept` list is what decides\n // whether it is allowed, and that is checked before the upload is offered.\n app.addContentTypeParser('*', { parseAs: 'buffer', bodyLimit: maxFileBytes }, (_request, body, done) => {\n done(null, body)\n })\n\n const submissionLimit = options.submissionRateLimit ?? { max: 30, timeWindowMs: 60_000 }\n const loginLimit = options.loginRateLimit ?? { max: 10, timeWindowMs: 60_000 }\n const challengeSecret = options.challengeSecret\n await app.register(rateLimit, {\n global: false, // opted into per route: the management plane is authenticated\n max: submissionLimit.max,\n timeWindow: submissionLimit.timeWindowMs,\n })\n\n const deps: ServerDeps = {\n storage,\n newId: () => randomUUID(),\n nowIso: () => new Date().toISOString(),\n // The host's own signing key. A draft token is a server-signed bearer\n // token, which is what that key is already for, and a separate optional\n // secret would mean drafts are unprotected whenever nobody set one.\n draftSecret: options.authSecret,\n ...(options.optionsSources === undefined ? {} : { optionsSources: options.optionsSources }),\n capabilities: {\n now: () => Date.now(),\n today: () => new Date().toISOString().slice(0, 10),\n random: () => Math.random(),\n },\n }\n\n const authDeps: AuthDeps = {\n storage,\n newId: deps.newId,\n nowIso: deps.nowIso,\n randomToken: realRandomToken,\n ...realSecretHashing(),\n }\n\n const sessions = createSessionTokens(options.authSecret)\n\n if (options.bootstrapAdmin !== undefined) {\n const alreadyThere = await storage.getUserByEmail(options.bootstrapAdmin.email)\n if (alreadyThere === undefined) {\n const outcome = await createLocalUser(authDeps, { ...options.bootstrapAdmin, role: 'admin' })\n if (!outcome.ok) throw new Error(`Bootstrap admin rejected: ${outcome.message}`)\n }\n }\n\n async function actorOf(request: FastifyRequest): Promise<Actor | undefined> {\n const bearer = request.headers.authorization\n if (typeof bearer === 'string' && bearer.startsWith('Bearer ')) {\n return sessions.verify(bearer.slice('Bearer '.length))\n }\n const apiKey = request.headers[API_KEY_HEADER]\n if (typeof apiKey === 'string' && apiKey !== '') {\n const outcome = await authenticateApiKey(authDeps, apiKey)\n return outcome.ok ? outcome.actor : undefined\n }\n return undefined\n }\n\n /** 401 without an identity, 403 with one that lacks the permission — the\n * difference tells a client whether to log in or to give up. */\n function requires(action: Action): preHandlerHookHandler {\n return async (request, reply) => {\n const actor = await actorOf(request)\n if (actor === undefined) {\n return reply.code(401).send({ error: 'unauthenticated' })\n }\n if (!can(actor, action)) {\n return reply.code(403).send({ error: 'forbidden', action })\n }\n ;(request as FastifyRequest & { actor: Actor }).actor = actor\n }\n }\n\n /**\n * Append an audit row for something that has already happened.\n *\n * Never throws. An export that succeeded and an audit row that did not is\n * bad; an export that 500s because the audit row failed, after the rows have\n * already been written to the response, is worse and helps nobody. So the\n * failure is logged loudly at error level and the request stands.\n *\n * For a mutation that HAS a transaction, do not use this — pass the entry\n * into that call so the two commit together. `insertSubmission` is currently\n * the only one.\n */\n async function audit(request: FastifyRequest, draft: AuditDraft): Promise<void> {\n const actor = (request as FastifyRequest & { actor?: Actor }).actor\n try {\n await deps.storage.recordAudit({\n id: deps.newId(),\n at: deps.nowIso(),\n requestId: request.id,\n ...auditedBy(actor, draft),\n })\n } catch (error) {\n request.log.error({ err: error, action: draft.action }, 'audit row could not be written')\n }\n }\n\n // -------------------------------------------------------------------- auth\n\n app.post(\n '/auth/login',\n {\n // Enumeration resistance makes a wrong guess cost a full argon2\n // verification, which is deliberate — but it also means an unlimited\n // login endpoint is an unlimited invitation to spend the server's CPU.\n // Tighter than submission: nobody logs in ten times a minute honestly.\n config: { rateLimit: { max: loginLimit.max, timeWindow: loginLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const body = request.body as { email?: unknown; password?: unknown } | null\n if (body === null || typeof body.email !== 'string' || typeof body.password !== 'string') {\n return reply\n .code(400)\n .send({ error: 'bad_request', message: 'Body needs { email, password }.' })\n }\n const outcome = await authenticateLocal(authDeps, {\n email: body.email,\n password: body.password,\n })\n if (!outcome.ok) {\n // Failures too, and this is the pair that matters: a hundred failures\n // then one success is the shape of an attack, and recording only the\n // success hides it. The email is the subject because that is what was\n // tried — there may be no such user.\n await audit(request, { action: 'auth.login.failed', subject: body.email })\n return reply.code(401).send({ error: 'invalid_credentials' })\n }\n await audit(request, {\n action: 'auth.login',\n subject: body.email,\n actorKind: 'user',\n actorId: outcome.actor.id,\n })\n return reply.send({ token: await sessions.issue(outcome.actor), role: outcome.actor.role })\n },\n )\n\n app.post('/users', { preHandler: requires('user.create') }, async (request, reply) => {\n const body = request.body as { email?: unknown; password?: unknown; role?: unknown } | null\n if (\n body === null ||\n typeof body.email !== 'string' ||\n typeof body.password !== 'string' ||\n (body.role !== 'admin' && body.role !== 'editor' && body.role !== 'viewer')\n ) {\n return reply\n .code(400)\n .send({ error: 'bad_request', message: 'Body needs { email, password, role }.' })\n }\n const outcome = await createLocalUser(authDeps, {\n email: body.email,\n password: body.password,\n role: body.role as Role,\n })\n if (!outcome.ok) return reply.code(422).send({ error: 'rejected', message: outcome.message })\n return reply.code(201).send({ id: outcome.id })\n })\n\n app.post('/api-keys', { preHandler: requires('apiKey.create') }, async (request, reply) => {\n const body = request.body as { name?: unknown; role?: unknown } | null\n if (\n body === null ||\n typeof body.name !== 'string' ||\n (body.role !== 'admin' && body.role !== 'editor' && body.role !== 'viewer')\n ) {\n return reply.code(400).send({ error: 'bad_request', message: 'Body needs { name, role }.' })\n }\n const outcome = await createApiKey(authDeps, { name: body.name, role: body.role as Role })\n if (!outcome.ok) return reply.code(422).send({ error: 'rejected', message: outcome.message })\n // The one and only time the secret exists in full outside a hash.\n return reply.code(201).send({ id: outcome.id, secret: outcome.secret })\n })\n\n // -------------------------------------------------------------- management\n\n // Publishing is its own plugin: one route family per plugin, which is\n // Fastify's own unit and what this file's size budget named as the seam.\n await app.register(publishRoutes, { deps, requires })\n\n app.put('/f/:path/access', { preHandler: requires('form.publish') }, async (request, reply) => {\n const { path } = request.params as { path: string }\n const body = request.body as { submit?: unknown; allowedOrigins?: unknown } | null\n\n const submit = body?.submit\n if (submit !== 'authenticated' && submit !== 'public') {\n return reply.code(400).send({\n error: 'invalid_access',\n message: 'submit must be \"authenticated\" or \"public\".',\n })\n }\n\n const origins = body?.allowedOrigins\n if (origins !== undefined && !(Array.isArray(origins) && origins.every((o) => typeof o === 'string'))) {\n return reply.code(400).send({\n error: 'invalid_access',\n message: 'allowedOrigins must be an array of strings, or absent for no allowlist.',\n })\n }\n\n const outcome = await setFormAccess(deps, {\n path,\n submit,\n ...(origins === undefined ? {} : { allowedOrigins: origins as string[] }),\n })\n if (!outcome.ok) return reply.code(404).send({ error: 'unknown_form' })\n // A permission change is the event a security review looks for first.\n await audit(request, {\n action: 'form.access.changed',\n subject: path,\n detail: { submit, allowedOrigins: origins === undefined ? 'unchanged' : (origins as string[]).join(' ') },\n })\n return reply.code(204).send()\n })\n\n /**\n * The log itself.\n *\n * Admin only, and reading it is NOT audited. A log that records its own\n * reads grows without bound from a dashboard that polls it, and the entry\n * would say nothing an access log does not — the events worth recording\n * are the ones that touch somebody else's data.\n */\n app.get('/audit', { preHandler: requires('user.create') }, async (request, reply) => {\n const raw = (request.query as { limit?: unknown } | undefined)?.limit\n const asked = typeof raw === 'string' ? Number.parseInt(raw, 10) : 100\n const limit = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), 500) : 100\n return reply.send({ entries: await deps.storage.listAudit(limit) })\n })\n\n /**\n * Webhook health, and the deliveries that died.\n *\n * The reason the breaker keeps its counters on the row rather than in the\n * worker's memory. A self-hoster has no operations team watching a\n * dashboard, so a destination refusing deliveries since Tuesday has to be\n * answerable from the product — otherwise it is found when somebody asks\n * why the CRM has no leads this week.\n */\n app.get('/webhooks', { preHandler: requires('form.publish') }, async (_request, reply) => {\n const now = new Date(deps.nowIso())\n const hooks = await deps.storage.listWebhooks()\n return reply.send({ webhooks: hooks.map((hook) => healthOf(hook, now)) })\n })\n\n app.get('/deliveries/dead', { preHandler: requires('form.publish') }, async (request, reply) => {\n const raw = (request.query as { limit?: unknown } | undefined)?.limit\n const asked = typeof raw === 'string' ? Number.parseInt(raw, 10) : 50\n const limit = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), 200) : 50\n const dead = await deps.storage.deadDeliveries(limit)\n // Not the body. That is the submission's data in another coat, and this\n // endpoint answers \"what failed\", not \"what was in it\".\n return reply.send({\n deliveries: dead.map((delivery) => ({\n id: delivery.id,\n webhookId: delivery.webhookId,\n submissionId: delivery.submissionId,\n eventId: delivery.eventId,\n attempt: delivery.attempt,\n lastError: delivery.lastError,\n })),\n })\n })\n\n app.post(\n '/deliveries/:id/replay',\n { preHandler: requires('form.publish') },\n async (request, reply) => {\n const { id } = request.params as { id: string }\n const outcome = await replayDelivery(\n { storage: deps.storage, now: () => new Date(deps.nowIso()) },\n id,\n )\n\n if (!outcome.ok) {\n return outcome.kind === 'unknown'\n ? reply.code(404).send({ error: 'unknown_delivery' })\n : reply.code(409).send({\n error: 'not_dead',\n state: outcome.state,\n message:\n 'Only a dead delivery can be replayed. A pending one is already queued, and a delivered one would be sent twice.',\n })\n }\n\n // Recorded: it sends data to a third party on a person's say-so.\n await audit(request, {\n action: 'delivery.replayed',\n subject: id,\n detail: { webhookId: outcome.delivery.webhookId },\n })\n return reply.code(202).send({ id, state: outcome.delivery.state })\n },\n )\n\n /**\n * A puzzle for an anonymous visitor to solve before submitting.\n *\n * Public because the form is: requiring a session to obtain a challenge for\n * an unauthenticated submission would be a circle. It is cheap to mint and\n * stateless, so handing one out costs this server a hash.\n */\n app.get(\n '/f/:path/challenge',\n {\n // One per submission attempt is the legitimate rate, so the submission\n // limit is the right one. The point of a proof of work is that the\n // ATTACKER pays; handing out unlimited puzzles for free is the one part\n // of it that costs us instead.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n if (challengeSecret === undefined) {\n // Not configured is not an error: a deployment may decide its forms are\n // not public enough to need one, and the field should say so rather\n // than fail.\n return reply.code(404).send({ error: 'challenge_not_enabled' })\n }\n const { path } = request.params as { path: string }\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n return reply.send(\n await mintChallenge({\n secret: challengeSecret,\n nowSeconds: Math.floor(new Date(deps.nowIso()).getTime() / 1000),\n }),\n )\n },\n )\n\n /**\n * Check a submitted solution, and spend it.\n *\n * Returns the refusal to send, or undefined when it passed. The spend is\n * separate from the verification and comes last: the stateless checks say\n * the solution is correct, and only the database can say it has not been\n * used — a correct solution stays correct, so without this one puzzle\n * would buy a thousand submissions.\n */\n async function checkChallenge(\n request: FastifyRequest,\n ): Promise<{ error: string; message: string } | undefined> {\n const header = request.headers[CHALLENGE_HEADER]\n if (typeof header !== 'string' || header === '') {\n return {\n error: 'challenge_required',\n message: `This form needs a solved challenge. Ask GET /f/:path/challenge for one and return it in ${CHALLENGE_HEADER}.`,\n }\n }\n\n const solution = decodeSolution(header)\n if (solution === undefined) {\n return { error: 'challenge_malformed', message: 'The challenge header is not base64 JSON.' }\n }\n\n const nowSeconds = Math.floor(new Date(deps.nowIso()).getTime() / 1000)\n const verified = await verifySolution(challengeSecret!, solution, nowSeconds)\n if (!verified.ok) {\n return {\n error: `challenge_${verified.reason}`,\n message: 'The challenge was not solved. Ask for a new one and try again.',\n }\n }\n\n // The expiry comes back from verification rather than being parsed out of\n // the salt a second time — one place reads that format.\n const spent = await deps.storage.spendChallenge(\n verified.challenge,\n new Date(verified.expiresAtSeconds * 1000).toISOString(),\n )\n if (!spent) {\n return {\n error: 'challenge_spent',\n message: 'That challenge has already been used. Each one is good for one submission.',\n }\n }\n return undefined\n }\n\n app.get('/forms', { preHandler: requires('form.read') }, async (_request, reply) => {\n return reply.send({ forms: await listForms(deps) })\n })\n\n app.get('/f/:path/versions', { preHandler: requires('form.read') }, async (request, reply) => {\n const { path } = request.params as { path: string }\n const versions = await listVersions(deps, path)\n if (versions === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.send({ versions })\n })\n\n app.get(\n '/f/:path/submissions',\n { preHandler: requires('submission.read') },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const listed = await listSubmissions(deps, path)\n if (listed === undefined) return reply.code(404).send({ error: 'unknown_form' })\n // The event a data protection officer asks about, and the one an audit\n // log that covers only mutations cannot answer: who read other people's\n // answers. The count, never the answers.\n await audit(request, {\n action: 'submission.read',\n subject: path,\n detail: { count: listed.length },\n })\n return reply.send({ submissions: listed })\n },\n )\n\n app.get(\n '/f/:path/submissions/export.csv',\n { preHandler: requires('submission.export') },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const csv = await exportCsv(deps, path)\n if (csv === undefined) return reply.code(404).send({ error: 'unknown_form' })\n // An export leaves the building. Bytes rather than rows, because that is\n // what was actually handed over.\n await audit(request, {\n action: 'submission.exported',\n subject: path,\n detail: { bytes: Buffer.byteLength(csv, 'utf8') },\n })\n return reply\n .header('content-type', 'text/csv; charset=utf-8')\n .header('content-disposition', `attachment; filename=\"${path}-submissions.csv\"`)\n .send(csv)\n },\n )\n\n // ------------------------------------------------------------------ public\n\n /**\n * Fetch a published form. The one public route deliberately NOT rate-limited.\n *\n * Every other unauthenticated route is: submitting, starting a draft, writing\n * one, reading one, minting a challenge, asking for an upload target. Those are\n * writes, or guesses, or work somebody can demand of the server. This is the\n * read every visitor has to make before they can do anything at all.\n *\n * Limiting it by IP would refuse the form to real people sharing an address —\n * an office, a school, a phone network behind CGNAT — and the failure would\n * look like the form being broken rather than like a limit. That is a worse\n * outcome than the cheap read it would prevent, and the read is cached by\n * `schemaHash` anyway.\n *\n * Written down because the asymmetry is deliberate and looks like an omission.\n */\n app.get('/f/:path', async (request, reply) => {\n const { path } = request.params as { path: string }\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.send({\n version: resolved.version,\n schemaHash: resolved.schemaHash,\n schema: resolved.schema,\n })\n })\n\n /**\n * Start a draft, and get the only key to it.\n *\n * The public plane is anonymous, so a draft has no account behind it. It was\n * previously addressed by an id the CALLER chose, with no check on either\n * route: anybody who knew or guessed an id could read a stranger's part-filled\n * form, and overwrite it — after which that person submits the substituted\n * content under their own name and nothing says otherwise\n * ([0062](../../../docs/decisions/0062-a-draft-carries-its-own-key.md)).\n *\n * So the server picks the id and signs it, and the token comes back exactly\n * once. Losing it means losing the draft, which is the correct trade for the\n * alternative.\n */\n app.post(\n '/f/:path/drafts',\n {\n // Unauthenticated, like the submission route, and every call hands out a\n // key. Cheap per call, but nothing should be free on the public plane.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const started = await startDraft(deps, { path })\n if (started === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.code(201).send(started)\n },\n )\n\n app.put(\n '/f/:path/drafts/:draftId',\n {\n // A database row per request, reachable without an account. The submission\n // route used to be described as \"the one unauthenticated write in the\n // product\"; that stopped being true when drafts were exposed here, and\n // nobody moved the limit across.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path, draftId } = request.params as { path: string; draftId: string }\n const token = request.headers[DRAFT_TOKEN_HEADER]\n if (typeof token !== 'string') {\n return reply.code(401).send({ error: 'draft_token_required' })\n }\n\n const saved = await saveDraft(deps, { path, draftId, token, data: request.body ?? {} })\n if (saved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n if (!saved.saved) return reply.code(403).send({ error: 'draft_token_invalid' })\n return reply.send({ version: saved.version })\n },\n )\n\n app.get(\n '/f/:path/drafts/:draftId',\n {\n // Limited because this is where a token would be tried one after another.\n // An HMAC is not realistically guessable, but limiting the write and\n // leaving the guess surface open is not a position worth defending.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path, draftId } = request.params as { path: string; draftId: string }\n const token = request.headers[DRAFT_TOKEN_HEADER]\n if (typeof token !== 'string') {\n return reply.code(401).send({ error: 'draft_token_required' })\n }\n\n const resumed = await resumeDraft(deps, { path, draftId, token })\n // 404 for a wrong token as well as a missing draft. Answering differently\n // would confirm which ids exist, which is the enumeration this closed.\n if (resumed === undefined) return reply.code(404).send({ error: 'unknown_draft' })\n return reply.send(resumed)\n },\n )\n\n /**\n * Ask where to put a file.\n *\n * Refusing here, before a byte is sent, is the difference between rejecting\n * a 2 GB upload and receiving one first. The reply carries the id the\n * submission will reference and the URL to PUT the bytes to.\n */\n app.post('/f/:path/files', {\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n }, async (request, reply) => {\n if (fileStore === undefined) {\n return reply.code(501).send({\n error: 'uploads_unavailable',\n message: 'This deployment has no file store configured, so it cannot accept uploads.',\n })\n }\n\n const { path } = request.params as { path: string }\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const form = await storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const actor = await actorOf(request)\n const origin = request.headers.origin\n\n const body = (request.body ?? {}) as {\n field?: unknown\n name?: unknown\n size?: unknown\n contentType?: unknown\n }\n const field = typeof body.field === 'string' ? body.field : undefined\n const name = typeof body.name === 'string' ? body.name : undefined\n const size = typeof body.size === 'number' ? body.size : undefined\n const contentType = typeof body.contentType === 'string' ? body.contentType : 'application/octet-stream'\n\n if (field === undefined || name === undefined || size === undefined) {\n return reply.code(400).send({\n error: 'invalid_request',\n message: 'Send the field, the name and the size of the file you want to upload.',\n })\n }\n\n if (size > maxFileBytes) {\n return reply.code(413).send({\n error: 'too_large',\n message: `This deployment accepts files up to ${String(maxFileBytes)} bytes.`,\n })\n }\n\n const outcome = await offerUpload(\n { storage, now: () => new Date(), newId: () => randomUUID() },\n {\n schema: resolved.schema,\n form,\n actor: actor === undefined ? 'anonymous' : 'authenticated',\n origin: typeof origin === 'string' ? origin : undefined,\n field,\n name,\n size,\n contentType,\n },\n )\n\n if (!outcome.ok) {\n const status =\n outcome.reason === 'not_allowed' ? 403 : outcome.reason === 'too_large' ? 413 : 400\n return reply.code(status).send({ error: outcome.reason })\n }\n\n return reply.code(201).send({\n id: outcome.file.id,\n name: outcome.file.name,\n size: outcome.file.size,\n contentType: outcome.file.contentType,\n storageKey: outcome.file.storageKey,\n // Where to PUT the bytes. A URL rather than a key so an S3 store can\n // hand back a presigned one here and the client never learns the\n // difference.\n uploadUrl: `/f/${encodeURIComponent(path)}/files/${outcome.file.id}`,\n })\n })\n\n /** Receive the bytes for a file that was offered. */\n app.put('/f/:path/files/:id', async (request, reply) => {\n if (fileStore === undefined) return reply.code(501).send({ error: 'uploads_unavailable' })\n\n const { path, id } = request.params as { path: string; id: string }\n const form = await storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const file = await storage.getFile(id)\n // Checked against the form in the URL: an id alone must not be enough to\n // write bytes into a form somebody cannot submit to.\n if (file === undefined || file.formId !== form.id) {\n return reply.code(404).send({ error: 'unknown_file' })\n }\n if (file.state !== 'offered') {\n // An upload is once. Re-writing a claimed file would change what a\n // stored submission says was attached to it.\n return reply.code(409).send({ error: 'already_uploaded' })\n }\n\n const bytes = request.body\n if (!Buffer.isBuffer(bytes)) return reply.code(400).send({ error: 'no_body' })\n if (bytes.byteLength > maxFileBytes) return reply.code(413).send({ error: 'too_large' })\n if (bytes.byteLength !== file.size) {\n // The offer named a size and the offer is what was checked against the\n // field's limit. Accepting a different number of bytes would make that\n // check a suggestion.\n return reply.code(400).send({\n error: 'size_mismatch',\n message: `This upload was offered as ${String(file.size)} bytes and ${String(bytes.byteLength)} arrived.`,\n })\n }\n\n await fileStore.put(file.storageKey, bytes)\n await storage.updateFile({ ...file, state: 'stored' })\n\n return reply.code(204).send()\n })\n\n /**\n * Serve a file back.\n *\n * Always as an attachment, never inline, and `nosniff` alongside it. Stored\n * cross-site scripting through an uploaded HTML or SVG file is the most\n * commonly exploited vulnerability in this product category, and the real\n * answer is a separate hostname — which a single-container deployment does\n * not have. Forcing a download is the accommodation, and it is the one place\n * this deployment model genuinely costs something.\n */\n app.get('/f/:path/files/:id', async (request, reply) => {\n if (fileStore === undefined) return reply.code(501).send({ error: 'uploads_unavailable' })\n\n const { path, id } = request.params as { path: string; id: string }\n const actor = await actorOf(request)\n if (actor === undefined) return reply.code(401).send({ error: 'unauthenticated' })\n\n const form = await storage.getFormByPath(path)\n const file = await storage.getFile(id)\n if (form === undefined || file === undefined || file.formId !== form.id) {\n return reply.code(404).send({ error: 'unknown_file' })\n }\n\n const stream = await fileStore.open(file.storageKey)\n if (stream === undefined) return reply.code(404).send({ error: 'unknown_file' })\n\n return reply\n .header('content-type', 'application/octet-stream')\n .header('x-content-type-options', 'nosniff')\n .header('content-security-policy', \"default-src 'none'; sandbox\")\n // The reader's own name for it, quoted and stripped of anything that\n // could end the header early.\n .header('content-disposition', `attachment; filename=\"${file.name.replace(/[\"\\r\\n]/g, '')}\"`)\n .send(stream)\n })\n\n app.post(\n '/f/:path/submissions',\n {\n // The unauthenticated write that matters most, and no longer the only\n // one: drafts write too, and are limited the same way. Keyed by IP, which\n // is the only identity an anonymous submitter has.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const declaredSchemaHash = request.headers[SCHEMA_HASH_HEADER]\n if (typeof declaredSchemaHash !== 'string' || declaredSchemaHash === '') {\n return reply.code(400).send({\n error: 'missing_schema_hash',\n message: `Send the schema hash you rendered in the ${SCHEMA_HASH_HEADER} header.`,\n })\n }\n\n // The public plane is public, so an identity is optional here — but if one\n // is presented and valid, the access policy does not apply to it. An\n // invalid credential is treated as no credential rather than as an error:\n // this route's job is to accept submissions, not to adjudicate logins.\n const actor = await actorOf(request)\n const origin = request.headers.origin\n\n // Before the engine runs, and only for a visitor who is not signed in.\n // Somebody with a session has already paid a cost the challenge is a\n // substitute for, and making them solve a puzzle as well would be\n // ceremony — while an anonymous submission is the surface this exists\n // to protect.\n if (challengeSecret !== undefined && actor === undefined) {\n const refusal = await checkChallenge(request)\n if (refusal !== undefined) return reply.code(400).send(refusal)\n }\n\n const outcome = await createSubmission(deps, {\n path,\n declaredSchemaHash,\n data: request.body ?? {},\n actor: actor === undefined ? 'anonymous' : 'authenticated',\n ...(typeof origin === 'string' ? { origin } : {}),\n })\n\n if (outcome.ok) return reply.code(201).send({ id: outcome.id, data: outcome.canonicalData })\n\n switch (outcome.kind) {\n case 'unknown_form':\n return reply.code(404).send({ error: 'unknown_form' })\n case 'version_changed':\n // 409 carries the CURRENT schema so the client can re-render and\n // preserve what it can, instead of guessing why it was refused.\n return reply.code(409).send({\n error: 'FORM_VERSION_CHANGED',\n current:\n outcome.current === undefined\n ? undefined\n : {\n version: outcome.current.version,\n schemaHash: outcome.current.schemaHash,\n schema: outcome.current.schema,\n },\n })\n case 'invalid':\n // The identical error shape the client's engine produces, so server\n // errors render through the same code path as local ones.\n return reply.code(422).send({ error: 'invalid', errors: outcome.errors })\n case 'forbidden':\n // Deliberately says nothing about WHICH rule refused. \"Not public\" and\n // \"not from your origin\" are the same answer to someone probing.\n return reply.code(403).send({ error: 'forbidden' })\n case 'source_unavailable':\n // 503 and not 422: nothing about the submission is wrong, a list this\n // deployment owns could not vouch for it. Retryable, and the draft still\n // holds the answers — where accepting the value unchecked would store\n // something nobody can detect afterwards.\n return reply\n .code(503)\n .send({ error: 'source_unavailable', source: outcome.source })\n }\n },\n )\n\n return app\n}\n","import { jsonb, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'\nimport type postgres from 'postgres'\n\n/**\n * The thin slice's tables. Three rules from the design spec are enforced HERE,\n * in the database, because application code can be bypassed and these must not\n * be:\n *\n * - published versions are immutable (a trigger refuses UPDATE),\n * - a submission binds to its exact version by real foreign key,\n * - that key is ON DELETE RESTRICT, so orphaning a submission from the schema\n * that produced it is structurally impossible.\n */\nexport const forms = pgTable('forms', {\n id: uuid('id').primaryKey(),\n path: text('path').notNull().unique(),\n currentVersionId: uuid('current_version_id'),\n // Defaults to the safe value in the DATABASE as well as in the code, so a\n // row inserted by a migration or a fixture is private unless it says\n // otherwise. A default of 'public' one refactor away from being forgotten is\n // how a private form becomes a public one.\n accessSubmit: text('access_submit').notNull().default('authenticated'),\n // NULL means no allowlist is in force. An empty array means nothing is\n // allowed — a different thing, and the reason this is nullable.\n allowedOrigins: text('allowed_origins').array(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport const formVersions = pgTable('form_versions', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n version: integer('version').notNull(),\n schema: jsonb('schema').notNull(),\n schemaHash: text('schema_hash').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport const users = pgTable('users', {\n id: uuid('id').primaryKey(),\n email: text('email').notNull().unique(),\n passwordHash: text('password_hash').notNull(),\n role: text('role').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n})\n\nexport const apiKeys = pgTable('api_keys', {\n id: uuid('id').primaryKey(),\n name: text('name').notNull(),\n prefix: text('prefix').notNull(),\n secretHash: text('secret_hash').notNull(),\n role: text('role').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n revokedAt: timestamp('revoked_at', { withTimezone: true }),\n})\n\nexport const drafts = pgTable('drafts', {\n id: text('id').notNull(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n formVersionId: uuid('form_version_id')\n .notNull()\n .references(() => formVersions.id),\n data: jsonb('data').notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n})\n\nexport const webhooks = pgTable('webhooks', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n url: text('url').notNull(),\n secret: text('secret').notNull(),\n consecutiveFailures: integer('consecutive_failures').notNull().default(0),\n openedAt: timestamp('opened_at', { withTimezone: true }),\n})\n\n/**\n * The outbox. A row here and the submission that caused it are written by the\n * same transaction, which is what makes \"accepted\" and \"will be delivered\" one\n * decision rather than two.\n */\nexport const deliveries = pgTable('deliveries', {\n id: uuid('id').primaryKey(),\n webhookId: uuid('webhook_id').notNull(),\n submissionId: uuid('submission_id').notNull(),\n eventId: uuid('event_id').notNull(),\n body: text('body').notNull(),\n attempt: integer('attempt').notNull().default(0),\n nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }).notNull(),\n state: text('state').notNull().default('pending'),\n lastError: text('last_error'),\n})\n\nexport const spentChallenges = pgTable('spent_challenges', {\n challenge: text('challenge').primaryKey(),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n})\n\nexport const auditLog = pgTable('audit_log', {\n id: uuid('id').primaryKey(),\n at: timestamp('at', { withTimezone: true }).notNull(),\n action: text('action').notNull(),\n actorKind: text('actor_kind'),\n actorId: text('actor_id'),\n subject: text('subject'),\n requestId: text('request_id'),\n detail: jsonb('detail'),\n})\n\nexport const files = pgTable('files', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id').notNull(),\n name: text('name').notNull(),\n size: integer('size').notNull(),\n contentType: text('content_type').notNull(),\n storageKey: text('storage_key').notNull(),\n state: text('state').notNull().default('offered'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n submissionId: uuid('submission_id'),\n})\n\nexport const submissions = pgTable('submissions', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n formVersionId: uuid('form_version_id')\n .notNull()\n .references(() => formVersions.id, { onDelete: 'restrict' }),\n data: jsonb('data').notNull(),\n submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull(),\n})\n\n/**\n * Bootstrap DDL for the thin slice — drizzle-kit migrations arrive with the\n * lifecycle work; a self-hosted skeleton that creates its own tables on first\n * start is worth more right now than a migration pipeline.\n */\nexport async function bootstrapSchema(sql: postgres.Sql): Promise<void> {\n await sql`\n CREATE TABLE IF NOT EXISTS forms (\n id uuid PRIMARY KEY,\n path text NOT NULL UNIQUE,\n current_version_id uuid,\n -- Private by default in the DATABASE, not only in the code, so a row\n -- inserted by a migration or a fixture is not accidentally open.\n access_submit text NOT NULL DEFAULT 'authenticated'\n CHECK (access_submit IN ('authenticated', 'public')),\n -- NULL: no allowlist in force. Empty array: nothing allowed. Different\n -- things, which is why this is nullable rather than defaulting to '{}'.\n allowed_origins text[],\n created_at timestamptz NOT NULL DEFAULT now()\n )`\n // Existing databases predate the two columns above. Adding them here rather\n // than in a separate migration keeps bootstrap idempotent for both a fresh\n // database and one created before access control existed.\n await sql`\n ALTER TABLE forms\n ADD COLUMN IF NOT EXISTS access_submit text NOT NULL DEFAULT 'authenticated'`\n await sql`ALTER TABLE forms ADD COLUMN IF NOT EXISTS allowed_origins text[]`\n await sql`\n CREATE TABLE IF NOT EXISTS form_versions (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id),\n version integer NOT NULL,\n schema jsonb NOT NULL,\n schema_hash text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (form_id, version),\n UNIQUE (form_id, schema_hash)\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS submissions (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id),\n form_version_id uuid NOT NULL REFERENCES form_versions(id) ON DELETE RESTRICT,\n data jsonb NOT NULL,\n submitted_at timestamptz NOT NULL\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS webhooks (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id) ON DELETE CASCADE,\n url text NOT NULL,\n secret text NOT NULL\n )`\n await sql`\n ALTER TABLE webhooks\n ADD COLUMN IF NOT EXISTS consecutive_failures integer NOT NULL DEFAULT 0,\n -- When the breaker opened, null while the destination is answering. On\n -- the row rather than in the worker's memory: memory does not survive a\n -- restart, and a self-hoster with no operations team needs a failing\n -- webhook to be visible on a screen rather than in a log nobody tails.\n ADD COLUMN IF NOT EXISTS opened_at timestamptz`\n await sql`\n CREATE TABLE IF NOT EXISTS deliveries (\n id uuid PRIMARY KEY,\n webhook_id uuid NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,\n -- RESTRICT, not CASCADE: a delivery is the record that something was\n -- sent about this submission, and deleting the submission must not\n -- quietly erase it.\n submission_id uuid NOT NULL REFERENCES submissions(id) ON DELETE RESTRICT,\n event_id uuid NOT NULL,\n body text NOT NULL,\n attempt integer NOT NULL DEFAULT 0,\n next_attempt_at timestamptz NOT NULL,\n state text NOT NULL DEFAULT 'pending'\n CHECK (state IN ('pending', 'delivered', 'dead')),\n last_error text\n )`\n await sql`\n CREATE INDEX IF NOT EXISTS deliveries_due\n ON deliveries (next_attempt_at) WHERE state = 'pending'`\n await sql`\n CREATE TABLE IF NOT EXISTS files (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id) ON DELETE RESTRICT,\n -- What the reader called it. Display only: the storage key is minted by\n -- the server, because a key built from a submitted filename is a path\n -- traversal waiting for somebody to try it.\n name text NOT NULL,\n size integer NOT NULL,\n content_type text NOT NULL,\n storage_key text NOT NULL,\n state text NOT NULL DEFAULT 'offered'\n CHECK (state IN ('offered', 'stored', 'claimed')),\n created_at timestamptz NOT NULL,\n -- RESTRICT for the same reason a delivery uses it: the row is the record\n -- that something was attached, and deleting the submission must not\n -- quietly erase what it carried.\n submission_id uuid REFERENCES submissions(id) ON DELETE RESTRICT,\n -- One file, one submission. Enforced here rather than in application\n -- code, because two concurrent submissions claiming the same file is\n -- exactly the race application code loses — and the loser would be a\n -- submission referencing bytes another submission owns.\n CONSTRAINT files_one_submission UNIQUE (id, submission_id)\n )`\n await sql`\n CREATE INDEX IF NOT EXISTS files_abandoned\n ON files (created_at) WHERE state <> 'claimed'`\n await sql`\n CREATE TABLE IF NOT EXISTS users (\n id uuid PRIMARY KEY,\n email text NOT NULL UNIQUE,\n password_hash text NOT NULL,\n role text NOT NULL,\n created_at timestamptz NOT NULL\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS api_keys (\n id uuid PRIMARY KEY,\n name text NOT NULL,\n prefix text NOT NULL,\n secret_hash text NOT NULL,\n role text NOT NULL,\n created_at timestamptz NOT NULL,\n revoked_at timestamptz\n )`\n await sql`CREATE INDEX IF NOT EXISTS api_keys_prefix ON api_keys (prefix)`\n await sql`\n CREATE TABLE IF NOT EXISTS spent_challenges (\n -- The primary key IS the mechanism. Two requests arriving with one\n -- solution both pass every stateless check — the signature is ours\n -- and the hash is right — and only the database can decide which of\n -- them spends it. A read-then-write would race exactly where somebody\n -- is looking.\n challenge text PRIMARY KEY,\n expires_at timestamptz NOT NULL\n )`\n await sql`CREATE INDEX IF NOT EXISTS spent_challenges_expiry ON spent_challenges (expires_at)`\n\n await sql`\n CREATE TABLE IF NOT EXISTS audit_log (\n id uuid PRIMARY KEY,\n at timestamptz NOT NULL,\n action text NOT NULL,\n -- Null for an anonymous public submission, which is an answer rather\n -- than a gap: the form was open and nobody was signed in.\n actor_kind text CHECK (actor_kind IN ('user', 'apiKey')),\n actor_id text,\n subject text,\n request_id text,\n -- Identifiers and counts. NEVER a submission's answers: an audit log is\n -- read by more people and kept longer than the data it describes, so\n -- data inside it is a second copy of the thing being protected.\n detail jsonb\n )`\n // Reading the log is the common query and it is always \"recently, and\n // usually about one thing\".\n await sql`CREATE INDEX IF NOT EXISTS audit_log_at ON audit_log (at DESC)`\n await sql`CREATE INDEX IF NOT EXISTS audit_log_subject ON audit_log (subject, at DESC)`\n\n await sql`\n CREATE TABLE IF NOT EXISTS drafts (\n id text NOT NULL,\n form_id uuid NOT NULL REFERENCES forms(id),\n form_version_id uuid NOT NULL REFERENCES form_versions(id),\n data jsonb NOT NULL,\n updated_at timestamptz NOT NULL,\n PRIMARY KEY (form_id, id)\n )`\n // Immutability lives in the database, not in application discipline: a\n // published version row can never change, because every submission's audit\n // story depends on it staying exactly what the user saw.\n await sql`\n CREATE OR REPLACE FUNCTION refuse_version_update() RETURNS trigger AS $$\n BEGIN\n RAISE EXCEPTION 'form_versions rows are immutable; publish a new version instead';\n END\n $$ LANGUAGE plpgsql`\n await sql`DROP TRIGGER IF EXISTS form_versions_immutable ON form_versions`\n await sql`\n CREATE TRIGGER form_versions_immutable\n BEFORE UPDATE ON form_versions\n FOR EACH ROW EXECUTE FUNCTION refuse_version_update()`\n\n // Append-only, in the database rather than by application discipline — for\n // the same reason the version rows are immutable there. An audit log the\n // application can rewrite is a log that says whatever the person who got\n // into the application wants it to say, and the one moment it matters is\n // exactly the moment somebody has a reason to edit it.\n //\n // A deployment should ALSO give the application's role INSERT and SELECT and\n // nothing else. This trigger is the belt: the grant is the braces, and it is\n // the one a self-hoster has to do themselves.\n await sql`\n CREATE OR REPLACE FUNCTION refuse_audit_change() RETURNS trigger AS $$\n BEGIN\n RAISE EXCEPTION 'audit_log is append-only';\n END\n $$ LANGUAGE plpgsql`\n await sql`DROP TRIGGER IF EXISTS audit_log_append_only ON audit_log`\n await sql`\n CREATE TRIGGER audit_log_append_only\n BEFORE UPDATE OR DELETE ON audit_log\n FOR EACH ROW EXECUTE FUNCTION refuse_audit_change()`\n}\n","import { and, desc, eq, inArray, lt, lte, max, ne } from 'drizzle-orm'\nimport { drizzle } from 'drizzle-orm/postgres-js'\nimport type postgres from 'postgres'\nimport type { FormSchema } from '@formancy/spec'\nimport type { AuditEntry, FileRecord, Role, Storage } from '@formancy/server-core'\nimport { apiKeys, auditLog, spentChallenges, deliveries, drafts, files, forms, formVersions, submissions, webhooks } from './db.js'\nimport { users } from './db.js'\n\n/** The Storage port over Postgres — the mirror of server-core's in-memory one. */\nexport function createPostgresStorage(sql: postgres.Sql): Storage {\n const db = drizzle(sql)\n\n return {\n async getFormByPath(path) {\n const rows = await db.select().from(forms).where(eq(forms.path, path)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n path: row.path,\n currentVersionId: row.currentVersionId,\n accessSubmit: row.accessSubmit === 'public' ? 'public' : 'authenticated',\n allowedOrigins: row.allowedOrigins,\n }\n },\n\n async listForms() {\n const rows = await db.select().from(forms)\n return rows.map((row) => ({\n id: row.id,\n path: row.path,\n currentVersionId: row.currentVersionId,\n accessSubmit: row.accessSubmit === 'public' ? ('public' as const) : ('authenticated' as const),\n allowedOrigins: row.allowedOrigins,\n }))\n },\n\n async createForm(record) {\n await db.insert(forms).values({\n id: record.id,\n path: record.path,\n currentVersionId: record.currentVersionId,\n accessSubmit: record.accessSubmit,\n allowedOrigins: record.allowedOrigins,\n })\n },\n\n async updateFormAccess(formId, access) {\n await db\n .update(forms)\n .set({ accessSubmit: access.accessSubmit, allowedOrigins: access.allowedOrigins })\n .where(eq(forms.id, formId))\n },\n\n async setCurrentVersion(formId, versionId) {\n await db.update(forms).set({ currentVersionId: versionId }).where(eq(forms.id, formId))\n },\n\n async insertVersion(record) {\n await db.insert(formVersions).values({\n id: record.id,\n formId: record.formId,\n version: record.version,\n schema: record.schema,\n schemaHash: record.schemaHash,\n })\n },\n\n async publishVersion({ form, version, audit }) {\n // ONE transaction. Three calls left a window where the form row existed\n // with a null pointer, which is a form that answers its URL and has no\n // schema to render — present in the list, 404 when opened.\n await db.transaction(async (tx) => {\n if (form !== undefined) {\n await tx.insert(forms).values({\n id: form.id,\n path: form.path,\n currentVersionId: form.currentVersionId,\n accessSubmit: form.accessSubmit,\n allowedOrigins: form.allowedOrigins,\n })\n }\n\n await tx.insert(formVersions).values({\n id: version.id,\n formId: version.formId,\n version: version.version,\n schema: version.schema,\n schemaHash: version.schemaHash,\n })\n\n const pointed = await tx\n .update(forms)\n .set({ currentVersionId: version.id })\n .where(eq(forms.id, version.formId))\n .returning({ id: forms.id })\n\n // An UPDATE that matched nothing is not an error in SQL, so it has to\n // be looked at: without this, publishing to a form that has been\n // deleted underneath would insert an orphan version and report success.\n if (pointed.length !== 1) {\n throw new Error(\n `Publishing to form ${version.formId} matched no form row. Rolling back, so there is no version pointing at a form that is not there.`,\n )\n }\n\n if (audit !== undefined) await tx.insert(auditLog).values(auditRow(audit))\n })\n },\n\n async getVersionById(id) {\n const rows = await db.select().from(formVersions).where(eq(formVersions.id, id)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }\n },\n\n async findVersionByHash(formId, schemaHash) {\n const rows = await db\n .select()\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n const row = rows.find((candidate) => candidate.schemaHash === schemaHash)\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }\n },\n\n async latestVersionNumber(formId) {\n const rows = await db\n .select({ latest: max(formVersions.version) })\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n return rows[0]?.latest ?? 0\n },\n\n async insertSubmission(record, queued, claimFileIds, audit) {\n // ONE transaction. The submission and the deliveries it triggers commit\n // together or not at all — the requirement that chose this database\n // (docs/decisions/0024-postgres-over-mongodb.md).\n await db.transaction(async (tx) => {\n await tx.insert(submissions).values({\n id: record.id,\n formId: record.formId,\n formVersionId: record.formVersionId,\n data: record.data,\n submittedAt: new Date(record.submittedAt),\n })\n\n if (queued !== undefined && queued.length > 0) {\n await tx.insert(deliveries).values(\n queued.map((delivery) => ({\n id: delivery.id,\n webhookId: delivery.webhookId,\n submissionId: delivery.submissionId,\n eventId: delivery.eventId,\n body: delivery.body,\n attempt: delivery.attempt,\n nextAttemptAt: new Date(delivery.nextAttemptAt),\n state: delivery.state,\n lastError: delivery.lastError,\n })),\n )\n }\n\n if (claimFileIds !== undefined && claimFileIds.length > 0) {\n // In the same transaction, and only from `stored`: two concurrent\n // submissions naming the same file both pass the check made outside\n // the transaction, and this is where the second one loses. The row\n // count is what says it lost — an UPDATE that matched nothing is not\n // an error in SQL, so it has to be looked at.\n const claimed = await tx\n .update(files)\n .set({ state: 'claimed', submissionId: record.id })\n .where(and(inArray(files.id, [...claimFileIds]), eq(files.state, 'stored')))\n .returning({ id: files.id })\n\n if (claimed.length !== claimFileIds.length) {\n throw new Error(\n 'A file this submission names was claimed by another one first. Rolling back, so the submission does not reference bytes it does not own.',\n )\n }\n }\n\n // Last, and inside: a submission that rolled back must leave nothing\n // behind saying it happened.\n if (audit !== undefined) await tx.insert(auditLog).values(auditRow(audit))\n })\n },\n\n async spendChallenge(challenge, expiresAtIso) {\n // `ON CONFLICT DO NOTHING` and look at what came back: the insert is the\n // claim, and the row count is the answer. Checking first and inserting\n // second would leave a window between the two.\n const claimed = await db\n .insert(spentChallenges)\n .values({ challenge, expiresAt: new Date(expiresAtIso) })\n .onConflictDoNothing()\n .returning({ challenge: spentChallenges.challenge })\n return claimed.length === 1\n },\n\n async forgetExpiredChallenges(beforeIso) {\n const gone = await db\n .delete(spentChallenges)\n .where(lt(spentChallenges.expiresAt, new Date(beforeIso)))\n .returning({ challenge: spentChallenges.challenge })\n return gone.length\n },\n\n async recordAudit(entry) {\n await db.insert(auditLog).values(auditRow(entry))\n },\n\n async listAudit(limit) {\n const rows = await db.select().from(auditLog).orderBy(desc(auditLog.at)).limit(limit)\n return rows.map((row) => ({\n id: row.id,\n at: row.at.toISOString(),\n action: row.action as AuditEntry['action'],\n ...(row.actorKind === null ? {} : { actorKind: row.actorKind as 'user' | 'apiKey' }),\n ...(row.actorId === null ? {} : { actorId: row.actorId }),\n ...(row.subject === null ? {} : { subject: row.subject }),\n ...(row.requestId === null ? {} : { requestId: row.requestId }),\n ...(row.detail === null\n ? {}\n : { detail: row.detail as NonNullable<AuditEntry['detail']> }),\n }))\n },\n\n async insertFile(record) {\n await db.insert(files).values({ ...record, createdAt: new Date(record.createdAt) })\n },\n\n async getFile(id) {\n const [row] = await db.select().from(files).where(eq(files.id, id)).limit(1)\n return row === undefined ? undefined : toFileRecord(row)\n },\n\n async updateFile(record) {\n await db\n .update(files)\n .set({ state: record.state, submissionId: record.submissionId })\n .where(eq(files.id, record.id))\n },\n\n async abandonedFiles(beforeIso) {\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 const rows = await db\n .select()\n .from(files)\n .where(and(ne(files.state, 'claimed'), lt(files.createdAt, new Date(beforeIso))))\n return rows.map(toFileRecord)\n },\n\n async deleteFiles(ids) {\n if (ids.length === 0) return\n await db.delete(files).where(inArray(files.id, [...ids]))\n },\n\n async webhooksForForm(formId) {\n const rows = await db.select().from(webhooks).where(eq(webhooks.formId, formId))\n return rows.map(toWebhookRecord)\n },\n\n async insertWebhook(record) {\n // Spelled out rather than spread: `openedAt` is an ISO string on the\n // record and a timestamp in the column, and a spread would have passed\n // the string straight through.\n await db.insert(webhooks).values({\n id: record.id,\n formId: record.formId,\n url: record.url,\n secret: record.secret,\n consecutiveFailures: record.consecutiveFailures,\n openedAt: record.openedAt === null ? null : new Date(record.openedAt),\n })\n },\n\n async updateWebhook(record) {\n // Health only. The url and the secret are not the worker's to change,\n // and a full row update here would let a failed delivery quietly\n // rewrite where the next one goes.\n await db\n .update(webhooks)\n .set({\n consecutiveFailures: record.consecutiveFailures,\n openedAt: record.openedAt === null ? null : new Date(record.openedAt),\n })\n .where(eq(webhooks.id, record.id))\n },\n\n async listWebhooks() {\n const rows = await db.select().from(webhooks)\n return rows.map(toWebhookRecord)\n },\n\n async deadDeliveries(limit) {\n const rows = await db\n .select()\n .from(deliveries)\n .where(eq(deliveries.state, 'dead'))\n .orderBy(desc(deliveries.nextAttemptAt))\n .limit(limit)\n return rows.map(toDeliveryRecord)\n },\n\n async getDelivery(id) {\n const [row] = await db.select().from(deliveries).where(eq(deliveries.id, id)).limit(1)\n return row === undefined ? undefined : toDeliveryRecord(row)\n },\n\n async claimDueDeliveries(nowIso, limit) {\n const rows = await db\n .select()\n .from(deliveries)\n .where(and(eq(deliveries.state, 'pending'), lte(deliveries.nextAttemptAt, new Date(nowIso))))\n .orderBy(deliveries.nextAttemptAt)\n .limit(limit)\n return rows.map(toDeliveryRecord)\n },\n\n async updateDelivery(record) {\n await db\n .update(deliveries)\n .set({\n attempt: record.attempt,\n nextAttemptAt: new Date(record.nextAttemptAt),\n state: record.state,\n lastError: record.lastError,\n })\n .where(eq(deliveries.id, record.id))\n },\n\n async listSubmissions() {\n const rows = await db.select().from(submissions)\n return rows.map(toSubmissionRecord)\n },\n\n async listSubmissionsByForm(formId) {\n const rows = await db\n .select()\n .from(submissions)\n .where(eq(submissions.formId, formId))\n .orderBy(desc(submissions.submittedAt), desc(submissions.id))\n return rows.map(toSubmissionRecord)\n },\n\n async listVersionsByForm(formId) {\n const rows = await db\n .select()\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n .orderBy(desc(formVersions.version))\n return rows.map((row) => ({\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }))\n },\n\n async upsertDraft(record) {\n await db\n .insert(drafts)\n .values({\n id: record.id,\n formId: record.formId,\n formVersionId: record.formVersionId,\n data: record.data,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoUpdate({\n target: [drafts.formId, drafts.id],\n set: {\n formVersionId: record.formVersionId,\n data: record.data,\n updatedAt: new Date(record.updatedAt),\n },\n })\n },\n\n async getDraft(formId, draftId) {\n const rows = await db\n .select()\n .from(drafts)\n .where(and(eq(drafts.formId, formId), eq(drafts.id, draftId)))\n .limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n formVersionId: row.formVersionId,\n data: row.data,\n updatedAt: row.updatedAt.toISOString(),\n }\n },\n\n async insertUser(record) {\n await db.insert(users).values({\n id: record.id,\n email: record.email,\n passwordHash: record.passwordHash,\n role: record.role,\n createdAt: new Date(record.createdAt),\n })\n },\n\n async getUserByEmail(email) {\n const rows = await db.select().from(users).where(eq(users.email, email)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n email: row.email,\n passwordHash: row.passwordHash,\n role: row.role as Role,\n createdAt: row.createdAt.toISOString(),\n }\n },\n\n async insertApiKey(record) {\n await db.insert(apiKeys).values({\n id: record.id,\n name: record.name,\n prefix: record.prefix,\n secretHash: record.secretHash,\n role: record.role,\n createdAt: new Date(record.createdAt),\n revokedAt: record.revokedAt === null ? null : new Date(record.revokedAt),\n })\n },\n\n async listApiKeys() {\n const rows = await db.select().from(apiKeys)\n return rows.map(toApiKeyRecord)\n },\n\n async findApiKeysByPrefix(prefix) {\n const rows = await db.select().from(apiKeys).where(eq(apiKeys.prefix, prefix))\n return rows.map(toApiKeyRecord)\n },\n\n async revokeApiKey(id, atIso) {\n await db.update(apiKeys).set({ revokedAt: new Date(atIso) }).where(eq(apiKeys.id, id))\n },\n }\n}\n\nfunction toApiKeyRecord(row: {\n id: string\n name: string\n prefix: string\n secretHash: string\n role: string\n createdAt: Date\n revokedAt: Date | null\n}) {\n return {\n id: row.id,\n name: row.name,\n prefix: row.prefix,\n secretHash: row.secretHash,\n role: row.role as Role,\n createdAt: row.createdAt.toISOString(),\n revokedAt: row.revokedAt === null ? null : row.revokedAt.toISOString(),\n }\n}\n\nfunction toSubmissionRecord(row: {\n id: string\n formId: string\n formVersionId: string\n data: unknown\n submittedAt: Date\n}) {\n return {\n id: row.id,\n formId: row.formId,\n formVersionId: row.formVersionId,\n data: row.data,\n submittedAt: row.submittedAt.toISOString(),\n }\n}\n\n/** A delivery row as the port describes it. */\nfunction toDeliveryRecord(row: typeof deliveries.$inferSelect) {\n return {\n id: row.id,\n webhookId: row.webhookId,\n submissionId: row.submissionId,\n eventId: row.eventId,\n body: row.body,\n attempt: row.attempt,\n nextAttemptAt: row.nextAttemptAt.toISOString(),\n state: row.state as 'pending' | 'delivered' | 'dead',\n lastError: row.lastError,\n }\n}\n\n/** A files row as the port describes it: timestamps as ISO strings. */\nfunction toFileRecord(row: typeof files.$inferSelect): FileRecord {\n return {\n id: row.id,\n formId: row.formId,\n name: row.name,\n size: row.size,\n contentType: row.contentType,\n storageKey: row.storageKey,\n state: row.state as FileRecord['state'],\n createdAt: row.createdAt.toISOString(),\n submissionId: row.submissionId,\n }\n}\n\n/** A webhook row as the port describes it, health included. */\nfunction toWebhookRecord(row: typeof webhooks.$inferSelect) {\n return {\n id: row.id,\n formId: row.formId,\n url: row.url,\n secret: row.secret,\n consecutiveFailures: row.consecutiveFailures,\n openedAt: row.openedAt === null ? null : row.openedAt.toISOString(),\n }\n}\n\n/**\n * An entry as the table holds it.\n *\n * Absent and null are the same thing here, and the conversion is in one place\n * so two call sites cannot disagree about which they write.\n */\nfunction auditRow(entry: AuditEntry) {\n return {\n id: entry.id,\n at: new Date(entry.at),\n action: entry.action,\n actorKind: entry.actorKind ?? null,\n actorId: entry.actorId ?? null,\n subject: entry.subject ?? null,\n requestId: entry.requestId ?? null,\n detail: entry.detail ?? null,\n }\n}\n","import { createHash } from 'node:crypto'\nimport { createReadStream } from 'node:fs'\nimport { mkdir, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join, resolve, sep } from 'node:path'\nimport type { Readable } from 'node:stream'\n\n/**\n * Where uploaded bytes live.\n *\n * An interface with one implementation, because the second one is the point:\n * S3 or a customer's own service replaces this file and nothing else. The\n * use-cases in `@formancy/server-core` never see it at all — they decide what\n * is rubbish, and the host deletes it.\n *\n * Local disk is the default because `docker compose up` has to produce\n * something that works, and a self-hoster with one container and one volume is\n * the deployment this product is for. It is genuinely the weakest option: it\n * does not survive more than one replica, and the section below says so rather\n * than leaving somebody to find out.\n */\nexport interface FileStore {\n /** Write the bytes. The key was minted by the server, never by a client. */\n put(key: string, bytes: Buffer): Promise<void>\n /** Read them back, as a stream, so a large file is not held in memory. */\n open(key: string): Promise<Readable | undefined>\n /** Remove them. Missing is success: a collector must be safe to re-run. */\n remove(key: string): Promise<void>\n /** The size on disk, for checking the bytes match what was promised. */\n sizeOf(key: string): Promise<number | undefined>\n}\n\n/**\n * Bytes under one directory, one file per key.\n *\n * **Keys are checked, not trusted.** A key is minted by the server from a form\n * id and a file id, so it cannot contain a traversal — but this resolves it\n * and refuses anything landing outside the root regardless. A path check that\n * only runs when the input is untrusted is a path check that stops running the\n * day somebody adds a caller.\n */\nexport function createLocalFileStore(root: string): FileStore {\n const base = resolve(root)\n\n const pathFor = (key: string): string => {\n // Keys use forward slashes on every platform. Reject Windows separators\n // before native path resolution: POSIX treats them as filename characters.\n if (key.includes('\\\\')) {\n throw new Error(`Refusing a storage key that could resolve outside the store: ${key}`)\n }\n const full = resolve(join(base, key))\n // `startsWith(base)` alone matches `/data/formancy-evil` against\n // `/data/formancy`, so the separator is part of the test.\n if (full !== base && !full.startsWith(base + sep)) {\n throw new Error(`Refusing a storage key that resolves outside the store: ${key}`)\n }\n return full\n }\n\n return {\n async put(key, bytes) {\n const full = pathFor(key)\n await mkdir(dirname(full), { recursive: true })\n await writeFile(full, bytes)\n },\n\n async open(key) {\n const full = pathFor(key)\n try {\n await stat(full)\n } catch {\n return undefined\n }\n return createReadStream(full)\n },\n\n async remove(key) {\n // `force` so a collector that ran twice, or crashed after deleting the\n // bytes and before deleting the row, is not an error the second time.\n await rm(pathFor(key), { force: true })\n },\n\n async sizeOf(key) {\n try {\n return (await stat(pathFor(key))).size\n } catch {\n return undefined\n }\n },\n }\n}\n\n/**\n * The checksum a receiver can compare against.\n *\n * Returned when the bytes are stored and never asked of the client: a checksum\n * the uploader computed is a checksum the uploader can lie about, so this one\n * is only useful for telling a *later* reader that the bytes have not changed.\n */\nexport function sha256(bytes: Buffer): string {\n return createHash('sha256').update(bytes).digest('hex')\n}\n","import { collectAbandonedFiles } from '@formancy/server-core'\nimport type { Storage } from '@formancy/server-core'\nimport type { FileStore } from './file-store.js'\n\n/**\n * Deleting files nobody claimed.\n *\n * Somebody who attaches a file and then closes the tab leaves bytes behind.\n * Without this they stay forever, and a disk that fills up for a reason nobody\n * is watching is the most tedious kind of outage.\n *\n * **Bytes first, then the row.** A row with no bytes is a broken reference and\n * shows up the moment anybody tries to download it. Bytes with no row are\n * invisible: nothing will ever look for them again, and the disk fills up\n * silently. Of the two ways to be interrupted halfway, the first is the one\n * you find out about.\n *\n * The same shape as the outbox worker, for the same reasons: a timer in the\n * server process rather than a queue library, one pass at a time, and a\n * thrown pass complains and comes back rather than stopping the timer. It is\n * likewise not a distributed job — `abandonedFiles` takes no lock, so two\n * replicas would both try to delete the same file. That is harmless here,\n * unlike a webhook sent twice, because deleting a file twice is deleting it\n * once.\n */\nexport interface CollectorHandle {\n stop: () => void\n}\n\nexport function startFileCollector(\n storage: Storage,\n store: FileStore,\n options: { intervalMs?: number; afterHours?: number } = {},\n): CollectorHandle {\n const interval = options.intervalMs ?? 15 * 60_000\n const afterHours = options.afterHours ?? 24\n let stopped = false\n let running = false\n\n const tick = async (): Promise<void> => {\n if (running || stopped) return\n running = true\n try {\n const abandoned = await collectAbandonedFiles(storage, new Date(), afterHours)\n if (abandoned.length === 0) return\n\n const removed: string[] = []\n for (const file of abandoned) {\n // One at a time, and the row only once its bytes are gone. A failure\n // partway leaves the rest for the next pass rather than losing track\n // of what was already done.\n await store.remove(file.storageKey)\n removed.push(file.id)\n }\n await storage.deleteFiles(removed)\n } catch (error) {\n // A collector that throws is a collector that stops, and a disk that\n // fills up. Complain and come back.\n console.error('files: a collection pass failed', error)\n } finally {\n running = false\n }\n }\n\n const timer = setInterval(() => void tick(), interval)\n timer.unref?.()\n\n return {\n stop: () => {\n stopped = true\n clearInterval(timer)\n },\n }\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { FastifyInstance } from "fastify";
2
1
  import { ServerOptionsSources, Storage } from "@formancy/server-core";
2
+ import { FastifyInstance } from "fastify";
3
3
  import postgres from "postgres";
4
4
  import { Readable } from "node:stream";
5
5
  //#region src/file-store.d.ts
@@ -46,8 +46,18 @@ export declare function createLocalFileStore(root: string): FileStore;
46
46
  */
47
47
  export declare function sha256(bytes: Buffer): string;
48
48
  //#endregion
49
- //#region src/app.d.ts
49
+ //#region src/headers.d.ts
50
+ /**
51
+ * The header a client declares what it was looking at with.
52
+ *
53
+ * One name, read by the submission route and by the publish plugin: a
54
+ * submission says which version it rendered, and a publish says which version it
55
+ * opened. Two spellings of one header is a bug nobody sees until a client sends
56
+ * the other one.
57
+ */
50
58
  export declare const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
59
+ //#endregion
60
+ //#region src/app.d.ts
51
61
  interface AppOptions {
52
62
  /** Signs the short-lived session tokens. At least 32 characters. */
53
63
  authSecret: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/app.ts","../src/postgres-storage.ts","../src/db.ts","../src/file-collector.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UAoBiB;;EAEf,IAAI,aAAa,OAAO,SAAS;;EAEjC,KAAK,cAAc,QAAQ;;EAE3B,OAAO,cAAc;;EAErB,OAAO,cAAc;;;;;;;;;;;wBAYP,qBAAqB,eAAe;;;;;;;;wBA0DpC,OAAO,OAAO;;;qBCxDjB;UAQI;;EAEf;;;;;;EAMA;IAAmB;IAAe;;;;;;;;;;;EAUlC;;;;;;;;;;;;;;EAcA,iBAAiB;;;;;;;;;;EAUjB;IAAwB;IAAa;;;EAErC;IAAmB;IAAa;;;;;;;EAMhC;;;;;;;EAOA,YAAY;;;;;;EAMZ;;;;;;;;;;;;;;;;;wBAkBoB,UAAU,SAAS,SAAS,SAAS,aAAa,QAAQ;;;;wBC1HhE,sBAAsB,KAAK,SAAS,MAAM;;;;;;;;wBCqIpC,gBAAgB,KAAK,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;UCrHzC;EACf;;wBAGc,mBACd,SAAS,SACT,OAAO,WACP;EAAW;EAAqB;IAC/B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/headers.ts","../src/app.ts","../src/postgres-storage.ts","../src/db.ts","../src/file-collector.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UAoBiB;;EAEf,IAAI,aAAa,OAAO,SAAS;;EAEjC,KAAK,cAAc,QAAQ;;EAE3B,OAAO,cAAc;;EAErB,OAAO,cAAc;;;;;;;;;;;wBAYP,qBAAqB,eAAe;;;;;;;;wBA0DpC,OAAO,OAAO;;;;;;;;;;;qBC1FjB;;;UC4CI;;EAEf;;;;;;EAMA;IAAmB;IAAe;;;;;;;;;;;EAUlC;;;;;;;;;;;;;;EAcA,iBAAiB;;;;;;;;;;EAUjB;IAAwB;IAAa;;;EAErC;IAAmB;IAAa;;;;;;;EAMhC;;;;;;;EAOA,YAAY;;;;;;EAMZ;;;;;;;;;;;;;;;;;wBAkBoB,UAAU,SAAS,SAAS,SAAS,aAAa,QAAQ;;;;wBC5HhE,sBAAsB,KAAK,SAAS,MAAM;;;;;;;;wBCqIpC,gBAAgB,KAAK,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;UCrHzC;EACf;;wBAGc,mBACd,SAAS,SACT,OAAO,WACP;EAAW;EAAqB;IAC/B"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as bootstrapSchema, i as createPostgresStorage, n as createLocalFileStore, o as SCHEMA_HASH_HEADER, r as sha256, s as createApp, t as startFileCollector } from "./file-collector-WWc-eo8h.mjs";
1
+ import { a as bootstrapSchema, i as createPostgresStorage, n as createLocalFileStore, o as createApp, r as sha256, s as SCHEMA_HASH_HEADER, t as startFileCollector } from "./file-collector-LmfD8Y_d.mjs";
2
2
  export { SCHEMA_HASH_HEADER, bootstrapSchema, createApp, createLocalFileStore, createPostgresStorage, sha256, startFileCollector };
package/dist/main.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as bootstrapSchema, i as createPostgresStorage, n as createLocalFileStore, s as createApp, t as startFileCollector } from "./file-collector-WWc-eo8h.mjs";
1
+ import { a as bootstrapSchema, i as createPostgresStorage, n as createLocalFileStore, o as createApp, t as startFileCollector } from "./file-collector-LmfD8Y_d.mjs";
2
2
  import { createHash, createHmac, randomBytes } from "node:crypto";
3
3
  import { deliveryHeaders, drainOutbox, isIpLiteral, isPrivateAddress } from "@formancy/server-core";
4
4
  import postgres from "postgres";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formancy/server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The self-hostable formancy backend: Fastify routes and Postgres storage over @formancy/server-core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,8 +17,8 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@fastify/rate-limit": "^11.2.0",
20
- "@formancy/server-core": "0.2.0",
21
- "@formancy/spec": "0.2.0",
20
+ "@formancy/server-core": "0.3.0",
21
+ "@formancy/spec": "0.3.0",
22
22
  "@node-rs/argon2": "^2.2.1",
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "fastify": "^5.12.5",
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-collector-WWc-eo8h.mjs","names":["argon2Hash","argon2Verify"],"sources":["../src/auth-runtime.ts","../src/app.ts","../src/db.ts","../src/postgres-storage.ts","../src/file-store.ts","../src/file-collector.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { hash as argon2Hash, verify as argon2Verify } from '@node-rs/argon2'\nimport { SignJWT, jwtVerify } from 'jose'\nimport type { Actor, Role } from '@formancy/server-core'\n\n/**\n * The real cryptography, confined to the composition root: argon2id for\n * secrets at rest, an HMAC-signed short-lived access token for sessions, and\n * a CSPRNG for API keys. server-core never sees any of this — it is handed\n * hash/verify/random functions and stays deterministic under test.\n *\n * HS256 rather than EdDSA for the session token, deliberately for this phase:\n * one secret in one env var is the self-hosting story a single-container\n * deployment can operate, and the signing scheme is an implementation detail\n * of a token that lives fifteen minutes. Revisit with key rotation.\n */\nexport function realSecretHashing(): {\n hashSecret(secret: string): Promise<string>\n verifySecret(secret: string, hash: string): Promise<boolean>\n} {\n return {\n hashSecret: (secret) => argon2Hash(secret),\n verifySecret: async (secret, digest) => {\n try {\n return await argon2Verify(digest, secret)\n } catch {\n // A malformed digest is a failed verification, not a crash: this runs\n // on the login path with attacker-influenced timing.\n return false\n }\n },\n }\n}\n\nexport function realRandomToken(): string {\n return randomBytes(30).toString('base64url')\n}\n\nconst TOKEN_LIFETIME = '15m'\n\nexport interface SessionTokens {\n issue(actor: Actor): Promise<string>\n verify(token: string): Promise<Actor | undefined>\n}\n\nexport function createSessionTokens(secret: string): SessionTokens {\n if (secret.length < 32) {\n throw new Error(\n 'FORMANCY_AUTH_SECRET must be at least 32 characters. Generate one: openssl rand -base64 33',\n )\n }\n const key = new TextEncoder().encode(secret)\n\n return {\n async issue(actor) {\n return new SignJWT({ role: actor.role, kind: actor.kind })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(actor.id)\n .setIssuedAt()\n .setExpirationTime(TOKEN_LIFETIME)\n .sign(key)\n },\n\n async verify(token) {\n try {\n const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] })\n const role = payload['role']\n const kind = payload['kind']\n if (\n typeof payload.sub !== 'string' ||\n (role !== 'admin' && role !== 'editor' && role !== 'viewer') ||\n (kind !== 'user' && kind !== 'apiKey')\n ) {\n return undefined\n }\n return { id: payload.sub, role: role as Role, kind }\n } catch {\n return undefined\n }\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport Fastify from 'fastify'\nimport type { FileStore } from './file-store.js'\nimport rateLimit from '@fastify/rate-limit'\nimport type { FastifyInstance, FastifyRequest, preHandlerHookHandler } from 'fastify'\nimport {\n auditedBy,\n authenticateApiKey,\n decodeSolution,\n mintChallenge,\n verifySolution,\n healthOf,\n replayDelivery,\n authenticateLocal,\n can,\n createApiKey,\n createLocalUser,\n createSubmission,\n exportCsv,\n listForms,\n listSubmissions,\n listVersions,\n offerUpload,\n publishForm,\n resolveForm,\n resumeDraft,\n saveDraft,\n startDraft,\n setFormAccess,\n} from '@formancy/server-core'\nimport type {\n Action,\n Actor,\n AuditDraft,\n AuthDeps,\n Role,\n ServerDeps,\n ServerOptionsSources,\n Storage,\n} from '@formancy/server-core'\nimport { createSessionTokens, realRandomToken, realSecretHashing } from './auth-runtime.js'\n\nexport const SCHEMA_HASH_HEADER = 'x-formancy-schema-hash'\n/** Base64 JSON, the shape an ALTCHA client already produces. */\nexport const CHALLENGE_HEADER = 'x-formancy-challenge'\n\n/** Proves the bearer started this draft. Lower-case: Fastify normalises. */\nconst DRAFT_TOKEN_HEADER = 'x-formancy-draft-token'\nexport const API_KEY_HEADER = 'x-formancy-api-key'\n\nexport interface AppOptions {\n /** Signs the short-lived session tokens. At least 32 characters. */\n authSecret: string\n /**\n * Created at startup IF no user exists yet — the docker-compose path to a\n * first login. Ignored once any user exists, so it cannot re-seed an admin\n * into a running installation.\n */\n bootstrapAdmin?: { email: string; password: string }\n /**\n * Turns the proof-of-work challenge on, and signs the challenges it mints.\n *\n * Absent means anonymous submissions are defended by the rate limits, the\n * origin allowlist and the body cap alone. That is a supported state rather\n * than a broken one — a deployment whose forms all require a session has\n * no anonymous surface to protect — and the challenge route says so with a\n * 404 rather than failing.\n */\n challengeSecret?: string\n /**\n * The lists a form document may name with `optionsSource`, and how to check a value\n * against one.\n *\n * Without this the membership check in `server-core` is unreachable, which is how it\n * shipped: the port existed, the use-case called it, and the HTTP server never passed\n * anything — so `SAFETY-ANALYSIS.md`'s A7 stated a constraint that could not run.\n * Found by review before the beta.\n *\n * Absent is still a supported state, and it means what A7 says it means: the server\n * has no vocabulary, so it cannot refuse a publish for naming an unknown list and\n * cannot check a submitted value. A deployment with no sourced field never needs one.\n */\n optionsSources?: ServerOptionsSources\n /**\n * Anonymous submissions allowed per IP per minute. Off in tests by setting\n * it high; a real deployment should leave the default.\n *\n * NOTE: @fastify/rate-limit's default store is in-memory and therefore\n * PER PROCESS. Behind more than one replica this counts a fraction of the\n * traffic and silently permits N times the limit. A multi-replica\n * deployment must supply a shared store.\n */\n submissionRateLimit?: { max: number; timeWindowMs: number }\n /** Login attempts per IP per minute. Defaults to 10. */\n loginRateLimit?: { max: number; timeWindowMs: number }\n /**\n * Largest accepted request body, in bytes. A structural cap BEFORE parsing:\n * the JSON parser should never be handed something enormous in the first\n * place, whatever the schema says afterwards.\n */\n bodyLimitBytes?: number\n /**\n * Where uploaded bytes go. Absent means this deployment accepts no files:\n * a form with a file field still renders and still submits, and the field\n * says plainly that there is nowhere to put one\n * ([0055](../../../docs/decisions/0055-files-are-claimed.md)).\n */\n fileStore?: FileStore\n /**\n * Largest file this deployment will accept, whatever a form says. A form\n * author sets the per-field limit; this is the operator's ceiling over all\n * of them, because the disk is theirs. Defaults to 10 MB.\n */\n maxFileBytes?: number\n}\n\n/**\n * The HTTP surface, split into two planes.\n *\n * PUBLIC — what a person filling a form needs: resolve, submit, drafts. These\n * stay unauthenticated by design; anonymous-submission hardening is its own\n * upcoming step and is opt-in per form.\n *\n * MANAGEMENT — everything an operator does: publish, catalog, submissions,\n * export, users, keys. Authenticated via a session token (login) or an API\n * key header, authorized through server-core's one-table can().\n *\n * This file is the composition root, so THIS is where ambient reality enters:\n * randomUUID, the real clock, argon2 and the CSPRNG. Everything below stays\n * injected and replayable.\n */\nexport async function createApp(storage: Storage, options: AppOptions): Promise<FastifyInstance> {\n // 256 kB by default. Large enough for a long form with a repeater, small\n // enough that a request cannot cost meaningful memory before it is rejected.\n const app = Fastify({ logger: false, bodyLimit: options.bodyLimitBytes ?? 256 * 1024 })\n\n const maxFileBytes = options.maxFileBytes ?? 10 * 1024 * 1024\n const fileStore = options.fileStore\n\n // Bytes arrive as a body, not as JSON, so the parser is told to hand them\n // over untouched. Registered for every content type because a file is\n // whatever the reader had — the form's `accept` list is what decides\n // whether it is allowed, and that is checked before the upload is offered.\n app.addContentTypeParser('*', { parseAs: 'buffer', bodyLimit: maxFileBytes }, (_request, body, done) => {\n done(null, body)\n })\n\n const submissionLimit = options.submissionRateLimit ?? { max: 30, timeWindowMs: 60_000 }\n const loginLimit = options.loginRateLimit ?? { max: 10, timeWindowMs: 60_000 }\n const challengeSecret = options.challengeSecret\n await app.register(rateLimit, {\n global: false, // opted into per route: the management plane is authenticated\n max: submissionLimit.max,\n timeWindow: submissionLimit.timeWindowMs,\n })\n\n const deps: ServerDeps = {\n storage,\n newId: () => randomUUID(),\n nowIso: () => new Date().toISOString(),\n // The host's own signing key. A draft token is a server-signed bearer\n // token, which is what that key is already for, and a separate optional\n // secret would mean drafts are unprotected whenever nobody set one.\n draftSecret: options.authSecret,\n ...(options.optionsSources === undefined ? {} : { optionsSources: options.optionsSources }),\n capabilities: {\n now: () => Date.now(),\n today: () => new Date().toISOString().slice(0, 10),\n random: () => Math.random(),\n },\n }\n\n const authDeps: AuthDeps = {\n storage,\n newId: deps.newId,\n nowIso: deps.nowIso,\n randomToken: realRandomToken,\n ...realSecretHashing(),\n }\n\n const sessions = createSessionTokens(options.authSecret)\n\n if (options.bootstrapAdmin !== undefined) {\n const alreadyThere = await storage.getUserByEmail(options.bootstrapAdmin.email)\n if (alreadyThere === undefined) {\n const outcome = await createLocalUser(authDeps, { ...options.bootstrapAdmin, role: 'admin' })\n if (!outcome.ok) throw new Error(`Bootstrap admin rejected: ${outcome.message}`)\n }\n }\n\n async function actorOf(request: FastifyRequest): Promise<Actor | undefined> {\n const bearer = request.headers.authorization\n if (typeof bearer === 'string' && bearer.startsWith('Bearer ')) {\n return sessions.verify(bearer.slice('Bearer '.length))\n }\n const apiKey = request.headers[API_KEY_HEADER]\n if (typeof apiKey === 'string' && apiKey !== '') {\n const outcome = await authenticateApiKey(authDeps, apiKey)\n return outcome.ok ? outcome.actor : undefined\n }\n return undefined\n }\n\n /** 401 without an identity, 403 with one that lacks the permission — the\n * difference tells a client whether to log in or to give up. */\n function requires(action: Action): preHandlerHookHandler {\n return async (request, reply) => {\n const actor = await actorOf(request)\n if (actor === undefined) {\n return reply.code(401).send({ error: 'unauthenticated' })\n }\n if (!can(actor, action)) {\n return reply.code(403).send({ error: 'forbidden', action })\n }\n ;(request as FastifyRequest & { actor: Actor }).actor = actor\n }\n }\n\n /**\n * Append an audit row for something that has already happened.\n *\n * Never throws. An export that succeeded and an audit row that did not is\n * bad; an export that 500s because the audit row failed, after the rows have\n * already been written to the response, is worse and helps nobody. So the\n * failure is logged loudly at error level and the request stands.\n *\n * For a mutation that HAS a transaction, do not use this — pass the entry\n * into that call so the two commit together. `insertSubmission` is currently\n * the only one.\n */\n async function audit(request: FastifyRequest, draft: AuditDraft): Promise<void> {\n const actor = (request as FastifyRequest & { actor?: Actor }).actor\n try {\n await deps.storage.recordAudit({\n id: deps.newId(),\n at: deps.nowIso(),\n requestId: request.id,\n ...auditedBy(actor, draft),\n })\n } catch (error) {\n request.log.error({ err: error, action: draft.action }, 'audit row could not be written')\n }\n }\n\n // -------------------------------------------------------------------- auth\n\n app.post(\n '/auth/login',\n {\n // Enumeration resistance makes a wrong guess cost a full argon2\n // verification, which is deliberate — but it also means an unlimited\n // login endpoint is an unlimited invitation to spend the server's CPU.\n // Tighter than submission: nobody logs in ten times a minute honestly.\n config: { rateLimit: { max: loginLimit.max, timeWindow: loginLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const body = request.body as { email?: unknown; password?: unknown } | null\n if (body === null || typeof body.email !== 'string' || typeof body.password !== 'string') {\n return reply\n .code(400)\n .send({ error: 'bad_request', message: 'Body needs { email, password }.' })\n }\n const outcome = await authenticateLocal(authDeps, {\n email: body.email,\n password: body.password,\n })\n if (!outcome.ok) {\n // Failures too, and this is the pair that matters: a hundred failures\n // then one success is the shape of an attack, and recording only the\n // success hides it. The email is the subject because that is what was\n // tried — there may be no such user.\n await audit(request, { action: 'auth.login.failed', subject: body.email })\n return reply.code(401).send({ error: 'invalid_credentials' })\n }\n await audit(request, {\n action: 'auth.login',\n subject: body.email,\n actorKind: 'user',\n actorId: outcome.actor.id,\n })\n return reply.send({ token: await sessions.issue(outcome.actor), role: outcome.actor.role })\n },\n )\n\n app.post('/users', { preHandler: requires('user.create') }, async (request, reply) => {\n const body = request.body as { email?: unknown; password?: unknown; role?: unknown } | null\n if (\n body === null ||\n typeof body.email !== 'string' ||\n typeof body.password !== 'string' ||\n (body.role !== 'admin' && body.role !== 'editor' && body.role !== 'viewer')\n ) {\n return reply\n .code(400)\n .send({ error: 'bad_request', message: 'Body needs { email, password, role }.' })\n }\n const outcome = await createLocalUser(authDeps, {\n email: body.email,\n password: body.password,\n role: body.role as Role,\n })\n if (!outcome.ok) return reply.code(422).send({ error: 'rejected', message: outcome.message })\n return reply.code(201).send({ id: outcome.id })\n })\n\n app.post('/api-keys', { preHandler: requires('apiKey.create') }, async (request, reply) => {\n const body = request.body as { name?: unknown; role?: unknown } | null\n if (\n body === null ||\n typeof body.name !== 'string' ||\n (body.role !== 'admin' && body.role !== 'editor' && body.role !== 'viewer')\n ) {\n return reply.code(400).send({ error: 'bad_request', message: 'Body needs { name, role }.' })\n }\n const outcome = await createApiKey(authDeps, { name: body.name, role: body.role as Role })\n if (!outcome.ok) return reply.code(422).send({ error: 'rejected', message: outcome.message })\n // The one and only time the secret exists in full outside a hash.\n return reply.code(201).send({ id: outcome.id, secret: outcome.secret })\n })\n\n // -------------------------------------------------------------- management\n\n app.post('/forms', { preHandler: requires('form.publish') }, async (request, reply) => {\n const body = request.body as { path?: unknown; schema?: unknown } | null\n if (body === null || typeof body.path !== 'string' || body.path === '' || body.schema === undefined) {\n return reply.code(400).send({ error: 'bad_request', message: 'Body needs { path, schema }.' })\n }\n\n const outcome = await publishForm(deps, {\n path: body.path,\n schema: body.schema,\n // Passed in so the audit row can be written INSIDE the publish's\n // transaction. Appended here afterwards, it would record a publish that\n // half-applied as having happened.\n actor: (request as FastifyRequest & { actor: Actor }).actor,\n })\n if (!outcome.ok) {\n // A switch rather than a ternary chain, so adding a refusal kind to\n // PublishOutcome fails to compile here until it is given a shape.\n switch (outcome.kind) {\n case 'invalid_schema':\n return reply.code(422).send({ error: outcome.kind, errors: outcome.errors })\n case 'unsafe_pattern':\n // The pattern and its complexity, so the author can see which field\n // and why rather than being told no.\n return reply.code(422).send({ error: outcome.kind, patterns: outcome.patterns })\n case 'invalid_logic':\n return reply.code(422).send({ error: outcome.kind, message: outcome.message })\n case 'unknown_options_source':\n // The names, so an author can see which list this deployment has never\n // heard of rather than being told no. A published version is frozen\n // forever, so a form naming an unresolvable list would render a message\n // instead of a chooser with nothing ever having said so.\n return reply.code(422).send({ error: outcome.kind, sources: outcome.sources })\n }\n }\n return reply.code(201).send({ version: outcome.version, schemaHash: outcome.schemaHash })\n })\n\n app.put('/f/:path/access', { preHandler: requires('form.publish') }, async (request, reply) => {\n const { path } = request.params as { path: string }\n const body = request.body as { submit?: unknown; allowedOrigins?: unknown } | null\n\n const submit = body?.submit\n if (submit !== 'authenticated' && submit !== 'public') {\n return reply.code(400).send({\n error: 'invalid_access',\n message: 'submit must be \"authenticated\" or \"public\".',\n })\n }\n\n const origins = body?.allowedOrigins\n if (origins !== undefined && !(Array.isArray(origins) && origins.every((o) => typeof o === 'string'))) {\n return reply.code(400).send({\n error: 'invalid_access',\n message: 'allowedOrigins must be an array of strings, or absent for no allowlist.',\n })\n }\n\n const outcome = await setFormAccess(deps, {\n path,\n submit,\n ...(origins === undefined ? {} : { allowedOrigins: origins as string[] }),\n })\n if (!outcome.ok) return reply.code(404).send({ error: 'unknown_form' })\n // A permission change is the event a security review looks for first.\n await audit(request, {\n action: 'form.access.changed',\n subject: path,\n detail: { submit, allowedOrigins: origins === undefined ? 'unchanged' : (origins as string[]).join(' ') },\n })\n return reply.code(204).send()\n })\n\n /**\n * The log itself.\n *\n * Admin only, and reading it is NOT audited. A log that records its own\n * reads grows without bound from a dashboard that polls it, and the entry\n * would say nothing an access log does not — the events worth recording\n * are the ones that touch somebody else's data.\n */\n app.get('/audit', { preHandler: requires('user.create') }, async (request, reply) => {\n const raw = (request.query as { limit?: unknown } | undefined)?.limit\n const asked = typeof raw === 'string' ? Number.parseInt(raw, 10) : 100\n const limit = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), 500) : 100\n return reply.send({ entries: await deps.storage.listAudit(limit) })\n })\n\n /**\n * Webhook health, and the deliveries that died.\n *\n * The reason the breaker keeps its counters on the row rather than in the\n * worker's memory. A self-hoster has no operations team watching a\n * dashboard, so a destination refusing deliveries since Tuesday has to be\n * answerable from the product — otherwise it is found when somebody asks\n * why the CRM has no leads this week.\n */\n app.get('/webhooks', { preHandler: requires('form.publish') }, async (_request, reply) => {\n const now = new Date(deps.nowIso())\n const hooks = await deps.storage.listWebhooks()\n return reply.send({ webhooks: hooks.map((hook) => healthOf(hook, now)) })\n })\n\n app.get('/deliveries/dead', { preHandler: requires('form.publish') }, async (request, reply) => {\n const raw = (request.query as { limit?: unknown } | undefined)?.limit\n const asked = typeof raw === 'string' ? Number.parseInt(raw, 10) : 50\n const limit = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), 200) : 50\n const dead = await deps.storage.deadDeliveries(limit)\n // Not the body. That is the submission's data in another coat, and this\n // endpoint answers \"what failed\", not \"what was in it\".\n return reply.send({\n deliveries: dead.map((delivery) => ({\n id: delivery.id,\n webhookId: delivery.webhookId,\n submissionId: delivery.submissionId,\n eventId: delivery.eventId,\n attempt: delivery.attempt,\n lastError: delivery.lastError,\n })),\n })\n })\n\n app.post(\n '/deliveries/:id/replay',\n { preHandler: requires('form.publish') },\n async (request, reply) => {\n const { id } = request.params as { id: string }\n const outcome = await replayDelivery(\n { storage: deps.storage, now: () => new Date(deps.nowIso()) },\n id,\n )\n\n if (!outcome.ok) {\n return outcome.kind === 'unknown'\n ? reply.code(404).send({ error: 'unknown_delivery' })\n : reply.code(409).send({\n error: 'not_dead',\n state: outcome.state,\n message:\n 'Only a dead delivery can be replayed. A pending one is already queued, and a delivered one would be sent twice.',\n })\n }\n\n // Recorded: it sends data to a third party on a person's say-so.\n await audit(request, {\n action: 'delivery.replayed',\n subject: id,\n detail: { webhookId: outcome.delivery.webhookId },\n })\n return reply.code(202).send({ id, state: outcome.delivery.state })\n },\n )\n\n /**\n * A puzzle for an anonymous visitor to solve before submitting.\n *\n * Public because the form is: requiring a session to obtain a challenge for\n * an unauthenticated submission would be a circle. It is cheap to mint and\n * stateless, so handing one out costs this server a hash.\n */\n app.get(\n '/f/:path/challenge',\n {\n // One per submission attempt is the legitimate rate, so the submission\n // limit is the right one. The point of a proof of work is that the\n // ATTACKER pays; handing out unlimited puzzles for free is the one part\n // of it that costs us instead.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n if (challengeSecret === undefined) {\n // Not configured is not an error: a deployment may decide its forms are\n // not public enough to need one, and the field should say so rather\n // than fail.\n return reply.code(404).send({ error: 'challenge_not_enabled' })\n }\n const { path } = request.params as { path: string }\n const form = await deps.storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n return reply.send(\n await mintChallenge({\n secret: challengeSecret,\n nowSeconds: Math.floor(new Date(deps.nowIso()).getTime() / 1000),\n }),\n )\n },\n )\n\n /**\n * Check a submitted solution, and spend it.\n *\n * Returns the refusal to send, or undefined when it passed. The spend is\n * separate from the verification and comes last: the stateless checks say\n * the solution is correct, and only the database can say it has not been\n * used — a correct solution stays correct, so without this one puzzle\n * would buy a thousand submissions.\n */\n async function checkChallenge(\n request: FastifyRequest,\n ): Promise<{ error: string; message: string } | undefined> {\n const header = request.headers[CHALLENGE_HEADER]\n if (typeof header !== 'string' || header === '') {\n return {\n error: 'challenge_required',\n message: `This form needs a solved challenge. Ask GET /f/:path/challenge for one and return it in ${CHALLENGE_HEADER}.`,\n }\n }\n\n const solution = decodeSolution(header)\n if (solution === undefined) {\n return { error: 'challenge_malformed', message: 'The challenge header is not base64 JSON.' }\n }\n\n const nowSeconds = Math.floor(new Date(deps.nowIso()).getTime() / 1000)\n const verified = await verifySolution(challengeSecret!, solution, nowSeconds)\n if (!verified.ok) {\n return {\n error: `challenge_${verified.reason}`,\n message: 'The challenge was not solved. Ask for a new one and try again.',\n }\n }\n\n // The expiry comes back from verification rather than being parsed out of\n // the salt a second time — one place reads that format.\n const spent = await deps.storage.spendChallenge(\n verified.challenge,\n new Date(verified.expiresAtSeconds * 1000).toISOString(),\n )\n if (!spent) {\n return {\n error: 'challenge_spent',\n message: 'That challenge has already been used. Each one is good for one submission.',\n }\n }\n return undefined\n }\n\n app.get('/forms', { preHandler: requires('form.read') }, async (_request, reply) => {\n return reply.send({ forms: await listForms(deps) })\n })\n\n app.get('/f/:path/versions', { preHandler: requires('form.read') }, async (request, reply) => {\n const { path } = request.params as { path: string }\n const versions = await listVersions(deps, path)\n if (versions === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.send({ versions })\n })\n\n app.get(\n '/f/:path/submissions',\n { preHandler: requires('submission.read') },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const listed = await listSubmissions(deps, path)\n if (listed === undefined) return reply.code(404).send({ error: 'unknown_form' })\n // The event a data protection officer asks about, and the one an audit\n // log that covers only mutations cannot answer: who read other people's\n // answers. The count, never the answers.\n await audit(request, {\n action: 'submission.read',\n subject: path,\n detail: { count: listed.length },\n })\n return reply.send({ submissions: listed })\n },\n )\n\n app.get(\n '/f/:path/submissions/export.csv',\n { preHandler: requires('submission.export') },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const csv = await exportCsv(deps, path)\n if (csv === undefined) return reply.code(404).send({ error: 'unknown_form' })\n // An export leaves the building. Bytes rather than rows, because that is\n // what was actually handed over.\n await audit(request, {\n action: 'submission.exported',\n subject: path,\n detail: { bytes: Buffer.byteLength(csv, 'utf8') },\n })\n return reply\n .header('content-type', 'text/csv; charset=utf-8')\n .header('content-disposition', `attachment; filename=\"${path}-submissions.csv\"`)\n .send(csv)\n },\n )\n\n // ------------------------------------------------------------------ public\n\n /**\n * Fetch a published form. The one public route deliberately NOT rate-limited.\n *\n * Every other unauthenticated route is: submitting, starting a draft, writing\n * one, reading one, minting a challenge, asking for an upload target. Those are\n * writes, or guesses, or work somebody can demand of the server. This is the\n * read every visitor has to make before they can do anything at all.\n *\n * Limiting it by IP would refuse the form to real people sharing an address —\n * an office, a school, a phone network behind CGNAT — and the failure would\n * look like the form being broken rather than like a limit. That is a worse\n * outcome than the cheap read it would prevent, and the read is cached by\n * `schemaHash` anyway.\n *\n * Written down because the asymmetry is deliberate and looks like an omission.\n */\n app.get('/f/:path', async (request, reply) => {\n const { path } = request.params as { path: string }\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.send({\n version: resolved.version,\n schemaHash: resolved.schemaHash,\n schema: resolved.schema,\n })\n })\n\n /**\n * Start a draft, and get the only key to it.\n *\n * The public plane is anonymous, so a draft has no account behind it. It was\n * previously addressed by an id the CALLER chose, with no check on either\n * route: anybody who knew or guessed an id could read a stranger's part-filled\n * form, and overwrite it — after which that person submits the substituted\n * content under their own name and nothing says otherwise\n * ([0062](../../../docs/decisions/0062-a-draft-carries-its-own-key.md)).\n *\n * So the server picks the id and signs it, and the token comes back exactly\n * once. Losing it means losing the draft, which is the correct trade for the\n * alternative.\n */\n app.post(\n '/f/:path/drafts',\n {\n // Unauthenticated, like the submission route, and every call hands out a\n // key. Cheap per call, but nothing should be free on the public plane.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const started = await startDraft(deps, { path })\n if (started === undefined) return reply.code(404).send({ error: 'unknown_form' })\n return reply.code(201).send(started)\n },\n )\n\n app.put(\n '/f/:path/drafts/:draftId',\n {\n // A database row per request, reachable without an account. The submission\n // route used to be described as \"the one unauthenticated write in the\n // product\"; that stopped being true when drafts were exposed here, and\n // nobody moved the limit across.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path, draftId } = request.params as { path: string; draftId: string }\n const token = request.headers[DRAFT_TOKEN_HEADER]\n if (typeof token !== 'string') {\n return reply.code(401).send({ error: 'draft_token_required' })\n }\n\n const saved = await saveDraft(deps, { path, draftId, token, data: request.body ?? {} })\n if (saved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n if (!saved.saved) return reply.code(403).send({ error: 'draft_token_invalid' })\n return reply.send({ version: saved.version })\n },\n )\n\n app.get(\n '/f/:path/drafts/:draftId',\n {\n // Limited because this is where a token would be tried one after another.\n // An HMAC is not realistically guessable, but limiting the write and\n // leaving the guess surface open is not a position worth defending.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path, draftId } = request.params as { path: string; draftId: string }\n const token = request.headers[DRAFT_TOKEN_HEADER]\n if (typeof token !== 'string') {\n return reply.code(401).send({ error: 'draft_token_required' })\n }\n\n const resumed = await resumeDraft(deps, { path, draftId, token })\n // 404 for a wrong token as well as a missing draft. Answering differently\n // would confirm which ids exist, which is the enumeration this closed.\n if (resumed === undefined) return reply.code(404).send({ error: 'unknown_draft' })\n return reply.send(resumed)\n },\n )\n\n /**\n * Ask where to put a file.\n *\n * Refusing here, before a byte is sent, is the difference between rejecting\n * a 2 GB upload and receiving one first. The reply carries the id the\n * submission will reference and the URL to PUT the bytes to.\n */\n app.post('/f/:path/files', {\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n }, async (request, reply) => {\n if (fileStore === undefined) {\n return reply.code(501).send({\n error: 'uploads_unavailable',\n message: 'This deployment has no file store configured, so it cannot accept uploads.',\n })\n }\n\n const { path } = request.params as { path: string }\n const resolved = await resolveForm(deps, path)\n if (resolved === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const form = await storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const actor = await actorOf(request)\n const origin = request.headers.origin\n\n const body = (request.body ?? {}) as {\n field?: unknown\n name?: unknown\n size?: unknown\n contentType?: unknown\n }\n const field = typeof body.field === 'string' ? body.field : undefined\n const name = typeof body.name === 'string' ? body.name : undefined\n const size = typeof body.size === 'number' ? body.size : undefined\n const contentType = typeof body.contentType === 'string' ? body.contentType : 'application/octet-stream'\n\n if (field === undefined || name === undefined || size === undefined) {\n return reply.code(400).send({\n error: 'invalid_request',\n message: 'Send the field, the name and the size of the file you want to upload.',\n })\n }\n\n if (size > maxFileBytes) {\n return reply.code(413).send({\n error: 'too_large',\n message: `This deployment accepts files up to ${String(maxFileBytes)} bytes.`,\n })\n }\n\n const outcome = await offerUpload(\n { storage, now: () => new Date(), newId: () => randomUUID() },\n {\n schema: resolved.schema,\n form,\n actor: actor === undefined ? 'anonymous' : 'authenticated',\n origin: typeof origin === 'string' ? origin : undefined,\n field,\n name,\n size,\n contentType,\n },\n )\n\n if (!outcome.ok) {\n const status =\n outcome.reason === 'not_allowed' ? 403 : outcome.reason === 'too_large' ? 413 : 400\n return reply.code(status).send({ error: outcome.reason })\n }\n\n return reply.code(201).send({\n id: outcome.file.id,\n name: outcome.file.name,\n size: outcome.file.size,\n contentType: outcome.file.contentType,\n storageKey: outcome.file.storageKey,\n // Where to PUT the bytes. A URL rather than a key so an S3 store can\n // hand back a presigned one here and the client never learns the\n // difference.\n uploadUrl: `/f/${encodeURIComponent(path)}/files/${outcome.file.id}`,\n })\n })\n\n /** Receive the bytes for a file that was offered. */\n app.put('/f/:path/files/:id', async (request, reply) => {\n if (fileStore === undefined) return reply.code(501).send({ error: 'uploads_unavailable' })\n\n const { path, id } = request.params as { path: string; id: string }\n const form = await storage.getFormByPath(path)\n if (form === undefined) return reply.code(404).send({ error: 'unknown_form' })\n\n const file = await storage.getFile(id)\n // Checked against the form in the URL: an id alone must not be enough to\n // write bytes into a form somebody cannot submit to.\n if (file === undefined || file.formId !== form.id) {\n return reply.code(404).send({ error: 'unknown_file' })\n }\n if (file.state !== 'offered') {\n // An upload is once. Re-writing a claimed file would change what a\n // stored submission says was attached to it.\n return reply.code(409).send({ error: 'already_uploaded' })\n }\n\n const bytes = request.body\n if (!Buffer.isBuffer(bytes)) return reply.code(400).send({ error: 'no_body' })\n if (bytes.byteLength > maxFileBytes) return reply.code(413).send({ error: 'too_large' })\n if (bytes.byteLength !== file.size) {\n // The offer named a size and the offer is what was checked against the\n // field's limit. Accepting a different number of bytes would make that\n // check a suggestion.\n return reply.code(400).send({\n error: 'size_mismatch',\n message: `This upload was offered as ${String(file.size)} bytes and ${String(bytes.byteLength)} arrived.`,\n })\n }\n\n await fileStore.put(file.storageKey, bytes)\n await storage.updateFile({ ...file, state: 'stored' })\n\n return reply.code(204).send()\n })\n\n /**\n * Serve a file back.\n *\n * Always as an attachment, never inline, and `nosniff` alongside it. Stored\n * cross-site scripting through an uploaded HTML or SVG file is the most\n * commonly exploited vulnerability in this product category, and the real\n * answer is a separate hostname — which a single-container deployment does\n * not have. Forcing a download is the accommodation, and it is the one place\n * this deployment model genuinely costs something.\n */\n app.get('/f/:path/files/:id', async (request, reply) => {\n if (fileStore === undefined) return reply.code(501).send({ error: 'uploads_unavailable' })\n\n const { path, id } = request.params as { path: string; id: string }\n const actor = await actorOf(request)\n if (actor === undefined) return reply.code(401).send({ error: 'unauthenticated' })\n\n const form = await storage.getFormByPath(path)\n const file = await storage.getFile(id)\n if (form === undefined || file === undefined || file.formId !== form.id) {\n return reply.code(404).send({ error: 'unknown_file' })\n }\n\n const stream = await fileStore.open(file.storageKey)\n if (stream === undefined) return reply.code(404).send({ error: 'unknown_file' })\n\n return reply\n .header('content-type', 'application/octet-stream')\n .header('x-content-type-options', 'nosniff')\n .header('content-security-policy', \"default-src 'none'; sandbox\")\n // The reader's own name for it, quoted and stripped of anything that\n // could end the header early.\n .header('content-disposition', `attachment; filename=\"${file.name.replace(/[\"\\r\\n]/g, '')}\"`)\n .send(stream)\n })\n\n app.post(\n '/f/:path/submissions',\n {\n // The unauthenticated write that matters most, and no longer the only\n // one: drafts write too, and are limited the same way. Keyed by IP, which\n // is the only identity an anonymous submitter has.\n config: { rateLimit: { max: submissionLimit.max, timeWindow: submissionLimit.timeWindowMs } },\n },\n async (request, reply) => {\n const { path } = request.params as { path: string }\n const declaredSchemaHash = request.headers[SCHEMA_HASH_HEADER]\n if (typeof declaredSchemaHash !== 'string' || declaredSchemaHash === '') {\n return reply.code(400).send({\n error: 'missing_schema_hash',\n message: `Send the schema hash you rendered in the ${SCHEMA_HASH_HEADER} header.`,\n })\n }\n\n // The public plane is public, so an identity is optional here — but if one\n // is presented and valid, the access policy does not apply to it. An\n // invalid credential is treated as no credential rather than as an error:\n // this route's job is to accept submissions, not to adjudicate logins.\n const actor = await actorOf(request)\n const origin = request.headers.origin\n\n // Before the engine runs, and only for a visitor who is not signed in.\n // Somebody with a session has already paid a cost the challenge is a\n // substitute for, and making them solve a puzzle as well would be\n // ceremony — while an anonymous submission is the surface this exists\n // to protect.\n if (challengeSecret !== undefined && actor === undefined) {\n const refusal = await checkChallenge(request)\n if (refusal !== undefined) return reply.code(400).send(refusal)\n }\n\n const outcome = await createSubmission(deps, {\n path,\n declaredSchemaHash,\n data: request.body ?? {},\n actor: actor === undefined ? 'anonymous' : 'authenticated',\n ...(typeof origin === 'string' ? { origin } : {}),\n })\n\n if (outcome.ok) return reply.code(201).send({ id: outcome.id, data: outcome.canonicalData })\n\n switch (outcome.kind) {\n case 'unknown_form':\n return reply.code(404).send({ error: 'unknown_form' })\n case 'version_changed':\n // 409 carries the CURRENT schema so the client can re-render and\n // preserve what it can, instead of guessing why it was refused.\n return reply.code(409).send({\n error: 'FORM_VERSION_CHANGED',\n current:\n outcome.current === undefined\n ? undefined\n : {\n version: outcome.current.version,\n schemaHash: outcome.current.schemaHash,\n schema: outcome.current.schema,\n },\n })\n case 'invalid':\n // The identical error shape the client's engine produces, so server\n // errors render through the same code path as local ones.\n return reply.code(422).send({ error: 'invalid', errors: outcome.errors })\n case 'forbidden':\n // Deliberately says nothing about WHICH rule refused. \"Not public\" and\n // \"not from your origin\" are the same answer to someone probing.\n return reply.code(403).send({ error: 'forbidden' })\n case 'source_unavailable':\n // 503 and not 422: nothing about the submission is wrong, a list this\n // deployment owns could not vouch for it. Retryable, and the draft still\n // holds the answers — where accepting the value unchecked would store\n // something nobody can detect afterwards.\n return reply\n .code(503)\n .send({ error: 'source_unavailable', source: outcome.source })\n }\n },\n )\n\n return app\n}\n","import { jsonb, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'\nimport type postgres from 'postgres'\n\n/**\n * The thin slice's tables. Three rules from the design spec are enforced HERE,\n * in the database, because application code can be bypassed and these must not\n * be:\n *\n * - published versions are immutable (a trigger refuses UPDATE),\n * - a submission binds to its exact version by real foreign key,\n * - that key is ON DELETE RESTRICT, so orphaning a submission from the schema\n * that produced it is structurally impossible.\n */\nexport const forms = pgTable('forms', {\n id: uuid('id').primaryKey(),\n path: text('path').notNull().unique(),\n currentVersionId: uuid('current_version_id'),\n // Defaults to the safe value in the DATABASE as well as in the code, so a\n // row inserted by a migration or a fixture is private unless it says\n // otherwise. A default of 'public' one refactor away from being forgotten is\n // how a private form becomes a public one.\n accessSubmit: text('access_submit').notNull().default('authenticated'),\n // NULL means no allowlist is in force. An empty array means nothing is\n // allowed — a different thing, and the reason this is nullable.\n allowedOrigins: text('allowed_origins').array(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport const formVersions = pgTable('form_versions', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n version: integer('version').notNull(),\n schema: jsonb('schema').notNull(),\n schemaHash: text('schema_hash').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport const users = pgTable('users', {\n id: uuid('id').primaryKey(),\n email: text('email').notNull().unique(),\n passwordHash: text('password_hash').notNull(),\n role: text('role').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n})\n\nexport const apiKeys = pgTable('api_keys', {\n id: uuid('id').primaryKey(),\n name: text('name').notNull(),\n prefix: text('prefix').notNull(),\n secretHash: text('secret_hash').notNull(),\n role: text('role').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n revokedAt: timestamp('revoked_at', { withTimezone: true }),\n})\n\nexport const drafts = pgTable('drafts', {\n id: text('id').notNull(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n formVersionId: uuid('form_version_id')\n .notNull()\n .references(() => formVersions.id),\n data: jsonb('data').notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n})\n\nexport const webhooks = pgTable('webhooks', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n url: text('url').notNull(),\n secret: text('secret').notNull(),\n consecutiveFailures: integer('consecutive_failures').notNull().default(0),\n openedAt: timestamp('opened_at', { withTimezone: true }),\n})\n\n/**\n * The outbox. A row here and the submission that caused it are written by the\n * same transaction, which is what makes \"accepted\" and \"will be delivered\" one\n * decision rather than two.\n */\nexport const deliveries = pgTable('deliveries', {\n id: uuid('id').primaryKey(),\n webhookId: uuid('webhook_id').notNull(),\n submissionId: uuid('submission_id').notNull(),\n eventId: uuid('event_id').notNull(),\n body: text('body').notNull(),\n attempt: integer('attempt').notNull().default(0),\n nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }).notNull(),\n state: text('state').notNull().default('pending'),\n lastError: text('last_error'),\n})\n\nexport const spentChallenges = pgTable('spent_challenges', {\n challenge: text('challenge').primaryKey(),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n})\n\nexport const auditLog = pgTable('audit_log', {\n id: uuid('id').primaryKey(),\n at: timestamp('at', { withTimezone: true }).notNull(),\n action: text('action').notNull(),\n actorKind: text('actor_kind'),\n actorId: text('actor_id'),\n subject: text('subject'),\n requestId: text('request_id'),\n detail: jsonb('detail'),\n})\n\nexport const files = pgTable('files', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id').notNull(),\n name: text('name').notNull(),\n size: integer('size').notNull(),\n contentType: text('content_type').notNull(),\n storageKey: text('storage_key').notNull(),\n state: text('state').notNull().default('offered'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n submissionId: uuid('submission_id'),\n})\n\nexport const submissions = pgTable('submissions', {\n id: uuid('id').primaryKey(),\n formId: uuid('form_id')\n .notNull()\n .references(() => forms.id),\n formVersionId: uuid('form_version_id')\n .notNull()\n .references(() => formVersions.id, { onDelete: 'restrict' }),\n data: jsonb('data').notNull(),\n submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull(),\n})\n\n/**\n * Bootstrap DDL for the thin slice — drizzle-kit migrations arrive with the\n * lifecycle work; a self-hosted skeleton that creates its own tables on first\n * start is worth more right now than a migration pipeline.\n */\nexport async function bootstrapSchema(sql: postgres.Sql): Promise<void> {\n await sql`\n CREATE TABLE IF NOT EXISTS forms (\n id uuid PRIMARY KEY,\n path text NOT NULL UNIQUE,\n current_version_id uuid,\n -- Private by default in the DATABASE, not only in the code, so a row\n -- inserted by a migration or a fixture is not accidentally open.\n access_submit text NOT NULL DEFAULT 'authenticated'\n CHECK (access_submit IN ('authenticated', 'public')),\n -- NULL: no allowlist in force. Empty array: nothing allowed. Different\n -- things, which is why this is nullable rather than defaulting to '{}'.\n allowed_origins text[],\n created_at timestamptz NOT NULL DEFAULT now()\n )`\n // Existing databases predate the two columns above. Adding them here rather\n // than in a separate migration keeps bootstrap idempotent for both a fresh\n // database and one created before access control existed.\n await sql`\n ALTER TABLE forms\n ADD COLUMN IF NOT EXISTS access_submit text NOT NULL DEFAULT 'authenticated'`\n await sql`ALTER TABLE forms ADD COLUMN IF NOT EXISTS allowed_origins text[]`\n await sql`\n CREATE TABLE IF NOT EXISTS form_versions (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id),\n version integer NOT NULL,\n schema jsonb NOT NULL,\n schema_hash text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (form_id, version),\n UNIQUE (form_id, schema_hash)\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS submissions (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id),\n form_version_id uuid NOT NULL REFERENCES form_versions(id) ON DELETE RESTRICT,\n data jsonb NOT NULL,\n submitted_at timestamptz NOT NULL\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS webhooks (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id) ON DELETE CASCADE,\n url text NOT NULL,\n secret text NOT NULL\n )`\n await sql`\n ALTER TABLE webhooks\n ADD COLUMN IF NOT EXISTS consecutive_failures integer NOT NULL DEFAULT 0,\n -- When the breaker opened, null while the destination is answering. On\n -- the row rather than in the worker's memory: memory does not survive a\n -- restart, and a self-hoster with no operations team needs a failing\n -- webhook to be visible on a screen rather than in a log nobody tails.\n ADD COLUMN IF NOT EXISTS opened_at timestamptz`\n await sql`\n CREATE TABLE IF NOT EXISTS deliveries (\n id uuid PRIMARY KEY,\n webhook_id uuid NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,\n -- RESTRICT, not CASCADE: a delivery is the record that something was\n -- sent about this submission, and deleting the submission must not\n -- quietly erase it.\n submission_id uuid NOT NULL REFERENCES submissions(id) ON DELETE RESTRICT,\n event_id uuid NOT NULL,\n body text NOT NULL,\n attempt integer NOT NULL DEFAULT 0,\n next_attempt_at timestamptz NOT NULL,\n state text NOT NULL DEFAULT 'pending'\n CHECK (state IN ('pending', 'delivered', 'dead')),\n last_error text\n )`\n await sql`\n CREATE INDEX IF NOT EXISTS deliveries_due\n ON deliveries (next_attempt_at) WHERE state = 'pending'`\n await sql`\n CREATE TABLE IF NOT EXISTS files (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES forms(id) ON DELETE RESTRICT,\n -- What the reader called it. Display only: the storage key is minted by\n -- the server, because a key built from a submitted filename is a path\n -- traversal waiting for somebody to try it.\n name text NOT NULL,\n size integer NOT NULL,\n content_type text NOT NULL,\n storage_key text NOT NULL,\n state text NOT NULL DEFAULT 'offered'\n CHECK (state IN ('offered', 'stored', 'claimed')),\n created_at timestamptz NOT NULL,\n -- RESTRICT for the same reason a delivery uses it: the row is the record\n -- that something was attached, and deleting the submission must not\n -- quietly erase what it carried.\n submission_id uuid REFERENCES submissions(id) ON DELETE RESTRICT,\n -- One file, one submission. Enforced here rather than in application\n -- code, because two concurrent submissions claiming the same file is\n -- exactly the race application code loses — and the loser would be a\n -- submission referencing bytes another submission owns.\n CONSTRAINT files_one_submission UNIQUE (id, submission_id)\n )`\n await sql`\n CREATE INDEX IF NOT EXISTS files_abandoned\n ON files (created_at) WHERE state <> 'claimed'`\n await sql`\n CREATE TABLE IF NOT EXISTS users (\n id uuid PRIMARY KEY,\n email text NOT NULL UNIQUE,\n password_hash text NOT NULL,\n role text NOT NULL,\n created_at timestamptz NOT NULL\n )`\n await sql`\n CREATE TABLE IF NOT EXISTS api_keys (\n id uuid PRIMARY KEY,\n name text NOT NULL,\n prefix text NOT NULL,\n secret_hash text NOT NULL,\n role text NOT NULL,\n created_at timestamptz NOT NULL,\n revoked_at timestamptz\n )`\n await sql`CREATE INDEX IF NOT EXISTS api_keys_prefix ON api_keys (prefix)`\n await sql`\n CREATE TABLE IF NOT EXISTS spent_challenges (\n -- The primary key IS the mechanism. Two requests arriving with one\n -- solution both pass every stateless check — the signature is ours\n -- and the hash is right — and only the database can decide which of\n -- them spends it. A read-then-write would race exactly where somebody\n -- is looking.\n challenge text PRIMARY KEY,\n expires_at timestamptz NOT NULL\n )`\n await sql`CREATE INDEX IF NOT EXISTS spent_challenges_expiry ON spent_challenges (expires_at)`\n\n await sql`\n CREATE TABLE IF NOT EXISTS audit_log (\n id uuid PRIMARY KEY,\n at timestamptz NOT NULL,\n action text NOT NULL,\n -- Null for an anonymous public submission, which is an answer rather\n -- than a gap: the form was open and nobody was signed in.\n actor_kind text CHECK (actor_kind IN ('user', 'apiKey')),\n actor_id text,\n subject text,\n request_id text,\n -- Identifiers and counts. NEVER a submission's answers: an audit log is\n -- read by more people and kept longer than the data it describes, so\n -- data inside it is a second copy of the thing being protected.\n detail jsonb\n )`\n // Reading the log is the common query and it is always \"recently, and\n // usually about one thing\".\n await sql`CREATE INDEX IF NOT EXISTS audit_log_at ON audit_log (at DESC)`\n await sql`CREATE INDEX IF NOT EXISTS audit_log_subject ON audit_log (subject, at DESC)`\n\n await sql`\n CREATE TABLE IF NOT EXISTS drafts (\n id text NOT NULL,\n form_id uuid NOT NULL REFERENCES forms(id),\n form_version_id uuid NOT NULL REFERENCES form_versions(id),\n data jsonb NOT NULL,\n updated_at timestamptz NOT NULL,\n PRIMARY KEY (form_id, id)\n )`\n // Immutability lives in the database, not in application discipline: a\n // published version row can never change, because every submission's audit\n // story depends on it staying exactly what the user saw.\n await sql`\n CREATE OR REPLACE FUNCTION refuse_version_update() RETURNS trigger AS $$\n BEGIN\n RAISE EXCEPTION 'form_versions rows are immutable; publish a new version instead';\n END\n $$ LANGUAGE plpgsql`\n await sql`DROP TRIGGER IF EXISTS form_versions_immutable ON form_versions`\n await sql`\n CREATE TRIGGER form_versions_immutable\n BEFORE UPDATE ON form_versions\n FOR EACH ROW EXECUTE FUNCTION refuse_version_update()`\n\n // Append-only, in the database rather than by application discipline — for\n // the same reason the version rows are immutable there. An audit log the\n // application can rewrite is a log that says whatever the person who got\n // into the application wants it to say, and the one moment it matters is\n // exactly the moment somebody has a reason to edit it.\n //\n // A deployment should ALSO give the application's role INSERT and SELECT and\n // nothing else. This trigger is the belt: the grant is the braces, and it is\n // the one a self-hoster has to do themselves.\n await sql`\n CREATE OR REPLACE FUNCTION refuse_audit_change() RETURNS trigger AS $$\n BEGIN\n RAISE EXCEPTION 'audit_log is append-only';\n END\n $$ LANGUAGE plpgsql`\n await sql`DROP TRIGGER IF EXISTS audit_log_append_only ON audit_log`\n await sql`\n CREATE TRIGGER audit_log_append_only\n BEFORE UPDATE OR DELETE ON audit_log\n FOR EACH ROW EXECUTE FUNCTION refuse_audit_change()`\n}\n","import { and, desc, eq, inArray, lt, lte, max, ne } from 'drizzle-orm'\nimport { drizzle } from 'drizzle-orm/postgres-js'\nimport type postgres from 'postgres'\nimport type { FormSchema } from '@formancy/spec'\nimport type { AuditEntry, FileRecord, Role, Storage } from '@formancy/server-core'\nimport { apiKeys, auditLog, spentChallenges, deliveries, drafts, files, forms, formVersions, submissions, webhooks } from './db.js'\nimport { users } from './db.js'\n\n/** The Storage port over Postgres — the mirror of server-core's in-memory one. */\nexport function createPostgresStorage(sql: postgres.Sql): Storage {\n const db = drizzle(sql)\n\n return {\n async getFormByPath(path) {\n const rows = await db.select().from(forms).where(eq(forms.path, path)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n path: row.path,\n currentVersionId: row.currentVersionId,\n accessSubmit: row.accessSubmit === 'public' ? 'public' : 'authenticated',\n allowedOrigins: row.allowedOrigins,\n }\n },\n\n async listForms() {\n const rows = await db.select().from(forms)\n return rows.map((row) => ({\n id: row.id,\n path: row.path,\n currentVersionId: row.currentVersionId,\n accessSubmit: row.accessSubmit === 'public' ? ('public' as const) : ('authenticated' as const),\n allowedOrigins: row.allowedOrigins,\n }))\n },\n\n async createForm(record) {\n await db.insert(forms).values({\n id: record.id,\n path: record.path,\n currentVersionId: record.currentVersionId,\n accessSubmit: record.accessSubmit,\n allowedOrigins: record.allowedOrigins,\n })\n },\n\n async updateFormAccess(formId, access) {\n await db\n .update(forms)\n .set({ accessSubmit: access.accessSubmit, allowedOrigins: access.allowedOrigins })\n .where(eq(forms.id, formId))\n },\n\n async setCurrentVersion(formId, versionId) {\n await db.update(forms).set({ currentVersionId: versionId }).where(eq(forms.id, formId))\n },\n\n async insertVersion(record) {\n await db.insert(formVersions).values({\n id: record.id,\n formId: record.formId,\n version: record.version,\n schema: record.schema,\n schemaHash: record.schemaHash,\n })\n },\n\n async publishVersion({ form, version, audit }) {\n // ONE transaction. Three calls left a window where the form row existed\n // with a null pointer, which is a form that answers its URL and has no\n // schema to render — present in the list, 404 when opened.\n await db.transaction(async (tx) => {\n if (form !== undefined) {\n await tx.insert(forms).values({\n id: form.id,\n path: form.path,\n currentVersionId: form.currentVersionId,\n accessSubmit: form.accessSubmit,\n allowedOrigins: form.allowedOrigins,\n })\n }\n\n await tx.insert(formVersions).values({\n id: version.id,\n formId: version.formId,\n version: version.version,\n schema: version.schema,\n schemaHash: version.schemaHash,\n })\n\n const pointed = await tx\n .update(forms)\n .set({ currentVersionId: version.id })\n .where(eq(forms.id, version.formId))\n .returning({ id: forms.id })\n\n // An UPDATE that matched nothing is not an error in SQL, so it has to\n // be looked at: without this, publishing to a form that has been\n // deleted underneath would insert an orphan version and report success.\n if (pointed.length !== 1) {\n throw new Error(\n `Publishing to form ${version.formId} matched no form row. Rolling back, so there is no version pointing at a form that is not there.`,\n )\n }\n\n if (audit !== undefined) await tx.insert(auditLog).values(auditRow(audit))\n })\n },\n\n async getVersionById(id) {\n const rows = await db.select().from(formVersions).where(eq(formVersions.id, id)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }\n },\n\n async findVersionByHash(formId, schemaHash) {\n const rows = await db\n .select()\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n const row = rows.find((candidate) => candidate.schemaHash === schemaHash)\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }\n },\n\n async latestVersionNumber(formId) {\n const rows = await db\n .select({ latest: max(formVersions.version) })\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n return rows[0]?.latest ?? 0\n },\n\n async insertSubmission(record, queued, claimFileIds, audit) {\n // ONE transaction. The submission and the deliveries it triggers commit\n // together or not at all — the requirement that chose this database\n // (docs/decisions/0024-postgres-over-mongodb.md).\n await db.transaction(async (tx) => {\n await tx.insert(submissions).values({\n id: record.id,\n formId: record.formId,\n formVersionId: record.formVersionId,\n data: record.data,\n submittedAt: new Date(record.submittedAt),\n })\n\n if (queued !== undefined && queued.length > 0) {\n await tx.insert(deliveries).values(\n queued.map((delivery) => ({\n id: delivery.id,\n webhookId: delivery.webhookId,\n submissionId: delivery.submissionId,\n eventId: delivery.eventId,\n body: delivery.body,\n attempt: delivery.attempt,\n nextAttemptAt: new Date(delivery.nextAttemptAt),\n state: delivery.state,\n lastError: delivery.lastError,\n })),\n )\n }\n\n if (claimFileIds !== undefined && claimFileIds.length > 0) {\n // In the same transaction, and only from `stored`: two concurrent\n // submissions naming the same file both pass the check made outside\n // the transaction, and this is where the second one loses. The row\n // count is what says it lost — an UPDATE that matched nothing is not\n // an error in SQL, so it has to be looked at.\n const claimed = await tx\n .update(files)\n .set({ state: 'claimed', submissionId: record.id })\n .where(and(inArray(files.id, [...claimFileIds]), eq(files.state, 'stored')))\n .returning({ id: files.id })\n\n if (claimed.length !== claimFileIds.length) {\n throw new Error(\n 'A file this submission names was claimed by another one first. Rolling back, so the submission does not reference bytes it does not own.',\n )\n }\n }\n\n // Last, and inside: a submission that rolled back must leave nothing\n // behind saying it happened.\n if (audit !== undefined) await tx.insert(auditLog).values(auditRow(audit))\n })\n },\n\n async spendChallenge(challenge, expiresAtIso) {\n // `ON CONFLICT DO NOTHING` and look at what came back: the insert is the\n // claim, and the row count is the answer. Checking first and inserting\n // second would leave a window between the two.\n const claimed = await db\n .insert(spentChallenges)\n .values({ challenge, expiresAt: new Date(expiresAtIso) })\n .onConflictDoNothing()\n .returning({ challenge: spentChallenges.challenge })\n return claimed.length === 1\n },\n\n async forgetExpiredChallenges(beforeIso) {\n const gone = await db\n .delete(spentChallenges)\n .where(lt(spentChallenges.expiresAt, new Date(beforeIso)))\n .returning({ challenge: spentChallenges.challenge })\n return gone.length\n },\n\n async recordAudit(entry) {\n await db.insert(auditLog).values(auditRow(entry))\n },\n\n async listAudit(limit) {\n const rows = await db.select().from(auditLog).orderBy(desc(auditLog.at)).limit(limit)\n return rows.map((row) => ({\n id: row.id,\n at: row.at.toISOString(),\n action: row.action as AuditEntry['action'],\n ...(row.actorKind === null ? {} : { actorKind: row.actorKind as 'user' | 'apiKey' }),\n ...(row.actorId === null ? {} : { actorId: row.actorId }),\n ...(row.subject === null ? {} : { subject: row.subject }),\n ...(row.requestId === null ? {} : { requestId: row.requestId }),\n ...(row.detail === null\n ? {}\n : { detail: row.detail as NonNullable<AuditEntry['detail']> }),\n }))\n },\n\n async insertFile(record) {\n await db.insert(files).values({ ...record, createdAt: new Date(record.createdAt) })\n },\n\n async getFile(id) {\n const [row] = await db.select().from(files).where(eq(files.id, id)).limit(1)\n return row === undefined ? undefined : toFileRecord(row)\n },\n\n async updateFile(record) {\n await db\n .update(files)\n .set({ state: record.state, submissionId: record.submissionId })\n .where(eq(files.id, record.id))\n },\n\n async abandonedFiles(beforeIso) {\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 const rows = await db\n .select()\n .from(files)\n .where(and(ne(files.state, 'claimed'), lt(files.createdAt, new Date(beforeIso))))\n return rows.map(toFileRecord)\n },\n\n async deleteFiles(ids) {\n if (ids.length === 0) return\n await db.delete(files).where(inArray(files.id, [...ids]))\n },\n\n async webhooksForForm(formId) {\n const rows = await db.select().from(webhooks).where(eq(webhooks.formId, formId))\n return rows.map(toWebhookRecord)\n },\n\n async insertWebhook(record) {\n // Spelled out rather than spread: `openedAt` is an ISO string on the\n // record and a timestamp in the column, and a spread would have passed\n // the string straight through.\n await db.insert(webhooks).values({\n id: record.id,\n formId: record.formId,\n url: record.url,\n secret: record.secret,\n consecutiveFailures: record.consecutiveFailures,\n openedAt: record.openedAt === null ? null : new Date(record.openedAt),\n })\n },\n\n async updateWebhook(record) {\n // Health only. The url and the secret are not the worker's to change,\n // and a full row update here would let a failed delivery quietly\n // rewrite where the next one goes.\n await db\n .update(webhooks)\n .set({\n consecutiveFailures: record.consecutiveFailures,\n openedAt: record.openedAt === null ? null : new Date(record.openedAt),\n })\n .where(eq(webhooks.id, record.id))\n },\n\n async listWebhooks() {\n const rows = await db.select().from(webhooks)\n return rows.map(toWebhookRecord)\n },\n\n async deadDeliveries(limit) {\n const rows = await db\n .select()\n .from(deliveries)\n .where(eq(deliveries.state, 'dead'))\n .orderBy(desc(deliveries.nextAttemptAt))\n .limit(limit)\n return rows.map(toDeliveryRecord)\n },\n\n async getDelivery(id) {\n const [row] = await db.select().from(deliveries).where(eq(deliveries.id, id)).limit(1)\n return row === undefined ? undefined : toDeliveryRecord(row)\n },\n\n async claimDueDeliveries(nowIso, limit) {\n const rows = await db\n .select()\n .from(deliveries)\n .where(and(eq(deliveries.state, 'pending'), lte(deliveries.nextAttemptAt, new Date(nowIso))))\n .orderBy(deliveries.nextAttemptAt)\n .limit(limit)\n return rows.map(toDeliveryRecord)\n },\n\n async updateDelivery(record) {\n await db\n .update(deliveries)\n .set({\n attempt: record.attempt,\n nextAttemptAt: new Date(record.nextAttemptAt),\n state: record.state,\n lastError: record.lastError,\n })\n .where(eq(deliveries.id, record.id))\n },\n\n async listSubmissions() {\n const rows = await db.select().from(submissions)\n return rows.map(toSubmissionRecord)\n },\n\n async listSubmissionsByForm(formId) {\n const rows = await db\n .select()\n .from(submissions)\n .where(eq(submissions.formId, formId))\n .orderBy(desc(submissions.submittedAt), desc(submissions.id))\n return rows.map(toSubmissionRecord)\n },\n\n async listVersionsByForm(formId) {\n const rows = await db\n .select()\n .from(formVersions)\n .where(eq(formVersions.formId, formId))\n .orderBy(desc(formVersions.version))\n return rows.map((row) => ({\n id: row.id,\n formId: row.formId,\n version: row.version,\n schema: row.schema as FormSchema,\n schemaHash: row.schemaHash,\n }))\n },\n\n async upsertDraft(record) {\n await db\n .insert(drafts)\n .values({\n id: record.id,\n formId: record.formId,\n formVersionId: record.formVersionId,\n data: record.data,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoUpdate({\n target: [drafts.formId, drafts.id],\n set: {\n formVersionId: record.formVersionId,\n data: record.data,\n updatedAt: new Date(record.updatedAt),\n },\n })\n },\n\n async getDraft(formId, draftId) {\n const rows = await db\n .select()\n .from(drafts)\n .where(and(eq(drafts.formId, formId), eq(drafts.id, draftId)))\n .limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n formId: row.formId,\n formVersionId: row.formVersionId,\n data: row.data,\n updatedAt: row.updatedAt.toISOString(),\n }\n },\n\n async insertUser(record) {\n await db.insert(users).values({\n id: record.id,\n email: record.email,\n passwordHash: record.passwordHash,\n role: record.role,\n createdAt: new Date(record.createdAt),\n })\n },\n\n async getUserByEmail(email) {\n const rows = await db.select().from(users).where(eq(users.email, email)).limit(1)\n const row = rows[0]\n if (row === undefined) return undefined\n return {\n id: row.id,\n email: row.email,\n passwordHash: row.passwordHash,\n role: row.role as Role,\n createdAt: row.createdAt.toISOString(),\n }\n },\n\n async insertApiKey(record) {\n await db.insert(apiKeys).values({\n id: record.id,\n name: record.name,\n prefix: record.prefix,\n secretHash: record.secretHash,\n role: record.role,\n createdAt: new Date(record.createdAt),\n revokedAt: record.revokedAt === null ? null : new Date(record.revokedAt),\n })\n },\n\n async listApiKeys() {\n const rows = await db.select().from(apiKeys)\n return rows.map(toApiKeyRecord)\n },\n\n async findApiKeysByPrefix(prefix) {\n const rows = await db.select().from(apiKeys).where(eq(apiKeys.prefix, prefix))\n return rows.map(toApiKeyRecord)\n },\n\n async revokeApiKey(id, atIso) {\n await db.update(apiKeys).set({ revokedAt: new Date(atIso) }).where(eq(apiKeys.id, id))\n },\n }\n}\n\nfunction toApiKeyRecord(row: {\n id: string\n name: string\n prefix: string\n secretHash: string\n role: string\n createdAt: Date\n revokedAt: Date | null\n}) {\n return {\n id: row.id,\n name: row.name,\n prefix: row.prefix,\n secretHash: row.secretHash,\n role: row.role as Role,\n createdAt: row.createdAt.toISOString(),\n revokedAt: row.revokedAt === null ? null : row.revokedAt.toISOString(),\n }\n}\n\nfunction toSubmissionRecord(row: {\n id: string\n formId: string\n formVersionId: string\n data: unknown\n submittedAt: Date\n}) {\n return {\n id: row.id,\n formId: row.formId,\n formVersionId: row.formVersionId,\n data: row.data,\n submittedAt: row.submittedAt.toISOString(),\n }\n}\n\n/** A delivery row as the port describes it. */\nfunction toDeliveryRecord(row: typeof deliveries.$inferSelect) {\n return {\n id: row.id,\n webhookId: row.webhookId,\n submissionId: row.submissionId,\n eventId: row.eventId,\n body: row.body,\n attempt: row.attempt,\n nextAttemptAt: row.nextAttemptAt.toISOString(),\n state: row.state as 'pending' | 'delivered' | 'dead',\n lastError: row.lastError,\n }\n}\n\n/** A files row as the port describes it: timestamps as ISO strings. */\nfunction toFileRecord(row: typeof files.$inferSelect): FileRecord {\n return {\n id: row.id,\n formId: row.formId,\n name: row.name,\n size: row.size,\n contentType: row.contentType,\n storageKey: row.storageKey,\n state: row.state as FileRecord['state'],\n createdAt: row.createdAt.toISOString(),\n submissionId: row.submissionId,\n }\n}\n\n/** A webhook row as the port describes it, health included. */\nfunction toWebhookRecord(row: typeof webhooks.$inferSelect) {\n return {\n id: row.id,\n formId: row.formId,\n url: row.url,\n secret: row.secret,\n consecutiveFailures: row.consecutiveFailures,\n openedAt: row.openedAt === null ? null : row.openedAt.toISOString(),\n }\n}\n\n/**\n * An entry as the table holds it.\n *\n * Absent and null are the same thing here, and the conversion is in one place\n * so two call sites cannot disagree about which they write.\n */\nfunction auditRow(entry: AuditEntry) {\n return {\n id: entry.id,\n at: new Date(entry.at),\n action: entry.action,\n actorKind: entry.actorKind ?? null,\n actorId: entry.actorId ?? null,\n subject: entry.subject ?? null,\n requestId: entry.requestId ?? null,\n detail: entry.detail ?? null,\n }\n}\n","import { createHash } from 'node:crypto'\nimport { createReadStream } from 'node:fs'\nimport { mkdir, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join, resolve, sep } from 'node:path'\nimport type { Readable } from 'node:stream'\n\n/**\n * Where uploaded bytes live.\n *\n * An interface with one implementation, because the second one is the point:\n * S3 or a customer's own service replaces this file and nothing else. The\n * use-cases in `@formancy/server-core` never see it at all — they decide what\n * is rubbish, and the host deletes it.\n *\n * Local disk is the default because `docker compose up` has to produce\n * something that works, and a self-hoster with one container and one volume is\n * the deployment this product is for. It is genuinely the weakest option: it\n * does not survive more than one replica, and the section below says so rather\n * than leaving somebody to find out.\n */\nexport interface FileStore {\n /** Write the bytes. The key was minted by the server, never by a client. */\n put(key: string, bytes: Buffer): Promise<void>\n /** Read them back, as a stream, so a large file is not held in memory. */\n open(key: string): Promise<Readable | undefined>\n /** Remove them. Missing is success: a collector must be safe to re-run. */\n remove(key: string): Promise<void>\n /** The size on disk, for checking the bytes match what was promised. */\n sizeOf(key: string): Promise<number | undefined>\n}\n\n/**\n * Bytes under one directory, one file per key.\n *\n * **Keys are checked, not trusted.** A key is minted by the server from a form\n * id and a file id, so it cannot contain a traversal — but this resolves it\n * and refuses anything landing outside the root regardless. A path check that\n * only runs when the input is untrusted is a path check that stops running the\n * day somebody adds a caller.\n */\nexport function createLocalFileStore(root: string): FileStore {\n const base = resolve(root)\n\n const pathFor = (key: string): string => {\n // Keys use forward slashes on every platform. Reject Windows separators\n // before native path resolution: POSIX treats them as filename characters.\n if (key.includes('\\\\')) {\n throw new Error(`Refusing a storage key that could resolve outside the store: ${key}`)\n }\n const full = resolve(join(base, key))\n // `startsWith(base)` alone matches `/data/formancy-evil` against\n // `/data/formancy`, so the separator is part of the test.\n if (full !== base && !full.startsWith(base + sep)) {\n throw new Error(`Refusing a storage key that resolves outside the store: ${key}`)\n }\n return full\n }\n\n return {\n async put(key, bytes) {\n const full = pathFor(key)\n await mkdir(dirname(full), { recursive: true })\n await writeFile(full, bytes)\n },\n\n async open(key) {\n const full = pathFor(key)\n try {\n await stat(full)\n } catch {\n return undefined\n }\n return createReadStream(full)\n },\n\n async remove(key) {\n // `force` so a collector that ran twice, or crashed after deleting the\n // bytes and before deleting the row, is not an error the second time.\n await rm(pathFor(key), { force: true })\n },\n\n async sizeOf(key) {\n try {\n return (await stat(pathFor(key))).size\n } catch {\n return undefined\n }\n },\n }\n}\n\n/**\n * The checksum a receiver can compare against.\n *\n * Returned when the bytes are stored and never asked of the client: a checksum\n * the uploader computed is a checksum the uploader can lie about, so this one\n * is only useful for telling a *later* reader that the bytes have not changed.\n */\nexport function sha256(bytes: Buffer): string {\n return createHash('sha256').update(bytes).digest('hex')\n}\n","import { collectAbandonedFiles } from '@formancy/server-core'\nimport type { Storage } from '@formancy/server-core'\nimport type { FileStore } from './file-store.js'\n\n/**\n * Deleting files nobody claimed.\n *\n * Somebody who attaches a file and then closes the tab leaves bytes behind.\n * Without this they stay forever, and a disk that fills up for a reason nobody\n * is watching is the most tedious kind of outage.\n *\n * **Bytes first, then the row.** A row with no bytes is a broken reference and\n * shows up the moment anybody tries to download it. Bytes with no row are\n * invisible: nothing will ever look for them again, and the disk fills up\n * silently. Of the two ways to be interrupted halfway, the first is the one\n * you find out about.\n *\n * The same shape as the outbox worker, for the same reasons: a timer in the\n * server process rather than a queue library, one pass at a time, and a\n * thrown pass complains and comes back rather than stopping the timer. It is\n * likewise not a distributed job — `abandonedFiles` takes no lock, so two\n * replicas would both try to delete the same file. That is harmless here,\n * unlike a webhook sent twice, because deleting a file twice is deleting it\n * once.\n */\nexport interface CollectorHandle {\n stop: () => void\n}\n\nexport function startFileCollector(\n storage: Storage,\n store: FileStore,\n options: { intervalMs?: number; afterHours?: number } = {},\n): CollectorHandle {\n const interval = options.intervalMs ?? 15 * 60_000\n const afterHours = options.afterHours ?? 24\n let stopped = false\n let running = false\n\n const tick = async (): Promise<void> => {\n if (running || stopped) return\n running = true\n try {\n const abandoned = await collectAbandonedFiles(storage, new Date(), afterHours)\n if (abandoned.length === 0) return\n\n const removed: string[] = []\n for (const file of abandoned) {\n // One at a time, and the row only once its bytes are gone. A failure\n // partway leaves the rest for the next pass rather than losing track\n // of what was already done.\n await store.remove(file.storageKey)\n removed.push(file.id)\n }\n await storage.deleteFiles(removed)\n } catch (error) {\n // A collector that throws is a collector that stops, and a disk that\n // fills up. Complain and come back.\n console.error('files: a collection pass failed', error)\n } finally {\n running = false\n }\n }\n\n const timer = setInterval(() => void tick(), interval)\n timer.unref?.()\n\n return {\n stop: () => {\n stopped = true\n clearInterval(timer)\n },\n }\n}\n"],"mappings":"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"}