@reddoorla/maintenance 0.90.1 → 0.92.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 (161) hide show
  1. package/dist/{announce-NDTLRQCN.js → announce-6OS4C57D.js} +3 -3
  2. package/dist/{blux-YGBGS24U.js → blux-NXQLMS4W.js} +4 -4
  3. package/dist/blux-NXQLMS4W.js.map +1 -0
  4. package/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
  5. package/dist/{chunk-WS6NU475.js → chunk-22GKJO5F.js} +2 -2
  6. package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
  7. package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
  8. package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
  9. package/dist/{chunk-JRHIFA5R.js → chunk-5XMKJX74.js} +10 -4
  10. package/dist/chunk-5XMKJX74.js.map +1 -0
  11. package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
  12. package/dist/{chunk-NYUYNYQL.js → chunk-7MKT4I5T.js} +15 -1
  13. package/dist/chunk-7MKT4I5T.js.map +1 -0
  14. package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
  15. package/dist/chunk-BJQCIJOY.js +3534 -0
  16. package/dist/chunk-BJQCIJOY.js.map +1 -0
  17. package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
  18. package/dist/{chunk-W45FZEGE.js → chunk-DMPHP7UP.js} +2 -2
  19. package/dist/{chunk-KGUKL3CV.js → chunk-E22WAIF2.js} +2 -2
  20. package/dist/{chunk-VBEKL445.js → chunk-FCS36FDT.js} +2 -2
  21. package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
  22. package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
  23. package/dist/{chunk-SJOXWJ5Y.js → chunk-M2XF6JAI.js} +80 -9
  24. package/dist/chunk-M2XF6JAI.js.map +1 -0
  25. package/dist/{chunk-ZOCJBJQV.js → chunk-MVDMKKBN.js} +2 -2
  26. package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
  27. package/dist/{chunk-MFHS7IQ2.js → chunk-OCNSIBMP.js} +27 -1
  28. package/dist/{chunk-MFHS7IQ2.js.map → chunk-OCNSIBMP.js.map} +1 -1
  29. package/dist/chunk-PJ23RDHT.js +48 -0
  30. package/dist/chunk-PJ23RDHT.js.map +1 -0
  31. package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
  32. package/dist/chunk-T7ZUUOZ2.js +496 -0
  33. package/dist/chunk-T7ZUUOZ2.js.map +1 -0
  34. package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
  35. package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
  36. package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
  37. package/dist/cli/bin.js +30 -27
  38. package/dist/cli/bin.js.map +1 -1
  39. package/dist/cli/commands/audit.js +14 -14
  40. package/dist/client-M2V6FHH5.js +11 -0
  41. package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
  42. package/dist/{db-LOSJ74ZJ.js → db-DJVW6ZSE.js} +11 -11
  43. package/dist/{digest-CS6KLCZB.js → digest-EOA4JLTV.js} +13 -13
  44. package/dist/{digest-collectors-NI2M3UTC.js → digest-collectors-ZA3BTS3R.js} +7 -7
  45. package/dist/{email-LJQT5J7F.js → email-I4BCETI4.js} +16 -64
  46. package/dist/email-I4BCETI4.js.map +1 -0
  47. package/dist/{ensure-site-FFDUMY6E.js → ensure-site-SF6KICTN.js} +2 -2
  48. package/dist/forms/index.d.ts +5 -1
  49. package/dist/forms/index.js +1 -1
  50. package/dist/forms/index.js.map +1 -1
  51. package/dist/forms/prismic.d.ts +69 -0
  52. package/dist/forms/prismic.js +75 -0
  53. package/dist/forms/prismic.js.map +1 -0
  54. package/dist/{forms-notify-target-IHJX7NWA.js → forms-notify-target-MNNOGSJ7.js} +5 -4
  55. package/dist/{forms-notify-target-IHJX7NWA.js.map → forms-notify-target-MNNOGSJ7.js.map} +1 -1
  56. package/dist/{github-signals-FCLNHKMF.js → github-signals-3WLIJ5RR.js} +5 -5
  57. package/dist/{header-image-7A7XLVEU.js → header-image-DOCDFFRO.js} +2 -2
  58. package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
  59. package/dist/{health-mirror-GMSF3Z5R.js → health-mirror-55RRU6BI.js} +3 -3
  60. package/dist/index.js +36 -29
  61. package/dist/index.js.map +1 -1
  62. package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
  63. package/dist/{launch-5V7RLUV7.js → launch-6X5OXPF2.js} +13 -13
  64. package/dist/migrate-K4JETR36.js +7 -0
  65. package/dist/{notify-4HP4GZ2H.js → notify-FYSOWKMJ.js} +4 -3
  66. package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
  67. package/dist/{orchestrate-6HROYTM6.js → orchestrate-KMQIUP3H.js} +5 -5
  68. package/dist/pipeline-AQBFG5WM.js +31 -0
  69. package/dist/{preflight-LIN5NCYU.js → preflight-VPHQYWTY.js} +6 -6
  70. package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
  71. package/dist/{prismic-models-VNGIKS2B.js → prismic-models-QTKIMFBC.js} +5 -5
  72. package/dist/prospect/types.d.ts +809 -2
  73. package/dist/prospect/types.js +8 -0
  74. package/dist/{prospect-audit-JCWTKJTS.js → prospect-audit-RKUVSTLN.js} +27 -6
  75. package/dist/prospect-audit-RKUVSTLN.js.map +1 -0
  76. package/dist/{prospect-audits-3PMIO73D.js → prospect-audits-RVYEA4KH.js} +6 -4
  77. package/dist/recipes/sync-configs.js +3 -3
  78. package/dist/{chunk-5PWB3JHJ.js → render-7G43BSK5.js} +28 -9
  79. package/dist/render-7G43BSK5.js.map +1 -0
  80. package/dist/{replay-4S6ENYCH.js → replay-HV7MGKBU.js} +3 -2
  81. package/dist/{replay-4S6ENYCH.js.map → replay-HV7MGKBU.js.map} +1 -1
  82. package/dist/{report-AAY2HZZR.js → report-Y4LXDHRV.js} +11 -11
  83. package/dist/{report-mirror-GMNSH5HF.js → report-mirror-6ZDCAQZB.js} +3 -3
  84. package/dist/{schema-75PJECAE.js → schema-2V7BGYVT.js} +1 -1
  85. package/dist/{schema-75PJECAE.js.map → schema-2V7BGYVT.js.map} +1 -1
  86. package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
  87. package/dist/{selftest-CUX2FIQD.js → selftest-IFGSJP4X.js} +5 -5
  88. package/dist/{site-mirror-GCFLBBWK.js → site-mirror-3E7JYTJG.js} +3 -3
  89. package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
  90. package/dist/{submissions-7LGJJSDL.js → submissions-UMJS3YST.js} +2 -2
  91. package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
  92. package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
  93. package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
  94. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  95. package/package.json +6 -1
  96. package/dist/blux-YGBGS24U.js.map +0 -1
  97. package/dist/chunk-5PWB3JHJ.js.map +0 -1
  98. package/dist/chunk-DXKF552B.js +0 -1630
  99. package/dist/chunk-DXKF552B.js.map +0 -1
  100. package/dist/chunk-JRHIFA5R.js.map +0 -1
  101. package/dist/chunk-NYUYNYQL.js.map +0 -1
  102. package/dist/chunk-SJOXWJ5Y.js.map +0 -1
  103. package/dist/client-JYKPVCJX.js +0 -11
  104. package/dist/email-LJQT5J7F.js.map +0 -1
  105. package/dist/migrate-YITCXBLS.js +0 -7
  106. package/dist/pipeline-MQJD4CKN.js +0 -16
  107. package/dist/prospect-audit-JCWTKJTS.js.map +0 -1
  108. package/dist/render-P3SBUFVB.js +0 -10
  109. package/dist/render-P3SBUFVB.js.map +0 -1
  110. /package/dist/{announce-NDTLRQCN.js.map → announce-6OS4C57D.js.map} +0 -0
  111. /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
  112. /package/dist/{chunk-WS6NU475.js.map → chunk-22GKJO5F.js.map} +0 -0
  113. /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
  114. /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
  115. /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
  116. /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
  117. /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
  118. /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
  119. /package/dist/{chunk-W45FZEGE.js.map → chunk-DMPHP7UP.js.map} +0 -0
  120. /package/dist/{chunk-KGUKL3CV.js.map → chunk-E22WAIF2.js.map} +0 -0
  121. /package/dist/{chunk-VBEKL445.js.map → chunk-FCS36FDT.js.map} +0 -0
  122. /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
  123. /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
  124. /package/dist/{chunk-ZOCJBJQV.js.map → chunk-MVDMKKBN.js.map} +0 -0
  125. /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
  126. /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
  127. /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
  128. /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
  129. /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
  130. /package/dist/{client-JYKPVCJX.js.map → client-M2V6FHH5.js.map} +0 -0
  131. /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
  132. /package/dist/{db-LOSJ74ZJ.js.map → db-DJVW6ZSE.js.map} +0 -0
  133. /package/dist/{digest-CS6KLCZB.js.map → digest-EOA4JLTV.js.map} +0 -0
  134. /package/dist/{digest-collectors-NI2M3UTC.js.map → digest-collectors-ZA3BTS3R.js.map} +0 -0
  135. /package/dist/{ensure-site-FFDUMY6E.js.map → ensure-site-SF6KICTN.js.map} +0 -0
  136. /package/dist/{github-signals-FCLNHKMF.js.map → github-signals-3WLIJ5RR.js.map} +0 -0
  137. /package/dist/{header-image-7A7XLVEU.js.map → header-image-DOCDFFRO.js.map} +0 -0
  138. /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
  139. /package/dist/{health-mirror-GMSF3Z5R.js.map → health-mirror-55RRU6BI.js.map} +0 -0
  140. /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
  141. /package/dist/{launch-5V7RLUV7.js.map → launch-6X5OXPF2.js.map} +0 -0
  142. /package/dist/{migrate-YITCXBLS.js.map → migrate-K4JETR36.js.map} +0 -0
  143. /package/dist/{notify-4HP4GZ2H.js.map → notify-FYSOWKMJ.js.map} +0 -0
  144. /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
  145. /package/dist/{orchestrate-6HROYTM6.js.map → orchestrate-KMQIUP3H.js.map} +0 -0
  146. /package/dist/{pipeline-MQJD4CKN.js.map → pipeline-AQBFG5WM.js.map} +0 -0
  147. /package/dist/{preflight-LIN5NCYU.js.map → preflight-VPHQYWTY.js.map} +0 -0
  148. /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
  149. /package/dist/{prismic-models-VNGIKS2B.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
  150. /package/dist/{prospect-audits-3PMIO73D.js.map → prospect-audits-RVYEA4KH.js.map} +0 -0
  151. /package/dist/{report-AAY2HZZR.js.map → report-Y4LXDHRV.js.map} +0 -0
  152. /package/dist/{report-mirror-GMNSH5HF.js.map → report-mirror-6ZDCAQZB.js.map} +0 -0
  153. /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
  154. /package/dist/{selftest-CUX2FIQD.js.map → selftest-IFGSJP4X.js.map} +0 -0
  155. /package/dist/{site-mirror-GCFLBBWK.js.map → site-mirror-3E7JYTJG.js.map} +0 -0
  156. /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
  157. /package/dist/{submissions-7LGJJSDL.js.map → submissions-UMJS3YST.js.map} +0 -0
  158. /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
  159. /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
  160. /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
  161. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -10,9 +10,9 @@ function freezeGuardsDbWrite(action, force, authoritative) {
10
10
  }
11
11
  async function runDbCommand(action, opts, deps = {}) {
12
12
  if (action === "migrate") {
13
- const { readDbConfig } = await import("./client-JYKPVCJX.js");
13
+ const { readDbConfig } = await import("./client-M2V6FHH5.js");
14
14
  const cfg = opts.url ? { url: opts.url } : readDbConfig();
15
- const { runMigrations } = await import("./migrate-YITCXBLS.js");
15
+ const { runMigrations } = await import("./migrate-K4JETR36.js");
16
16
  const { createClient } = await import("@libsql/client");
17
17
  const client = createClient(cfg.url === ":memory:" ? { url: ":memory:" } : cfg);
18
18
  const ran = await runMigrations(client);
@@ -22,7 +22,7 @@ async function runDbCommand(action, opts, deps = {}) {
22
22
  };
23
23
  }
24
24
  if (action === "replay-deadletters") {
25
- const { readDbConfig, openDb } = await import("./client-JYKPVCJX.js");
25
+ const { readDbConfig, openDb } = await import("./client-M2V6FHH5.js");
26
26
  const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());
27
27
  const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
28
28
  const { getWebsiteBySlug } = await import("./websites-D6WYFAES.js");
@@ -35,12 +35,12 @@ async function runDbCommand(action, opts, deps = {}) {
35
35
  listRecentSubmissionsForEmail,
36
36
  markSubmissionsSpamRetro
37
37
  } = await import("./submissions-3I3GAQAX.js");
38
- const { makeNotify } = await import("./notify-4HP4GZ2H.js");
38
+ const { makeNotify } = await import("./notify-FYSOWKMJ.js");
39
39
  const { classifySpam } = await import("./spam-classifier-T5H4VTP6.js");
40
40
  const { forwardNewsletterToWebhook } = await import("./webhook-XM6EV4XT.js");
41
41
  const { addMailchimpMember, mailchimpTagsFor } = await import("./mailchimp-XYCTEEJY.js");
42
42
  const { defaultResendClient } = await import("./resend-G2BJMAQV.js");
43
- const { replayDeadLetters } = await import("./replay-4S6ENYCH.js");
43
+ const { replayDeadLetters } = await import("./replay-HV7MGKBU.js");
44
44
  let send = null;
45
45
  try {
46
46
  send = defaultResendClient().send;
@@ -87,7 +87,7 @@ async function runDbCommand(action, opts, deps = {}) {
87
87
  const { TURSO_IS_AUTHORITATIVE } = await import("./freeze-A6MUCZU4.js");
88
88
  const refused = freezeGuardsDbWrite(action, opts.force === true, TURSO_IS_AUTHORITATIVE);
89
89
  if (refused) return refused;
90
- const { readDbConfig, openDb } = await import("./client-JYKPVCJX.js");
90
+ const { readDbConfig, openDb } = await import("./client-M2V6FHH5.js");
91
91
  const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());
92
92
  const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
93
93
  const base = openBase(readAirtableConfig());
@@ -150,7 +150,7 @@ async function runDbCommand(action, opts, deps = {}) {
150
150
  return { output: formatParityResult(result), code: result.mismatches.length > 0 ? 1 : 0 };
151
151
  }
152
152
  if (action === "backfill-header-images") {
153
- const { readDbConfig, openDb } = await import("./client-JYKPVCJX.js");
153
+ const { readDbConfig, openDb } = await import("./client-M2V6FHH5.js");
154
154
  const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());
155
155
  const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
156
156
  const base = openBase(readAirtableConfig());
@@ -172,7 +172,7 @@ async function runDbCommand(action, opts, deps = {}) {
172
172
  return { output: formatBackfillResult(result), code: result.failed.length > 0 ? 1 : 0 };
173
173
  }
174
174
  if (action === "backfill-digest-state") {
175
- const { readDbConfig, openDb } = await import("./client-JYKPVCJX.js");
175
+ const { readDbConfig, openDb } = await import("./client-M2V6FHH5.js");
176
176
  const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());
177
177
  const { readDigestState: readTurso, writeDigestState: writeTurso } = await import("./digest-state-3KHZF7GD.js");
178
178
  const existing = await readTurso(db);
@@ -197,7 +197,7 @@ async function runDbCommand(action, opts, deps = {}) {
197
197
  };
198
198
  }
199
199
  if (action === "dump") {
200
- const { readDbConfig } = await import("./client-JYKPVCJX.js");
200
+ const { readDbConfig } = await import("./client-M2V6FHH5.js");
201
201
  const cfg = opts.url ? { url: opts.url } : readDbConfig();
202
202
  const { createClient } = await import("@libsql/client");
203
203
  const client = createClient(cfg.url === ":memory:" ? { url: ":memory:" } : cfg);
@@ -325,7 +325,7 @@ async function runDbCommand(action, opts, deps = {}) {
325
325
  for (const table of Object.keys(restored)) {
326
326
  if (!(table in manifest.tables)) mismatches.push(`${table}: not in origin manifest`);
327
327
  }
328
- const { DATABASE_TABLES } = await import("./schema-75PJECAE.js");
328
+ const { DATABASE_TABLES } = await import("./schema-2V7BGYVT.js");
329
329
  for (const table of DATABASE_TABLES) {
330
330
  if (!(table in restored)) mismatches.push(`${table}: MISSING from the backup entirely`);
331
331
  }
@@ -350,4 +350,4 @@ export {
350
350
  freezeGuardsDbWrite,
351
351
  runDbCommand
352
352
  };
353
- //# sourceMappingURL=db-LOSJ74ZJ.js.map
353
+ //# sourceMappingURL=db-DJVW6ZSE.js.map
@@ -13,12 +13,12 @@ import {
13
13
  collectRenovateAlerts,
14
14
  collectTurnstileGuardrailAlerts,
15
15
  collectVulnAlerts
16
- } from "./chunk-ZOCJBJQV.js";
17
- import "./chunk-KGUKL3CV.js";
16
+ } from "./chunk-MVDMKKBN.js";
17
+ import "./chunk-E22WAIF2.js";
18
18
  import "./chunk-PZR4EBPY.js";
19
19
  import {
20
20
  isIdempotencyConflict
21
- } from "./chunk-VBEKL445.js";
21
+ } from "./chunk-FCS36FDT.js";
22
22
  import {
23
23
  isPendingApproval,
24
24
  listAllReports
@@ -47,10 +47,10 @@ import {
47
47
  import {
48
48
  defaultResendClient
49
49
  } from "./chunk-WEKL2HYJ.js";
50
- import "./chunk-W45FZEGE.js";
51
- import "./chunk-WS6NU475.js";
50
+ import "./chunk-DMPHP7UP.js";
51
+ import "./chunk-22GKJO5F.js";
52
+ import "./chunk-OCNSIBMP.js";
52
53
  import "./chunk-RARREDQE.js";
53
- import "./chunk-MFHS7IQ2.js";
54
54
  import "./chunk-6VAL7XJT.js";
55
55
 
56
56
  // src/reports/digest.ts
@@ -149,7 +149,7 @@ async function listPendingApproval(base) {
149
149
  async function fetchNotifyBounceCounts(now) {
150
150
  try {
151
151
  const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] = await Promise.all([
152
- import("./client-JYKPVCJX.js"),
152
+ import("./client-M2V6FHH5.js"),
153
153
  import("./submissions-3I3GAQAX.js"),
154
154
  import("./screenouts-7P5GOZMV.js")
155
155
  ]);
@@ -162,7 +162,7 @@ async function fetchNotifyBounceCounts(now) {
162
162
  }
163
163
  async function readDigestStateFromDb() {
164
164
  const [{ openDb, readDbConfig }, { readDigestState }] = await Promise.all([
165
- import("./client-JYKPVCJX.js"),
165
+ import("./client-M2V6FHH5.js"),
166
166
  import("./digest-state-3KHZF7GD.js")
167
167
  ]);
168
168
  return readDigestState(await openDb(readDbConfig()));
@@ -176,7 +176,7 @@ async function writeRollupOnce(options, now) {
176
176
  }
177
177
  async function writeDigestStateToDb(next) {
178
178
  const [{ openDb, readDbConfig }, { writeDigestState: writeDigestState2 }] = await Promise.all([
179
- import("./client-JYKPVCJX.js"),
179
+ import("./client-M2V6FHH5.js"),
180
180
  import("./digest-state-3KHZF7GD.js")
181
181
  ]);
182
182
  await writeDigestState2(await openDb(readDbConfig()), next);
@@ -184,11 +184,11 @@ async function writeDigestStateToDb(next) {
184
184
  async function writeCockpitRollupToDb(now) {
185
185
  if (!process.env.TURSO_DATABASE_URL) return "absent";
186
186
  const [{ openDb, readDbConfig }, { writeCockpitRollup }, screenouts, submissions, collectors] = await Promise.all([
187
- import("./client-JYKPVCJX.js"),
187
+ import("./client-M2V6FHH5.js"),
188
188
  import("./digest-state-3KHZF7GD.js"),
189
189
  import("./screenouts-7P5GOZMV.js"),
190
190
  import("./submissions-3I3GAQAX.js"),
191
- import("./digest-collectors-NI2M3UTC.js")
191
+ import("./digest-collectors-ZA3BTS3R.js")
192
192
  ]);
193
193
  const db = await openDb(readDbConfig());
194
194
  const SCREEN_OUT_WINDOW_DAYS = 30;
@@ -242,7 +242,7 @@ var SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1e3;
242
242
  async function fetchSubmissionsDigestCounts(now) {
243
243
  try {
244
244
  const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([
245
- import("./client-JYKPVCJX.js"),
245
+ import("./client-M2V6FHH5.js"),
246
246
  import("./submissions-3I3GAQAX.js")
247
247
  ]);
248
248
  const db = await openDb(readDbConfig());
@@ -424,4 +424,4 @@ export {
424
424
  renderDigestHtml,
425
425
  runDigest
426
426
  };
427
- //# sourceMappingURL=digest-CS6KLCZB.js.map
427
+ //# sourceMappingURL=digest-EOA4JLTV.js.map
@@ -12,10 +12,10 @@ import {
12
12
  collectTurnstileGuardrailAlerts,
13
13
  collectVulnAlerts,
14
14
  prismicAckIsLive
15
- } from "./chunk-ZOCJBJQV.js";
16
- import "./chunk-KGUKL3CV.js";
15
+ } from "./chunk-MVDMKKBN.js";
16
+ import "./chunk-E22WAIF2.js";
17
17
  import "./chunk-PZR4EBPY.js";
18
- import "./chunk-VBEKL445.js";
18
+ import "./chunk-FCS36FDT.js";
19
19
  import "./chunk-F3JWOPTG.js";
20
20
  import "./chunk-VAD43MOR.js";
21
21
  import "./chunk-IY6256GK.js";
@@ -27,10 +27,10 @@ import "./chunk-5UINJNAJ.js";
27
27
  import "./chunk-BGTYPVLT.js";
28
28
  import "./chunk-7NYN2P6I.js";
29
29
  import "./chunk-WEKL2HYJ.js";
30
- import "./chunk-W45FZEGE.js";
31
- import "./chunk-WS6NU475.js";
30
+ import "./chunk-DMPHP7UP.js";
31
+ import "./chunk-22GKJO5F.js";
32
+ import "./chunk-OCNSIBMP.js";
32
33
  import "./chunk-RARREDQE.js";
33
- import "./chunk-MFHS7IQ2.js";
34
34
  import "./chunk-6VAL7XJT.js";
35
35
  export {
36
36
  NOTIFY_BOUNCE_THRESHOLD,
@@ -47,4 +47,4 @@ export {
47
47
  collectVulnAlerts,
48
48
  prismicAckIsLive
49
49
  };
50
- //# sourceMappingURL=digest-collectors-NI2M3UTC.js.map
50
+ //# sourceMappingURL=digest-collectors-ZA3BTS3R.js.map
@@ -1,7 +1,3 @@
1
- import {
2
- renderProspectReport
3
- } from "./chunk-5PWB3JHJ.js";
4
- import "./chunk-DXKF552B.js";
5
1
  import {
6
2
  escapeHtml,
7
3
  safeUrl
@@ -15,8 +11,6 @@ import {
15
11
 
16
12
  // src/prospect/email.ts
17
13
  var FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
18
- var IMPACT_ORDER = { high: 0, medium: 1, low: 2 };
19
- var TOP_FIX_COUNT = 3;
20
14
  function resolveName(result) {
21
15
  return result.businessName && result.businessName.trim() ? result.businessName : hostnameOf(result.url);
22
16
  }
@@ -74,51 +68,21 @@ function notMeasuredLines(result) {
74
68
  }
75
69
  return lines;
76
70
  }
77
- var SCORE_FIELDS = [
78
- { key: "findability", label: "Findability" },
79
- { key: "readability", label: "Readability" },
80
- { key: "answers", label: "Answers" }
81
- ];
82
- function scoresTableHtml(scores) {
83
- const cells = SCORE_FIELDS.map(({ key, label }) => {
84
- const value = scores[key];
85
- const display = value === null ? "Not measured" : String(value);
86
- const valueStyle = value === null ? "color:#8a857e;font-size:14px;" : "color:#d71920;font-size:28px;";
87
- return `<td style="padding:12px 16px;border:1px solid #e6e1da;text-align:center;">
88
- <div style="font-weight:600;${valueStyle}">${escapeHtml(display)}</div>
89
- <div style="font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#57544f;margin-top:4px;">${escapeHtml(label)}</div>
90
- </td>`;
91
- }).join("");
92
- return `<table role="presentation" style="border-collapse:collapse;margin:16px 0;"><tr>${cells}</tr></table>`;
93
- }
94
71
  function notMeasuredHtml(lines) {
95
72
  if (lines.length === 0) return "";
96
73
  const items = lines.map((l) => `<li><strong>${escapeHtml(l.label)}</strong> \u2014 ${escapeHtml(l.reason)}</li>`).join("");
97
74
  return `<h2 style="font-size:16px;margin:24px 0 8px;">Not measured</h2><ul style="margin:0;padding-left:20px;">${items}</ul>`;
98
75
  }
99
- function fixesHtml(analyze) {
100
- if (!analyze.ok) {
101
- return `<p style="color:#8a857e;margin:8px 0 0;">Fixes not available \u2014 ${escapeHtml(analyze.error)}</p>`;
102
- }
103
- const top = [...analyze.data.fixes].sort((a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact]).slice(0, TOP_FIX_COUNT);
104
- if (top.length === 0) {
105
- return `<p style="margin:8px 0 0;">Nothing urgent \u2014 the essentials are already in place.</p>`;
106
- }
107
- const items = top.map(
108
- (f) => `<li><strong>${escapeHtml(f.title)}</strong> \u2014 ${escapeHtml(f.why)} <span style="color:#8a857e;">(${escapeHtml(f.impact)} impact, ${escapeHtml(f.effort)} effort)</span></li>`
109
- ).join("");
110
- return `<ol style="margin:8px 0 0;padding-left:20px;">${items}</ol>`;
111
- }
112
76
  function linkHtml(link) {
113
77
  if (!link) {
114
- return `<p style="margin:24px 0 0;color:#8a857e;">No shareable link \u2014 the audit could not be saved to the database. The full report is attached to this email instead.</p>`;
78
+ return `<p style="margin:24px 0 0;color:#8a857e;">No shareable link \u2014 the audit could not be saved to the database. Re-run it; there is nothing to forward from this email.</p>`;
115
79
  }
116
80
  const safe = safeUrl(link);
117
81
  return `<p style="margin:24px 0 0;"><strong>Full report:</strong> <a href="${escapeHtml(safe)}">${escapeHtml(link)}</a></p>`;
118
82
  }
119
83
  var INTERNAL_NOTE = `<p style="margin:24px 0 0;padding:12px 14px;background:#f4f2ef;border-left:3px solid #d71920;color:#57544f;font-size:14px;">
120
- Internal sheet \u2014 it names the reasons a section could not be measured. The
121
- version to send a prospect is the linked report, also attached to this email.
84
+ Internal note \u2014 it names the reasons a section could not be measured. The
85
+ report itself is the link above; there is no other version.
122
86
  </p>`;
123
87
  function buildAuditEmail(result, opts) {
124
88
  const name = resolveName(result);
@@ -127,19 +91,16 @@ function buildAuditEmail(result, opts) {
127
91
  <html lang="en">
128
92
  <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
129
93
  <body style="margin:0;padding:24px;background:#faf8f5;color:#1a1a1a;font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
130
- <h1 style="font-size:22px;margin:0 0 4px;">Prospect audit \u2014 ${escapeHtml(name)}</h1>
94
+ <h1 style="font-size:22px;margin:0 0 4px;">Audit ready \u2014 ${escapeHtml(name)}</h1>
131
95
  <p style="margin:0 0 16px;color:#57544f;">
132
96
  <a href="${escapeHtml(safeUrl(result.url))}">${escapeHtml(result.url)}</a> \xB7 audited ${escapeHtml(formatIsoDate(result.generatedAt))}
133
97
  </p>
134
- ${scoresTableHtml(result.scores)}
135
- ${notMeasuredHtml(notMeasuredLines(result))}
136
- <h2 style="font-size:16px;margin:24px 0 8px;">Top fixes</h2>
137
- ${fixesHtml(result.analyze)}
138
98
  ${linkHtml(opts.link)}
99
+ ${notMeasuredHtml(notMeasuredLines(result))}
139
100
  ${INTERNAL_NOTE}
140
101
  </body>
141
102
  </html>`;
142
- return { subject, html, attachmentHtml: renderProspectReport(result) };
103
+ return { subject, html };
143
104
  }
144
105
  function parseProspectAuditRecipients(raw) {
145
106
  if (!raw) return [];
@@ -166,32 +127,23 @@ async function sendAuditEmail(result, opts) {
166
127
  };
167
128
  }
168
129
  const built = buildAuditEmail(result, { link: opts.link });
169
- const attachmentHtml = opts.renderReport ? opts.renderReport(result) : built.attachmentHtml;
170
130
  const client = opts.client ?? defaultResendClient();
171
131
  const idempotencyKey = `prospect-audit:${opts.auditId ?? `${result.url}#${result.generatedAt}`}`;
172
- const filename = `prospect-audit-${slugify(hostnameOf(result.url))}.html`;
173
132
  const { messageId } = await client.send({
174
133
  from: FROM_ADDRESS,
175
134
  to: opts.recipients,
176
135
  subject: built.subject,
177
136
  html: built.html,
178
- attachments: [
137
+ // The print sheet, when one was rendered, and nothing else. The old HTML
138
+ // attachment was a second rendering of the report that drifted from the
139
+ // page; the report is the link.
140
+ attachments: opts.pdf ? [
179
141
  {
180
- filename,
181
- content: Buffer.from(attachmentHtml, "utf-8").toString("base64"),
182
- contentType: "text/html"
183
- },
184
- // The PDF goes second so the HTML sheet stays the first attachment it has
185
- // always been, and appears only when one was actually rendered — an empty
186
- // or broken attachment is worse than none.
187
- ...opts.pdf ? [
188
- {
189
- filename: filename.replace(/\.html$/, ".pdf"),
190
- content: opts.pdf.toString("base64"),
191
- contentType: "application/pdf"
192
- }
193
- ] : []
194
- ],
142
+ filename: `prospect-audit-${slugify(hostnameOf(result.url))}.pdf`,
143
+ content: opts.pdf.toString("base64"),
144
+ contentType: "application/pdf"
145
+ }
146
+ ] : [],
195
147
  idempotencyKey
196
148
  });
197
149
  return { sent: true, messageId, recipients: opts.recipients };
@@ -201,4 +153,4 @@ export {
201
153
  parseProspectAuditRecipients,
202
154
  sendAuditEmail
203
155
  };
204
- //# sourceMappingURL=email-LJQT5J7F.js.map
156
+ //# sourceMappingURL=email-I4BCETI4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prospect/email.ts"],"sourcesContent":["import { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { hostnameOf } from \"../util/url.js\";\nimport { defaultResendClient, type ResendClient } from \"../reports/send/resend.js\";\nimport type { ProspectAuditResult } from \"./types.js\";\n\n/**\n * Mirrors `reports/send/orchestrate.ts`'s `FROM_ADDRESS` (its line 18) — copied,\n * not imported, exactly like `digest.ts` and `recipes/selftest-email.ts` already\n * do. orchestrate.ts's own `FROM_ADDRESS` isn't exported, and importing it would\n * drag its whole Airtable/MJML dependency chain into this module's graph, which\n * stays lazy-loaded on purpose (see bin.ts's central-dep-blocker comment — this\n * module reaches `resend`, a devDependency, and must only ever be reached via a\n * dynamic `import()` from the CLI).\n */\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n\nexport type AuditEmailContent = {\n subject: string;\n html: string;\n};\n\n/** The searchable business name when the pipeline resolved one, else the\n * hostname — mirrors render.ts's identical fallback (types.ts's businessName\n * doc: \"Null when no name was ever resolved... itself a finding, not an\n * error\"). Used for both the subject line and the sheet's own heading. */\nfunction resolveName(result: ProspectAuditResult): string {\n return result.businessName && result.businessName.trim()\n ? result.businessName\n : hostnameOf(result.url);\n}\n\n/**\n * Mail subject headers are plain text, never HTML-rendered by a client — no\n * `escapeHtml` needed there (that would wrongly show literal \"&lt;\" etc. in\n * the subject line). A raw newline/control character IS worth collapsing:\n * a business name that embeds one (scraped page title, model output) would\n * otherwise visibly break the subject, and a stray CR/LF in a header value\n * is the classic email-header-injection vector — defense-in-depth even\n * though the Resend API's JSON transport already neutralizes it here.\n */\nfunction subjectSafe(s: string): string {\n return s.replace(/\\s+/g, \" \").trim();\n}\n\n/** Local copy of render.ts's date formatter — small enough that importing it\n * (render.ts doesn't export it) isn't worth the coupling. */\nfunction formatIsoDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n timeZone: \"UTC\",\n });\n}\n\ntype NotMeasuredLine = { label: string; reason: string };\n\n/**\n * Everything on the sheet that reads \"not measured\", and why — in plain\n * words. Unlike render.ts's client-facing report (which collapses every\n * stage failure to STAGE_FAILED_MESSAGE so a stranger never sees operator\n * vocabulary), this email goes to Tucker/Tim/Erik: the real diagnostic\n * (a fetch error, an LLM timeout) is more useful to them than a sanitized\n * phrase, so stage `error` strings are surfaced verbatim (still escaped —\n * they can embed arbitrary text from a fetch response).\n *\n * The four-Scores branches mirror checks.ts's `computeScores` exactly: which\n * stage feeds which score, and the cases where that stage's OWN data still\n * nulls the score even though the stage itself succeeded (crawlerAccessMeasured\n * false, avgMissing null, empty buyerQuestions, a null visibilityScore). If\n * computeScores' conditions change, this needs the matching update.\n */\nfunction notMeasuredLines(result: ProspectAuditResult): NotMeasuredLine[] {\n const lines: NotMeasuredLine[] = [];\n\n if (!result.checks.ok) {\n lines.push({\n label: \"Findability\",\n reason: `the checks stage failed — ${result.checks.error}`,\n });\n lines.push({\n label: \"Readability\",\n reason: `the checks stage failed — ${result.checks.error}`,\n });\n } else {\n const c = result.checks.data;\n if (result.scores.findability === null) {\n lines.push({\n label: \"Findability\",\n reason: !c.crawlerAccessMeasured\n ? \"robots.txt could not be fetched, so crawler access wasn't measured\"\n : \"every crawled page failed to produce readable content\",\n });\n }\n if (result.scores.readability === null) {\n lines.push({\n label: \"Readability\",\n reason: \"no page produced a comparable raw/rendered pair to measure JavaScript dependence\",\n });\n }\n }\n\n if (!result.lighthouse.ok) {\n lines.push({ label: \"Lighthouse\", reason: result.lighthouse.error });\n }\n\n if (!result.analyze.ok) {\n lines.push({ label: \"Answers\", reason: result.analyze.error });\n } else if (result.scores.answers === null) {\n lines.push({ label: \"Answers\", reason: \"the model found no buyer questions to check\" });\n }\n\n // Named for what the stage measures (the receipts section), not for the\n // removed score label — the email no longer presents an AI Visibility score.\n if (!result.probes.ok) {\n lines.push({ label: \"AI engine answers\", reason: result.probes.error });\n } else if (result.scores.aiVisibility === null) {\n lines.push({ label: \"AI engine answers\", reason: \"no category (buyer-question) query ran\" });\n }\n\n return lines;\n}\n\nfunction notMeasuredHtml(lines: NotMeasuredLine[]): string {\n if (lines.length === 0) return \"\";\n const items = lines\n .map((l) => `<li><strong>${escapeHtml(l.label)}</strong> — ${escapeHtml(l.reason)}</li>`)\n .join(\"\");\n return `<h2 style=\"font-size:16px;margin:24px 0 8px;\">Not measured</h2><ul style=\"margin:0;padding-left:20px;\">${items}</ul>`;\n}\n\n/** analyze.error and fix titles/why are untrusted (LLM output, or text lifted\n * from the prospect's own site) — every interpolation below is escaped. */\n\nfunction linkHtml(link: string | null): string {\n if (!link) {\n return `<p style=\"margin:24px 0 0;color:#8a857e;\">No shareable link — the audit could not be saved to the database. Re-run it; there is nothing to forward from this email.</p>`;\n }\n const safe = safeUrl(link);\n return `<p style=\"margin:24px 0 0;\"><strong>Full report:</strong> <a href=\"${escapeHtml(safe)}\">${escapeHtml(link)}</a></p>`;\n}\n\n/** This sheet is for us, and it says so. It carries the real reasons a stage did\n * not run — useful internally, wrong in front of a prospect — so it has to be\n * obvious that the shareable artefact is the attachment and the link, not this\n * body. Without the label, the natural thing to do with a useful email is\n * forward it. */\nconst INTERNAL_NOTE = `<p style=\"margin:24px 0 0;padding:12px 14px;background:#f4f2ef;border-left:3px solid #d71920;color:#57544f;font-size:14px;\">\n Internal note — it names the reasons a section could not be measured. The\n report itself is the link above; there is no other version.\n</p>`;\n\n/**\n * Build the internal sheet — a SHORT summary for Tucker/Tim/Erik, distinct\n * and deliberately NOT a second rendering of the report. The old renderer\n * carried a score the web page abolished and a closing promise the web page\n * calls snake-oil; two surfaces drift, so there is one. Pure: no IO, so it's\n * safe to call from a test with no network.\n *\n * Every value that can trace back to the prospect's site or the model —\n * the business name, fixes, stage error text, the narrative-adjacent bits —\n * is escaped with `escapeHtml`/`safeUrl`, the same honesty-and-safety rule\n * render.ts follows for the client-facing report.\n */\nexport function buildAuditEmail(\n result: ProspectAuditResult,\n opts: { link: string | null },\n): AuditEmailContent {\n const name = resolveName(result);\n const subject = `Prospect audit — ${subjectSafe(name)}`;\n\n const html = `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /></head>\n<body style=\"margin:0;padding:24px;background:#faf8f5;color:#1a1a1a;font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;\">\n <h1 style=\"font-size:22px;margin:0 0 4px;\">Audit ready — ${escapeHtml(name)}</h1>\n <p style=\"margin:0 0 16px;color:#57544f;\">\n <a href=\"${escapeHtml(safeUrl(result.url))}\">${escapeHtml(result.url)}</a> · audited ${escapeHtml(formatIsoDate(result.generatedAt))}\n </p>\n ${linkHtml(opts.link)}\n ${notMeasuredHtml(notMeasuredLines(result))}\n ${INTERNAL_NOTE}\n</body>\n</html>`;\n\n return { subject, html };\n}\n\n/**\n * Parse `PROSPECT_AUDIT_RECIPIENTS` (comma-separated) into a clean address\n * list: trimmed, empties dropped, case-insensitively deduped — mirrors\n * `reports/send/orchestrate.ts`'s `parseAddresses` for the same reasons (an\n * operator-typed env var, same typo surface). No address is ever hardcoded\n * anywhere in this module; an unset or empty var yields `[]`, which\n * `sendAuditEmail` treats as \"skip, and say why\" rather than an error.\n */\nexport function parseProspectAuditRecipients(raw: string | undefined | null): string[] {\n if (!raw) return [];\n const seen = new Set<string>();\n const list: string[] = [];\n for (const part of raw.split(\",\")) {\n const trimmed = part.trim().toLowerCase();\n if (!trimmed) continue;\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n list.push(trimmed);\n }\n return list;\n}\n\n/** Filesystem-safe attachment-filename fragment. Tiny local copy of the CLI's\n * private `slugify` (prospect-audit.ts) — not worth exporting/sharing one\n * three-line predicate across a module boundary. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\nexport type SendAuditEmailOptions = {\n link: string | null;\n recipients: string[];\n /**\n * The persisted `prospect_audits` row id (from `createProspectAudit`), when\n * persistence succeeded. Mirrors orchestrate.ts's\n * `idempotencyKey: \\`report:${report.id}\\`` pattern, so re-sending for the\n * SAME already-persisted audit dedupes at Resend instead of double-mailing.\n * Null when persistence failed — the sheet can still be sent (it's built to\n * survive independently of Turso), but then there is no stable row id, so\n * the key falls back to `url`+`generatedAt`: that still protects a\n * same-process double-call, though — unlike a DB id — it can't survive an\n * entire pipeline rerun, which mints genuinely new content anyway.\n */\n auditId?: string | null;\n /**\n * The print-designed PDF leave-behind, when one was rendered.\n *\n * Optional on purpose: rendering it needs a live page and a headless browser,\n * and neither is worth losing a delivered report over. Absent, the email goes\n * exactly as it did before — the HTML sheet and the link — and the caller has\n * already recorded a warning saying why.\n */\n pdf?: Buffer | null;\n /** Defaults to `defaultResendClient()`. */\n client?: ResendClient;\n};\n\nexport type SendAuditEmailResult =\n { sent: true; messageId: string; recipients: string[] } | { sent: false; reason: string };\n\n/**\n * Send the audit sheet with the full rendered report attached as an `.html`\n * file (base64), so the sheet survives independently of the link and of\n * Turso. Empty `recipients` is a legitimate, non-exceptional outcome (no\n * `PROSPECT_AUDIT_RECIPIENTS` configured) — returns `{sent:false}` rather\n * than throwing, so a caller can report it as a warning; a genuine send\n * failure (a throwing `client.send`) still propagates as a real exception.\n */\nexport async function sendAuditEmail(\n result: ProspectAuditResult,\n opts: SendAuditEmailOptions,\n): Promise<SendAuditEmailResult> {\n if (opts.recipients.length === 0) {\n return {\n sent: false,\n reason: \"no recipients configured (PROSPECT_AUDIT_RECIPIENTS is unset or empty)\",\n };\n }\n\n const built = buildAuditEmail(result, { link: opts.link });\n const client = opts.client ?? defaultResendClient();\n const idempotencyKey = `prospect-audit:${opts.auditId ?? `${result.url}#${result.generatedAt}`}`;\n\n const { messageId } = await client.send({\n from: FROM_ADDRESS,\n to: opts.recipients,\n subject: built.subject,\n html: built.html,\n // The print sheet, when one was rendered, and nothing else. The old HTML\n // attachment was a second rendering of the report that drifted from the\n // page; the report is the link.\n attachments: opts.pdf\n ? [\n {\n filename: `prospect-audit-${slugify(hostnameOf(result.url))}.pdf`,\n content: opts.pdf.toString(\"base64\"),\n contentType: \"application/pdf\",\n },\n ]\n : [],\n idempotencyKey,\n });\n\n return { sent: true, messageId, recipients: opts.recipients };\n}\n"],"mappings":"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","names":[]}
@@ -66,7 +66,7 @@ async function runEnsureSiteCommand(slug, opts) {
66
66
  if (!slug) return { output: "Provide a <slug> (e.g. `ensure-site roalson`).", code: 2 };
67
67
  try {
68
68
  const base = openBase(readAirtableConfig());
69
- const { makeSiteMirror } = await import("./site-mirror-GCFLBBWK.js");
69
+ const { makeSiteMirror } = await import("./site-mirror-3E7JYTJG.js");
70
70
  const result = await ensureSite(
71
71
  base,
72
72
  {
@@ -93,4 +93,4 @@ async function runEnsureSiteCommand(slug, opts) {
93
93
  export {
94
94
  runEnsureSiteCommand
95
95
  };
96
- //# sourceMappingURL=ensure-site-FFDUMY6E.js.map
96
+ //# sourceMappingURL=ensure-site-SF6KICTN.js.map
@@ -147,8 +147,12 @@ type CreateIngestEndpointOptions = {
147
147
  /**
148
148
  * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed
149
149
  * `formType` option is provided (then that is authoritative and overrides it).
150
+ *
151
+ * May be async. A site that resolves confirmation copy from its own CMS does a
152
+ * read here — see `@reddoorla/maintenance/forms/prismic`. A rejection is
153
+ * handled exactly like a throw from a sync one: a 400, never a 500.
150
154
  */
151
- buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;
155
+ buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload | Promise<SubmissionPayload>;
152
156
  /** Fixed formType for single-type endpoints; omit for multi-type endpoints
153
157
  * where `buildPayload` derives formType from the body. */
154
158
  formType?: string;
@@ -136,7 +136,7 @@ function createIngestEndpoint(opts) {
136
136
  let payload;
137
137
  try {
138
138
  payload = {
139
- ...opts.buildPayload(body, event),
139
+ ...await opts.buildPayload(body, event),
140
140
  ...opts.formType ? { formType: opts.formType } : {},
141
141
  _meta: buildSubmissionMeta(event, str(body[turnstileFieldName]))
142
142
  };
@@ -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 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 * 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":[]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Copy for the submitter's confirmation email, resolved by the SITE from its own
3
+ * CMS and forwarded in the reserved `_reply` envelope.
4
+ *
5
+ * Everything here arrives over an untrusted boundary twice — once off the wire,
6
+ * once back out of the persisted `extraFields` JSON — so `parseReplyCopy` is the
7
+ * single gate both paths go through. It drops field by field rather than
8
+ * rejecting whole: a usable subject should still improve the email when the
9
+ * calendar block is malformed.
10
+ */
11
+ type ReplyCalendar = {
12
+ title: string;
13
+ /** ISO 8601. Validated as parseable, not as any particular shape. */
14
+ start: string;
15
+ end?: string;
16
+ location?: string;
17
+ url?: string;
18
+ description?: string;
19
+ };
20
+ type ReplyCopy = {
21
+ subject?: string;
22
+ paragraphs?: string[];
23
+ signature?: string;
24
+ calendar?: ReplyCalendar;
25
+ };
26
+
27
+ /**
28
+ * Resolve a submitter's confirmation copy from a Prismic repository.
29
+ *
30
+ * Published as its own subpath (`@reddoorla/maintenance/forms/prismic`) so the
31
+ * CMS-agnostic core stays that way: `./forms` must remain importable by a site
32
+ * that uses no CMS at all.
33
+ *
34
+ * The client is STRUCTURALLY typed rather than imported from
35
+ * `@prismicio/client`. The two reads below are the whole surface, every fleet
36
+ * site already constructs its own client, and duck-typing keeps this package's
37
+ * dependency list — and a consuming site's bundle — untouched. It also makes the
38
+ * tests a two-line object instead of a mocked SDK.
39
+ *
40
+ * Every read is wrapped: a CMS failure must cost the visitor nothing, because
41
+ * the submission itself is captured either way.
42
+ */
43
+
44
+ type PrismicReader = {
45
+ getSingle: (type: string) => Promise<unknown>;
46
+ getByUID: (type: string, uid: string) => Promise<unknown>;
47
+ };
48
+ type ResolveReplyCopyOptions = {
49
+ /** The submission's form type; selects the entry in the singleton. */
50
+ formType: string;
51
+ /** Present for event forms only. A miss is not an error. */
52
+ eventUid?: string;
53
+ /** Custom type holding events. Default "rsvp". */
54
+ eventType?: string;
55
+ /** Singleton holding site-level defaults. Default "form_replies". */
56
+ settingsType?: string;
57
+ /** Calendar location when the event names none — usually the venue address. */
58
+ defaultLocation?: string;
59
+ /** Canonical URL of the event page, attached to the calendar entry. */
60
+ eventUrl?: string;
61
+ };
62
+ /**
63
+ * Site defaults for `formType`, overridden field-by-field by the event document
64
+ * when `eventUid` names one. Undefined means "nothing authored" — the caller
65
+ * omits `_reply` entirely and the shared package's own fallbacks apply.
66
+ */
67
+ declare function resolveReplyCopy(client: PrismicReader, opts: ResolveReplyCopyOptions): Promise<ReplyCopy | undefined>;
68
+
69
+ export { type PrismicReader, type ResolveReplyCopyOptions, resolveReplyCopy };