@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.
- package/dist/{announce-NDTLRQCN.js → announce-6OS4C57D.js} +3 -3
- package/dist/{blux-YGBGS24U.js → blux-NXQLMS4W.js} +4 -4
- package/dist/blux-NXQLMS4W.js.map +1 -0
- package/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
- package/dist/{chunk-WS6NU475.js → chunk-22GKJO5F.js} +2 -2
- package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
- package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
- package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
- package/dist/{chunk-JRHIFA5R.js → chunk-5XMKJX74.js} +10 -4
- package/dist/chunk-5XMKJX74.js.map +1 -0
- package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
- package/dist/{chunk-NYUYNYQL.js → chunk-7MKT4I5T.js} +15 -1
- package/dist/chunk-7MKT4I5T.js.map +1 -0
- package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
- package/dist/chunk-BJQCIJOY.js +3534 -0
- package/dist/chunk-BJQCIJOY.js.map +1 -0
- package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
- package/dist/{chunk-W45FZEGE.js → chunk-DMPHP7UP.js} +2 -2
- package/dist/{chunk-KGUKL3CV.js → chunk-E22WAIF2.js} +2 -2
- package/dist/{chunk-VBEKL445.js → chunk-FCS36FDT.js} +2 -2
- package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
- package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
- package/dist/{chunk-SJOXWJ5Y.js → chunk-M2XF6JAI.js} +80 -9
- package/dist/chunk-M2XF6JAI.js.map +1 -0
- package/dist/{chunk-ZOCJBJQV.js → chunk-MVDMKKBN.js} +2 -2
- package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
- package/dist/{chunk-MFHS7IQ2.js → chunk-OCNSIBMP.js} +27 -1
- package/dist/{chunk-MFHS7IQ2.js.map → chunk-OCNSIBMP.js.map} +1 -1
- package/dist/chunk-PJ23RDHT.js +48 -0
- package/dist/chunk-PJ23RDHT.js.map +1 -0
- package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
- package/dist/chunk-T7ZUUOZ2.js +496 -0
- package/dist/chunk-T7ZUUOZ2.js.map +1 -0
- package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
- package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
- package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
- package/dist/cli/bin.js +30 -27
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +14 -14
- package/dist/client-M2V6FHH5.js +11 -0
- package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
- package/dist/{db-LOSJ74ZJ.js → db-DJVW6ZSE.js} +11 -11
- package/dist/{digest-CS6KLCZB.js → digest-EOA4JLTV.js} +13 -13
- package/dist/{digest-collectors-NI2M3UTC.js → digest-collectors-ZA3BTS3R.js} +7 -7
- package/dist/{email-LJQT5J7F.js → email-I4BCETI4.js} +16 -64
- package/dist/email-I4BCETI4.js.map +1 -0
- package/dist/{ensure-site-FFDUMY6E.js → ensure-site-SF6KICTN.js} +2 -2
- package/dist/forms/index.d.ts +5 -1
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/prismic.d.ts +69 -0
- package/dist/forms/prismic.js +75 -0
- package/dist/forms/prismic.js.map +1 -0
- package/dist/{forms-notify-target-IHJX7NWA.js → forms-notify-target-MNNOGSJ7.js} +5 -4
- package/dist/{forms-notify-target-IHJX7NWA.js.map → forms-notify-target-MNNOGSJ7.js.map} +1 -1
- package/dist/{github-signals-FCLNHKMF.js → github-signals-3WLIJ5RR.js} +5 -5
- package/dist/{header-image-7A7XLVEU.js → header-image-DOCDFFRO.js} +2 -2
- package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
- package/dist/{health-mirror-GMSF3Z5R.js → health-mirror-55RRU6BI.js} +3 -3
- package/dist/index.js +36 -29
- package/dist/index.js.map +1 -1
- package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
- package/dist/{launch-5V7RLUV7.js → launch-6X5OXPF2.js} +13 -13
- package/dist/migrate-K4JETR36.js +7 -0
- package/dist/{notify-4HP4GZ2H.js → notify-FYSOWKMJ.js} +4 -3
- package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
- package/dist/{orchestrate-6HROYTM6.js → orchestrate-KMQIUP3H.js} +5 -5
- package/dist/pipeline-AQBFG5WM.js +31 -0
- package/dist/{preflight-LIN5NCYU.js → preflight-VPHQYWTY.js} +6 -6
- package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
- package/dist/{prismic-models-VNGIKS2B.js → prismic-models-QTKIMFBC.js} +5 -5
- package/dist/prospect/types.d.ts +809 -2
- package/dist/prospect/types.js +8 -0
- package/dist/{prospect-audit-JCWTKJTS.js → prospect-audit-RKUVSTLN.js} +27 -6
- package/dist/prospect-audit-RKUVSTLN.js.map +1 -0
- package/dist/{prospect-audits-3PMIO73D.js → prospect-audits-RVYEA4KH.js} +6 -4
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{chunk-5PWB3JHJ.js → render-7G43BSK5.js} +28 -9
- package/dist/render-7G43BSK5.js.map +1 -0
- package/dist/{replay-4S6ENYCH.js → replay-HV7MGKBU.js} +3 -2
- package/dist/{replay-4S6ENYCH.js.map → replay-HV7MGKBU.js.map} +1 -1
- package/dist/{report-AAY2HZZR.js → report-Y4LXDHRV.js} +11 -11
- package/dist/{report-mirror-GMNSH5HF.js → report-mirror-6ZDCAQZB.js} +3 -3
- package/dist/{schema-75PJECAE.js → schema-2V7BGYVT.js} +1 -1
- package/dist/{schema-75PJECAE.js.map → schema-2V7BGYVT.js.map} +1 -1
- package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
- package/dist/{selftest-CUX2FIQD.js → selftest-IFGSJP4X.js} +5 -5
- package/dist/{site-mirror-GCFLBBWK.js → site-mirror-3E7JYTJG.js} +3 -3
- package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
- package/dist/{submissions-7LGJJSDL.js → submissions-UMJS3YST.js} +2 -2
- package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
- package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
- package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
- package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
- package/package.json +6 -1
- package/dist/blux-YGBGS24U.js.map +0 -1
- package/dist/chunk-5PWB3JHJ.js.map +0 -1
- package/dist/chunk-DXKF552B.js +0 -1630
- package/dist/chunk-DXKF552B.js.map +0 -1
- package/dist/chunk-JRHIFA5R.js.map +0 -1
- package/dist/chunk-NYUYNYQL.js.map +0 -1
- package/dist/chunk-SJOXWJ5Y.js.map +0 -1
- package/dist/client-JYKPVCJX.js +0 -11
- package/dist/email-LJQT5J7F.js.map +0 -1
- package/dist/migrate-YITCXBLS.js +0 -7
- package/dist/pipeline-MQJD4CKN.js +0 -16
- package/dist/prospect-audit-JCWTKJTS.js.map +0 -1
- package/dist/render-P3SBUFVB.js +0 -10
- package/dist/render-P3SBUFVB.js.map +0 -1
- /package/dist/{announce-NDTLRQCN.js.map → announce-6OS4C57D.js.map} +0 -0
- /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
- /package/dist/{chunk-WS6NU475.js.map → chunk-22GKJO5F.js.map} +0 -0
- /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
- /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
- /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
- /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
- /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
- /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
- /package/dist/{chunk-W45FZEGE.js.map → chunk-DMPHP7UP.js.map} +0 -0
- /package/dist/{chunk-KGUKL3CV.js.map → chunk-E22WAIF2.js.map} +0 -0
- /package/dist/{chunk-VBEKL445.js.map → chunk-FCS36FDT.js.map} +0 -0
- /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
- /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
- /package/dist/{chunk-ZOCJBJQV.js.map → chunk-MVDMKKBN.js.map} +0 -0
- /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
- /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
- /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
- /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
- /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
- /package/dist/{client-JYKPVCJX.js.map → client-M2V6FHH5.js.map} +0 -0
- /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
- /package/dist/{db-LOSJ74ZJ.js.map → db-DJVW6ZSE.js.map} +0 -0
- /package/dist/{digest-CS6KLCZB.js.map → digest-EOA4JLTV.js.map} +0 -0
- /package/dist/{digest-collectors-NI2M3UTC.js.map → digest-collectors-ZA3BTS3R.js.map} +0 -0
- /package/dist/{ensure-site-FFDUMY6E.js.map → ensure-site-SF6KICTN.js.map} +0 -0
- /package/dist/{github-signals-FCLNHKMF.js.map → github-signals-3WLIJ5RR.js.map} +0 -0
- /package/dist/{header-image-7A7XLVEU.js.map → header-image-DOCDFFRO.js.map} +0 -0
- /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
- /package/dist/{health-mirror-GMSF3Z5R.js.map → health-mirror-55RRU6BI.js.map} +0 -0
- /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
- /package/dist/{launch-5V7RLUV7.js.map → launch-6X5OXPF2.js.map} +0 -0
- /package/dist/{migrate-YITCXBLS.js.map → migrate-K4JETR36.js.map} +0 -0
- /package/dist/{notify-4HP4GZ2H.js.map → notify-FYSOWKMJ.js.map} +0 -0
- /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
- /package/dist/{orchestrate-6HROYTM6.js.map → orchestrate-KMQIUP3H.js.map} +0 -0
- /package/dist/{pipeline-MQJD4CKN.js.map → pipeline-AQBFG5WM.js.map} +0 -0
- /package/dist/{preflight-LIN5NCYU.js.map → preflight-VPHQYWTY.js.map} +0 -0
- /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
- /package/dist/{prismic-models-VNGIKS2B.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
- /package/dist/{prospect-audits-3PMIO73D.js.map → prospect-audits-RVYEA4KH.js.map} +0 -0
- /package/dist/{report-AAY2HZZR.js.map → report-Y4LXDHRV.js.map} +0 -0
- /package/dist/{report-mirror-GMNSH5HF.js.map → report-mirror-6ZDCAQZB.js.map} +0 -0
- /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
- /package/dist/{selftest-CUX2FIQD.js.map → selftest-IFGSJP4X.js.map} +0 -0
- /package/dist/{site-mirror-GCFLBBWK.js.map → site-mirror-3E7JYTJG.js.map} +0 -0
- /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
- /package/dist/{submissions-7LGJJSDL.js.map → submissions-UMJS3YST.js.map} +0 -0
- /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
- /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
- /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
- /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-
|
|
13
|
+
const { readDbConfig } = await import("./client-M2V6FHH5.js");
|
|
14
14
|
const cfg = opts.url ? { url: opts.url } : readDbConfig();
|
|
15
|
-
const { runMigrations } = await import("./migrate-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
353
|
+
//# sourceMappingURL=db-DJVW6ZSE.js.map
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
collectRenovateAlerts,
|
|
14
14
|
collectTurnstileGuardrailAlerts,
|
|
15
15
|
collectVulnAlerts
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
51
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
427
|
+
//# sourceMappingURL=digest-EOA4JLTV.js.map
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
collectTurnstileGuardrailAlerts,
|
|
13
13
|
collectVulnAlerts,
|
|
14
14
|
prismicAckIsLive
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-MVDMKKBN.js";
|
|
16
|
+
import "./chunk-E22WAIF2.js";
|
|
17
17
|
import "./chunk-PZR4EBPY.js";
|
|
18
|
-
import "./chunk-
|
|
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-
|
|
31
|
-
import "./chunk-
|
|
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-
|
|
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.
|
|
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
|
|
121
|
-
|
|
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;">
|
|
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
|
|
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
|
-
|
|
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:
|
|
182
|
-
contentType: "
|
|
183
|
-
}
|
|
184
|
-
|
|
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-
|
|
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 \"<\" 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-
|
|
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-
|
|
96
|
+
//# sourceMappingURL=ensure-site-SF6KICTN.js.map
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/forms/index.js
CHANGED
|
@@ -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
|
};
|
package/dist/forms/index.js.map
CHANGED
|
@@ -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 };
|