@reddoorla/maintenance 0.93.0 → 0.94.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 (139) hide show
  1. package/README.md +14 -13
  2. package/dist/airtable-JGCPS7JH.js +9 -0
  3. package/dist/{announce-6OS4C57D.js → announce-PGT4SZZB.js} +6 -6
  4. package/dist/{bump-deps-GI7PHMOC.js → bump-deps-6OR7LL4E.js} +2 -2
  5. package/dist/{chunk-7NYN2P6I.js → chunk-3Z6X3BHU.js} +15 -8
  6. package/dist/chunk-3Z6X3BHU.js.map +1 -0
  7. package/dist/{chunk-MVDMKKBN.js → chunk-4AIYWWKV.js} +12 -5
  8. package/dist/chunk-4AIYWWKV.js.map +1 -0
  9. package/dist/{chunk-QLIUDTY6.js → chunk-5EEMTJMT.js} +10 -7
  10. package/dist/chunk-5EEMTJMT.js.map +1 -0
  11. package/dist/{chunk-4NUKMPOO.js → chunk-5FCBYK6L.js} +3 -3
  12. package/dist/{chunk-PR343K7F.js → chunk-5GUJ36TL.js} +2 -2
  13. package/dist/{chunk-P6AORG32.js → chunk-6R56OKJZ.js} +2 -2
  14. package/dist/{chunk-E22WAIF2.js → chunk-AEUABPAJ.js} +3 -3
  15. package/dist/{chunk-ZRI5VRPG.js → chunk-BYGPUUCT.js} +2 -2
  16. package/dist/{chunk-BJQCIJOY.js → chunk-DHVJ7H2F.js} +230 -805
  17. package/dist/chunk-DHVJ7H2F.js.map +1 -0
  18. package/dist/{chunk-SKI23ZZH.js → chunk-HTABUCF4.js} +2 -2
  19. package/dist/{chunk-FCS36FDT.js → chunk-KQB2BX4V.js} +3 -3
  20. package/dist/{chunk-AX22DH6S.js → chunk-KZRR4NFG.js} +3 -3
  21. package/dist/{chunk-CVJF2IC5.js → chunk-TGUESYGF.js} +17 -3
  22. package/dist/{chunk-CVJF2IC5.js.map → chunk-TGUESYGF.js.map} +1 -1
  23. package/dist/{chunk-JB4Q3C3M.js → chunk-TO7DJYLZ.js} +92 -15
  24. package/dist/{chunk-JB4Q3C3M.js.map → chunk-TO7DJYLZ.js.map} +1 -1
  25. package/dist/chunk-VELWHWOW.js +4194 -0
  26. package/dist/chunk-VELWHWOW.js.map +1 -0
  27. package/dist/{chunk-NDCOOI3V.js → chunk-WMBLR5VA.js} +3 -3
  28. package/dist/{chunk-QBAAZY7W.js → chunk-WVNDEYXL.js} +3 -3
  29. package/dist/{chunk-7BZUOZPU.js → chunk-XN37VTDB.js} +2 -2
  30. package/dist/{chunk-Q47DU255.js → chunk-Z2K4JPQ3.js} +2 -2
  31. package/dist/cli/bin.js +26 -26
  32. package/dist/cli/commands/audit.js +11 -11
  33. package/dist/{convert-to-pnpm-ONFP3JKF.js → convert-to-pnpm-3RVGUBBR.js} +2 -2
  34. package/dist/{db-HSZ6D5DU.js → db-FL7ZBF6H.js} +5 -5
  35. package/dist/{digest-EOA4JLTV.js → digest-TZ3QQOSF.js} +7 -7
  36. package/dist/{digest-collectors-ZA3BTS3R.js → digest-collectors-EHFY2OQC.js} +6 -6
  37. package/dist/{ensure-site-SF6KICTN.js → ensure-site-WIDO6LGU.js} +3 -3
  38. package/dist/{fleet-state-3LQUQHYD.js → fleet-state-UEVZ3X3J.js} +4 -4
  39. package/dist/forms/index.d.ts +6 -1
  40. package/dist/forms/index.js +1 -1
  41. package/dist/forms/index.js.map +1 -1
  42. package/dist/{forms-notify-target-RMZPKT6S.js → forms-notify-target-2LWF5C5B.js} +3 -3
  43. package/dist/{github-signals-3WLIJ5RR.js → github-signals-GAHNOAHS.js} +5 -5
  44. package/dist/{header-image-DOCDFFRO.js → header-image-YN3AQRY3.js} +2 -2
  45. package/dist/{health-endpoint-DHKJUPRM.js → health-endpoint-XLNBWVHK.js} +2 -2
  46. package/dist/{health-mirror-55RRU6BI.js → health-mirror-EB76EERV.js} +4 -4
  47. package/dist/{import-airtable-OHVY344G.js → import-airtable-TAKSN4NV.js} +3 -3
  48. package/dist/index.d.ts +87 -6
  49. package/dist/index.js +14 -11
  50. package/dist/index.js.map +1 -1
  51. package/dist/{init-ZRKWYCKA.js → init-F5OBBWA5.js} +6 -6
  52. package/dist/{launch-6X5OXPF2.js → launch-SUTMI4NG.js} +9 -9
  53. package/dist/{lighthouse-airtable-3GUWHETW.js → lighthouse-airtable-YMH4UA7R.js} +3 -3
  54. package/dist/{onboard-6NHGCKGB.js → onboard-NKE6GZKL.js} +2 -2
  55. package/dist/{orchestrate-KMQIUP3H.js → orchestrate-MWXENQOS.js} +4 -4
  56. package/dist/{parity-623WNFZG.js → parity-CAJ477PS.js} +5 -5
  57. package/dist/{pipeline-AQBFG5WM.js → pipeline-PRMD4EJT.js} +3 -2
  58. package/dist/{preflight-VPHQYWTY.js → preflight-4VQP7JJL.js} +5 -5
  59. package/dist/{prismic-ci-A2HI7NNJ.js → prismic-ci-QIO3I4KB.js} +2 -2
  60. package/dist/{prismic-models-QTKIMFBC.js → prismic-models-RWDVKZ2W.js} +6 -6
  61. package/dist/prospect/types.d.ts +584 -1
  62. package/dist/prospect/types.js +10 -1
  63. package/dist/{prospect-audit-RKUVSTLN.js → prospect-audit-CIL3NCPM.js} +3 -3
  64. package/dist/{render-7G43BSK5.js → render-ZUQBJYJR.js} +3 -2
  65. package/dist/{render-7G43BSK5.js.map → render-ZUQBJYJR.js.map} +1 -1
  66. package/dist/{render-from-row-P5CWM3MQ.js → render-from-row-IFSBHWDH.js} +3 -3
  67. package/dist/{renovate-dispatch-6GQWTRKX.js → renovate-dispatch-U5Q52SKT.js} +2 -2
  68. package/dist/{report-Y4LXDHRV.js → report-6FFHGUUS.js} +14 -14
  69. package/dist/{report-mirror-6ZDCAQZB.js → report-mirror-IP64R7B3.js} +4 -4
  70. package/dist/{self-updating-D5XOWS7V.js → self-updating-5DVDIEHB.js} +2 -2
  71. package/dist/{selftest-IFGSJP4X.js → selftest-LJYXBIO5.js} +6 -6
  72. package/dist/{site-mirror-3E7JYTJG.js → site-mirror-UKHREAD6.js} +4 -4
  73. package/dist/{smoke-suite-GXC5JZPH.js → smoke-suite-NUTC7IUK.js} +3 -3
  74. package/dist/{submissions-UMJS3YST.js → submissions-5GKJPCIH.js} +2 -2
  75. package/dist/{svelte-codemods-A3EARSRP.js → svelte-codemods-U4NIT6CV.js} +2 -2
  76. package/dist/{sync-ESP5ZQUU.js → sync-K3PKHVLH.js} +5 -5
  77. package/dist/{sync-configs-KSESOHFF.js → sync-configs-S2JYY6QU.js} +2 -2
  78. package/dist/{upgrade-ZLNVEIEA.js → upgrade-YFZCMUPE.js} +2 -2
  79. package/dist/{websites-D6WYFAES.js → websites-TB4TCAWM.js} +2 -2
  80. package/dist/{write-audits-to-airtable-K352LZQY.js → write-audits-to-airtable-53EVXKMU.js} +4 -4
  81. package/package.json +1 -1
  82. package/dist/airtable-QG5ICI3Y.js +0 -9
  83. package/dist/chunk-7NYN2P6I.js.map +0 -1
  84. package/dist/chunk-BJQCIJOY.js.map +0 -1
  85. package/dist/chunk-MVDMKKBN.js.map +0 -1
  86. package/dist/chunk-QLIUDTY6.js.map +0 -1
  87. /package/dist/{airtable-QG5ICI3Y.js.map → airtable-JGCPS7JH.js.map} +0 -0
  88. /package/dist/{announce-6OS4C57D.js.map → announce-PGT4SZZB.js.map} +0 -0
  89. /package/dist/{bump-deps-GI7PHMOC.js.map → bump-deps-6OR7LL4E.js.map} +0 -0
  90. /package/dist/{chunk-4NUKMPOO.js.map → chunk-5FCBYK6L.js.map} +0 -0
  91. /package/dist/{chunk-PR343K7F.js.map → chunk-5GUJ36TL.js.map} +0 -0
  92. /package/dist/{chunk-P6AORG32.js.map → chunk-6R56OKJZ.js.map} +0 -0
  93. /package/dist/{chunk-E22WAIF2.js.map → chunk-AEUABPAJ.js.map} +0 -0
  94. /package/dist/{chunk-ZRI5VRPG.js.map → chunk-BYGPUUCT.js.map} +0 -0
  95. /package/dist/{chunk-SKI23ZZH.js.map → chunk-HTABUCF4.js.map} +0 -0
  96. /package/dist/{chunk-FCS36FDT.js.map → chunk-KQB2BX4V.js.map} +0 -0
  97. /package/dist/{chunk-AX22DH6S.js.map → chunk-KZRR4NFG.js.map} +0 -0
  98. /package/dist/{chunk-NDCOOI3V.js.map → chunk-WMBLR5VA.js.map} +0 -0
  99. /package/dist/{chunk-QBAAZY7W.js.map → chunk-WVNDEYXL.js.map} +0 -0
  100. /package/dist/{chunk-7BZUOZPU.js.map → chunk-XN37VTDB.js.map} +0 -0
  101. /package/dist/{chunk-Q47DU255.js.map → chunk-Z2K4JPQ3.js.map} +0 -0
  102. /package/dist/{convert-to-pnpm-ONFP3JKF.js.map → convert-to-pnpm-3RVGUBBR.js.map} +0 -0
  103. /package/dist/{db-HSZ6D5DU.js.map → db-FL7ZBF6H.js.map} +0 -0
  104. /package/dist/{digest-EOA4JLTV.js.map → digest-TZ3QQOSF.js.map} +0 -0
  105. /package/dist/{digest-collectors-ZA3BTS3R.js.map → digest-collectors-EHFY2OQC.js.map} +0 -0
  106. /package/dist/{ensure-site-SF6KICTN.js.map → ensure-site-WIDO6LGU.js.map} +0 -0
  107. /package/dist/{fleet-state-3LQUQHYD.js.map → fleet-state-UEVZ3X3J.js.map} +0 -0
  108. /package/dist/{forms-notify-target-RMZPKT6S.js.map → forms-notify-target-2LWF5C5B.js.map} +0 -0
  109. /package/dist/{github-signals-3WLIJ5RR.js.map → github-signals-GAHNOAHS.js.map} +0 -0
  110. /package/dist/{header-image-DOCDFFRO.js.map → header-image-YN3AQRY3.js.map} +0 -0
  111. /package/dist/{health-endpoint-DHKJUPRM.js.map → health-endpoint-XLNBWVHK.js.map} +0 -0
  112. /package/dist/{health-mirror-55RRU6BI.js.map → health-mirror-EB76EERV.js.map} +0 -0
  113. /package/dist/{import-airtable-OHVY344G.js.map → import-airtable-TAKSN4NV.js.map} +0 -0
  114. /package/dist/{init-ZRKWYCKA.js.map → init-F5OBBWA5.js.map} +0 -0
  115. /package/dist/{launch-6X5OXPF2.js.map → launch-SUTMI4NG.js.map} +0 -0
  116. /package/dist/{lighthouse-airtable-3GUWHETW.js.map → lighthouse-airtable-YMH4UA7R.js.map} +0 -0
  117. /package/dist/{onboard-6NHGCKGB.js.map → onboard-NKE6GZKL.js.map} +0 -0
  118. /package/dist/{orchestrate-KMQIUP3H.js.map → orchestrate-MWXENQOS.js.map} +0 -0
  119. /package/dist/{parity-623WNFZG.js.map → parity-CAJ477PS.js.map} +0 -0
  120. /package/dist/{pipeline-AQBFG5WM.js.map → pipeline-PRMD4EJT.js.map} +0 -0
  121. /package/dist/{preflight-VPHQYWTY.js.map → preflight-4VQP7JJL.js.map} +0 -0
  122. /package/dist/{prismic-ci-A2HI7NNJ.js.map → prismic-ci-QIO3I4KB.js.map} +0 -0
  123. /package/dist/{prismic-models-QTKIMFBC.js.map → prismic-models-RWDVKZ2W.js.map} +0 -0
  124. /package/dist/{prospect-audit-RKUVSTLN.js.map → prospect-audit-CIL3NCPM.js.map} +0 -0
  125. /package/dist/{render-from-row-P5CWM3MQ.js.map → render-from-row-IFSBHWDH.js.map} +0 -0
  126. /package/dist/{renovate-dispatch-6GQWTRKX.js.map → renovate-dispatch-U5Q52SKT.js.map} +0 -0
  127. /package/dist/{report-Y4LXDHRV.js.map → report-6FFHGUUS.js.map} +0 -0
  128. /package/dist/{report-mirror-6ZDCAQZB.js.map → report-mirror-IP64R7B3.js.map} +0 -0
  129. /package/dist/{self-updating-D5XOWS7V.js.map → self-updating-5DVDIEHB.js.map} +0 -0
  130. /package/dist/{selftest-IFGSJP4X.js.map → selftest-LJYXBIO5.js.map} +0 -0
  131. /package/dist/{site-mirror-3E7JYTJG.js.map → site-mirror-UKHREAD6.js.map} +0 -0
  132. /package/dist/{smoke-suite-GXC5JZPH.js.map → smoke-suite-NUTC7IUK.js.map} +0 -0
  133. /package/dist/{submissions-UMJS3YST.js.map → submissions-5GKJPCIH.js.map} +0 -0
  134. /package/dist/{svelte-codemods-A3EARSRP.js.map → svelte-codemods-U4NIT6CV.js.map} +0 -0
  135. /package/dist/{sync-ESP5ZQUU.js.map → sync-K3PKHVLH.js.map} +0 -0
  136. /package/dist/{sync-configs-KSESOHFF.js.map → sync-configs-S2JYY6QU.js.map} +0 -0
  137. /package/dist/{upgrade-ZLNVEIEA.js.map → upgrade-YFZCMUPE.js.map} +0 -0
  138. /package/dist/{websites-D6WYFAES.js.map → websites-TB4TCAWM.js.map} +0 -0
  139. /package/dist/{write-audits-to-airtable-K352LZQY.js.map → write-audits-to-airtable-53EVXKMU.js.map} +0 -0
@@ -1 +1 @@
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 * May be async. A site that resolves confirmation copy from its own CMS does a\n * read here — see `@reddoorla/maintenance/forms/prismic`. A rejection is\n * handled exactly like a throw from a sync one: a 400, never a 500.\n */\n buildPayload: (\n body: Record<string, unknown>,\n event: RequestEvent,\n ) => SubmissionPayload | Promise<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 ...(await 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 * May be async, matching `createIngestEndpoint` — a site that resolves\n * confirmation copy from its CMS does a read here (see\n * `@reddoorla/maintenance/forms/prismic`). A rejection is handled exactly\n * like a throw from a sync one: a failure result, never a 500.\n */\n buildPayload: (\n form: FormData,\n event: RequestEvent,\n ) => SubmissionPayload | Promise<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 ...(await 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 * May be async. A site that resolves confirmation copy from its own CMS does a\n * read here — see `@reddoorla/maintenance/forms/prismic`. A rejection is\n * handled exactly like a throw from a sync one: a 400, never a 500.\n */\n buildPayload: (\n body: Record<string, unknown>,\n event: RequestEvent,\n ) => SubmissionPayload | Promise<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 ...(await 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":[]}
@@ -16,7 +16,7 @@ import {
16
16
  siteSlug,
17
17
  toAirtableStatus,
18
18
  updateSiteField
19
- } from "./chunk-7NYN2P6I.js";
19
+ } from "./chunk-3Z6X3BHU.js";
20
20
  import "./chunk-2OP4JKIQ.js";
21
21
  import "./chunk-6VAL7XJT.js";
22
22
 
@@ -144,7 +144,7 @@ async function runFormsNotifyTargetCommand(site, opts) {
144
144
  return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };
145
145
  }
146
146
  try {
147
- const { makeSiteMirror } = await import("./site-mirror-3E7JYTJG.js");
147
+ const { makeSiteMirror } = await import("./site-mirror-UKHREAD6.js");
148
148
  const result = await formsNotifyTarget({
149
149
  siteMirror: await makeSiteMirror(),
150
150
  site: site.trim(),
@@ -164,4 +164,4 @@ export {
164
164
  formatNotifyTarget,
165
165
  runFormsNotifyTargetCommand
166
166
  };
167
- //# sourceMappingURL=forms-notify-target-RMZPKT6S.js.map
167
+ //# sourceMappingURL=forms-notify-target-2LWF5C5B.js.map
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-PZQ3WED2.js";
4
4
  import {
5
5
  formatFleetWriteSummary
6
- } from "./chunk-QLIUDTY6.js";
7
- import "./chunk-7BZUOZPU.js";
6
+ } from "./chunk-5EEMTJMT.js";
7
+ import "./chunk-XN37VTDB.js";
8
8
  import "./chunk-SVAAONYB.js";
9
9
  import {
10
10
  detectSignalEvents,
@@ -18,7 +18,7 @@ import {
18
18
  listWebsites,
19
19
  siteSlug,
20
20
  updateGitHubSignals
21
- } from "./chunk-7NYN2P6I.js";
21
+ } from "./chunk-3Z6X3BHU.js";
22
22
  import {
23
23
  makeGitHub
24
24
  } from "./chunk-XHHMM5HX.js";
@@ -91,7 +91,7 @@ async function runGitHubSignalsCommand(opts, deps = {}) {
91
91
  );
92
92
  const sweptAt = (/* @__PURE__ */ new Date()).toISOString();
93
93
  const makeMirror = deps.makeMirror ?? (async () => {
94
- const { makeHealthMirrorBestEffort } = await import("./health-mirror-55RRU6BI.js");
94
+ const { makeHealthMirrorBestEffort } = await import("./health-mirror-EB76EERV.js");
95
95
  return makeHealthMirrorBestEffort();
96
96
  });
97
97
  const mirror = await makeMirror();
@@ -156,4 +156,4 @@ export {
156
156
  githubSignalsExitCode,
157
157
  runGitHubSignalsCommand
158
158
  };
159
- //# sourceMappingURL=github-signals-3WLIJ5RR.js.map
159
+ //# sourceMappingURL=github-signals-GAHNOAHS.js.map
@@ -12,7 +12,7 @@ import {
12
12
  ACTIVE_STATUSES,
13
13
  listWebsites,
14
14
  siteSlug
15
- } from "./chunk-7NYN2P6I.js";
15
+ } from "./chunk-3Z6X3BHU.js";
16
16
  import "./chunk-6VAL7XJT.js";
17
17
 
18
18
  // src/cli/commands/header-image.ts
@@ -115,4 +115,4 @@ export {
115
115
  resolveTargets,
116
116
  runHeaderImageCommand
117
117
  };
118
- //# sourceMappingURL=header-image-DOCDFFRO.js.map
118
+ //# sourceMappingURL=header-image-YN3AQRY3.js.map
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-TCF6S7AD.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-Q47DU255.js";
11
+ } from "./chunk-Z2K4JPQ3.js";
12
12
  import {
13
13
  healthEndpoint
14
14
  } from "./chunk-4R3YAQNV.js";
@@ -51,4 +51,4 @@ async function runHealthEndpointCommand(site, opts) {
51
51
  export {
52
52
  runHealthEndpointCommand
53
53
  };
54
- //# sourceMappingURL=health-endpoint-DHKJUPRM.js.map
54
+ //# sourceMappingURL=health-endpoint-XLNBWVHK.js.map
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  mirrorHealthFields,
3
3
  mirrorScheduleFields
4
- } from "./chunk-QBAAZY7W.js";
5
- import "./chunk-P6AORG32.js";
4
+ } from "./chunk-WVNDEYXL.js";
5
+ import "./chunk-6R56OKJZ.js";
6
6
  import {
7
7
  TURSO_IS_AUTHORITATIVE
8
8
  } from "./chunk-SVAAONYB.js";
9
9
  import "./chunk-F3JWOPTG.js";
10
10
  import "./chunk-VAD43MOR.js";
11
- import "./chunk-7NYN2P6I.js";
11
+ import "./chunk-3Z6X3BHU.js";
12
12
  import {
13
13
  openDb,
14
14
  readDbConfig
@@ -43,4 +43,4 @@ export {
43
43
  makeHealthMirrorBestEffort,
44
44
  makeScheduleMirrorBestEffort
45
45
  };
46
- //# sourceMappingURL=health-mirror-55RRU6BI.js.map
46
+ //# sourceMappingURL=health-mirror-EB76EERV.js.map
@@ -14,9 +14,9 @@ import {
14
14
  renderedHtmlUrl,
15
15
  scheduleColumnFor,
16
16
  siteValueFor
17
- } from "./chunk-P6AORG32.js";
17
+ } from "./chunk-6R56OKJZ.js";
18
18
  import "./chunk-VAD43MOR.js";
19
- import "./chunk-7NYN2P6I.js";
19
+ import "./chunk-3Z6X3BHU.js";
20
20
  export {
21
21
  EXCLUDED_WEBSITE_FIELDS,
22
22
  HEALTH_BOOLEAN,
@@ -34,4 +34,4 @@ export {
34
34
  scheduleColumnFor,
35
35
  siteValueFor
36
36
  };
37
- //# sourceMappingURL=import-airtable-OHVY344G.js.map
37
+ //# sourceMappingURL=import-airtable-TAKSN4NV.js.map
package/dist/index.d.ts CHANGED
@@ -116,6 +116,7 @@ type VerifyDeps = {
116
116
  * which never manufactures a verdict. */
117
117
  type FormSubmitOutcome = {
118
118
  formPresent: false;
119
+ formsHealth?: FormsHealth;
119
120
  } | {
120
121
  formPresent: true;
121
122
  success: boolean;
@@ -136,9 +137,70 @@ type FormSubmitOutcome = {
136
137
  * re-filled once before the click. On a pass this is production proof the
137
138
  * wipe happens; on a failure it says one refill wasn't enough. */
138
139
  refilled?: boolean;
140
+ /** What the site's REAL Turnstile widget did while the probe was on the
141
+ * page. The probe does NOT swap the sitekey — `testSitekey` only names the
142
+ * fake token VALUE it injects — so the real widget renders with the real
143
+ * `PUBLIC_TURNSTILE_SITE_KEY` on every run, and this records what it did.
144
+ * Undefined = not observed (an older runner, or the page never loaded). */
145
+ turnstile?: TurnstileObservation;
146
+ /** The `forms` block the preflight already read. Carried out rather than
147
+ * re-fetched so the verdict cannot disagree with the gate that admitted
148
+ * the probe. Undefined = an older runner that did not report it. */
149
+ formsHealth?: FormsHealth;
139
150
  } | {
140
151
  testModeUndeclared: true;
141
152
  };
153
+ /**
154
+ * What a browser saw the site's own Turnstile widget do. Only two of Cloudflare's
155
+ * failure modes are BROWSER-INDEPENDENT, and the distinction is the whole point:
156
+ *
157
+ * - `110200` is a domain-binding rejection, emitted before any challenge is
158
+ * attempted. It means this hostname is not on the widget's allowlist, the widget
159
+ * mints NO token, and on a `Require Turnstile` site every real lead is bucketed
160
+ * as spam. It fires identically for a human and for automation, so a probe can
161
+ * assert on it.
162
+ * - `600010` is a challenge failure, and Cloudflare returns it to ANY driven
163
+ * browser whatever the configuration — measured 2026-09-04 against the
164
+ * known-good reddoorla.com canary and against Playwright headed and headless,
165
+ * while the same page in an ordinary Chrome window minted a token siteverify
166
+ * accepted. It carries NO information about the site and must never be a verdict.
167
+ *
168
+ * A "pass" therefore needs POSITIVE evidence — the mount point AND Cloudflare's
169
+ * script — never merely the absence of an error string. The SSR'd container alone
170
+ * proves only that the env var is set.
171
+ */
172
+ type TurnstileObservation = {
173
+ /** A `.cf-turnstile` mount point was in the DOM on the route the probe settled
174
+ * on. NOT sufficient on its own: the starter server-renders that div from
175
+ * `{#if turnstileSiteKey}`, so it is present whenever the env var is set —
176
+ * including when the widget never initialises. Pairing it with `scriptLoaded`
177
+ * is what makes "pass" positive evidence rather than the absence of a string. */
178
+ containerPresent: boolean;
179
+ /** Cloudflare's `api.js` was actually fetched and answered 2xx. Without it
180
+ * `window.turnstile` never exists, no widget is created and no token is ever
181
+ * minted — while the SSR'd container sits there looking healthy. */
182
+ scriptLoaded: boolean;
183
+ /** An uncaught (or logged) `110200` — this hostname is not on the widget. */
184
+ hostnameRejected: boolean;
185
+ /** The page reported the widget could not render. The starter catches a
186
+ * `loadTurnstile()` rejection and logs `[turnstile] widget did not render` at
187
+ * console.WARN — not error — so this is deliberately matched across every
188
+ * console level, not just errors. */
189
+ initFailed: boolean;
190
+ /** Cloudflare raised some OTHER error against the widget — an invalid, deleted,
191
+ * rotated or disabled sitekey (110100, 110110, 400020, 400070), or another
192
+ * `110xxx`. Its whole job is to deny the green: without it, a widget deleted at
193
+ * Cloudflare scores "pass", because api.js still answers 2xx (its URL carries no
194
+ * sitekey), the mount point is still server-rendered, and the pass arm's negative
195
+ * half was the absence of one six-digit string.
196
+ *
197
+ * Never a fail. This fleet has MEASURED only 110200 and 600010 verbatim, and an
198
+ * unmeasured code does not get the authority to raise a red on a gated site — it
199
+ * lands as `null`, "looked, cannot tell", which the cockpit already renders as
200
+ * the amber `turnstile-unverified` watch. That asymmetry is what makes it safe to
201
+ * match a family by prefix rather than a list by measurement. */
202
+ widgetError: boolean;
203
+ };
142
204
  /** Injected browser IO. The real impl drives Playwright; tests pass a fake. */
143
205
  type FormRunner = {
144
206
  submit: (opts: {
@@ -148,6 +210,17 @@ type FormRunner = {
148
210
  }) => Promise<FormSubmitOutcome>;
149
211
  close?: () => Promise<void>;
150
212
  };
213
+ /**
214
+ * The site's `/health` `forms` block, read once in the preflight. `testMode` gates
215
+ * whether the probe may submit at all; `turnstile` is `!!PUBLIC_TURNSTILE_SITE_KEY`
216
+ * on the deployed site — it says a sitekey is CONFIGURED and nothing more, since
217
+ * /health never contacts Cloudflare. Null = absent or malformed (an older site
218
+ * package whose /health has no such key).
219
+ */
220
+ type FormsHealth = {
221
+ testMode: boolean;
222
+ turnstile: boolean | null;
223
+ };
151
224
 
152
225
  /** Result of probing a site's latest PRODUCTION Netlify deploy. Every field is
153
226
  * nullable: a site with no production deploy yet degrades to all-nulls (a real
@@ -649,12 +722,20 @@ type WebsiteRow = {
649
722
  * "ok"`. Single-select `pass`/`fail`; null = never ran. No per-site Prismic token or identity
650
723
  * column is ever built — this rides `/health`. */
651
724
  cmsReachable: "pass" | "fail" | null;
652
- /** From the same `/health` body's `forms.turnstile` boolean: does the deployed site report
653
- * the Turnstile widget configured (`PUBLIC_TURNSTILE_SITE_KEY` set)? Single-select
654
- * `pass`/`fail`; null = never ran / older site package whose `/health` has no `forms`
655
- * block. Freshness gated by `functionHealthCheckedAt`. Powers the cockpit Require-Turnstile
656
- * guardrail: a site with `requireTurnstile` ON but no widget silently buckets 100% of its
657
- * real leads (and form-e2e can't see it testMode bypasses the gate). */
725
+ /** Does the deployed site's Turnstile widget actually work? Single-select `pass`/`fail`;
726
+ * null = **unverified**, which is the normal state see below. Freshness gated by
727
+ * `formE2eCheckedAt`, the clock of the audit that writes it. Powers the cockpit Require-Turnstile guardrail: a site with
728
+ * `requireTurnstile` ON but no working widget silently buckets 100% of its real leads.
729
+ *
730
+ * One of the two `"fail"` arms is derived from `/health` (the other is a browser seeing
731
+ * 110200 on the live hostname), whose `forms.turnstile` is a truthiness check
732
+ * on `PUBLIC_TURNSTILE_SITE_KEY` that never contacts Cloudflare (see
733
+ * audits/function-health-airtable.ts for the incident). No key IS proof the widget can't
734
+ * work; a key is NOT proof that it can — a sitekey whose widget is full at Cloudflare's
735
+ * 10-hostname cap sets the var and still mints no token. So a `"pass"` here has to be
736
+ * earned by a real browser rendering the real widget (form-e2e), and until it is, null
737
+ * keeps the site in the cockpit's amber "can't verify" watch rather than claiming health
738
+ * nobody measured. */
658
739
  turnstileWidget: "pass" | "fail" | null;
659
740
  /** When the `function-health` audit last ran — the freshness gate for `functionHealth`,
660
741
  * `cmsReachable`, and `turnstileWidget`. Null = never ran. */
package/dist/index.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  DEFAULT_INIT_STEPS,
4
4
  a11yFixturesPage,
5
5
  init
6
- } from "./chunk-4NUKMPOO.js";
6
+ } from "./chunk-5FCBYK6L.js";
7
7
  import {
8
8
  draftReportForSite
9
- } from "./chunk-ZRI5VRPG.js";
9
+ } from "./chunk-BYGPUUCT.js";
10
10
  import "./chunk-YFAXOD7Z.js";
11
11
  import {
12
12
  upgradeSvelte4to5
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-LS6QPANO.js";
20
20
  import "./chunk-J7SZQUCW.js";
21
21
  import "./chunk-4R3YAQNV.js";
22
- import "./chunk-CVJF2IC5.js";
22
+ import "./chunk-TGUESYGF.js";
23
23
  import "./chunk-R53VL3RI.js";
24
24
  import {
25
25
  onboard,
@@ -29,10 +29,10 @@ import {
29
29
  import {
30
30
  bumpDeps
31
31
  } from "./chunk-PPIZHDJL.js";
32
- import "./chunk-MVDMKKBN.js";
32
+ import "./chunk-4AIYWWKV.js";
33
33
  import {
34
34
  approveBlockers
35
- } from "./chunk-E22WAIF2.js";
35
+ } from "./chunk-AEUABPAJ.js";
36
36
  import {
37
37
  findDueReports
38
38
  } from "./chunk-PZR4EBPY.js";
@@ -40,7 +40,7 @@ import {
40
40
  parseAddresses,
41
41
  sendApprovedReports,
42
42
  withGlobalCc
43
- } from "./chunk-FCS36FDT.js";
43
+ } from "./chunk-KQB2BX4V.js";
44
44
  import {
45
45
  isPendingApproval
46
46
  } from "./chunk-F3JWOPTG.js";
@@ -53,10 +53,10 @@ import "./chunk-BRYXFH77.js";
53
53
  import "./chunk-7MKT4I5T.js";
54
54
  import {
55
55
  fromAirtableBase
56
- } from "./chunk-PR343K7F.js";
56
+ } from "./chunk-5GUJ36TL.js";
57
57
  import "./chunk-IY6256GK.js";
58
58
  import "./chunk-HN6I2XWI.js";
59
- import "./chunk-SKI23ZZH.js";
59
+ import "./chunk-HTABUCF4.js";
60
60
  import "./chunk-GECESMVS.js";
61
61
  import {
62
62
  BLURRED_CID,
@@ -80,7 +80,7 @@ import {
80
80
  SEVERITY_RANK,
81
81
  siteSlug,
82
82
  toAirtableStatus
83
- } from "./chunk-7NYN2P6I.js";
83
+ } from "./chunk-3Z6X3BHU.js";
84
84
  import "./chunk-WEKL2HYJ.js";
85
85
  import "./chunk-T7ZUUOZ2.js";
86
86
  import {
@@ -97,7 +97,7 @@ import {
97
97
  runAudits,
98
98
  runAuditsAcross,
99
99
  securityAudit
100
- } from "./chunk-JB4Q3C3M.js";
100
+ } from "./chunk-TO7DJYLZ.js";
101
101
  import "./chunk-LKGVSM2O.js";
102
102
  import "./chunk-GASBX52O.js";
103
103
  import "./chunk-XHHMM5HX.js";
@@ -114,11 +114,11 @@ import {
114
114
  import "./chunk-UDRHAQT7.js";
115
115
  import "./chunk-PTDMVGKL.js";
116
116
  import "./chunk-LBYOLBW7.js";
117
- import "./chunk-RARREDQE.js";
118
117
  import "./chunk-A3WDWNN5.js";
119
118
  import {
120
119
  isLeadFormType
121
120
  } from "./chunk-2OP4JKIQ.js";
121
+ import "./chunk-RARREDQE.js";
122
122
  import "./chunk-XXTZBPUY.js";
123
123
  import "./chunk-XTK5VIQB.js";
124
124
  import "./chunk-6VAL7XJT.js";
@@ -667,6 +667,9 @@ function siteDetailsSection(site) {
667
667
  selectRow("Status", "status", SITE_STATUS_OPTIONS, site.statusRaw, url),
668
668
  selectRow("Maintenance cadence", "maintenanceFreq", FREQ_OPTIONS, site.maintenanceFreq, url),
669
669
  selectRow("Testing cadence", "testingFreq", FREQ_OPTIONS, site.testingFreq, url),
670
+ // First, because it is the site's own address and the target every deployed
671
+ // audit drives. `site.url` is the row's value; `url` is this page's endpoint.
672
+ inputRow("Site URL", "url", site.url, url),
670
673
  inputRow("Report recipients (To)", "reportRecipientsTo", site.reportRecipientsTo, url),
671
674
  inputRow("Report recipients (CC)", "reportRecipientsCc", site.reportRecipientsCc, url),
672
675
  inputRow("Point of contact", "pointOfContact", site.pointOfContact, url),