@formancy/server 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { AUDIT_ACTIONS, auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, completeBuilderRequest, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, formToFill, formVersion, healthOf, keepExamples, listForms, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, readExamples, replayDelivery, resolveForm, resumeDraft, saveDraft, screenUpload, setFormAccess, startDraft, verifySolution } from "@formancy/server-core";
2
+ import { AUDIT_ACTIONS, auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, completeBuilderRequest, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, formToFill, formVersion, healthOf, keepExamples, listForms, listSubmissionPage, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, readExamples, replayDelivery, resolveForm, resumeDraft, saveDraft, screenUpload, setFormAccess, startDraft, summariseResponses, verifySolution } from "@formancy/server-core";
3
3
  import { MODEL_REQUEST_KINDS } from "@formancy/builder-core";
4
4
  import Fastify, { LogController } from "fastify";
5
5
  import rateLimit from "@fastify/rate-limit";
6
6
  import { hash, verify } from "@node-rs/argon2";
7
7
  import { SignJWT, jwtVerify } from "jose";
8
8
  import { METHODS, STATUS_CODES, validateHeaderName, validateHeaderValue } from "node:http";
9
- import { and, desc, eq, inArray, isNull, lt, lte, max, ne, or } from "drizzle-orm";
9
+ import { and, desc, eq, inArray, isNull, lt, lte, max, ne, or, sql } from "drizzle-orm";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
11
11
  import { integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
12
12
  import postgres from "postgres";
@@ -267,17 +267,18 @@ async function accessRoutes(app, { deps, requires, audit }) {
267
267
  error: "invalid_access",
268
268
  message: "allowedOrigins must be an array of strings, or absent for no allowlist."
269
269
  });
270
+ const allowed = origins;
270
271
  if (!(await setFormAccess(deps, {
271
272
  path,
272
273
  submit,
273
- ...origins === void 0 ? {} : { allowedOrigins: origins }
274
+ ...allowed === void 0 ? {} : { allowedOrigins: allowed }
274
275
  })).ok) return reply.code(404).send({ error: "unknown_form" });
275
276
  await audit(request, {
276
277
  action: "form.access.changed",
277
278
  subject: path,
278
279
  detail: {
279
280
  submit,
280
- allowedOrigins: origins === void 0 ? "unchanged" : origins.join(" ")
281
+ allowedOrigins: allowed === void 0 ? "unchanged" : allowed.join(" ")
281
282
  }
282
283
  });
283
284
  return reply.code(204).send();
@@ -508,7 +509,7 @@ async function submissionRoutes(app, { deps, challengeSecret, limit, actorOf })
508
509
  if (challengeSecret === void 0) return reply.code(404).send({ error: "challenge_not_enabled" });
509
510
  const { path } = request.params;
510
511
  if (await deps.storage.getFormByPath(path) === void 0) return reply.code(404).send({ error: "unknown_form" });
511
- return reply.send(await mintChallenge({
512
+ return reply.send(mintChallenge({
512
513
  secret: challengeSecret,
513
514
  nowSeconds: Math.floor(new Date(deps.nowIso()).getTime() / 1e3)
514
515
  }));
@@ -587,7 +588,7 @@ async function checkChallenge(deps, challengeSecret, request) {
587
588
  message: "The challenge header is not base64 JSON."
588
589
  };
589
590
  const nowSeconds = Math.floor(new Date(deps.nowIso()).getTime() / 1e3);
590
- const verified = await verifySolution(challengeSecret, solution, nowSeconds);
591
+ const verified = verifySolution(challengeSecret, solution, nowSeconds);
591
592
  if (!verified.ok) return {
592
593
  error: `challenge_${verified.reason}`,
593
594
  message: "The challenge was not solved. Ask for a new one and try again."
@@ -1193,6 +1194,26 @@ async function createApp(storage, options) {
1193
1194
  });
1194
1195
  app.get("/f/:path/submissions", { preHandler: requires("submission.read") }, async (request, reply) => {
1195
1196
  const { path } = request.params;
1197
+ const query = request.query ?? {};
1198
+ if (query.limit !== void 0 || query.after !== void 0) {
1199
+ const asked = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : 100;
1200
+ const after = query.after === void 0 ? void 0 : typeof query.after === "string" ? query.after : "";
1201
+ const page = await listSubmissionPage(deps, path, {
1202
+ limit: Number.isFinite(asked) ? asked : 100,
1203
+ after
1204
+ });
1205
+ if (page === void 0) return reply.code(404).send({ error: "unknown_form" });
1206
+ if (page.kind === "bad_cursor") return reply.code(400).send({ error: "bad_cursor" });
1207
+ await audit(request, {
1208
+ action: "submission.read",
1209
+ subject: path,
1210
+ detail: { count: page.submissions.length }
1211
+ });
1212
+ return reply.send({
1213
+ submissions: page.submissions,
1214
+ next: page.next
1215
+ });
1216
+ }
1196
1217
  const listed = await listSubmissions(deps, path);
1197
1218
  if (listed === void 0) return reply.code(404).send({ error: "unknown_form" });
1198
1219
  await audit(request, {
@@ -1202,6 +1223,21 @@ async function createApp(storage, options) {
1202
1223
  });
1203
1224
  return reply.send({ submissions: listed });
1204
1225
  });
1226
+ app.get("/f/:path/summary", { preHandler: requires("submission.read") }, async (request, reply) => {
1227
+ const { path } = request.params;
1228
+ const versions = await summariseResponses(deps, path);
1229
+ if (versions === void 0) return reply.code(404).send({ error: "unknown_form" });
1230
+ const count = versions.reduce((total, version) => total + version.responses, 0);
1231
+ await audit(request, {
1232
+ action: "submission.read",
1233
+ subject: path,
1234
+ detail: {
1235
+ count,
1236
+ summary: true
1237
+ }
1238
+ });
1239
+ return reply.send({ versions });
1240
+ });
1205
1241
  app.get("/f/:path/submissions/export.csv", { preHandler: requires("submission.export") }, async (request, reply) => {
1206
1242
  const { path } = request.params;
1207
1243
  const csv = await exportCsv(deps, path);
@@ -1387,6 +1423,7 @@ async function bootstrapSchema(sql) {
1387
1423
  data jsonb NOT NULL,
1388
1424
  submitted_at timestamptz NOT NULL
1389
1425
  )`;
1426
+ await sql`CREATE INDEX IF NOT EXISTS submissions_by_form ON submissions (form_id, submitted_at DESC, id DESC)`;
1390
1427
  await sql`
1391
1428
  CREATE TABLE IF NOT EXISTS webhooks (
1392
1429
  id uuid PRIMARY KEY,
@@ -1638,8 +1675,8 @@ function toApiKeyRecord(row) {
1638
1675
  }
1639
1676
  //#endregion
1640
1677
  //#region src/postgres-storage.ts
1641
- function createPostgresStorage(sql) {
1642
- const db = drizzle(sql);
1678
+ function createPostgresStorage(sql$1) {
1679
+ const db = drizzle(sql$1);
1643
1680
  return {
1644
1681
  ...examplesStorage(db),
1645
1682
  ...identitiesStorage(db),
@@ -1873,6 +1910,9 @@ function createPostgresStorage(sql) {
1873
1910
  async listSubmissionsByForm(formId) {
1874
1911
  return (await db.select().from(submissions).where(eq(submissions.formId, formId)).orderBy(desc(submissions.submittedAt), desc(submissions.id))).map(toSubmissionRecord);
1875
1912
  },
1913
+ async pageSubmissionsByForm(formId, limit, before) {
1914
+ return (await db.select().from(submissions).where(and(eq(submissions.formId, formId), before === void 0 ? void 0 : sql`(${submissions.submittedAt}, ${submissions.id}) < (${before.submittedAt}::timestamptz, ${before.id}::uuid)`)).orderBy(desc(submissions.submittedAt), desc(submissions.id)).limit(limit)).map(toSubmissionRecord);
1915
+ },
1876
1916
  async listVersionsByForm(formId) {
1877
1917
  return (await db.select().from(formVersions).where(eq(formVersions.formId, formId)).orderBy(desc(formVersions.version))).map((row) => ({
1878
1918
  id: row.id,
@@ -2380,4 +2420,4 @@ function createCompleter(settings, fetch) {
2380
2420
  //#endregion
2381
2421
  export { SUBMISSION_TOKEN_HEADER as _, createLocalFileStore as a, createPostgresStorage as c, LOG_LEVELS as d, createServerLog as f, SCHEMA_HASH_HEADER as g, maxFileBytesFrom as h, startFileCollector as i, bootstrapSchema as l, workerLog as m, MODEL_PROVIDERS as n, sha256 as o, databaseNotices as p, modelSettings as r, createPostgresRateLimitStore as s, createCompleter as t, createApp as u };
2382
2422
 
2383
- //# sourceMappingURL=completers-CQQDS4qr.mjs.map
2423
+ //# sourceMappingURL=completers-DWLA1A-r.mjs.map