@reddoorla/maintenance 0.76.0 → 0.79.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.
Files changed (125) hide show
  1. package/README.md +34 -7
  2. package/dist/{blux-RIJBRVCF.js → blux-2P4FGCUL.js} +5 -5
  3. package/dist/{blux-RIJBRVCF.js.map → blux-2P4FGCUL.js.map} +1 -1
  4. package/dist/{bump-deps-OQR3IXLN.js → bump-deps-XB2LJTVN.js} +3 -3
  5. package/dist/{chunk-RHPDQW6D.js → chunk-2SLED3NB.js} +1 -1
  6. package/dist/{chunk-RHPDQW6D.js.map → chunk-2SLED3NB.js.map} +1 -1
  7. package/dist/{chunk-3U3IGVAQ.js → chunk-3LWKIKZ2.js} +47 -9
  8. package/dist/chunk-3LWKIKZ2.js.map +1 -0
  9. package/dist/{chunk-HIN3P4ZN.js → chunk-4SOCPK7A.js} +10 -1
  10. package/dist/chunk-4SOCPK7A.js.map +1 -0
  11. package/dist/{chunk-CASCWQQX.js → chunk-73UFRZKY.js} +34 -19
  12. package/dist/chunk-73UFRZKY.js.map +1 -0
  13. package/dist/chunk-A3WDWNN5.js +70 -0
  14. package/dist/chunk-A3WDWNN5.js.map +1 -0
  15. package/dist/{chunk-D3DQFC32.js → chunk-ATJ6ZA2V.js} +2 -2
  16. package/dist/{chunk-ZO7WNY57.js → chunk-B5ZUQZFL.js} +2 -2
  17. package/dist/{chunk-PH3AGF4Z.js → chunk-DC5H7KEQ.js} +2 -2
  18. package/dist/{chunk-DYKG53IR.js → chunk-FAQIMC26.js} +9 -9
  19. package/dist/{chunk-4VAUSRSS.js → chunk-FVVGDIGX.js} +2 -2
  20. package/dist/{chunk-7MJBTV2R.js → chunk-FX7RAMGC.js} +2 -2
  21. package/dist/{chunk-PKVZMFEB.js → chunk-K5JZ5KLO.js} +2 -2
  22. package/dist/{chunk-CZAMYVOP.js → chunk-KPQXB5CR.js} +2 -2
  23. package/dist/{chunk-BTEBRTW4.js → chunk-LS6QPANO.js} +2 -2
  24. package/dist/{chunk-2X322ZES.js → chunk-MGJ72BA4.js} +2 -2
  25. package/dist/chunk-MNDIED4M.js +137 -0
  26. package/dist/chunk-MNDIED4M.js.map +1 -0
  27. package/dist/{chunk-ZYBOT5PM.js → chunk-NT4WIP7I.js} +2 -2
  28. package/dist/{chunk-JNXCDUYB.js → chunk-Q276MZ7E.js} +3 -4
  29. package/dist/chunk-Q276MZ7E.js.map +1 -0
  30. package/dist/{chunk-GYNRZCM6.js → chunk-QG6OSL4M.js} +2 -2
  31. package/dist/{chunk-IRRZG66U.js → chunk-QMXQUL5Z.js} +34 -18
  32. package/dist/chunk-QMXQUL5Z.js.map +1 -0
  33. package/dist/{chunk-YEGHUODU.js → chunk-VSHC4HOI.js} +96 -1
  34. package/dist/chunk-VSHC4HOI.js.map +1 -0
  35. package/dist/{chunk-JTRTG5CU.js → chunk-YIQ7IWE3.js} +2 -2
  36. package/dist/{chunk-XKOZUVK2.js → chunk-ZTPPCHDB.js} +1 -1
  37. package/dist/chunk-ZTPPCHDB.js.map +1 -0
  38. package/dist/cli/bin.js +32 -21
  39. package/dist/cli/bin.js.map +1 -1
  40. package/dist/cli/commands/audit.d.ts +1 -1
  41. package/dist/cli/commands/audit.js +7 -5
  42. package/dist/cli/commands/audit.js.map +1 -1
  43. package/dist/client-X5WUJWDE.js +11 -0
  44. package/dist/configs/eslint.js +10 -0
  45. package/dist/configs/eslint.js.map +1 -1
  46. package/dist/{convert-to-pnpm-OVMASATZ.js → convert-to-pnpm-FZHSLUQB.js} +3 -3
  47. package/dist/{db-3QW2XEXA.js → db-JSIZYT23.js} +3 -3
  48. package/dist/{digest-75FWSC74.js → digest-NQAQI34P.js} +11 -11
  49. package/dist/forms/index.js +6 -62
  50. package/dist/forms/index.js.map +1 -1
  51. package/dist/{github-signals-TF3VSNIN.js → github-signals-4LWQGD5V.js} +5 -5
  52. package/dist/{health-endpoint-JZGYCMTV.js → health-endpoint-LFVE26QS.js} +3 -3
  53. package/dist/index.d.ts +11 -3
  54. package/dist/index.js +32 -24
  55. package/dist/index.js.map +1 -1
  56. package/dist/{init-2KOISBCZ.js → init-Y5O33BUL.js} +14 -12
  57. package/dist/{init-2KOISBCZ.js.map → init-Y5O33BUL.js.map} +1 -1
  58. package/dist/{launch-K4EHKTXC.js → launch-3SYLX263.js} +8 -5
  59. package/dist/launch-3SYLX263.js.map +1 -0
  60. package/dist/migrate-LJUFGXQZ.js +7 -0
  61. package/dist/{onboard-PX36YDZA.js → onboard-RDGHVCNG.js} +3 -3
  62. package/dist/{orchestrate-CPR6KVS2.js → orchestrate-QOQXOJLI.js} +5 -5
  63. package/dist/{preflight-QRRMPI7K.js → preflight-2PWVVIRR.js} +6 -6
  64. package/dist/protection-audit-X3BHT2MB.js +140 -0
  65. package/dist/protection-audit-X3BHT2MB.js.map +1 -0
  66. package/dist/recipes/sync-configs.d.ts +1 -1
  67. package/dist/recipes/sync-configs.js +3 -3
  68. package/dist/{renovate-dispatch-6II2JVOX.js → renovate-dispatch-2VYCEH42.js} +2 -2
  69. package/dist/renovate-dispatch-2VYCEH42.js.map +1 -0
  70. package/dist/{report-SLGBDJ6O.js → report-IU4CLJS5.js} +3 -3
  71. package/dist/{self-updating-2YSVNMPP.js → self-updating-ALMUWJNX.js} +5 -4
  72. package/dist/{self-updating-2YSVNMPP.js.map → self-updating-ALMUWJNX.js.map} +1 -1
  73. package/dist/{selftest-RVASRO3Z.js → selftest-PJFABZDM.js} +5 -5
  74. package/dist/{smoke-suite-CSP23NZM.js → smoke-suite-SUEJFBYP.js} +3 -3
  75. package/dist/{submissions-MDKWJYMM.js → submissions-3I3GAQAX.js} +13 -5
  76. package/dist/submissions-3I3GAQAX.js.map +1 -0
  77. package/dist/{submissions-BNRGY3JQ.js → submissions-SURN7BV5.js} +3 -3
  78. package/dist/{svelte-codemods-V54KOBWU.js → svelte-codemods-IWO7X3F3.js} +3 -3
  79. package/dist/{sync-configs-UBCJRASZ.js → sync-configs-7XGHFC6P.js} +4 -4
  80. package/dist/{types-0jreMCF5.d.ts → types-DnEL3NRn.d.ts} +1 -1
  81. package/dist/{upgrade-WR6ICPAG.js → upgrade-AM4S7JF3.js} +3 -3
  82. package/package.json +1 -1
  83. package/dist/chunk-3U3IGVAQ.js.map +0 -1
  84. package/dist/chunk-CASCWQQX.js.map +0 -1
  85. package/dist/chunk-HIN3P4ZN.js.map +0 -1
  86. package/dist/chunk-IRRZG66U.js.map +0 -1
  87. package/dist/chunk-JNXCDUYB.js.map +0 -1
  88. package/dist/chunk-XKOZUVK2.js.map +0 -1
  89. package/dist/chunk-YEGHUODU.js.map +0 -1
  90. package/dist/client-NVVQK3HH.js +0 -11
  91. package/dist/launch-K4EHKTXC.js.map +0 -1
  92. package/dist/migrate-35DNNF3H.js +0 -7
  93. package/dist/renovate-dispatch-6II2JVOX.js.map +0 -1
  94. package/dist/submissions-MDKWJYMM.js.map +0 -1
  95. /package/dist/{bump-deps-OQR3IXLN.js.map → bump-deps-XB2LJTVN.js.map} +0 -0
  96. /package/dist/{chunk-D3DQFC32.js.map → chunk-ATJ6ZA2V.js.map} +0 -0
  97. /package/dist/{chunk-ZO7WNY57.js.map → chunk-B5ZUQZFL.js.map} +0 -0
  98. /package/dist/{chunk-PH3AGF4Z.js.map → chunk-DC5H7KEQ.js.map} +0 -0
  99. /package/dist/{chunk-DYKG53IR.js.map → chunk-FAQIMC26.js.map} +0 -0
  100. /package/dist/{chunk-4VAUSRSS.js.map → chunk-FVVGDIGX.js.map} +0 -0
  101. /package/dist/{chunk-7MJBTV2R.js.map → chunk-FX7RAMGC.js.map} +0 -0
  102. /package/dist/{chunk-PKVZMFEB.js.map → chunk-K5JZ5KLO.js.map} +0 -0
  103. /package/dist/{chunk-CZAMYVOP.js.map → chunk-KPQXB5CR.js.map} +0 -0
  104. /package/dist/{chunk-BTEBRTW4.js.map → chunk-LS6QPANO.js.map} +0 -0
  105. /package/dist/{chunk-2X322ZES.js.map → chunk-MGJ72BA4.js.map} +0 -0
  106. /package/dist/{chunk-ZYBOT5PM.js.map → chunk-NT4WIP7I.js.map} +0 -0
  107. /package/dist/{chunk-GYNRZCM6.js.map → chunk-QG6OSL4M.js.map} +0 -0
  108. /package/dist/{chunk-JTRTG5CU.js.map → chunk-YIQ7IWE3.js.map} +0 -0
  109. /package/dist/{client-NVVQK3HH.js.map → client-X5WUJWDE.js.map} +0 -0
  110. /package/dist/{convert-to-pnpm-OVMASATZ.js.map → convert-to-pnpm-FZHSLUQB.js.map} +0 -0
  111. /package/dist/{db-3QW2XEXA.js.map → db-JSIZYT23.js.map} +0 -0
  112. /package/dist/{digest-75FWSC74.js.map → digest-NQAQI34P.js.map} +0 -0
  113. /package/dist/{github-signals-TF3VSNIN.js.map → github-signals-4LWQGD5V.js.map} +0 -0
  114. /package/dist/{health-endpoint-JZGYCMTV.js.map → health-endpoint-LFVE26QS.js.map} +0 -0
  115. /package/dist/{migrate-35DNNF3H.js.map → migrate-LJUFGXQZ.js.map} +0 -0
  116. /package/dist/{onboard-PX36YDZA.js.map → onboard-RDGHVCNG.js.map} +0 -0
  117. /package/dist/{orchestrate-CPR6KVS2.js.map → orchestrate-QOQXOJLI.js.map} +0 -0
  118. /package/dist/{preflight-QRRMPI7K.js.map → preflight-2PWVVIRR.js.map} +0 -0
  119. /package/dist/{report-SLGBDJ6O.js.map → report-IU4CLJS5.js.map} +0 -0
  120. /package/dist/{selftest-RVASRO3Z.js.map → selftest-PJFABZDM.js.map} +0 -0
  121. /package/dist/{smoke-suite-CSP23NZM.js.map → smoke-suite-SUEJFBYP.js.map} +0 -0
  122. /package/dist/{submissions-BNRGY3JQ.js.map → submissions-SURN7BV5.js.map} +0 -0
  123. /package/dist/{svelte-codemods-V54KOBWU.js.map → svelte-codemods-IWO7X3F3.js.map} +0 -0
  124. /package/dist/{sync-configs-UBCJRASZ.js.map → sync-configs-7XGHFC6P.js.map} +0 -0
  125. /package/dist/{upgrade-WR6ICPAG.js.map → upgrade-AM4S7JF3.js.map} +0 -0
@@ -12,12 +12,12 @@ import {
12
12
  diffAttention,
13
13
  readDigestState,
14
14
  writeDigestState
15
- } from "./chunk-CZAMYVOP.js";
16
- import "./chunk-JTRTG5CU.js";
15
+ } from "./chunk-KPQXB5CR.js";
16
+ import "./chunk-YIQ7IWE3.js";
17
17
  import "./chunk-D4FVAIEJ.js";
18
18
  import {
19
19
  isIdempotencyConflict
20
- } from "./chunk-4VAUSRSS.js";
20
+ } from "./chunk-FVVGDIGX.js";
21
21
  import "./chunk-GECESMVS.js";
22
22
  import {
23
23
  escapeHtml,
@@ -28,9 +28,9 @@ import "./chunk-MPBXOJ3N.js";
28
28
  import {
29
29
  defaultResendClient
30
30
  } from "./chunk-WEKL2HYJ.js";
31
- import "./chunk-GYNRZCM6.js";
32
- import "./chunk-PH3AGF4Z.js";
33
- import "./chunk-HIN3P4ZN.js";
31
+ import "./chunk-QG6OSL4M.js";
32
+ import "./chunk-DC5H7KEQ.js";
33
+ import "./chunk-4SOCPK7A.js";
34
34
  import "./chunk-U5SZ4FOP.js";
35
35
  import {
36
36
  listWebsites,
@@ -139,8 +139,8 @@ async function listPendingApproval(base) {
139
139
  async function fetchNotifyBounceCounts(now) {
140
140
  try {
141
141
  const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] = await Promise.all([
142
- import("./client-NVVQK3HH.js"),
143
- import("./submissions-MDKWJYMM.js"),
142
+ import("./client-X5WUJWDE.js"),
143
+ import("./submissions-3I3GAQAX.js"),
144
144
  import("./screenouts-7P5GOZMV.js")
145
145
  ]);
146
146
  const db = await openDb(readDbConfig());
@@ -154,8 +154,8 @@ var SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1e3;
154
154
  async function fetchSubmissionsDigestCounts(now) {
155
155
  try {
156
156
  const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([
157
- import("./client-NVVQK3HH.js"),
158
- import("./submissions-MDKWJYMM.js")
157
+ import("./client-X5WUJWDE.js"),
158
+ import("./submissions-3I3GAQAX.js")
159
159
  ]);
160
160
  const db = await openDb(readDbConfig());
161
161
  return await countSubmissionsSinceBySite(
@@ -330,4 +330,4 @@ export {
330
330
  renderDigestHtml,
331
331
  runDigest
332
332
  };
333
- //# sourceMappingURL=digest-75FWSC74.js.map
333
+ //# sourceMappingURL=digest-NQAQI34P.js.map
@@ -1,69 +1,13 @@
1
+ import {
2
+ MIN_FILL_MS,
3
+ screenSubmission,
4
+ submitScreenOut,
5
+ submitToIngest
6
+ } from "../chunk-A3WDWNN5.js";
1
7
  import {
2
8
  SUBMISSION_FORM_TYPES
3
9
  } from "../chunk-2OP4JKIQ.js";
4
10
 
5
- // src/forms/client.ts
6
- var INGEST_TIMEOUT_MS = 8e3;
7
- async function submitToIngest(opts) {
8
- const doFetch = opts.fetch ?? fetch;
9
- const controller = new AbortController();
10
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);
11
- let res;
12
- try {
13
- res = await doFetch(opts.url, {
14
- method: "POST",
15
- headers: { "content-type": "application/json", "x-forms-token": opts.token },
16
- body: JSON.stringify(opts.payload),
17
- signal: controller.signal
18
- });
19
- } catch (err) {
20
- return { ok: false, status: 0, error: `network error: ${String(err)}` };
21
- } finally {
22
- clearTimeout(timer);
23
- }
24
- let body = null;
25
- try {
26
- body = await res.json();
27
- } catch {
28
- }
29
- const obj = body && typeof body === "object" ? body : null;
30
- if (res.ok && obj && obj.ok === true) {
31
- return { ok: true, id: String(obj.id ?? "") };
32
- }
33
- const error = obj && typeof obj.error === "string" ? obj.error : `ingest failed (${res.status})`;
34
- return { ok: false, status: res.status, error };
35
- }
36
- async function submitScreenOut(opts) {
37
- const doFetch = opts.fetch ?? fetch;
38
- const controller = new AbortController();
39
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);
40
- try {
41
- const res = await doFetch(opts.url, {
42
- method: "POST",
43
- headers: { "content-type": "application/json", "x-forms-token": opts.token },
44
- // `_screenOut` is a reserved wire key (underscore-namespaced like `_meta`)
45
- // so a site's real form field named "screenOut" can never read as a beacon.
46
- body: JSON.stringify({ _screenOut: opts.reason }),
47
- signal: controller.signal
48
- });
49
- return { ok: res.ok, status: res.status };
50
- } catch {
51
- return { ok: false, status: 0 };
52
- } finally {
53
- clearTimeout(timer);
54
- }
55
- }
56
- var MIN_FILL_MS = 800;
57
- function screenSubmission(input) {
58
- if (typeof input.botField === "string" && input.botField.trim().length > 0) {
59
- return { ok: false, reason: "honeypot" };
60
- }
61
- if (typeof input.elapsedMs === "number" && input.elapsedMs < MIN_FILL_MS) {
62
- return { ok: false, reason: "too-fast" };
63
- }
64
- return { ok: true };
65
- }
66
-
67
11
  // src/forms/action.ts
68
12
  import { fail, redirect } from "@sveltejs/kit";
69
13
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/forms/client.ts","../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n *\n * Each typed field allows `string | undefined` (not just `string`) so a\n * `buildPayload` mapping can use the idiomatic `form.get(\"name\")?.toString()`\n * pattern under `exactOptionalPropertyTypes` without a cast — an absent field\n * and an explicit `undefined` both serialize away in the JSON body.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string | undefined;\n name?: string | undefined;\n firstName?: string | undefined;\n lastName?: string | undefined;\n email?: string | undefined;\n phone?: string | undefined;\n message?: string | undefined;\n sourceUrl?: string | undefined;\n utm?: string | undefined;\n /** Reserved transport envelope (token/IP/UA); stripped centrally, never persisted. */\n _meta?: SubmissionMeta | undefined;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n /** Abort budget for the central call. Default INGEST_TIMEOUT_MS. */\n timeoutMs?: number;\n};\n\n/** Default abort budget for the site→central ingest call. A central function hung\n * mid-deploy otherwise leaves the visitor's submit awaiting until Netlify kills\n * the SITE function at its 10s sync limit — a broken response instead of the\n * friendly error copy (espada's 2026-07-10 form-e2e warns caught exactly this).\n * 8s clears warm (~0.3s) and cold (~1-3s) central latency by a wide margin while\n * leaving the action ~2s of the 10s envelope to respond cleanly. */\nexport const INGEST_TIMEOUT_MS = 8000;\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n signal: controller.signal,\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n } finally {\n clearTimeout(timer);\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type SubmitScreenOutOptions = {\n /** Same ingest endpoint the site already posts submissions to. */\n url: string;\n token: string;\n reason: \"honeypot\" | \"too-fast\";\n fetch?: typeof fetch;\n /** Abort budget so a slow/hung beacon can't delay the (already-successful) response. */\n timeoutMs?: number;\n};\n\n/**\n * Best-effort screen-out beacon: tells the central ingest \"a bot was screened here\"\n * (no PII) so caught-vs-delivered is observable. Never throws — a failure is returned\n * as { ok: false } and the caller ignores it (the visitor already saw success).\n */\nexport async function submitScreenOut(\n opts: SubmitScreenOutOptions,\n): Promise<{ ok: boolean; status: number }> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);\n try {\n const res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n // `_screenOut` is a reserved wire key (underscore-namespaced like `_meta`)\n // so a site's real form field named \"screenOut\" can never read as a beacon.\n body: JSON.stringify({ _screenOut: opts.reason }),\n signal: controller.signal,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/**\n * Minimum plausible fill time; faster than this reads as a bot. Kept low (800ms)\n * on purpose: a too-fast fill is dropped *silently* (the visitor sees success),\n * so a real human who happens to be quick — autofill, a short form, a returning\n * visitor — would lose their lead with no trace. Below this, a submit is\n * effectively instant (page render → fill → click → network all under ~0.8s),\n * which a human realistically never beats but a script does. The honeypot is the\n * primary bot signal; this is the secondary one, so it errs toward letting\n * borderline-fast humans through.\n */\nexport const MIN_FILL_MS = 800;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null/undefined) is NOT a\n * rejection — a prerendered/cached page can't plant a fresh timestamp, and the\n * honeypot remains the primary signal.\n *\n * Any numeric elapsed BELOW the floor — including a NEGATIVE value — is too-fast. A\n * negative elapsed time is impossible for a real fill: it means the client posted a\n * FUTURE timestamp. The earlier `>= 0` guard let that slip through (negative skipped\n * the `< MIN_FILL_MS` branch and returned ok), silently bypassing the timing gate.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (typeof input.elapsedMs === \"number\" && input.elapsedMs < MIN_FILL_MS) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n","import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n // buildPayload runs on untrusted form data; a careless field access (e.g.\n // `form.get(\"email\")!.toString()` on an absent field) would otherwise escape\n // as an uncaught 500. Treat a throw as a malformed request (400), mirroring\n // endpoint.ts's guard and its \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n };\n } catch (err) {\n console.error(`[forms-ingest] ${opts.formType}: buildPayload threw: ${String(err)}`);\n return fail(400, { error: failed });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload,\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields.\n * (Older package versions also sent a `userAgent` — central never consumed it,\n * so it is no longer forwarded; `readMeta` simply ignores it from old senders.) */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields\n * among the KNOWN keys, dropping everything else — a bot cannot smuggle a\n * non-string clientIp in, and unknown fields (e.g. the `userAgent` older package\n * versions still send) are silently ignored. The token/IP read here are used\n * transiently (Turnstile verify + `remoteip`) and are NEVER persisted; the token\n * is never stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp? }`. Returns `undefined` when no field yields a\n * value so callers can attach it unconditionally without polluting the payload\n * (an `undefined` value is dropped by `JSON.stringify`). `getClientAddress` is\n * guarded (some adapters lack a client address and can throw). The visitor's\n * user-agent is deliberately NOT forwarded — central never consumed it, so\n * shipping it was pure transient-PII surface. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n // buildPayload runs on untrusted form data; a careless field access (e.g.\n // `form.get(\"email\")!.toString()` on an absent field) would otherwise escape\n // as an uncaught 500. Treat a throw as a malformed request (400), mirroring\n // endpoint.ts's guard and its \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n };\n } catch (err) {\n console.error(`[forms-ingest] ${opts.formType}: buildPayload threw: ${String(err)}`);\n return fail(400, { error: failed });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload,\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields.\n * (Older package versions also sent a `userAgent` — central never consumed it,\n * so it is no longer forwarded; `readMeta` simply ignores it from old senders.) */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields\n * among the KNOWN keys, dropping everything else — a bot cannot smuggle a\n * non-string clientIp in, and unknown fields (e.g. the `userAgent` older package\n * versions still send) are silently ignored. The token/IP read here are used\n * transiently (Turnstile verify + `remoteip`) and are NEVER persisted; the token\n * is never stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp? }`. Returns `undefined` when no field yields a\n * value so callers can attach it unconditionally without polluting the payload\n * (an `undefined` value is dropped by `JSON.stringify`). `getClientAddress` is\n * guarded (some adapters lack a client address and can throw). The visitor's\n * user-agent is deliberately NOT forwarded — central never consumed it, so\n * shipping it was pure transient-PII surface. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":"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","names":[]}
@@ -11,12 +11,12 @@ import {
11
11
  } from "./chunk-5L3FPQIJ.js";
12
12
  import {
13
13
  makeGitHub
14
- } from "./chunk-YEGHUODU.js";
14
+ } from "./chunk-VSHC4HOI.js";
15
15
  import {
16
16
  recordFleetEventsBestEffort
17
- } from "./chunk-GYNRZCM6.js";
18
- import "./chunk-PH3AGF4Z.js";
19
- import "./chunk-HIN3P4ZN.js";
17
+ } from "./chunk-QG6OSL4M.js";
18
+ import "./chunk-DC5H7KEQ.js";
19
+ import "./chunk-4SOCPK7A.js";
20
20
  import "./chunk-HVOOCK6L.js";
21
21
  import {
22
22
  listWebsites,
@@ -134,4 +134,4 @@ export {
134
134
  githubSignalsExitCode,
135
135
  runGitHubSignalsCommand
136
136
  };
137
- //# sourceMappingURL=github-signals-TF3VSNIN.js.map
137
+ //# sourceMappingURL=github-signals-4LWQGD5V.js.map
@@ -11,9 +11,9 @@ import {
11
11
  } from "./chunk-4BHNP6Y2.js";
12
12
  import {
13
13
  healthEndpoint
14
- } from "./chunk-7MJBTV2R.js";
14
+ } from "./chunk-FX7RAMGC.js";
15
15
  import "./chunk-KRDLHAKK.js";
16
- import "./chunk-XKOZUVK2.js";
16
+ import "./chunk-ZTPPCHDB.js";
17
17
  import "./chunk-FDIFTIXC.js";
18
18
  import "./chunk-HVOOCK6L.js";
19
19
  import "./chunk-XXTZBPUY.js";
@@ -51,4 +51,4 @@ async function runHealthEndpointCommand(site, opts) {
51
51
  export {
52
52
  runHealthEndpointCommand
53
53
  };
54
- //# sourceMappingURL=health-endpoint-JZGYCMTV.js.map
54
+ //# sourceMappingURL=health-endpoint-LFVE26QS.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-0jreMCF5.js';
2
- export { C as ConfigName } from './types-0jreMCF5.js';
1
+ import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-DnEL3NRn.js';
2
+ export { C as ConfigName } from './types-DnEL3NRn.js';
3
3
  import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
4
4
  import { F as FormType } from './types-RXY-vY-5.js';
5
5
  export { SyncConfigsOptions, syncConfigs } from './recipes/sync-configs.js';
@@ -110,13 +110,16 @@ type VerifyDeps = {
110
110
  /** Outcome of driving one site's contact form. `formPresent:false` ⇒ n/a
111
111
  * (persisted). `testModeUndeclared` ⇒ the site's /health does not declare
112
112
  * `forms.testMode`, so the probe refused to submit — a plain skip, prior
113
- * verdict preserved. */
113
+ * verdict preserved. `elapsedMs` times the submit itself (click → success
114
+ * banner) and feeds the budget-headroom check; absent means "not measured",
115
+ * which never manufactures a verdict. */
114
116
  type FormSubmitOutcome = {
115
117
  formPresent: false;
116
118
  } | {
117
119
  formPresent: true;
118
120
  success: boolean;
119
121
  detail?: string;
122
+ elapsedMs?: number;
120
123
  } | {
121
124
  testModeUndeclared: true;
122
125
  };
@@ -1008,6 +1011,11 @@ type SubmissionRow = {
1008
1011
  spamScore?: number | null;
1009
1012
  /** Comma-joined classifier reason codes (e.g. "links:3,disposable-email"); null when unscored. */
1010
1013
  spamReason?: string | null;
1014
+ /** Comma-joined newsletter fan-out outcome per destination — `webhook:ok`,
1015
+ * `mailchimp:401`, `mailchimp:threw`, `mailchimp-tags:failed`. Null means nothing
1016
+ * was attempted: a non-newsletter form, a spam row, a site with no destination
1017
+ * configured, or a row written before 0005. See ingest.ts for the token grammar. */
1018
+ fanoutStatus?: string | null;
1011
1019
  };
1012
1020
 
1013
1021
  type ScreenOutTotals = {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import "./chunk-CZAMYVOP.js";
1
+ import "./chunk-KPQXB5CR.js";
2
2
  import {
3
3
  draftReportForSite
4
4
  } from "./chunk-6SZBNALD.js";
@@ -10,39 +10,39 @@ import {
10
10
  } from "./chunk-37S6L6G4.js";
11
11
  import {
12
12
  approveBlockers
13
- } from "./chunk-JTRTG5CU.js";
13
+ } from "./chunk-YIQ7IWE3.js";
14
14
  import {
15
15
  findDueReports
16
16
  } from "./chunk-D4FVAIEJ.js";
17
- import "./chunk-RHPDQW6D.js";
17
+ import "./chunk-2SLED3NB.js";
18
18
  import {
19
19
  upgradeSvelte4to5
20
- } from "./chunk-ZO7WNY57.js";
20
+ } from "./chunk-B5ZUQZFL.js";
21
21
  import {
22
22
  DEFAULT_INIT_STEPS,
23
23
  a11yFixturesPage,
24
24
  init
25
- } from "./chunk-DYKG53IR.js";
25
+ } from "./chunk-FAQIMC26.js";
26
26
  import {
27
27
  convertToPnpm
28
- } from "./chunk-2X322ZES.js";
28
+ } from "./chunk-MGJ72BA4.js";
29
29
  import {
30
30
  svelteCodemods
31
- } from "./chunk-BTEBRTW4.js";
31
+ } from "./chunk-LS6QPANO.js";
32
32
  import "./chunk-J7SZQUCW.js";
33
- import "./chunk-7MJBTV2R.js";
34
- import "./chunk-PKVZMFEB.js";
33
+ import "./chunk-FX7RAMGC.js";
34
+ import "./chunk-K5JZ5KLO.js";
35
35
  import "./chunk-KRDLHAKK.js";
36
36
  import {
37
37
  onboard,
38
38
  selfCaretRange,
39
39
  selfPackageVersion
40
- } from "./chunk-ZYBOT5PM.js";
40
+ } from "./chunk-NT4WIP7I.js";
41
41
  import {
42
42
  parseAddresses,
43
43
  sendApprovedReports,
44
44
  withGlobalCc
45
- } from "./chunk-4VAUSRSS.js";
45
+ } from "./chunk-FVVGDIGX.js";
46
46
  import "./chunk-GECESMVS.js";
47
47
  import {
48
48
  BLURRED_CID,
@@ -60,15 +60,12 @@ import "./chunk-MPBXOJ3N.js";
60
60
  import "./chunk-WEKL2HYJ.js";
61
61
  import {
62
62
  bumpDeps
63
- } from "./chunk-D3DQFC32.js";
64
- import {
65
- isLeadFormType
66
- } from "./chunk-2OP4JKIQ.js";
63
+ } from "./chunk-ATJ6ZA2V.js";
67
64
  import {
68
65
  syncConfigs
69
- } from "./chunk-JNXCDUYB.js";
70
- import "./chunk-CASCWQQX.js";
71
- import "./chunk-XKOZUVK2.js";
66
+ } from "./chunk-Q276MZ7E.js";
67
+ import "./chunk-73UFRZKY.js";
68
+ import "./chunk-ZTPPCHDB.js";
72
69
  import "./chunk-OTGB6YQO.js";
73
70
  import {
74
71
  ALL_AUDIT_NAMES,
@@ -79,18 +76,22 @@ import {
79
76
  runAudits,
80
77
  runAuditsAcross,
81
78
  securityAudit
82
- } from "./chunk-3U3IGVAQ.js";
79
+ } from "./chunk-3LWKIKZ2.js";
83
80
  import "./chunk-GASBX52O.js";
84
- import "./chunk-YEGHUODU.js";
81
+ import "./chunk-VSHC4HOI.js";
85
82
  import "./chunk-LKGVSM2O.js";
86
- import "./chunk-GYNRZCM6.js";
87
- import "./chunk-PH3AGF4Z.js";
88
- import "./chunk-HIN3P4ZN.js";
83
+ import "./chunk-QG6OSL4M.js";
84
+ import "./chunk-DC5H7KEQ.js";
85
+ import "./chunk-4SOCPK7A.js";
89
86
  import {
90
87
  fromJsonFile,
91
88
  localPath
92
89
  } from "./chunk-FDIFTIXC.js";
93
90
  import "./chunk-HVOOCK6L.js";
91
+ import "./chunk-A3WDWNN5.js";
92
+ import {
93
+ isLeadFormType
94
+ } from "./chunk-2OP4JKIQ.js";
94
95
  import "./chunk-XXTZBPUY.js";
95
96
  import "./chunk-XTK5VIQB.js";
96
97
  import {
@@ -195,6 +196,9 @@ function isVisibleInStrip(s) {
195
196
  function spamReasonTokens(reason) {
196
197
  return (reason ?? "").split(",").map((t) => t.trim()).filter((t) => t !== "");
197
198
  }
199
+ function fanoutFailures(fanoutStatus) {
200
+ return (fanoutStatus ?? "").split(",").map((t) => t.trim()).filter((t) => t !== "" && !t.endsWith(":ok"));
201
+ }
198
202
  var REASON_CHIP_MAX_TOKENS = 3;
199
203
  function reasonChipText(tokens) {
200
204
  const shown = tokens.slice(0, REASON_CHIP_MAX_TOKENS).join(" \xB7 ");
@@ -215,6 +219,8 @@ function renderSubmissionRowInner(s) {
215
219
  const provenance = s.status === "spam_auto" && s.spamScore !== null && s.spamScore !== void 0 ? ` <span class="subm-provenance" title="${escapeHtml(s.spamReason ?? "")}">auto-spam \xB7 ${escapeHtml(String(s.spamScore))}</span>${reasonChip}` : "";
216
220
  const recover = s.status === "spam_auto" ? btn("Not spam \u2192 new", "new") : "";
217
221
  const bounceChip = s.notifyStatus === "bounced" ? ` <span class="subm-bounce" title="The lead notification email bounced \u2014 the point-of-contact address may be dead">notify bounced</span>` : "";
222
+ const fanoutBad = fanoutFailures(s.fanoutStatus);
223
+ const fanoutChip = fanoutBad.length > 0 ? ` <span class="subm-fanout-fail" title="This signup did not reach every configured destination">fan-out: ${escapeHtml(fanoutBad.join(" \xB7 "))}</span>` : "";
218
224
  const kv = (label, value) => value === null || value === "" ? "" : `<div class="subm-kv"><span class="k">${label}</span> ${escapeHtml(String(value))}</div>`;
219
225
  const sourceLink = s.sourceUrl ? `<div class="subm-kv"><span class="k">Source</span> <a href="${escapeHtml(safeUrl(s.sourceUrl))}" rel="noopener noreferrer">${escapeHtml(s.sourceUrl)}</a></div>` : "";
220
226
  const messageBlock = s.message ? `<div class="subm-kv"><span class="k">Message</span></div><div class="subm-msg">${escapeHtml(s.message)}</div>` : "";
@@ -228,11 +234,12 @@ function renderSubmissionRowInner(s) {
228
234
  extraFieldsList(s.extraFields),
229
235
  kv("Spam", spamDetail),
230
236
  kv("Notify", s.notifyStatus),
237
+ kv("Fan-out", s.fanoutStatus ?? null),
231
238
  kv("Resend ID", s.resendMessageId),
232
239
  kv("Submission #", s.submissionId)
233
240
  ].join("");
234
241
  return `<details>
235
- <summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance}${bounceChip} <span class="muted">${when}</span></summary>
242
+ <summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance}${bounceChip}${fanoutChip} <span class="muted">${when}</span></summary>
236
243
  <div class="subm-detail">${details}</div>
237
244
  </details>
238
245
  <div class="subm-actions">${recover}${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>`;
@@ -262,6 +269,7 @@ button.subm-status:disabled { opacity: 0.6; cursor: default; }
262
269
  .pill.subm-spam_auto { background: #fff4e5; color: #a65a00; }
263
270
  .subm-provenance { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fff4e5; color: #a65a00; }
264
271
  .subm-bounce { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fdecea; color: #b00; font-weight: 700; }
272
+ .subm-fanout-fail { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fdecea; color: #b00; font-weight: 700; }
265
273
  .subm-reasons { font-size: 0.72rem; color: #999; }
266
274
  .subm-viewall { font-size: 0.8rem; font-weight: normal; margin-left: 0.4rem; white-space: nowrap; }`;
267
275
  var SUBMISSION_STATUS_SCRIPT = `document.querySelectorAll("button.subm-status").forEach((b) => {