@reddoorla/maintenance 0.95.1 → 0.97.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/README.md +41 -7
- package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
- package/dist/announce-5ZD2SBGD.js.map +1 -0
- package/dist/attachments-JFGFQRXQ.js +10 -0
- package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
- package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
- package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
- package/dist/{chunk-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
- package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/chunk-ASLDCWBF.js +41 -0
- package/dist/chunk-ASLDCWBF.js.map +1 -0
- package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
- package/dist/chunk-AYDQQBNG.js.map +1 -0
- package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
- package/dist/chunk-BINFZQ2T.js.map +1 -0
- package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
- package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
- package/dist/chunk-DRNZVORX.js.map +1 -0
- package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
- package/dist/chunk-DYTYPKFN.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
- package/dist/chunk-EN6CTBUN.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
- package/dist/chunk-FHFXJ3EM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
- package/dist/chunk-FP6HOW6J.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
- package/dist/chunk-ICSRWW5Q.js +148 -0
- package/dist/chunk-ICSRWW5Q.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
- package/dist/chunk-IDBPII6G.js.map +1 -0
- package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
- package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
- package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
- package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
- package/dist/chunk-L3S3QZXL.js.map +1 -0
- package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
- package/dist/chunk-LAG6IP7Y.js.map +1 -0
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
- package/dist/chunk-LDHRIQUW.js.map +1 -0
- package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
- package/dist/chunk-LF2TJN4X.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
- package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
- package/dist/chunk-LZMAG73U.js.map +1 -0
- package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
- package/dist/chunk-MVKNU4QD.js +37 -0
- package/dist/chunk-MVKNU4QD.js.map +1 -0
- package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
- package/dist/chunk-PARKXDXI.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/chunk-RQJFGK3A.js +51 -0
- package/dist/chunk-RQJFGK3A.js.map +1 -0
- package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
- package/dist/chunk-TC4GANMX.js.map +1 -0
- package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
- package/dist/chunk-TEMQPSCM.js.map +1 -0
- package/dist/chunk-TJLKWQOX.js +52 -0
- package/dist/chunk-TJLKWQOX.js.map +1 -0
- package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
- package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
- package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
- package/dist/chunk-U2ZI6G7H.js.map +1 -0
- package/dist/chunk-UPE6D4AT.js +69 -0
- package/dist/chunk-UPE6D4AT.js.map +1 -0
- package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
- package/dist/chunk-VMGHQUTV.js.map +1 -0
- package/dist/chunk-VQMAAQ5M.js +53 -0
- package/dist/chunk-VQMAAQ5M.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
- package/dist/chunk-WMF5S2MB.js.map +1 -0
- package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
- package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
- package/dist/{chunk-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
- package/dist/chunk-X56LAKPR.js.map +1 -0
- package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
- package/dist/chunk-Y6FCXRIC.js.map +1 -0
- package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
- package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
- package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
- package/dist/chunk-YJBSB4LP.js.map +1 -0
- package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/cli/bin.js +111 -51
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +33 -3
- package/dist/cli/commands/audit.js +28 -25
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-DGTO65WB.js +11 -0
- package/dist/configs/playwright-a11y.d.ts +11 -1
- package/dist/configs/playwright-a11y.js +3 -1
- package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-KLWA53N5.js} +6 -6
- package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
- package/dist/db-II5WAK4D.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
- package/dist/digest-CWKK2DMR.js.map +1 -0
- package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
- package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
- package/dist/digest-state-ZDPKR7MW.js.map +1 -0
- package/dist/ensure-site-2QZHFVQ3.js +70 -0
- package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
- package/dist/ensure-site-JEKV2ODE.js +105 -0
- package/dist/ensure-site-JEKV2ODE.js.map +1 -0
- package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
- package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
- package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
- package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
- package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
- package/dist/github-signals-QIRTHHET.js.map +1 -0
- package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
- package/dist/header-image-H7ZJAV26.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
- package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
- package/dist/index.d.ts +158 -419
- package/dist/index.js +91 -46
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
- package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
- package/dist/launch-7N3X2MXB.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
- package/dist/match-harness-GBCDSVPS.js +417 -0
- package/dist/match-harness-GBCDSVPS.js.map +1 -0
- package/dist/migrate-XZ7U5DFC.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
- package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
- package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
- package/dist/parity-2JGVUCZD.js +14 -0
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
- package/dist/preflight-O7Q6FWHU.js.map +1 -0
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
- package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
- package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
- package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
- package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
- package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
- package/dist/protection-audit-YXJKFU6O.js +413 -0
- package/dist/protection-audit-YXJKFU6O.js.map +1 -0
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
- package/dist/render-MHUUHPLN.js.map +1 -0
- package/dist/{render-from-row-IFSBHWDH.js → render-from-row-75JTED2Z.js} +6 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
- package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
- package/dist/report-IAHXN5CW.js.map +1 -0
- package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
- package/dist/report-mirror-4QM57LMS.js.map +1 -0
- package/dist/roster-GDNSIRAK.js +19 -0
- package/dist/roster-GDNSIRAK.js.map +1 -0
- package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
- package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
- package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
- package/dist/site-create-ZU3ACBTO.js +45 -0
- package/dist/site-create-ZU3ACBTO.js.map +1 -0
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
- package/dist/site-mirror-WR7ACXOV.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
- package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
- package/dist/submissions-2SHCFOKD.js.map +1 -0
- package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
- package/dist/submissions-JDZP35JM.js.map +1 -0
- package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-VOLOKKFI.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
- package/dist/turso-KQKBBQCK.js +24 -0
- package/dist/turso-KQKBBQCK.js.map +1 -0
- package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
- package/dist/util/git.d.ts +49 -10
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/announce-CLQYX5I4.js.map +0 -1
- package/dist/attachments-NLZIJ5NU.js +0 -9
- package/dist/chunk-2ZAZ7ETT.js.map +0 -1
- package/dist/chunk-3Z6X3BHU.js.map +0 -1
- package/dist/chunk-5EEMTJMT.js.map +0 -1
- package/dist/chunk-5GUJ36TL.js +0 -53
- package/dist/chunk-5GUJ36TL.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6R56OKJZ.js.map +0 -1
- package/dist/chunk-AVEQUR44.js.map +0 -1
- package/dist/chunk-BVEDKMNG.js.map +0 -1
- package/dist/chunk-BYGPUUCT.js.map +0 -1
- package/dist/chunk-CWXTNPNZ.js.map +0 -1
- package/dist/chunk-DFKBTBO3.js.map +0 -1
- package/dist/chunk-F3JWOPTG.js.map +0 -1
- package/dist/chunk-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.js.map +0 -1
- package/dist/chunk-IY6256GK.js.map +0 -1
- package/dist/chunk-KJRVMMM3.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-R5IMFAFR.js.map +0 -1
- package/dist/chunk-TO7DJYLZ.js.map +0 -1
- package/dist/chunk-TR3HFH6J.js.map +0 -1
- package/dist/chunk-WJMPCVOR.js.map +0 -1
- package/dist/chunk-WSJ7RO54.js.map +0 -1
- package/dist/chunk-WVNDEYXL.js.map +0 -1
- package/dist/chunk-XHHMM5HX.js.map +0 -1
- package/dist/chunk-XN37VTDB.js.map +0 -1
- package/dist/chunk-XPJQXSC4.js.map +0 -1
- package/dist/chunk-YFAXOD7Z.js +0 -42
- package/dist/chunk-YFAXOD7Z.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/chunk-Z2K4JPQ3.js.map +0 -1
- package/dist/chunk-ZCL7C5LH.js.map +0 -1
- package/dist/client-G2WGWPCA.js +0 -11
- package/dist/db-XKZYDCKM.js.map +0 -1
- package/dist/digest-7UBGUBRK.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-OAAE5YE2.js.map +0 -1
- package/dist/fleet-events-cF2Om88w.d.ts +0 -24
- package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
- package/dist/github-signals-TX5FPYP6.js.map +0 -1
- package/dist/header-image-PSPT7CUM.js.map +0 -1
- package/dist/launch-UD36AHUN.js.map +0 -1
- package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
- package/dist/match-harness-QRG6XZAR.js +0 -70
- package/dist/match-harness-QRG6XZAR.js.map +0 -1
- package/dist/migrate-Y7R4CESL.js +0 -7
- package/dist/parity-CAJ477PS.js +0 -14
- package/dist/preflight-RYIP54NY.js.map +0 -1
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prismic-models-P6FWWVRS.js.map +0 -1
- package/dist/prospect-audit-PRHU53HS.js.map +0 -1
- package/dist/protection-audit-SJO53MEQ.js +0 -168
- package/dist/protection-audit-SJO53MEQ.js.map +0 -1
- package/dist/render-YGNGTX3W.js.map +0 -1
- package/dist/replay-PCSQOGCZ.js.map +0 -1
- package/dist/report-LQS4ECTM.js.map +0 -1
- package/dist/report-mirror-HDTOJE64.js.map +0 -1
- package/dist/schema-A4WCPIF4.js.map +0 -1
- package/dist/site-mirror-WON4FTJ2.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- package/dist/submissions-OVZPVBYG.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
- /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
- /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
- /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
- /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
- /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
- /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/fleet/clone-if-needed.ts","../src/cli/fleet/prepare-sites.ts","../src/util/fleet-workdir.ts"],"sourcesContent":["import { stat, readdir, mkdir } from \"node:fs/promises\";\nimport { isAbsolute, join } from \"node:path\";\nimport type { Site } from \"../../types.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { sameOwnerRepo } from \"../../util/git.js\";\n\nexport type CloneIfNeededOptions = {\n workdir: string;\n spawn?: SpawnFn;\n};\n\nfunction deriveNameFromRepoUrl(repoUrl: string): string {\n const slash = repoUrl.split(\"/\").pop() ?? repoUrl;\n return slash.replace(/\\.git$/, \"\");\n}\n\n/** Reject names that would let an inventory entry write outside `workdir`. */\nfunction assertSafeName(name: string): void {\n if (name.length === 0) {\n // `join(workdir, \"\")` collapses to the workdir root — the clone would land\n // there (and two empty-named sites would collide). An empty name is never a\n // legitimate checkout dir.\n throw new Error(\"unsafe site name (empty)\");\n }\n if (isAbsolute(name)) {\n throw new Error(`unsafe site name (absolute path not allowed): ${name}`);\n }\n if (name.includes(\"/\") || name.includes(\"\\\\\")) {\n throw new Error(`unsafe site name (path separator not allowed): ${name}`);\n }\n if (name.split(/[\\\\/]/).some((seg) => seg === \"..\")) {\n throw new Error(`unsafe site name (traversal segment not allowed): ${name}`);\n }\n}\n\n/**\n * Reject repo URLs that don't look like a normal clone target. Without this,\n * a `repoUrl` starting with `-` would be interpreted by git as a flag\n * (CVE-2017-1000117 family) — e.g. `--upload-pack=…` allows arbitrary command\n * execution. Inventory files are usually trusted, but `.mjs`/`.js` inventories\n * could pull from environments or external sources, so harden the boundary.\n *\n * Accepts: `https://`, `http://`, `ssh://`, `git://`, `file://`, and\n * `[user@]host:path` shorthand (e.g. `git@github.com:org/repo.git`).\n */\nfunction assertSafeRepoUrl(repoUrl: string): void {\n if (\n !/^(https?|ssh|git|file):\\/\\//.test(repoUrl) &&\n !/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:/.test(repoUrl)\n ) {\n throw new Error(\n `unsafe repoUrl: must start with a scheme (https://, ssh://, git://, file://) ` +\n `or use scp-style \"user@host:path\" (got: ${JSON.stringify(repoUrl)})`,\n );\n }\n}\n\nasync function isNonEmptyDir(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n if (!s.isDirectory()) return false;\n const entries = await readdir(path);\n return entries.length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * The repo identity (`owner/repo` or a clone URL) the site is *expected* to be.\n * `gitRepo` (Airtable's `owner/repo`) is canonical; fall back to `repoUrl`.\n * `undefined` when the inventory carries neither — there's nothing to verify\n * against, so a reused checkout can't be checked (we keep current behavior).\n */\nfunction expectedRepoRef(site: Site): string | undefined {\n return site.gitRepo ?? site.repoUrl ?? undefined;\n}\n\n/**\n * Verify an existing checkout at `path` is the SAME repo as the site expects.\n * The `siteSlug` that derives the path is lossy (distinct site names can\n * collapse to one slug → one path), so without this a fleet run could read,\n * audit, or commit against the WRONG repo's working tree.\n *\n * Reads `git -C <path> remote get-url origin` (via the injected spawn so tests\n * can fake it) and compares `owner/repo` on both sides, normalizing scheme,\n * host, `.git`, and case. Throws on a mismatch — never silently proceeds.\n * No-ops when the site carries no expected identity, or the dir isn't a git\n * checkout (no origin) — those are handled by the normal clone path.\n */\nasync function assertCheckoutMatches(site: Site, path: string, spawn: SpawnFn): Promise<void> {\n const expected = expectedRepoRef(site);\n if (!expected) return; // nothing to verify against\n\n const r = await spawn(\"git\", [\"-C\", path, \"remote\", \"get-url\", \"origin\"], {\n timeoutMs: 30_000,\n });\n // Not a git checkout / no origin remote → can't verify; let the clone path\n // (or the caller) deal with a non-repo directory rather than guessing.\n if (r.code !== 0) return;\n const origin = r.stdout.trim();\n if (origin.length === 0) return;\n\n if (!sameOwnerRepo(origin, expected)) {\n throw new Error(\n `checkout at ${path} is the wrong repo: origin is ${JSON.stringify(origin)} ` +\n `but site expects ${JSON.stringify(expected)} (slug collision?) — refusing to reuse it`,\n );\n }\n}\n\n/** GitHub repo identity `owner/repo`: exactly two `[\\w.-]` segments. Constrained\n * so it can't smuggle a scheme, host, extra path segment, traversal, or an argv\n * flag into the derived clone URL below. */\nconst GIT_REPO_RE = /^[\\w.-]+\\/[\\w.-]+$/;\n\n/**\n * Resolve the URL to clone from. An explicit `repoUrl` wins; otherwise derive\n * one from `gitRepo` (`owner/repo` → `https://github.com/owner/repo.git`). The\n * Airtable inventory deliberately sets `gitRepo` and NOT `repoUrl` (a clone\n * source must never be the production `url`), so without this derivation every\n * checkout-based fleet recipe throws on the first site with an empty workdir.\n *\n * Returns `undefined` when neither is set; throws on a malformed `gitRepo`.\n */\nfunction resolveCloneUrl(site: Site): string | undefined {\n if (site.repoUrl) return site.repoUrl;\n if (!site.gitRepo) return undefined;\n if (!GIT_REPO_RE.test(site.gitRepo)) {\n throw new Error(`unsafe gitRepo: expected \"owner/repo\" (got: ${JSON.stringify(site.gitRepo)})`);\n }\n return `https://github.com/${site.gitRepo}.git`;\n}\n\nexport async function cloneIfNeeded(site: Site, opts: CloneIfNeededOptions): Promise<Site> {\n const spawn = opts.spawn ?? defaultSpawn;\n\n if (await isNonEmptyDir(site.path)) {\n // Reusing an existing checkout: confirm it's actually this site's repo\n // before any audit/recipe operates on it (slug collisions can point two\n // sites at the same path). Throws on mismatch.\n await assertCheckoutMatches(site, site.path, spawn);\n return site;\n }\n\n const repoUrl = resolveCloneUrl(site);\n if (!repoUrl) {\n throw new Error(\n `site path does not exist (${site.path}) and no repoUrl or gitRepo is set — cannot clone`,\n );\n }\n\n const name = site.name || deriveNameFromRepoUrl(repoUrl); // `||`: empty name derives from the repo\n assertSafeName(name);\n assertSafeRepoUrl(repoUrl);\n const target = join(opts.workdir, name);\n await mkdir(opts.workdir, { recursive: true });\n\n if (await isNonEmptyDir(target)) {\n // Same guard for the workdir/<name> reuse path.\n await assertCheckoutMatches(site, target, spawn);\n return { ...site, name, path: target };\n }\n\n // `--` separator so git won't treat repoUrl as a flag if validation slips.\n const result = await spawn(\"git\", [\"clone\", \"--\", repoUrl, target], {\n cwd: opts.workdir,\n timeoutMs: 5 * 60_000,\n });\n if (result.code !== 0) {\n throw new Error(`git clone failed (code ${result.code}): ${result.stderr}`);\n }\n return { ...site, name, path: target };\n}\n","import type { Site } from \"../../types.js\";\nimport { cloneIfNeeded } from \"./clone-if-needed.js\";\n\n/** A fleet site that couldn't be prepared for a command, with the reason. */\nexport type SkippedSite = { site: string; reason: string };\nexport type FleetPrepResult = { prepared: Site[]; skipped: SkippedSite[] };\n\ntype CloneFn = (site: Site, opts: { workdir: string }) => Promise<Site>;\n\n/**\n * Prepare every fleet site for a command, tolerating per-site failures.\n *\n * A site is cloned into `workdir` unless `needsCheckout` says otherwise (the\n * default is \"always clone\" — recipe commands operate on a local checkout; the\n * audit command overrides this so a deployed-URL site skips the clone). The\n * critical property is ISOLATION: a per-site prep failure (no clone source,\n * clone error, the wrong-repo guard) is CAUGHT and recorded in `skipped` rather\n * than thrown. A single malformed inventory row can therefore never abort the\n * whole fleet run — a bare `Promise.all(map(cloneIfNeeded))` used to (one site\n * with no repo threw and rejected prep for EVERY site, so nothing ran). Each\n * site is prepared independently (parallel) so one slow/failed clone can't\n * serialize or sink the rest.\n *\n * The skipped-site label prefers `site.name` but uses a truthiness check (not\n * `??`) so an empty-string name (an Airtable row whose slug came out empty)\n * still falls back to the path instead of rendering blank.\n */\nexport async function prepareFleetSites(\n sites: Site[],\n opts: { workdir: string; needsCheckout?: (site: Site) => boolean; clone?: CloneFn },\n): Promise<FleetPrepResult> {\n const clone = opts.clone ?? cloneIfNeeded;\n const needsCheckout = opts.needsCheckout ?? (() => true);\n const settled = await Promise.all(\n sites.map(async (site): Promise<{ ok: true; site: Site } | ({ ok: false } & SkippedSite)> => {\n if (!needsCheckout(site)) return { ok: true, site };\n try {\n return { ok: true, site: await clone(site, { workdir: opts.workdir }) };\n } catch (e) {\n return { ok: false, site: site.name || site.path, reason: (e as Error).message };\n }\n }),\n );\n const prepared: Site[] = [];\n const skipped: SkippedSite[] = [];\n for (const r of settled) {\n if (r.ok) prepared.push(r.site);\n else skipped.push({ site: r.site, reason: r.reason });\n }\n return { prepared, skipped };\n}\n\n/** One-line operator notice for sites dropped during fleet prep; null when none\n * were skipped. The leading \"⚠ … site(s) skipped (could not prepare)\" token is\n * what the nightly fleet-lighthouse workflow greps to raise a `::warning::`, so\n * a tolerated skip stays visible in the Actions UI rather than buried. */\nexport function formatSkippedNotice(skipped: SkippedSite[]): string | null {\n if (skipped.length === 0) return null;\n const detail = skipped.map((s) => `${s.site} (${s.reason})`).join(\"; \");\n return `⚠ ${skipped.length} site(s) skipped (could not prepare): ${detail}`;\n}\n\n/** Append the skip notice to a command's output on a blank line, or return the\n * output unchanged when nothing was skipped. Keeps every fleet command's\n * return paths uniform. */\nexport function appendSkipNotice(output: string, skipped: SkippedSite[]): string {\n const notice = formatSkippedNotice(skipped);\n return notice ? `${output}\\n\\n${notice}` : output;\n}\n","import { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** The default clone target for `--fleet` mode. Normally `~/.reddoor-maint/sites`.\n *\n * When `HOME` is unset or empty (cron, minimal CI containers) the old\n * `${HOME}/.reddoor-maint/sites` expression collapsed to `/.reddoor-maint/sites`\n * — a filesystem-root path the process almost never has permission to write,\n * and a dangerous one if it did. Fall back to a tmpdir-based path instead so a\n * HOME-less environment clones somewhere writable rather than at `/`.\n *\n * An explicit `--workdir` always wins; callers pass `opts.workdir ?? fleetWorkdir()`. */\nexport function fleetWorkdir(): string {\n const home = process.env.HOME?.trim();\n const base = home ? home : tmpdir();\n return join(base, \".reddoor-maint\", \"sites\");\n}\n"],"mappings":"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","names":["join"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/forms/payload.ts","../src/forms/ingest.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\nimport { parseReplyCopy, type ReplyCopy } from \"./reply-copy.js\";\n\n/** The JSON wire format a fleet site forwards to the ingest endpoint.\n *\n * Underscore-prefixed keys are RESERVED for transport concerns and are never\n * lead data: `_meta` (below) and `_screenOut` (the bot screen-out beacon —\n * a `{ _screenOut: \"honeypot\" | \"too-fast\" }` body is a counter ping, not a\n * submission; see ingest.ts `parseScreenOut`). Site field names must not\n * start with `_`. */\nexport type SubmissionPayload = {\n formType?: string;\n name?: string;\n firstName?: string;\n lastName?: string;\n email?: string;\n phone?: string;\n message?: string;\n sourceUrl?: string;\n utm?: string;\n /** Any additional, site-specific fields. */\n extra?: Record<string, unknown>;\n /** Reserved transport envelope (token/IP/UA); stripped by normalizeSubmission, never persisted. */\n _meta?: SubmissionMeta;\n /** Reserved: confirmation-email copy the SITE resolved from its own CMS.\n * Validated by parseReplyCopy, then stored under the same key in extraFields\n * so a replayed submission sends the same email it would have sent live. */\n _reply?: ReplyCopy;\n};\n\nexport type NormalizedSubmission = {\n formType: FormType;\n name: string;\n email: string;\n phone?: string;\n message?: string;\n sourceUrl?: string;\n utm?: string;\n extraFields: Record<string, unknown>;\n};\n\nexport type NormalizeResult =\n { ok: true; value: NormalizedSubmission } | { ok: false; errors: string[] };\n\nconst KNOWN_KEYS = new Set([\n \"formType\",\n \"name\",\n \"firstName\",\n \"lastName\",\n \"email\",\n \"phone\",\n \"message\",\n \"sourceUrl\",\n \"utm\",\n \"extra\",\n \"_meta\",\n \"_reply\",\n]);\n\n// Keys that, copied as own-properties into the captured object, could surprise a\n// downstream consumer (template/DB layer). This is an untrusted boundary, so the\n// extraFields merge drops them rather than trusting the caller.\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\nconst EMAIL_RE = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/;\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Resolve the submission's formType. An ABSENT/blank value defaults to \"contact\"\n * (the long-standing minimal-form contract). A PRESENT but unrecognized value\n * (e.g. a typo'd \"news\") returns null → the caller rejects it, rather than\n * silently storing it as contact — which would, for a newsletter signup, drop the\n * Mailchimp fan-out. Mirrors createIngestEndpoint's reject-invalid behavior.\n */\nfunction resolveFormType(raw: unknown): FormType | null {\n const s = str(raw);\n if (s === \"\") return \"contact\";\n return (SUBMISSION_FORM_TYPES as readonly string[]).includes(s) ? (s as FormType) : null;\n}\n\n/**\n * Defensively normalize an untrusted ingest payload into typed fields. Folds\n * name/first+last, lowercases email, and dumps every unclaimed key into\n * extraFields so no site-specific data is lost. Rejects only when there's\n * nothing to act on (no email AND no message) or a present email is malformed.\n */\nexport function normalizeSubmission(payload: unknown): NormalizeResult {\n if (typeof payload !== \"object\" || payload === null) {\n return { ok: false, errors: [\"payload must be a JSON object\"] };\n }\n const p = payload as Record<string, unknown>;\n const name = str(p.name) || [str(p.firstName), str(p.lastName)].filter(Boolean).join(\" \");\n const email = str(p.email).toLowerCase();\n const message = str(p.message);\n\n const formType = resolveFormType(p.formType);\n const errors: string[] = [];\n if (!email && !message) errors.push(\"at least one of email or message is required\");\n if (email && !EMAIL_RE.test(email)) errors.push(\"email is not a valid address\");\n if (formType === null) errors.push(\"formType is not a recognized form type\");\n // The `|| formType === null` is redundant for control flow (the push above\n // already made errors non-empty) but narrows formType to FormType past this line.\n if (errors.length > 0 || formType === null) return { ok: false, errors };\n\n const extraFields: Record<string, unknown> = {};\n const extra = p.extra;\n if (typeof extra === \"object\" && extra !== null) {\n for (const [k, v] of Object.entries(extra)) {\n if (!DANGEROUS_KEYS.has(k) && !k.startsWith(\"_\")) extraFields[k] = v;\n }\n }\n for (const [k, v] of Object.entries(p)) {\n if (!KNOWN_KEYS.has(k) && !DANGEROUS_KEYS.has(k) && !k.startsWith(\"_\")) extraFields[k] = v;\n }\n // The ONE underscore key a caller may set. Both loops above dropped the rest,\n // including any `_reply` smuggled in through `extra` — this is the only way a\n // value reaches that key, and it is validated on the way through. Without it,\n // a bot could POST its own copy and dictate the text of an email we send from\n // a domain with real sending reputation.\n const reply = parseReplyCopy(p._reply);\n if (reply) extraFields._reply = reply;\n\n const value: NormalizedSubmission = {\n formType,\n name,\n email,\n extraFields,\n };\n const phone = str(p.phone);\n if (phone) value.phone = phone;\n if (message) value.message = message;\n const sourceUrl = str(p.sourceUrl);\n if (sourceUrl) value.sourceUrl = sourceUrl;\n const utm = str(p.utm);\n if (utm) value.utm = utm;\n return { ok: true, value };\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport type {\n SubmissionRow,\n SubmissionInput,\n NotifyStatus,\n SubmissionStatus,\n} from \"../reports/submission-row.js\";\nimport { normalizeSubmission, type NormalizedSubmission } from \"./payload.js\";\nimport type { TurnstileOutcome, TurnstileVerification } from \"./turnstile.js\";\nimport { SPAM_THRESHOLD, type SpamVerdict } from \"./spam-classifier.js\";\n\n/** How far back the duplicate-spray AND repeat-sender lookups scan. Sprays arrive in\n * bursts but bots also re-run for weeks; 30 days catches repeats without an unbounded\n * table scan. */\nconst DUPLICATE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport type IngestDeps = {\n getWebsiteBySlug: (slug: string) => Promise<WebsiteRow | null>;\n createSubmission: (input: SubmissionInput) => Promise<SubmissionRow>;\n /** Optional last-resort capture for a lead whose site lookup THREW (a store\n * outage, not an unknown slug). When present, the lead is persisted here and\n * the visitor still gets \"accepted\"; `db replay-deadletters` re-runs it once\n * the lookup recovers. Absent → the error propagates exactly as before, so\n * callers that never wired it are byte-for-byte unchanged. */\n deadLetter?: (input: {\n siteSlug: string;\n payload: unknown;\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: Date;\n }) => Promise<{ id: string }>;\n notify: (\n site: WebsiteRow,\n submission: SubmissionRow,\n ) => Promise<{ status: NotifyStatus; messageId: string | null }>;\n stampNotified: (id: string, status: NotifyStatus, messageId: string | null) => Promise<void>;\n now: () => Date;\n /** Optional spam classifier. When present, its verdict drives the stored\n * status/score/reason; absent → every submission scores 0 (fail-open, clean).\n * Injected so the classifier stays a pure, transport-agnostic leaf. */\n classifySpam?: (n: NormalizedSubmission, turnstile: TurnstileOutcome) => SpamVerdict;\n /** Optional: fleet-wide duplicate/near-duplicate body lookup since `since`. Drives\n * the duplicate-spray signal — the same pitch blasted to many sites shows up as\n * identical (`exact`) or template-substituted (`similar`, token-set Jaccard) bodies.\n * Statuses let the retro re-bucket pick still-'new' prior copies; siteId+email let\n * the caller exempt a genuine same-sender resubmission on the same site. Absent →\n * the check is skipped (fail-open clean). */\n findRecentDuplicates?: (\n message: string,\n since: Date,\n ) => Promise<{\n exact: Array<{ id: string; status: string; siteId: string; email: string }>;\n similar: Array<{ id: string; status: string; siteId: string; email: string }>;\n }>;\n /** Optional: recent non-newsletter submissions from the same email since `since`.\n * Drives the cross-site repeat-sender signal — the fleet's sites are unrelated\n * businesses, so one address contacting 2+ of them within the window is a\n * solicitation tell. Absent → the check is skipped (fail-open clean). */\n listRecentSubmissionsForEmail?: (\n email: string,\n since: Date,\n ) => Promise<Array<{ id: string; siteId: string; status: string }>>;\n /** Optional: retroactively re-bucket prior still-'new' rows once a later copy\n * identifies a spray (the first copy is delivered by design). Implementations\n * must only touch status='new' rows. Best-effort — failures are swallowed. */\n retroBucket?: (ids: string[], reason: string) => Promise<void>;\n /** Optional: POST a newsletter submission to the site's configured webhook\n * (best-effort). Omitted in tests/handlers that don't need it. */\n forwardNewsletter?: (\n webhookUrl: string,\n submission: SubmissionRow,\n site: WebsiteRow,\n ) => Promise<{ ok: boolean; status: number }>;\n /** Optional: add a newsletter submitter to the site's Mailchimp audience\n * (best-effort). Omitted where unused. `tagged:false` reports a member who was\n * upserted but could NOT be tagged — recorded separately, see FANOUT below. */\n addToMailchimp?: (\n site: WebsiteRow,\n submission: SubmissionRow,\n ) => Promise<{ ok: boolean; status: number; tagged?: boolean }>;\n /** Optional: persist the newsletter fan-out outcome on the row (see FANOUT below).\n * Best-effort — failures are swallowed. Absent → the fan-out still runs, just\n * unrecorded (older callers and most tests). */\n stampFanout?: (id: string, fanoutStatus: string) => Promise<void>;\n /** Optional: run the best-effort tail (notify → stamp → fan-out) AFTER the\n * response instead of before it. Production passes Netlify's\n * `context.waitUntil`, which keeps the invocation alive until the promise\n * settles. Absent → the tail runs inline, exactly as it always did; this is a\n * latency change, never a behavioural one.\n *\n * Why it matters: the submitting site waits on this response under an abort\n * budget (forms/client.ts `INGEST_TIMEOUT_MS`). Once the row is written the\n * lead is captured, so every further second spent on email/webhook providers\n * is time in which a captured lead can still be reported to the visitor as a\n * failed submission — which is exactly what happened to 1836dig on\n * 2026-08-03. */\n defer?: (work: Promise<unknown>) => void;\n};\n\nexport type IngestResult =\n | { status: \"accepted\"; submissionId: string; notifyStatus: NotifyStatus | \"deferred\" }\n | { status: \"rejected\"; reason: \"invalid-payload\"; errors: string[] }\n | { status: \"unknown-site\"; slug: string };\n\nexport type ScreenOutDeps = {\n getWebsiteBySlug: (slug: string) => Promise<WebsiteRow | null>;\n recordScreenOut: (siteId: string, reason: \"honeypot\" | \"too-fast\") => Promise<void>;\n};\n\nexport type ScreenOutResult =\n { status: \"recorded\"; slug: string } | { status: \"unknown-site\"; slug: string };\n\n/** Extract the screen-out reason from a beacon body, or null if it isn't one.\n * The beacon key is the reserved `_screenOut` (underscore-namespaced like `_meta`,\n * see payload.ts). The bare `screenOut` key is the DEPRECATED pre-namespacing wire\n * shape — sites run older package versions for a while, so the central receiver\n * keeps accepting it. `_screenOut` wins when both are present. */\nexport function parseScreenOut(payload: unknown): \"honeypot\" | \"too-fast\" | null {\n if (!payload || typeof payload !== \"object\") return null;\n const body = payload as Record<string, unknown>;\n const v = \"_screenOut\" in body ? body[\"_screenOut\"] : body[\"screenOut\"];\n return v === \"honeypot\" || v === \"too-fast\" ? v : null;\n}\n\n/** Resolve the site and record a caught screen-out. Best-effort: a record failure is\n * the caller's to swallow — a missed count must never error a screened bot. */\nexport async function ingestScreenOut(\n deps: ScreenOutDeps,\n slug: string,\n reason: \"honeypot\" | \"too-fast\",\n): Promise<ScreenOutResult> {\n const site = await deps.getWebsiteBySlug(slug);\n if (!site) return { status: \"unknown-site\", slug };\n await deps.recordScreenOut(site.id, reason);\n return { status: \"recorded\", slug };\n}\n\n/**\n * Normalize → resolve site → persist → notify → stamp. The order is load-bearing:\n * the row is written BEFORE notify, and notify/stamp failures are swallowed (logged)\n * so a Resend or Airtable-write-back hiccup can never turn an accepted lead into a 502.\n */\nexport async function ingestSubmission(\n deps: IngestDeps,\n slug: string,\n rawPayload: unknown,\n turnstileInput: TurnstileOutcome | TurnstileVerification = \"unverifiable\",\n): Promise<IngestResult> {\n // Accept either the full verification (the production handler) or a bare outcome\n // string (older callers and virtually every test) — a string simply has no solved-\n // hostname to check. Normalized once so the body has a single shape to reason about.\n const verification: TurnstileVerification =\n typeof turnstileInput === \"string\"\n ? { outcome: turnstileInput, hostname: null }\n : turnstileInput;\n const turnstile = verification.outcome;\n const normalized = normalizeSubmission(rawPayload);\n if (!normalized.ok) {\n return { status: \"rejected\", reason: \"invalid-payload\", errors: normalized.errors };\n }\n // Persist before enrich (#539 Phase 0). The lookup THROWING is a store outage,\n // not an answer — and until 2026-08-23 it was the one await that could cost a\n // lead: the row write comes later, so a thrown lookup 502'd the visitor with\n // nothing recorded anywhere (the 2026-08-17 Airtable quota outage did exactly\n // that, while the submissions store itself was healthy the whole time). With\n // `deadLetter` wired, the lead lands there and the visitor gets an honest\n // \"accepted\"; `db replay-deadletters` runs it through this same function once\n // the lookup recovers, producing a normal row with real classification+notify.\n //\n // Three deliberate boundaries:\n // - A lookup that RESOLVES to null is still `unknown-site` — the store answered,\n // and a junk slug is a rejection, not a lead to save.\n // - A testMode probe rethrows: it persists nothing by design, so there is\n // nothing to save, and swallowing the outage would green the form-e2e audit\n // precisely when central ingest is degraded.\n // - `deadLetter` itself throwing propagates: both stores are down, and the 502\n // is honest — there is nowhere left to put the lead.\n let site: WebsiteRow | null;\n try {\n site = await deps.getWebsiteBySlug(slug);\n } catch (err) {\n if (!deps.deadLetter || isTestMode(rawPayload)) throw err;\n const dl = await deps.deadLetter({\n siteSlug: slug,\n payload: rawPayload,\n turnstile: verification,\n error: String(err),\n receivedAt: deps.now(),\n });\n console.error(\n `[ingest] site lookup failed for '${slug}' — lead dead-lettered as ${dl.id}: ${String(err)}`,\n );\n return { status: \"accepted\", submissionId: dl.id, notifyStatus: \"skipped\" };\n }\n if (!site) return { status: \"unknown-site\", slug };\n\n // Synthetic end-to-end probe (the `form-e2e` fleet audit). A central-only marker\n // on the payload routes the submission away from EVERY real sink: no row is\n // persisted, no spam classification, no operator/autoresponder email, no\n // newsletter fan-out — and Turnstile enforcement is bypassed (the short-circuit\n // sits before that check). The marker therefore grants a bot NO benefit: it\n // reaches no inbox/DB/webhook, so skipping Turnstile costs nothing. This\n // suppression MUST be central — the submitting site alone cannot stop the real\n // inbox firing. Validity + site resolution are still enforced above (a junk body\n // is rejected, an unknown slug is unknown-site), so the marker can't smuggle\n // anything through. Return accepted+skipped so the probe asserts success.\n if (isTestMode(rawPayload)) {\n return { status: \"accepted\", submissionId: \"test-mode\", notifyStatus: \"skipped\" };\n }\n\n const n = normalized.value;\n\n // A site still under construction is where the operator tests their OWN forms:\n // one address, several unrelated sites, minutes apart. That is byte-for-byte the\n // cross-site repeat-sender signature below, so a site in development reliably\n // auto-spams its own builder's test submissions — the row lands `spam_auto`,\n // notify is skipped and the cockpit hides it, which reads exactly like \"the form\n // is broken\" (it did, on 2026-08-17). Spam handling on a site with no real\n // visitors protects nothing, so skip ALL of it: content scoring, the Turnstile\n // escalation, the repeat-sender scan and the duplicate/spray scan — including\n // their retroactive re-bucketing of OTHER sites' rows, which a test submission\n // has no business triggering. Re-enabled the moment the site leaves development.\n const spamHandlingEnabled = site.status !== \"building\";\n\n // Fold the content signals + the Turnstile verdict into ONE spam decision.\n // Absent classifier → treat as clean (fail-open). A throwing classifier is\n // swallowed the same way — a bug in the heuristic must never turn an\n // otherwise-good lead into a 500; it just scores clean. On a `requireTurnstile`\n // site, both an ACTUAL \"fail\" (forged token) AND an \"absent\" token escalate to\n // auto-spam regardless of score — a real browser that renders the widget ALWAYS\n // sends a token, so a completely missing one is the direct-POST-bot signature. A\n // present-but-\"unverifiable\" token (expired/duplicate — a real browser DID render\n // the widget) stays neutral, as does anything on a site that hasn't opted in.\n let verdict: SpamVerdict = { score: 0, reasons: [] };\n if (spamHandlingEnabled && deps.classifySpam) {\n try {\n verdict = deps.classifySpam(n, turnstile);\n } catch (err) {\n console.error(`[ingest] classifySpam threw: ${String(err)}`);\n }\n }\n const reasons = [...verdict.reasons];\n let status: SubmissionStatus = verdict.score >= SPAM_THRESHOLD ? \"spam_auto\" : \"new\";\n if (\n spamHandlingEnabled &&\n site.requireTurnstile &&\n (turnstile === \"fail\" || turnstile === \"absent\")\n ) {\n status = \"spam_auto\";\n const reason = turnstile === \"fail\" ? \"turnstile-required-failed\" : \"turnstile-required-absent\";\n if (!reasons.includes(reason)) reasons.push(reason);\n } else if (\n spamHandlingEnabled &&\n site.requireTurnstile &&\n turnstile === \"pass\" &&\n verification.hostname !== null &&\n !turnstileHostnameAcceptable(verification.hostname, site.url)\n ) {\n // Defense-in-depth vs token farming: the token PASSED, but Cloudflare says it was\n // solved on a host unrelated to this site. Cloudflare domain-binds sitekeys, so\n // this only trips on a loose widget allowlist — still, a passing token from a\n // foreign host accompanying a gated site's submission is not a real visitor. A\n // null hostname (older responses) or an unparseable site.url skips the check\n // entirely (fail-open); subdomains of the site's host (www., previews) match.\n status = \"spam_auto\";\n if (!reasons.includes(\"turnstile-required-hostname\"))\n reasons.push(\"turnstile-required-hostname\");\n }\n\n // Cross-site repeat-sender signal: the fleet's sites are UNRELATED businesses\n // (art gallery, realtor, home builder…), so the same email writing to 2+ different\n // sites within the window is a solicitation tell no single-message scan can see.\n // Same-site repeats alone must NOT trigger — those are genuine follow-ups. The\n // operator explicitly accepts overblocking here (spam_auto is recoverable). Prior\n // still-'new' rows on OTHER sites are retro-bucketed too: the first copy of a\n // spray is delivered by design, so the copy that identifies it also cleans the\n // queue. The scan runs EVEN WHEN the incoming copy is already spam_auto — after the\n // classifier began catching whole spray families, an already-bucketed copy was\n // skipping this scan and the retro cleanup never fired for exactly the sprays it\n // was built for; escalation/reason still only applies when not already spam.\n // Best-effort — a lookup failure never blocks a lead.\n if (spamHandlingEnabled && n.formType !== \"newsletter\" && deps.listRecentSubmissionsForEmail) {\n try {\n const since = new Date(deps.now().getTime() - DUPLICATE_WINDOW_MS);\n const prior = await deps.listRecentSubmissionsForEmail(n.email, since);\n const otherSites = prior.filter((p) => p.siteId !== site.id);\n if (otherSites.length > 0) {\n if (status !== \"spam_auto\") {\n status = \"spam_auto\";\n if (!reasons.includes(\"repeat-sender\")) reasons.push(\"repeat-sender\");\n }\n const retroIds = otherSites.filter((p) => p.status === \"new\").map((p) => p.id);\n if (deps.retroBucket && retroIds.length > 0) {\n try {\n await deps.retroBucket(retroIds, \"retro:repeat-sender\");\n } catch (err) {\n console.error(`[ingest] retroBucket (repeat-sender) threw: ${String(err)}`);\n }\n }\n }\n } catch (err) {\n console.error(`[ingest] listRecentSubmissionsForEmail threw: ${String(err)}`);\n }\n }\n\n // Duplicate/spray signal: the same pitch blasted across the fleet (or repeated) is\n // a bot tell a lone content scan can't see. An identical body ('exact') OR a\n // template-substituted near-copy ('similar' — the live dog-harness spray differed\n // only in greeting; SEO sprays swap the target domain per site) → auto-spam\n // (recoverable). Prior still-'new' copies are retro-bucketed for the same reason\n // as above, and the scan runs even when the incoming copy is already spam_auto\n // (see the repeat-sender note). GENUINE-RESUBMIT EXEMPTION: a match from the SAME\n // sender on the SAME site is a real visitor double-submitting or resending after\n // silence, not spray evidence — without this exemption the resend was silently\n // bucketed AND retro-flipped the delivered original, vanishing an active lead\n // with no signal. Only cross-site or different-sender copies count. Guarded:\n // non-newsletter forms with a real body; the db helper ignores short bodies /\n // small token sets. Best-effort — a lookup failure never blocks a lead.\n if (\n spamHandlingEnabled &&\n n.formType !== \"newsletter\" &&\n n.message !== undefined &&\n deps.findRecentDuplicates\n ) {\n try {\n const since = new Date(deps.now().getTime() - DUPLICATE_WINDOW_MS);\n const dupes = await deps.findRecentDuplicates(n.message, since);\n const senderEmail = n.email.trim().toLowerCase();\n const isOwnResend = (m: { siteId: string; email: string }) =>\n m.siteId === site.id && m.email.trim().toLowerCase() === senderEmail;\n const exact = dupes.exact.filter((m) => !isOwnResend(m));\n const similar = dupes.similar.filter((m) => !isOwnResend(m));\n const reason =\n exact.length > 0 ? \"duplicate-body\" : similar.length > 0 ? \"similar-body\" : null;\n if (reason !== null) {\n if (status !== \"spam_auto\") {\n status = \"spam_auto\";\n if (!reasons.includes(reason)) reasons.push(reason);\n }\n const retroIds = [...exact, ...similar].filter((m) => m.status === \"new\").map((m) => m.id);\n if (deps.retroBucket && retroIds.length > 0) {\n try {\n await deps.retroBucket(retroIds, \"retro:duplicate-body\");\n } catch (err) {\n console.error(`[ingest] retroBucket (duplicate-body) threw: ${String(err)}`);\n }\n }\n }\n } catch (err) {\n console.error(`[ingest] findRecentDuplicates threw: ${String(err)}`);\n }\n }\n const spamReason = reasons.length > 0 ? reasons.join(\",\") : null;\n\n const row = await deps.createSubmission({\n siteId: site.id,\n formType: n.formType,\n name: n.name,\n email: n.email,\n extraFields: n.extraFields,\n status,\n spamScore: verdict.score,\n spamReason,\n // Optional fields spread only when present — exactOptionalPropertyTypes\n // forbids assigning `undefined` to an optional `phone?: string` etc.\n ...(n.phone !== undefined ? { phone: n.phone } : {}),\n ...(n.message !== undefined ? { message: n.message } : {}),\n ...(n.sourceUrl !== undefined ? { sourceUrl: n.sourceUrl } : {}),\n ...(n.utm !== undefined ? { utm: n.utm } : {}),\n submittedAt: deps.now(),\n });\n\n // Auto-spam (and operator-marked spam) is captured but silent: no operator\n // email, no autoresponder, no newsletter fan-out. Skip notify entirely and\n // record the honest \"skipped\" stamp. notify.ts also nulls both builders for\n // these statuses (defense in depth), but short-circuiting here means the\n // injected notify dep is never even invoked for a spam row.\n const isSpam = row.status === \"spam_auto\" || row.status === \"spam\";\n\n // ── best-effort tail ───────────────────────────────────────────────────────\n // notify → stamp → newsletter fan-out. NOTHING below can cost the lead: the\n // row is already durable, and every step here is swallowed+logged rather than\n // allowed to turn an accepted lead into a 502. That is precisely why it does\n // not belong on the visitor's critical path — the submitting site waits on\n // this response under an abort budget (forms/client.ts INGEST_TIMEOUT_MS), so\n // Resend latency used to be able to make a captured lead read as a failed\n // submission in the visitor's browser (1836dig, 2026-08-03).\n //\n // `deps.defer` (Netlify's `context.waitUntil` in production) runs the tail\n // AFTER the response goes out. Absent — every test, and any runtime without\n // post-response execution — it runs inline exactly as before, so this is a\n // latency change and never a behavioural one.\n const runTail = async (): Promise<NotifyStatus> => {\n let notify: { status: NotifyStatus; messageId: string | null };\n if (isSpam) {\n notify = { status: \"skipped\", messageId: null };\n } else {\n try {\n notify = await deps.notify(site, row);\n } catch (err) {\n console.error(`[ingest] notify threw: ${String(err)}`);\n notify = { status: \"failed\", messageId: null };\n }\n }\n try {\n await deps.stampNotified(row.id, notify.status, notify.messageId);\n } catch (err) {\n console.error(`[ingest] stampNotified failed: ${String(err)}`);\n }\n\n await runFanout();\n return notify.status;\n };\n\n // Newsletter fan-out: each configured destination fires best-effort and is\n // swallowed+logged — the lead is already persisted; never turn it into a 502.\n // Guarded on the row status so a spam signup is never forwarded to a site\n // webhook or added to a Mailchimp audience.\n //\n // FANOUT provenance (2026-07-31): the outcome is ALSO recorded on the row, one\n // `<destination>:<outcome>` token per attempt, comma-joined in attempt order —\n // `webhook:ok,mailchimp:401`. Outcome is `ok`, the HTTP status of a non-2xx\n // (`0` for a network error / misconfiguration, see mailchimp.ts), or `threw`.\n // `mailchimp-tags:failed` is appended when the member was upserted but the tag\n // write did not land. Until this existed the results were console.error-only, so\n // an expired Mailchimp key stopped signups reaching the audience while the row\n // still read `notify=sent` — healthy-looking, with the failure visible nowhere the\n // operator ever looks. Nothing attempted → nothing stamped (null stays \"no\n // destination configured\", NOT \"we tried and lost the answer\"). The stamp is\n // best-effort like everything else here: a provenance write must never cost a lead.\n // A const arrow, not a hoisted declaration: `site` is narrowed non-null above,\n // and that narrowing only survives into a closure created after it. Declared\n // below runTail (which calls it) but before either call site, so there is no TDZ.\n const runFanout = async (): Promise<void> => {\n const fanout: string[] = [];\n if (n.formType === \"newsletter\" && !isSpam) {\n if (site.newsletterWebhook && deps.forwardNewsletter) {\n try {\n const fwd = await deps.forwardNewsletter(site.newsletterWebhook, row, site);\n if (!fwd.ok)\n console.error(`[ingest] newsletter webhook → ${fwd.status} for ${site.name}`);\n fanout.push(fwd.ok ? \"webhook:ok\" : `webhook:${fwd.status}`);\n } catch (err) {\n console.error(`[ingest] newsletter webhook threw: ${String(err)}`);\n fanout.push(\"webhook:threw\");\n }\n }\n if (site.mailchimpApiKey && site.mailchimpAudienceId && deps.addToMailchimp) {\n try {\n const mc = await deps.addToMailchimp(site, row);\n if (!mc.ok) console.error(`[ingest] mailchimp add → ${mc.status} for ${site.name}`);\n fanout.push(mc.ok ? \"mailchimp:ok\" : `mailchimp:${mc.status}`);\n // Only meaningful alongside a successful add — a failed add was never tagged\n // either, and one token per real problem keeps the row readable.\n if (mc.ok && mc.tagged === false) {\n console.error(`[ingest] mailchimp tags not applied for ${site.name}`);\n fanout.push(\"mailchimp-tags:failed\");\n }\n } catch (err) {\n console.error(`[ingest] mailchimp add threw: ${String(err)}`);\n fanout.push(\"mailchimp:threw\");\n }\n }\n }\n if (fanout.length > 0 && deps.stampFanout) {\n try {\n await deps.stampFanout(row.id, fanout.join(\",\"));\n } catch (err) {\n console.error(`[ingest] stampFanout failed: ${String(err)}`);\n }\n }\n };\n\n if (deps.defer) {\n // A rejection escaping here would land as an unhandled rejection in the\n // platform's post-response context, where nothing is left to catch it — so\n // the boundary is made unrejectable. Everything inside is already swallowed;\n // this guards the shape, not a known throw.\n deps.defer(\n runTail().catch((err) => {\n console.error(`[ingest] deferred tail threw: ${String(err)}`);\n }),\n );\n // The lead is durable and the response can go out now. The real notify\n // outcome lands on the ROW via stampNotified moments later — this field is\n // only ever read by callers wanting the in-request outcome, which by\n // definition does not exist once the tail is deferred.\n return { status: \"accepted\", submissionId: row.id, notifyStatus: \"deferred\" };\n }\n return { status: \"accepted\", submissionId: row.id, notifyStatus: await runTail() };\n}\n\n/** True when `a` and `b` are the same host or one is a subdomain of the other\n * (case-insensitive): `www.reddoorla.com` vs `reddoorla.com` matches both ways.\n * Exported for tests. PURE. */\nexport function hostsMatch(a: string, b: string): boolean {\n const ha = a.trim().toLowerCase();\n const hb = b.trim().toLowerCase();\n if (ha.length === 0 || hb.length === 0) return false;\n return ha === hb || ha.endsWith(`.${hb}`) || hb.endsWith(`.${ha}`);\n}\n\n/** Whether a passing token's solved-hostname is acceptable for the site at `siteUrl`.\n * An unparseable/hostless `siteUrl` returns TRUE — the check self-disables rather\n * than punishing a possibly-real visitor for an operator data problem (fail-open,\n * same philosophy as verifyTurnstile). PURE. */\nexport function turnstileHostnameAcceptable(tokenHostname: string, siteUrl: string): boolean {\n let siteHost: string;\n try {\n siteHost = new URL(siteUrl).hostname;\n } catch {\n return true;\n }\n if (!siteHost) return true;\n return hostsMatch(tokenHostname, siteHost);\n}\n\n/** True when an untrusted ingest payload carries the synthetic-probe marker\n * (top-level `testMode: true`). Read from the RAW payload so the branch never\n * depends on normalization internals; any non-`true` value is ignored (a real\n * visitor's form never sets it — the starter only forwards it when the submitted\n * form field `testMode` equals \"true\"). */\nexport function isTestMode(rawPayload: unknown): boolean {\n if (!rawPayload || typeof rawPayload !== \"object\") return false;\n return (rawPayload as Record<string, unknown>).testMode === true;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/reports/preflight.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"./airtable/client.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { listWebsites, siteSlug } from \"./airtable/websites.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport { listAllReports } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { parseAddresses, isProbablyEmail } from \"./send/orchestrate.js\";\nimport { ELIGIBLE_STATUSES, reportPeriodKey } from \"./due.js\";\nimport type { ReportType } from \"./types.js\";\nimport { gatingHealth, isSendOverridden } from \"./checklist.js\";\n\nexport type PreflightLevel = \"fail\" | \"warn\" | \"info\";\n\nexport type PreflightFinding = {\n level: PreflightLevel;\n /** Stable machine-readable check id, e.g. \"recipients-missing\". */\n check: string;\n message: string;\n};\n\nexport type PreflightSiteResult = {\n site: string;\n findings: PreflightFinding[];\n};\n\n/** Domains whose addresses are the OPERATOR's, not a client's. A client site whose\n * resolved To contains one of these is almost always a test-send leftover — the\n * exact misconfiguration that would silently divert a client announcement. */\nconst OPERATOR_DOMAINS = [\"reddoorla.com\", \"tuckerlemos.com\"];\n\n/** Anchors older than this, WHEN the anchor is actually the schedule's base date\n * (no newer Sent-at — see nextDueDate's `lastSent ?? fallback`), mean the next\n * `report --due` run drafts a surprise back-dated report. */\nconst STALE_ANCHOR_DAYS = 396; // ~13 months\n\nfunction domainOf(addr: string): string {\n const at = addr.lastIndexOf(\"@\");\n return at === -1 ? \"\" : addr.slice(at + 1).toLowerCase();\n}\n\nfunction isOperatorSite(site: WebsiteRow): boolean {\n if (!site.url) return false;\n try {\n // Airtable `url` cells aren't guaranteed a scheme; retry with one before giving up.\n const host = new URL(\n /^[a-z][a-z0-9+.-]*:\\/\\//i.test(site.url) ? site.url : `https://${site.url}`,\n ).hostname.toLowerCase();\n return OPERATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));\n } catch {\n return false;\n }\n}\n\nfunction lastSentForType(reports: ReportRow[], type: ReportType): string | null {\n const sent = reports\n .filter((r) => r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return sent[sent.length - 1] ?? null;\n}\n\nfunction checkFrequency(\n site: WebsiteRow,\n which: \"maintenance\" | \"testing\",\n reports: ReportRow[],\n findings: PreflightFinding[],\n now: Date,\n): void {\n // Validate the RAW Airtable cell, not the coerced Frequency: toFrequency trims\n // whitespace (so \"Monthly \" schedules), then coerces any still-unrecognized value\n // (typo, renamed option) to \"None\" with a console.warn. That warn only lands in\n // logs — this finding is the structured, surfaced version of the same failure.\n const raw = which === \"maintenance\" ? site.maintenanceFreqRaw : site.testingFreqRaw;\n const coerced = which === \"maintenance\" ? site.maintenanceFreq : site.testingFreq;\n if (raw !== null && raw !== \"\" && coerced === \"None\" && raw.trim() !== \"None\") {\n findings.push({\n level: \"fail\",\n check: \"frequency-unrecognized\",\n message: `${which} frequency cell is '${raw}' — not a recognized Monthly/Quarterly/Yearly/None (even after trimming), so the scheduler treats it as None and the site drops off the calendar; fix the Airtable value`,\n });\n return;\n }\n if (coerced === \"None\") return;\n\n const anchor = which === \"maintenance\" ? site.maintenanceDay : site.testingDay;\n if (!anchor) return;\n // nextDueDate uses `lastSent ?? anchor`: once anything has been sent, the anchor\n // is inert and an old one is normal on every healthy mature site — only warn when\n // the anchor is genuinely the base date the scheduler would use.\n const type: ReportType = which === \"maintenance\" ? \"Maintenance\" : \"Testing\";\n const lastSent = lastSentForType(reports, type);\n if (lastSent && new Date(lastSent).getTime() >= new Date(anchor).getTime()) return;\n const ageDays = (now.getTime() - new Date(anchor).getTime()) / 86_400_000;\n if (ageDays > STALE_ANCHOR_DAYS) {\n findings.push({\n level: \"warn\",\n check: \"anchor-stale\",\n message: `${which} day anchor is ${anchor} (>13 months old) and nothing newer has been sent — \\`report --due\\` will draft a back-dated overdue report; reset the anchor or clear the schedule`,\n });\n }\n}\n\n/**\n * Pure per-site preflight: every check that, left unfixed, makes a report send fail,\n * go to the wrong inbox, or surprise the operator. Read-only over data already\n * fetched; no Airtable handle, no network — trivially testable.\n *\n * Mirrors the send/draft-time validation (recipients + header image in\n * send/orchestrate.ts, Websites-row scores in draft.ts) so problems surface BEFORE\n * draft/approve instead of exploding at `report --send-ready`, and adds the checks\n * send time cannot do: operator-address leftovers, To-override shadowing,\n * pending-draft races, schedule hygiene against the RAW Airtable values.\n */\nexport function preflightSite(\n site: WebsiteRow,\n reports: ReportRow[],\n type: ReportType,\n now: Date,\n): PreflightSiteResult {\n const findings: PreflightFinding[] = [];\n\n // A site that isn't on the checked calendar never drafts, so its send\n // requirements (recipients / header image / scores) can't hurt anyone —\n // checking them anyway would drown the fleet run in fails for legacy hosting\n // rows. Frequency hygiene and pending-draft checks still run: a typo'd cell\n // is exactly HOW a site falls off the calendar, and an already-drafted report\n // can still be approved + sent regardless of schedule.\n const scheduledForType =\n type === \"Announcement\"\n ? true\n : type === \"Maintenance\"\n ? site.maintenanceFreq !== \"None\"\n : site.testingFreq !== \"None\";\n if (!scheduledForType) {\n findings.push({\n level: \"info\",\n check: \"not-scheduled\",\n message: `no ${type} schedule (frequency is None/blank) — send-requirement checks skipped; schedule + pending-draft checks still apply`,\n });\n }\n\n // --- Recipients: same resolution order as the send path (To override, else contact).\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = scheduledForType ? (explicitTo ?? fallbackTo ?? []) : [];\n if (scheduledForType && to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — use bare addresses only in Report recipients (To) / point of contact`,\n });\n }\n }\n const cc = scheduledForType ? (parseAddresses(site.reportRecipientsCc) ?? []) : [];\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC)`,\n });\n }\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0) {\n const allOperator = operatorAddrs.length === to.length;\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: allOperator\n ? `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — probably a test-send leftover; the client will NOT receive this report`\n : `resolved To includes operator address(es) ${operatorAddrs.join(\", \")} alongside the client's — confirm that's intentional (the ops inbox is already CC'd on every send)`,\n });\n }\n }\n if (scheduledForType && explicitTo && fallbackTo) {\n const a = [...explicitTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n const b = [...fallbackTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n if (a !== b) {\n findings.push({\n level: \"info\",\n check: \"to-override-shadows-contact\",\n message: `Report recipients (To) [${explicitTo.join(\", \")}] overrides point of contact [${fallbackTo.join(\", \")}] — the contact won't receive this unless also listed`,\n });\n }\n }\n\n // --- Hard requirements of the draft/send paths, surfaced early.\n if (scheduledForType && !site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (scheduledForType && site.headerImage && !site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n const scores = [site.pScore, site.rScore, site.bpScore, site.seoScore];\n if (scheduledForType && scores.some((s) => s === null)) {\n if (type === \"Announcement\") {\n // announce() skips score-less sites (skipped-no-scores) rather than throwing.\n findings.push({\n level: \"warn\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — `announce` will skip this site; run `audit lighthouse --write-airtable` first\",\n });\n } else {\n // report <site> / report --due HARD-THROW here (scoresFromWebsite in draft.ts).\n findings.push({\n level: \"fail\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — drafting will throw (scoresFromWebsite); run `audit lighthouse --write-airtable` first\",\n });\n }\n }\n if (type === \"Announcement\" && site.status !== \"maintained\") {\n findings.push({\n level: \"info\",\n check: \"status-not-maintenance\",\n message: `status is '${site.status ?? \"(blank)\"}' — \\`announce --all\\` only drafts for maintenance sites`,\n });\n }\n\n // --- Pending drafts. For an Announcement these always race (the queue tier rule\n // supersedes or blocks). For Maintenance/Testing, the CURRENT cycle's own draft\n // sitting ready on send day IS the payload — that's the steady state, not a\n // problem — so only genuinely stale/foreign drafts get the warn.\n const pending = reports.filter((r) => r.draftReady && r.sentAt === null);\n if (pending.length > 0) {\n const currentPeriod = reportPeriodKey(now);\n const isExpectedPayload = (r: ReportRow): boolean =>\n type !== \"Announcement\" && r.reportType === type && r.period === currentPeriod;\n const expected = pending.filter(isExpectedPayload);\n const stale = pending.filter((r) => !isExpectedPayload(r));\n if (expected.length > 0) {\n findings.push({\n level: \"info\",\n check: \"pending-drafts\",\n message: `current-cycle ${type} draft is queued${expected[0]!.approvedToSend ? \" and approved\" : \" (awaiting approval)\"} — this is the report \\`--send-ready\\` will deliver`,\n });\n }\n if (stale.length > 0) {\n const summary = stale\n .map(\n (r) =>\n `${r.reportType} ${r.period ?? \"(no period)\"}${r.approvedToSend ? \" [APPROVED]\" : \"\"}`,\n )\n .join(\", \");\n findings.push({\n level: \"warn\",\n check: \"pending-drafts\",\n message: `${stale.length} unsent draft(s) queued: ${summary} — resolve before sending so the client doesn't get the wrong report first`,\n });\n }\n }\n\n // --- Schedule hygiene (validates the RAW Airtable frequency cells).\n checkFrequency(site, \"maintenance\", reports, findings, now);\n checkFrequency(site, \"testing\", reports, findings, now);\n if (type === \"Announcement\") {\n const anySent = lastSentForType(reports, \"Maintenance\") ?? lastSentForType(reports, \"Testing\");\n if (!site.maintenanceDay && !site.testingDay && !anySent) {\n findings.push({\n level: \"info\",\n check: \"anchor-missing\",\n message:\n \"no maintenance/testing day anchors and nothing ever sent — set both anchors to the announcement send date (the Sonder playbook) so recurrence starts from the announcement\",\n });\n }\n }\n\n return { site: site.name, findings };\n}\n\n/**\n * Fleet-level heuristics that need the whole selection. Airtable column renames\n * don't error — the mapper reads null forever (its own header comment admits this).\n * A load-bearing column empty on EVERY selected site is far more likely a rename\n * than N coincidences; say so. Likewise two different sites resolving to one\n * recipient is worth one eyeball (it can be legitimate — same owner, two sites).\n */\nexport function preflightFleet(sites: WebsiteRow[]): PreflightFinding[] {\n if (sites.length < 3) return []; // too few rows to distinguish rename from data\n const findings: PreflightFinding[] = [];\n const allEmpty = (get: (s: WebsiteRow) => unknown, column: string): void => {\n if (sites.every((s) => get(s) === null || get(s) === \"\")) {\n findings.push({\n level: \"warn\",\n check: \"column-possibly-renamed\",\n message: `'${column}' is empty on all ${sites.length} selected sites — if that column was renamed in Airtable the code reads null silently; verify the column name`,\n });\n }\n };\n allEmpty((s) => s.pointOfContact, \"point of contact\");\n allEmpty((s) => s.maintenanceFreqRaw, \"maintenence freq\");\n allEmpty((s) => s.headerImage, \"Header image\");\n\n const byContact = new Map<string, string[]>();\n for (const s of sites) {\n const to = parseAddresses(s.reportRecipientsTo) ?? parseAddresses(s.pointOfContact) ?? [];\n for (const addr of to) {\n const key = addr.toLowerCase();\n byContact.set(key, [...(byContact.get(key) ?? []), s.name]);\n }\n }\n for (const [addr, names] of byContact) {\n if (names.length > 1 && !OPERATOR_DOMAINS.includes(domainOf(addr))) {\n findings.push({\n level: \"info\",\n check: \"duplicate-contact\",\n message: `${names.join(\" and \")} resolve to the same recipient (${addr}) — fine if one person owns both; otherwise a copy-paste error on one row`,\n });\n }\n }\n return findings;\n}\n\nexport type PreflightDeps = {\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** Slug of a single site (matched via siteSlug). Mutually exclusive with `all`. */\n site?: string;\n /** Fleet mode: for Announcement, maintenance-status sites (announce's own filter);\n * for Maintenance/Testing, everything `report --due` schedules — ELIGIBLE_STATUSES\n * plus null-status rows (due.ts treats null as active for legacy rows). */\n all?: boolean;\n type?: ReportType;\n now?: Date;\n};\n\nexport type PreflightResult = {\n results: PreflightSiteResult[];\n fleet: PreflightFinding[];\n};\n\n/**\n * Read-only rollout preflight over the live Airtable base. One Websites fetch, one\n * Reports fetch (the --due path does the same for the same rate-limit reason), then\n * {@link preflightSite} per selected site plus {@link preflightFleet} across the\n * selection. NEVER writes and NEVER sends.\n */\nexport async function preflight(deps?: PreflightDeps): Promise<PreflightResult> {\n const base = deps?.base ?? openBase(readAirtableConfig());\n const type: ReportType = deps?.type ?? \"Announcement\";\n const now = deps?.now ?? new Date();\n\n const websites = await listWebsites(base);\n const selected = deps?.site\n ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site!))\n : type === \"Announcement\"\n ? websites.filter((w) => w.status === \"maintained\")\n : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));\n\n const allReports = selected.length > 0 ? await listAllReports(base) : [];\n const results = selected.map((site) =>\n preflightSite(\n site,\n allReports.filter((r) => r.siteId === site.id),\n type,\n now,\n ),\n );\n return { results, fleet: deps?.site ? [] : preflightFleet(selected) };\n}\n\n/**\n * Health-gate blockers for one report: every GATING field whose evidence is not `pass`/`n/a`\n * (i.e. `fail`, `unknown`, or absent) becomes a fail-level finding, keyed `health-gate`. PURE.\n * Folded into {@link approveBlockers} so health failures ride the existing send-blocked reason +\n * 409 + dashboard chip (the second gate — no third gate is added).\n *\n * A logged send-anyway override ({@link isSendOverridden}) suppresses these findings — but never\n * the REAL send blockers (recipients/header/scores), which are computed outside this function and\n * still gate an overridden send.\n */\nexport function healthBlockers(report: ReportRow): PreflightFinding[] {\n if (isSendOverridden(report)) return [];\n const findings: PreflightFinding[] = [];\n for (const { field, status } of gatingHealth({\n reportType: report.reportType,\n autoEvidence: report.autoEvidence ?? {},\n })) {\n if (status === \"pass\" || status === \"n/a\") continue;\n // `||`, not `??`: an explicit empty-string note (no message recorded) must\n // also fall back to the placeholder, not render as a trailing blank.\n const note = report.autoEvidence?.[field]?.note || \"no signal yet\";\n findings.push({\n level: \"fail\",\n check: \"health-gate\",\n message:\n status === \"fail\"\n ? `${field}: failing — ${note}`\n : `${field}: not yet green (${status}) — ${note}`,\n });\n }\n return findings;\n}\n\n/**\n * The send-blocking subset for ONE report, at approve time: exactly the\n * conditions that make `sendOne` throw (no recipients, malformed To/CC, no\n * header image, no report-level Lighthouse scores) plus the wrong-inbox warn\n * (operator address resolved as a client's To) plus every {@link healthBlockers}\n * finding (a gating checklist item whose auto-evidence isn't `pass`/`n/a` fails\n * the health gate). PURE — the approve gate, the dashboard's pending-row chip,\n * and the digest collector all call this one function so \"approvable\" can't\n * drift from \"sendable\".\n *\n * Deliberately narrower than {@link preflightSite}: schedule hygiene and\n * pending-draft races are fleet/site concerns — blocking THIS report's approval\n * on them would be over-gating.\n */\nexport function approveBlockers(site: WebsiteRow, report: ReportRow): PreflightFinding[] {\n const findings: PreflightFinding[] = [];\n\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — fix Report recipients (To) / point of contact in Airtable`,\n });\n }\n }\n for (const addr of parseAddresses(site.reportRecipientsCc) ?? []) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC) in Airtable`,\n });\n }\n }\n if (!site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (!site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n // sendOne throws on a null report.lighthouse (one blank/non-numeric cell nulls\n // all four) — the REPORT's snapshot, not the Websites-row scores.\n if (report.lighthouse === null) {\n findings.push({\n level: \"fail\",\n check: \"report-scores-missing\",\n message:\n \"the Reports row has no Lighthouse scores (one blank/non-numeric cell nulls all four) — the send will throw\",\n });\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0 && operatorAddrs.length === to.length) {\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — the client will NOT receive this report`,\n });\n }\n }\n findings.push(...healthBlockers(report));\n return findings;\n}\n\n/** Convenience: just the fail-level blockers, formatted for gate messages. */\nexport function formatBlockers(findings: PreflightFinding[]): string[] {\n return findings.filter((f) => f.level === \"fail\").map((f) => `${f.check}: ${f.message}`);\n}\n"],"mappings":"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","names":[]}
|