@reddoorla/maintenance 0.95.1 → 0.96.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 +37 -2
- package/dist/airtable-SJV4ZSMS.js +9 -0
- package/dist/{announce-CLQYX5I4.js → announce-45HVP4J4.js} +7 -7
- package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
- package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
- package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
- package/dist/chunk-3EA43S3R.js.map +1 -0
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
- package/dist/chunk-4UTJ4G6P.js +52 -0
- package/dist/chunk-4UTJ4G6P.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-KJRVMMM3.js → chunk-62YWOFVN.js} +36 -5
- package/dist/chunk-62YWOFVN.js.map +1 -0
- package/dist/{chunk-VSGMCCGQ.js → chunk-6INOBNRO.js} +2 -2
- package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
- package/dist/chunk-72FJZ6LO.js.map +1 -0
- package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.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-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
- package/dist/chunk-CGQZFQAY.js +38 -0
- package/dist/chunk-CGQZFQAY.js.map +1 -0
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
- package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
- package/dist/chunk-DVJNYPFR.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
- package/dist/chunk-EASYCD6F.js.map +1 -0
- package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
- package/dist/{chunk-BI2VI6EM.js → chunk-EQQ4H6L3.js} +2 -2
- package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
- package/dist/chunk-GEUC5VGM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
- package/dist/chunk-IHHAYXKY.js +351 -0
- package/dist/chunk-IHHAYXKY.js.map +1 -0
- package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
- package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
- package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
- package/dist/chunk-MZZDMBDT.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
- package/dist/chunk-NVLREB5R.js.map +1 -0
- package/dist/{chunk-R5IMFAFR.js → chunk-OTEVKRAH.js} +3 -3
- package/dist/{chunk-2ZAZ7ETT.js → chunk-P4A6D6HO.js} +4 -4
- package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
- package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
- package/dist/chunk-SHGIDTYF.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
- package/dist/{chunk-C3IQQKXL.js → chunk-T76MA2FH.js} +110 -1
- package/dist/{chunk-C3IQQKXL.js.map → chunk-T76MA2FH.js.map} +1 -1
- package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
- package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
- 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-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
- package/dist/chunk-XED2DNAI.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
- package/dist/chunk-XG2A6GIG.js.map +1 -0
- package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
- package/dist/chunk-XY7YV2YQ.js.map +1 -0
- package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
- 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-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
- package/dist/cli/bin.js +49 -31
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +18 -18
- package/dist/client-L7NAUG6J.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-4DON7IFS.js} +6 -6
- package/dist/{db-XKZYDCKM.js → db-5K4CDEGJ.js} +45 -20
- package/dist/db-5K4CDEGJ.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-7UBGUBRK.js → digest-5AJGWS33.js} +45 -22
- package/dist/digest-5AJGWS33.js.map +1 -0
- package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-MC5NWEUA.js} +12 -10
- 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-OAAE5YE2.js → ensure-site-XESZHZEB.js} +39 -11
- package/dist/ensure-site-XESZHZEB.js.map +1 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
- package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-4NPLX2UG.js} +5 -5
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-TX5FPYP6.js → github-signals-DMU74SAX.js} +10 -10
- package/dist/{header-image-PSPT7CUM.js → header-image-BRTOLHJF.js} +6 -5
- package/dist/header-image-BRTOLHJF.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
- package/dist/{health-mirror-XJAKA3EY.js → health-mirror-ONRT4DQ5.js} +7 -7
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
- package/dist/index.d.ts +135 -4
- package/dist/index.js +68 -34
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
- package/dist/{launch-UD36AHUN.js → launch-XTDMC6LD.js} +209 -61
- package/dist/launch-XTDMC6LD.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
- package/dist/{match-harness-QRG6XZAR.js → match-harness-OW45CB5H.js} +14 -7
- package/dist/match-harness-OW45CB5H.js.map +1 -0
- package/dist/migrate-NE3DUMTM.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
- package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
- package/dist/{orchestrate-HYVQPRCE.js → orchestrate-TGDFGY52.js} +8 -8
- package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-RYIP54NY.js → preflight-POZZI3GN.js} +9 -9
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
- package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
- package/dist/{prismic-models-P6FWWVRS.js → prismic-models-URYGODNY.js} +9 -9
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-AJEE3Z4L.js} +16 -7
- package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
- package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
- package/dist/protection-audit-3J3UEOVT.js +413 -0
- package/dist/protection-audit-3J3UEOVT.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-65DC6ZP3.js} +4 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-LQS4ECTM.js → report-RC5LGPU3.js} +22 -20
- package/dist/report-RC5LGPU3.js.map +1 -0
- package/dist/{report-mirror-HDTOJE64.js → report-mirror-KKZZ6RMZ.js} +13 -9
- package/dist/{report-mirror-HDTOJE64.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
- 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-RKKIYNV4.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
- package/dist/{selftest-VXTHRPRN.js → selftest-AFLR7YV7.js} +10 -10
- 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-H2FYMN55.js} +23 -9
- package/dist/site-mirror-H2FYMN55.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
- package/dist/{submissions-OVZPVBYG.js → submissions-II5J35QM.js} +4 -4
- 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-QWGIRNN6.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
- package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
- package/dist/util/git.d.ts +48 -9
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
- package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/chunk-5UINJNAJ.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-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.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-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-XPJQXSC4.js.map +0 -1
- package/dist/chunk-YLYXW5PH.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/header-image-PSPT7CUM.js.map +0 -1
- package/dist/launch-UD36AHUN.js.map +0 -1
- package/dist/match-harness-QRG6XZAR.js.map +0 -1
- package/dist/migrate-Y7R4CESL.js +0 -7
- package/dist/prismic-ci-6GKKZGB5.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/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/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
- /package/dist/{announce-CLQYX5I4.js.map → announce-45HVP4J4.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
- /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
- /package/dist/{chunk-VSGMCCGQ.js.map → chunk-6INOBNRO.js.map} +0 -0
- /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
- /package/dist/{chunk-BI2VI6EM.js.map → chunk-EQQ4H6L3.js.map} +0 -0
- /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
- /package/dist/{chunk-R5IMFAFR.js.map → chunk-OTEVKRAH.js.map} +0 -0
- /package/dist/{chunk-2ZAZ7ETT.js.map → chunk-P4A6D6HO.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
- /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
- /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
- /package/dist/{client-G2WGWPCA.js.map → client-L7NAUG6J.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
- /package/dist/{digest-collectors-DJFN5ZDK.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
- /package/dist/{forms-notify-target-UC6PF3AE.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{github-signals-TX5FPYP6.js.map → github-signals-DMU74SAX.js.map} +0 -0
- /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{migrate-Y7R4CESL.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
- /package/dist/{orchestrate-HYVQPRCE.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
- /package/dist/{prospect-audits-R3LWF6U7.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{preflight-RYIP54NY.js.map → preflight-POZZI3GN.js.map} +0 -0
- /package/dist/{prismic-models-P6FWWVRS.js.map → prismic-models-URYGODNY.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
- /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
- /package/dist/{selftest-VXTHRPRN.js.map → selftest-AFLR7YV7.js.map} +0 -0
- /package/dist/{submissions-OVZPVBYG.js.map → submissions-II5J35QM.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/db.ts"],"sourcesContent":["export type DbCommandOptions = {\n /** Override the libSQL url (tests use \":memory:\"); otherwise read from env. */\n url?: string;\n /** verify-dump: path to the dump file to load into a scratch engine. */\n file?: string;\n /** usage: org slug override; defaults to TURSO_ORG, else discovered. */\n org?: string;\n /** import-airtable / sync: run despite the freeze — a deliberate\n * rollback-window converge from the frozen Airtable shadow. */\n force?: boolean;\n /** replay-deadletters: abandon this slug's queued leads (or one `dl_…` row id)\n * as resolved-by-decision instead of replaying them (#786). */\n abandon?: string;\n /** replay-deadletters --abandon: why. Required — an undocumented write-off of\n * a client's leads is the thing this is meant to stop being necessary. */\n reason?: string;\n /** replay-deadletters --abandon: who decided. Defaults to OPERATOR_EMAIL. */\n by?: string;\n cwd?: string;\n verbose?: boolean;\n};\n\n/** Injected seams — deliberately NOT part of DbCommandOptions, which is the set\n * of things a shell can type (a registration gate asserts exactly that). The\n * platform token lives here rather than on a flag because a secret passed on\n * argv is readable from `ps`. */\nexport type DbCommandDeps = {\n /** Tests pass \"\" to exercise the unconfigured path, so the implementation\n * must use `?? env` and never `|| env`. */\n platformToken?: string;\n fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;\n now?: Date;\n /** restore: auth token for the TARGET database; defaults to\n * TURSO_RESTORE_AUTH_TOKEN. Deliberately does NOT fall back to the ambient\n * TURSO_AUTH_TOKEN — that one belongs to production, and inheriting it would\n * undo the whole point of making --url explicit. */\n restoreAuthToken?: string;\n};\n\n/** #643 (the freeze): the scheduled import retired with the flip, but the\n * MANUAL import survives as the rollback-window converge tool — and run out of\n * habit it would overwrite authoritative Turso rows with the frozen Airtable\n * archive, including any post-flip write whose best-effort shadow was\n * swallowed. So the writing actions refuse under the freeze unless the\n * operator says `--force`. `parity` stays unguarded: it only compares, and\n * \"did the shadow drift?\" is exactly the rollback-window question.\n *\n * Pure and exported so the test injects BOTH switch states; `runDbCommand`\n * passes the shipped constant. Returns the refusal, or null to proceed. */\nexport function freezeGuardsDbWrite(\n action: string,\n force: boolean,\n authoritative: boolean,\n): { output: string; code: number } | null {\n if (!authoritative) return null;\n if (action !== \"import-airtable\" && action !== \"sync\") return null;\n if (force) return null;\n return {\n output:\n `db ${action} refused: TURSO_IS_AUTHORITATIVE is on (the freeze, 2026-08-31). ` +\n `An import now OVERWRITES authoritative Turso rows with the frozen Airtable ` +\n `archive. Pass --force only for a deliberate rollback-window converge.`,\n code: 1,\n };\n}\n\n/** `db <action>` — migrate | replay-deadletters | import-airtable | parity | sync | dump | verify-dump. The db layer is imported\n * dynamically so a non-db CLI invocation (and `--help`) never loads\n * @libsql/client. Config is resolved inside each branch so an unknown action\n * returns without needing any Turso env. */\nexport async function runDbCommand(\n action: string,\n opts: DbCommandOptions,\n deps: DbCommandDeps = {},\n): Promise<{ output: string; code: number }> {\n if (action === \"migrate\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { runMigrations } = await import(\"../../db/migrate.js\");\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const ran = await runMigrations(client);\n return {\n output: ran.length ? `Applied migrations: ${ran.join(\", \")}` : \"Already up to date.\",\n code: 0,\n };\n }\n\n // Re-run every lead that dead-lettered during a site-lookup outage (#539\n // Phase 0) through the normal ingest pipeline. Exit 1 while any row is STILL\n // failing — the store hasn't recovered and the operator should not read the\n // run as \"all leads landed\". Zero rows is a clean 0: nothing owed.\n if (action === \"replay-deadletters\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n // #786. `--abandon` is the escape hatch for the queue #785 deliberately\n // stopped draining: a slug that is genuinely dead but still deployed keeps\n // dead-lettering leads, holds this command at exit 1, and leaves a standing\n // CRITICAL cockpit item. Validated BEFORE opening any store, so a missing\n // reason refuses without needing Turso creds.\n if (opts.abandon !== undefined) {\n const reason = (opts.reason ?? \"\").trim();\n if (reason === \"\") {\n return {\n output:\n 'db replay-deadletters --abandon refused: pass --reason \"…\". Abandoning writes ' +\n \"off a client's captured leads, and the record of WHY has to outlive the decision.\",\n code: 1,\n };\n }\n const by = opts.by ?? process.env.OPERATOR_EMAIL ?? \"operator\";\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { abandonDeadLetters } = await import(\"../../db/deadletter.js\");\n // Row ids are minted `dl_…` (newDeadLetterId), and a site slug never is —\n // so the one argument can name either without a second flag.\n const target = opts.abandon.startsWith(\"dl_\") ? { id: opts.abandon } : { slug: opts.abandon };\n const ids = await abandonDeadLetters(db, { ...target, by, reason, now: new Date() });\n const lines = [\n ...ids.map((id) => `abandoned ${id}`),\n `DEADLETTER_ABANDONED target=${opts.abandon} rows=${ids.length} by=${by}`,\n ];\n return { output: lines.join(\"\\n\"), code: 0 };\n }\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { getWebsiteBySlug } = await import(\"../../reports/airtable/websites.js\");\n const { getSiteBySlug } = await import(\"../../db/fleet-state.js\");\n const { makeLazySiteLookup } = await import(\"../../forms/site-lookup.js\");\n // #645. Recovery now resolves sites through the SAME lookup the live ingest\n // path uses. It used to call the Airtable `getWebsiteBySlug` directly, so\n // post-#643 the two disagreed about what the fleet is: a site created since\n // the freeze was invisible to the replay, and a row only Airtable still held\n // would have attached a recovered lead to a site the system no longer\n // believes in. `openBase` is passed UNCALLED — under the freeze no Airtable\n // credential is read at all, where before a missing PAT refused the whole\n // replay (`readAirtableConfig()` throws) with real leads sitting in the queue.\n const lookupSite = makeLazySiteLookup({\n fromDb: (s) => getSiteBySlug(db, s),\n openAirtable: () => openBase(readAirtableConfig()),\n fromAirtable: (base, s) => getWebsiteBySlug(base, s),\n });\n const {\n createSubmission,\n stampNotified,\n stampFanout,\n findRecentDuplicateSubmissions,\n listRecentSubmissionsForEmail,\n markSubmissionsSpamRetro,\n } = await import(\"../../db/submissions.js\");\n const { makeNotify } = await import(\"../../forms/notify.js\");\n const { classifySpam } = await import(\"../../forms/spam-classifier.js\");\n const { forwardNewsletterToWebhook } = await import(\"../../forms/webhook.js\");\n const { addMailchimpMember, mailchimpTagsFor } = await import(\"../../forms/mailchimp.js\");\n const { defaultResendClient } = await import(\"../../reports/send/resend.js\");\n const { replayDeadLetters } = await import(\"../../forms/replay.js\");\n\n // Same degradation as the ingest handler: an unconfigured Resend key means\n // replayed leads land un-emailed (notify=failed) rather than blocking replay.\n let send = null;\n try {\n send = defaultResendClient().send;\n } catch (err) {\n console.error(`[db] Resend unconfigured; replaying without email: ${String(err)}`);\n }\n\n // Mirrors the production handler's deps minus `deadLetter` (replayDeadLetters\n // forbids and strips it — a throwing lookup must retry, not duplicate) and\n // minus `defer` (a CLI has no post-response phase; the inline tail is fine).\n const result = await replayDeadLetters(db, {\n getWebsiteBySlug: lookupSite,\n createSubmission: (input) => createSubmission(db, input),\n notify: makeNotify(send),\n stampNotified: (id, status, messageId) => stampNotified(db, id, status, messageId),\n now: () => new Date(),\n classifySpam: (n, outcome) =>\n classifySpam({\n name: n.name,\n email: n.email,\n ...(n.message !== undefined ? { message: n.message } : {}),\n formType: n.formType,\n extraFields: n.extraFields,\n turnstile: outcome,\n }),\n findRecentDuplicates: (message, since) =>\n findRecentDuplicateSubmissions(db, message, since.toISOString()),\n listRecentSubmissionsForEmail: (email, since) =>\n listRecentSubmissionsForEmail(db, email, since.toISOString()),\n retroBucket: (ids, reason) => markSubmissionsSpamRetro(db, ids, reason),\n forwardNewsletter: (url, submission, site) =>\n forwardNewsletterToWebhook(url, submission, site),\n addToMailchimp: (site, submission) =>\n addMailchimpMember({\n apiKey: site.mailchimpApiKey ?? \"\",\n audienceId: site.mailchimpAudienceId ?? \"\",\n email: submission.email,\n name: submission.name,\n tags: mailchimpTagsFor(submission.formType),\n }),\n stampFanout: (id, fanoutStatus) => stampFanout(db, id, fanoutStatus),\n });\n\n const lines = [\n ...result.replayed.map(\n (r) => `replayed ${r.id} → ${r.outcome}${r.submissionId ? ` (${r.submissionId})` : \"\"}`,\n ),\n ...result.stillFailing.map((r) => `still failing ${r.id}: ${r.error}`),\n `DEADLETTER_REPLAY replayed=${result.replayed.length} still_failing=${result.stillFailing.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: result.stillFailing.length > 0 ? 1 : 0 };\n }\n\n // Phase 1.3/1.4 of #539. Both read the same two Airtable tables raw (id +\n // fields, no mapRow coercion — the importer's mapping is the authority) and\n // share that mapping, so parity is definitionally checked against what the\n // importer writes.\n if (action === \"import-airtable\" || action === \"parity\" || action === \"sync\") {\n const { TURSO_IS_AUTHORITATIVE } = await import(\"../../db/freeze.js\");\n const refused = freezeGuardsDbWrite(action, opts.force === true, TURSO_IS_AUTHORITATIVE);\n if (refused) return refused;\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const listRaw = async (table: string) =>\n (await base(table).select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n }));\n const io = {\n listWebsiteRecords: () => listRaw(\"Websites\"),\n listReportRecords: () => listRaw(\"Reports\"),\n now: () => new Date(),\n };\n\n if (action === \"import-airtable\") {\n const { importFleetState, formatReapSummary } = await import(\"../../db/import-airtable.js\");\n const summary = await importFleetState(db, {\n ...io,\n // Attachment bodies ride expiring signed URLs; a failed fetch imports the\n // row with rendered_html null and is NAMED in the summary, never silent.\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n const lines = [\n `imported ${summary.sites} site(s) → sites/site_health/site_schedule`,\n `imported ${summary.reports} report(s)`,\n ];\n if (summary.renderedHtmlMisses.length > 0) {\n lines.push(\n `⚠ ${summary.renderedHtmlMisses.length} report(s) imported WITHOUT Rendered HTML ` +\n `(fetch failed / URL expired): ${summary.renderedHtmlMisses.join(\", \")}`,\n );\n }\n // The import deletes rows Airtable no longer has, so this one-shot path\n // reports the reap exactly as `db sync` does — same formatter, no second\n // copy to fall out of step.\n lines.push(...formatReapSummary(summary.reaped));\n return { output: lines.join(\"\\n\"), code: 0 };\n }\n\n // Phase 2 backbone (#539): one hourly pass = import (attachment fetches\n // only where the stored row lacks a body) + parity + one retry to absorb\n // the import-read/parity-read race. Exit 1 on persistent mismatch.\n if (action === \"sync\") {\n const { syncFleetState, formatSyncResult } = await import(\"../../db/sync.js\");\n const result = await syncFleetState(db, {\n ...io,\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n return {\n output: formatSyncResult(result),\n code: result.parity.mismatches.length > 0 ? 1 : 0,\n };\n }\n\n const { checkFleetParity, formatParityResult } = await import(\"../../db/parity.js\");\n const result = await checkFleetParity(db, io);\n return { output: formatParityResult(result), code: result.mismatches.length > 0 ? 1 : 0 };\n }\n\n // One-shot completion of design D5 (#539 Phase 2): copy every site's CURRENT\n // Airtable \"Header image\" attachment into sites.header_image*. Idempotent —\n // an already-populated BLOB is never overwritten (a re-run must not clobber\n // a freshly generated image with a stale Airtable copy). Exit 1 when any\n // fetch failed, so a partial backfill is never read as complete.\n if (action === \"backfill-header-images\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { backfillHeaderImages, formatBackfillResult } =\n await import(\"../../db/header-images.js\");\n const result = await backfillHeaderImages(db, {\n listWebsiteRecords: async () =>\n (await base(\"Websites\").select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n })),\n fetchBytes: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;\n } catch {\n return null;\n }\n },\n });\n return { output: formatBackfillResult(result), code: result.failed.length > 0 ? 1 : 0 };\n }\n\n // #609: one-shot copy of the single Airtable \"Digest State\" row into Turso,\n // so the first digest run after the read repoint sees yesterday's snapshot\n // instead of an empty one. An empty read is not a crash — it badges EVERY\n // item NEW, which lands in the operator's inbox reading as \"the whole fleet\n // degraded overnight\". REFUSES to overwrite a snapshot Turso already holds:\n // a re-run must never replace a fresher snapshot with a stale Airtable copy.\n if (action === \"backfill-digest-state\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { readDigestState: readTurso, writeDigestState: writeTurso } =\n await import(\"../../db/digest-state.js\");\n const existing = await readTurso(db);\n if (Object.keys(existing).length > 0) {\n return {\n output: `DIGEST_BACKFILL skipped=1 reason=turso-already-populated keys=${Object.keys(existing).length}`,\n code: 0,\n };\n }\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { readDigestState: readAirtable, DIGEST_STATE_TABLE } =\n await import(\"../../alerts/digest-state.js\");\n // `source` separates \"Airtable has no row\" from \"Airtable has a row holding\n // an empty snapshot\" — the reader collapses BOTH to {}, so copied=0 alone\n // cannot tell a quiet fleet from a failed read. Learned by running this: the\n // first real run printed copied=0 and only a hand probe showed the row was\n // there and genuinely empty.\n const rows = await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).all();\n const snap = await readAirtable(base);\n const keys = Object.keys(snap).length;\n await writeTurso(db, snap);\n // Read it BACK. A returning write is not evidence the row landed — the same\n // rule forms-notify-target learned on 2026-08-03. `stored` counts the ROW,\n // not its keys, so an empty-but-present snapshot verifies as written.\n const stored = (await db.selectFrom(\"digest_state\").selectAll().execute()).length;\n const after = Object.keys(await readTurso(db)).length;\n return {\n output:\n `DIGEST_BACKFILL source=${rows.length > 0 ? \"row\" : \"absent\"} ` +\n `copied=${keys} verified=${after} rows=${stored}`,\n code: after === keys && stored === 1 ? 0 : 1,\n };\n }\n\n // Phase 1.5 of #539: platform-auth-free SQL dump to stdout-adjacent output.\n // The nightly backup workflow redirects this to a file, encrypts, uploads;\n // the rehearsed restore loads it into stock sqlite3 and compares row counts.\n if (action === \"dump\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const { dumpDatabase } = await import(\"../../db/dump.js\");\n const sql = await dumpDatabase(\n {\n execute: async (q) => {\n const r = await client.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n },\n new Date().toISOString(),\n );\n return { output: sql, code: 0 };\n }\n\n // Load a dump back into a REAL libSQL target (#612 review). The nightly\n // rehearsal loads into `:memory:`, which proves the SQL parses — it does not\n // prove you can get the data back into Turso, and that is the operation an\n // actual recovery needs. Replaying ~17 MB of SQL with megabytes of inline hex\n // over HTTP is materially different from an in-process load, and it had never\n // been done. Refuses to touch a database that already holds rows: a restore\n // is for an EMPTY target, and pointing this at production by mistake should\n // cost nothing.\n if (action === \"restore\") {\n const file = opts.file;\n if (!file) return { output: \"restore: pass the dump path via --file\", code: 1 };\n if (!opts.url) {\n return {\n output:\n \"restore: pass the TARGET database via --url (never defaults, to keep production out of reach)\",\n code: 1,\n };\n }\n // Classify the target BEFORE reading the dump, so a missing token names\n // itself instead of arriving as an opaque 401 (or, worse, as an ENOENT that\n // sends you hunting for the dump file). This command built its client from\n // a url alone until 2026-08-26, which worked against every target the tests\n // and rehearsals used — `:memory:` and a local `turso dev` — and failed\n // against every target an actual recovery has.\n const { parseDumpManifest, requiresAuthToken } = await import(\"../../db/dump.js\");\n const authToken = deps.restoreAuthToken ?? process.env.TURSO_RESTORE_AUTH_TOKEN ?? \"\";\n if (requiresAuthToken(opts.url) && !authToken) {\n return { output: \"RESTORE refused=auth-token-absent\", code: 1 };\n }\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) return { output: \"RESTORE refused=manifest-absent\", code: 1 };\n const { createClient } = await import(\"@libsql/client\");\n const target = createClient(authToken ? { url: opts.url, authToken } : { url: opts.url });\n const existing = await target.execute(\n \"SELECT COUNT(*) AS n FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n );\n if (Number(existing.rows[0]?.n ?? 0) > 0) {\n return { output: \"RESTORE refused=target-not-empty\", code: 1 };\n }\n await target.executeMultiple(sql);\n const { tableCounts, headerImageBytes } = await import(\"../../db/dump.js\");\n const exec = {\n execute: async (q: string) => {\n const r = await target.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const counts = await tableCounts(exec);\n const bytes = await headerImageBytes(exec);\n // Same origin-anchored comparison as verify-dump: a restore that \"succeeded\"\n // with fewer rows than the origin held is not a restore.\n const bad: string[] = [];\n for (const [t, want] of Object.entries(manifest.tables)) {\n if ((counts[t] ?? 0) !== want) bad.push(`${t}: origin=${want} restored=${counts[t] ?? 0}`);\n }\n if (bytes !== manifest.blobBytes)\n bad.push(`header_image bytes: origin=${manifest.blobBytes} restored=${bytes}`);\n const rows = Object.values(counts).reduce((a, b) => a + b, 0);\n return {\n output: [\n ...bad.map((b) => `✗ ${b}`),\n `RESTORE loaded=true tables=${Object.keys(counts).length} rows=${rows} blob_bytes=${bytes} mismatches=${bad.length}`,\n ].join(\"\\n\"),\n code: bad.length > 0 ? 1 : 0,\n };\n }\n\n // How much of the plan's quota the fleet has burned this billing cycle\n // (#539 HIGH-10). The starter plan carries `overages: false`, so crossing a\n // quota BLOCKS reads and writes rather than billing for them — and once the\n // Airtable cutover lands, Turso is the only store there is. This is the one\n // alarm that fires before a wall rather than after it.\n //\n // Needs a PLATFORM token, which is a different credential from the\n // database-level TURSO_AUTH_TOKEN the rest of the fleet runs on: the database\n // token cannot read quota state at all.\n if (action === \"usage\") {\n const token = deps.platformToken ?? process.env.TURSO_FLEET_USAGE ?? \"\";\n // An unconfigured alarm must not read as a quiet, healthy one. The tell for\n // \"never ran\" has to be a failure, not a missing line (#585).\n if (!token) {\n return {\n output:\n \"FLEET_DB_USAGE verdict=no-token — set TURSO_FLEET_USAGE (turso auth api-tokens mint …). \" +\n \"This is the PLATFORM token, not the database TURSO_AUTH_TOKEN.\",\n code: 1,\n };\n }\n const { collectUsage, assessUsage } = await import(\"../../db/usage.js\");\n const input = await collectUsage({\n token,\n org: opts.org ?? process.env.TURSO_ORG,\n fetchImpl: deps.fetchImpl,\n now: deps.now ?? new Date(),\n });\n const r = assessUsage(input);\n const window = `${input.cycleStart.toISOString().slice(0, 10)} → ${input.cycleEnd\n .toISOString()\n .slice(0, 10)}`;\n return {\n output: [`Turso plan=${input.plan} billing cycle ${window}`, ...r.lines, \"\", r.marker].join(\n \"\\n\",\n ),\n code: r.code,\n };\n }\n\n // The restore rehearsal (Phase 1.5's hard gate), runnable every night: load\n // the dump into a FRESH in-memory engine and compare what came back against\n // the ORIGIN MANIFEST the dump carries.\n //\n // It used to compare against INSERT counts parsed out of the dump text — i.e.\n // the dump against itself. Both sides derived from one artifact, so a dump\n // that collected 5 of 44 sites shrank both numbers together and verified\n // clean. The manifest is read from the live database before any row is\n // serialised, which is the only measurement that can notice a short dump.\n //\n // A dump that cannot restore is not a backup — and per the repo's instrument\n // rule the check emits its machine line on every run, clean included.\n if (action === \"verify-dump\") {\n const file = opts.file;\n if (!file) return { output: \"verify-dump: pass the dump path via --file\", code: 1 };\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const { createClient } = await import(\"@libsql/client\");\n const scratch = createClient({ url: \":memory:\" });\n try {\n await scratch.executeMultiple(sql);\n } catch (err) {\n return { output: `DUMP_VERIFY loaded=false error=${String(err)}`, code: 1 };\n }\n const { tableCounts, headerImageBytes, parseDumpManifest } = await import(\"../../db/dump.js\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) {\n // Refuse rather than fall back to self-comparison. Falling back would\n // re-enable exactly the blind spot the manifest exists to close, and it\n // would do so silently on the one artifact nobody was watching.\n return {\n output: \"DUMP_VERIFY loaded=true manifest=absent — dump predates the origin manifest\",\n code: 1,\n };\n }\n const exec = {\n execute: async (q: string) => {\n const r = await scratch.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const restored = await tableCounts(exec);\n const restoredBlobBytes = await headerImageBytes(exec);\n const mismatches: string[] = [];\n for (const [table, want] of Object.entries(manifest.tables)) {\n if ((restored[table] ?? 0) !== want) {\n mismatches.push(`${table}: origin=${want} restored=${restored[table] ?? 0}`);\n }\n }\n // A table the origin held and the dump never mentioned would otherwise be\n // invisible: absent from `restored` AND absent from the loop above.\n for (const table of Object.keys(restored)) {\n if (!(table in manifest.tables)) mismatches.push(`${table}: not in origin manifest`);\n }\n // Coverage: every table the APP owns must be present. `tables=N` used to be\n // printed and never asserted, so a table a migration failed to create — or\n // that the dump lost — rode green forever. `digest_state` and\n // `prospect_audits` were both absent from every artifact the night this was\n // found, purely because they postdated the last run.\n const { DATABASE_TABLES } = await import(\"../../db/schema.js\");\n for (const table of DATABASE_TABLES) {\n if (!(table in restored)) mismatches.push(`${table}: MISSING from the backup entirely`);\n }\n if (restoredBlobBytes !== manifest.blobBytes) {\n mismatches.push(\n `header_image bytes: origin=${manifest.blobBytes} restored=${restoredBlobBytes}`,\n );\n }\n const total = Object.values(restored).reduce((a, b) => a + b, 0);\n const lines = [\n ...mismatches.map((m) => `✗ ${m}`),\n `DUMP_VERIFY loaded=true tables=${Object.keys(restored).length} rows=${total} blob_bytes=${restoredBlobBytes} mismatches=${mismatches.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: mismatches.length > 0 ? 1 : 0 };\n }\n\n return {\n output: `unknown db action '${action}'. Use: migrate, replay-deadletters, import-airtable, parity, sync, backfill-header-images, backfill-digest-state, dump, verify-dump, restore.`,\n code: 1,\n };\n}\n"],"mappings":"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","names":["db","lines","result"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abandonDeadLetters,
|
|
3
|
+
countUnreplayedDeadLettersBySlug,
|
|
4
|
+
createDeadLetter,
|
|
5
|
+
listUnreplayedDeadLetters,
|
|
6
|
+
markDeadLetterReplayed,
|
|
7
|
+
newDeadLetterId
|
|
8
|
+
} from "./chunk-A5QKNOXW.js";
|
|
9
|
+
export {
|
|
10
|
+
abandonDeadLetters,
|
|
11
|
+
countUnreplayedDeadLettersBySlug,
|
|
12
|
+
createDeadLetter,
|
|
13
|
+
listUnreplayedDeadLetters,
|
|
14
|
+
markDeadLetterReplayed,
|
|
15
|
+
newDeadLetterId
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=deadletter-G4M3HSVM.js.map
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
NOTIFY_BOUNCE_WINDOW_DAYS,
|
|
6
6
|
collectAnalyticsFailures,
|
|
7
7
|
collectCiAlerts,
|
|
8
|
+
collectDeadLetterAlerts,
|
|
8
9
|
collectDeliveryFailures,
|
|
9
10
|
collectLighthouseAlerts,
|
|
10
11
|
collectNotifyBounceAlerts,
|
|
@@ -13,12 +14,12 @@ import {
|
|
|
13
14
|
collectRenovateAlerts,
|
|
14
15
|
collectTurnstileGuardrailAlerts,
|
|
15
16
|
collectVulnAlerts
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-62YWOFVN.js";
|
|
18
|
+
import "./chunk-OTEVKRAH.js";
|
|
18
19
|
import "./chunk-PZR4EBPY.js";
|
|
19
20
|
import {
|
|
20
21
|
isIdempotencyConflict
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-P4A6D6HO.js";
|
|
22
23
|
import {
|
|
23
24
|
isPendingApproval,
|
|
24
25
|
listAllReports
|
|
@@ -33,23 +34,23 @@ import {
|
|
|
33
34
|
openBase,
|
|
34
35
|
readAirtableConfig
|
|
35
36
|
} from "./chunk-HN6I2XWI.js";
|
|
36
|
-
import "./chunk-
|
|
37
|
+
import "./chunk-Y5BVR4HW.js";
|
|
37
38
|
import "./chunk-GECESMVS.js";
|
|
38
39
|
import "./chunk-KJGKZLCT.js";
|
|
39
40
|
import {
|
|
40
41
|
escapeHtml
|
|
41
42
|
} from "./chunk-BGTYPVLT.js";
|
|
42
|
-
import "./chunk-
|
|
43
|
+
import "./chunk-MZZDMBDT.js";
|
|
43
44
|
import {
|
|
44
45
|
listWebsites,
|
|
45
46
|
siteSlug
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-KN52PM63.js";
|
|
47
48
|
import {
|
|
48
49
|
defaultResendClient
|
|
49
50
|
} from "./chunk-WEKL2HYJ.js";
|
|
50
|
-
import "./chunk-
|
|
51
|
-
import "./chunk-
|
|
52
|
-
import "./chunk-
|
|
51
|
+
import "./chunk-EQQ4H6L3.js";
|
|
52
|
+
import "./chunk-6INOBNRO.js";
|
|
53
|
+
import "./chunk-T76MA2FH.js";
|
|
53
54
|
import "./chunk-RARREDQE.js";
|
|
54
55
|
import "./chunk-6VAL7XJT.js";
|
|
55
56
|
|
|
@@ -149,8 +150,8 @@ async function listPendingApproval(base) {
|
|
|
149
150
|
async function fetchNotifyBounceCounts(now) {
|
|
150
151
|
try {
|
|
151
152
|
const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] = await Promise.all([
|
|
152
|
-
import("./client-
|
|
153
|
-
import("./submissions-
|
|
153
|
+
import("./client-L7NAUG6J.js"),
|
|
154
|
+
import("./submissions-JDZP35JM.js"),
|
|
154
155
|
import("./screenouts-7P5GOZMV.js")
|
|
155
156
|
]);
|
|
156
157
|
const db = await openDb(readDbConfig());
|
|
@@ -160,10 +161,23 @@ async function fetchNotifyBounceCounts(now) {
|
|
|
160
161
|
return /* @__PURE__ */ new Map();
|
|
161
162
|
}
|
|
162
163
|
}
|
|
164
|
+
async function fetchDeadLetterCounts() {
|
|
165
|
+
try {
|
|
166
|
+
const [{ openDb, readDbConfig }, { countUnreplayedDeadLettersBySlug }] = await Promise.all([
|
|
167
|
+
import("./client-L7NAUG6J.js"),
|
|
168
|
+
import("./deadletter-G4M3HSVM.js")
|
|
169
|
+
]);
|
|
170
|
+
const db = await openDb(readDbConfig());
|
|
171
|
+
return await countUnreplayedDeadLettersBySlug(db);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.warn(`\u26A0 dead-letter counts unavailable (libSQL): ${e.message}`);
|
|
174
|
+
return /* @__PURE__ */ new Map();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
163
177
|
async function readDigestStateFromDb() {
|
|
164
178
|
const [{ openDb, readDbConfig }, { readDigestState }] = await Promise.all([
|
|
165
|
-
import("./client-
|
|
166
|
-
import("./digest-state-
|
|
179
|
+
import("./client-L7NAUG6J.js"),
|
|
180
|
+
import("./digest-state-ZDPKR7MW.js")
|
|
167
181
|
]);
|
|
168
182
|
return readDigestState(await openDb(readDbConfig()));
|
|
169
183
|
}
|
|
@@ -176,19 +190,19 @@ async function writeRollupOnce(options, now) {
|
|
|
176
190
|
}
|
|
177
191
|
async function writeDigestStateToDb(next) {
|
|
178
192
|
const [{ openDb, readDbConfig }, { writeDigestState: writeDigestState2 }] = await Promise.all([
|
|
179
|
-
import("./client-
|
|
180
|
-
import("./digest-state-
|
|
193
|
+
import("./client-L7NAUG6J.js"),
|
|
194
|
+
import("./digest-state-ZDPKR7MW.js")
|
|
181
195
|
]);
|
|
182
196
|
await writeDigestState2(await openDb(readDbConfig()), next);
|
|
183
197
|
}
|
|
184
198
|
async function writeCockpitRollupToDb(now) {
|
|
185
199
|
if (!process.env.TURSO_DATABASE_URL) return "absent";
|
|
186
200
|
const [{ openDb, readDbConfig }, { writeCockpitRollup }, screenouts, submissions, collectors] = await Promise.all([
|
|
187
|
-
import("./client-
|
|
188
|
-
import("./digest-state-
|
|
201
|
+
import("./client-L7NAUG6J.js"),
|
|
202
|
+
import("./digest-state-ZDPKR7MW.js"),
|
|
189
203
|
import("./screenouts-7P5GOZMV.js"),
|
|
190
|
-
import("./submissions-
|
|
191
|
-
import("./digest-collectors-
|
|
204
|
+
import("./submissions-JDZP35JM.js"),
|
|
205
|
+
import("./digest-collectors-MC5NWEUA.js")
|
|
192
206
|
]);
|
|
193
207
|
const db = await openDb(readDbConfig());
|
|
194
208
|
const SCREEN_OUT_WINDOW_DAYS = 30;
|
|
@@ -242,8 +256,8 @@ var SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
|
242
256
|
async function fetchSubmissionsDigestCounts(now) {
|
|
243
257
|
try {
|
|
244
258
|
const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([
|
|
245
|
-
import("./client-
|
|
246
|
-
import("./submissions-
|
|
259
|
+
import("./client-L7NAUG6J.js"),
|
|
260
|
+
import("./submissions-JDZP35JM.js")
|
|
247
261
|
]);
|
|
248
262
|
const db = await openDb(readDbConfig());
|
|
249
263
|
return await countSubmissionsSinceBySite(
|
|
@@ -288,6 +302,7 @@ async function collectAttention(deps) {
|
|
|
288
302
|
const websites = deps.websites ?? await listWebsites(deps.base);
|
|
289
303
|
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
290
304
|
const notifyBounces = deps.notifyBounces ?? await fetchNotifyBounceCounts(now);
|
|
305
|
+
const deadLetters = deps.deadLetters ?? await fetchDeadLetterCounts();
|
|
291
306
|
const sitesById = new Map(websites.map((w) => [w.id, w]));
|
|
292
307
|
return [
|
|
293
308
|
...runCollector("vuln", () => collectVulnAlerts(websites, deps.baseUrl)),
|
|
@@ -305,6 +320,14 @@ async function collectAttention(deps) {
|
|
|
305
320
|
"notify-bounce",
|
|
306
321
|
() => collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl)
|
|
307
322
|
),
|
|
323
|
+
// #645. The dead-letter alarm. The digest is the SURFACE that matters for
|
|
324
|
+
// this kind: the cockpit groups items onto site cards, and the worst flavour
|
|
325
|
+
// — a slug that resolves to no fleet site — has no card by construction. The
|
|
326
|
+
// digest renders every item flat by title, so it carries both flavours.
|
|
327
|
+
...runCollector(
|
|
328
|
+
"deadletter",
|
|
329
|
+
() => collectDeadLetterAlerts(websites, deadLetters, deps.baseUrl)
|
|
330
|
+
),
|
|
308
331
|
// Same collector the cockpit runs, over the same persisted `Prismic Models`
|
|
309
332
|
// columns and the same keys — so the snapshot this digest writes is the one
|
|
310
333
|
// the cockpit diffs NEW/WORSE against, and the two surfaces agree.
|
|
@@ -424,4 +447,4 @@ export {
|
|
|
424
447
|
renderDigestHtml,
|
|
425
448
|
runDigest
|
|
426
449
|
};
|
|
427
|
-
//# sourceMappingURL=digest-
|
|
450
|
+
//# sourceMappingURL=digest-5AJGWS33.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport type { NotifyBounceCounts } from \"../db/submissions.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n collectTurnstileGuardrailAlerts,\n collectNotifyBounceAlerts,\n collectDeadLetterAlerts,\n collectPrismicDriftAlerts,\n NOTIFY_BOUNCE_WINDOW_DAYS,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention } from \"../alerts/digest-state.js\";\nimport {\n writeDigestState as writeAirtableDigestState,\n type DigestSnapshot,\n} from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport { operatorEmail } from \"../util/operator.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n// Type-only: erased at compile time, so the kysely/libsql devDependency never loads\n// in a consuming fleet site (the runtime read is the dynamic import in\n// fetchSubmissionsDigestCounts, same rule as fetchNotifyBounceCounts).\nimport type { SiteSubmissionCounts } from \"../db/submissions.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review & approve</a>`\n : `review & approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\n/** The \"Submissions (24h)\" telemetry block: fleet totals + a per-site glance line.\n * Context, not an alarm — absent/null section or all-zero totals render nothing\n * (no-noise), and the section never triggers a send on its own (see runDigest). */\nfunction submissionsSection(s: SubmissionsDigestSection | null | undefined): string {\n if (!s) return \"\";\n if (s.leads === 0 && s.signups === 0 && s.spamAuto === 0) return \"\";\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Submissions (24h)</h2>`;\n // All three terms always render together — omitting a zero term next to a nonzero\n // one reads as \"not measured\" rather than \"none\"; simplest honest form.\n const totals = [\n `${s.leads} new lead${s.leads === 1 ? \"\" : \"s\"}`,\n `${s.signups} newsletter/RSVP signup${s.signups === 1 ? \"\" : \"s\"}`,\n `${s.spamAuto} auto-filtered spam`,\n ].join(\" · \");\n const totalLine = `<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">${totals}</p>`;\n if (s.bySite.length === 0) return `${heading}${totalLine}`;\n const rows = s.bySite\n .map((site) => {\n const parts: string[] = [];\n if (site.leads > 0) parts.push(`${site.leads} lead${site.leads === 1 ? \"\" : \"s\"}`);\n if (site.signups > 0) parts.push(`${site.signups} signup${site.signups === 1 ? \"\" : \"s\"}`);\n if (site.spamAuto > 0) parts.push(`${site.spamAuto} auto-filtered`);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(site.siteName)}</strong> — ${parts.join(\" · \")}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}${totalLine}<table role=\"presentation\" style=\"border-collapse:collapse;margin:8px 0 0\">${rows}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n /** Pre-fetched per-site bounced-lead-notification counts (tests, or a caller with\n * an open db). When omitted, collectAttention reads them from libSQL itself —\n * defensively, so a missing TURSO env or a Turso blip drops just this signal. */\n notifyBounces?: ReadonlyMap<string, NotifyBounceCounts>;\n /** Pre-fetched unreplayed dead-letter counts per SLUG (#645). Same contract as\n * `notifyBounces`: omitted → read from libSQL, defensively. */\n deadLetters?: ReadonlyMap<string, number>;\n};\n\n/** Per-site bounced-notification counts for the notify-bounce collector, read from\n * libSQL over the collector's own window. The db modules are imported DYNAMICALLY:\n * src/db/submissions.ts pulls kysely at top level, and kysely/libsql live in\n * devDependencies consuming fleet sites don't install — a static import here would\n * crash their CLI at require time (same rule as openDb's own lazy loads). Any\n * failure (no TURSO env, Turso down) logs and yields an empty map: the digest\n * must never blank over a missing optional signal. */\nasync function fetchNotifyBounceCounts(\n now: Date,\n): Promise<ReadonlyMap<string, NotifyBounceCounts>> {\n try {\n const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n import(\"../db/screenouts.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countNotifyBouncedBySite(db, screenOutsSince(now, NOTIFY_BOUNCE_WINDOW_DAYS));\n } catch (e) {\n console.warn(`⚠ notify-bounce counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** Unreplayed dead-letter counts per slug for the #645 collector. Dynamically\n * imported and defensive for exactly the same reasons as\n * `fetchNotifyBounceCounts` above: kysely/libsql are devDependencies consuming\n * fleet sites do not install, and the digest must never blank over a missing\n * optional signal. */\nasync function fetchDeadLetterCounts(): Promise<ReadonlyMap<string, number>> {\n try {\n const [{ openDb, readDbConfig }, { countUnreplayedDeadLettersBySlug }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/deadletter.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countUnreplayedDeadLettersBySlug(db);\n } catch (e) {\n console.warn(`⚠ dead-letter counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** The prior snapshot, from Turso (#609). Dynamically imported for the same\n * reason as fetchNotifyBounceCounts: kysely/libsql are devDependencies that\n * consuming fleet sites do not install, so a static import would crash their\n * CLI at require time.\n *\n * NOT defensive, deliberately — the Airtable read it replaces was not either.\n * Swallowing a failure here would silently badge every item NEW, which reads\n * as \"the whole fleet degraded overnight\" in the operator's inbox. A thrown\n * error is the honest outcome. */\nasync function readDigestStateFromDb(): Promise<DigestSnapshot> {\n const [{ openDb, readDbConfig }, { readDigestState }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n ]);\n return readDigestState(await openDb(readDbConfig()));\n}\n\n/** Bridge the injectable seam (returns void) onto the three-state contract the\n * log line reports. An injected writer that resolves is a real write. */\nasync function writeRollupOnce(\n options: DigestRunOptions,\n now: Date,\n): Promise<\"written\" | \"absent\"> {\n if (options.cockpitRollup) {\n await options.cockpitRollup.write(now);\n return \"written\";\n }\n return writeCockpitRollupToDb(now);\n}\n\n/** The write half of {@link readDigestStateFromDb}, same lazy-import rule. */\nasync function writeDigestStateToDb(next: DigestSnapshot): Promise<void> {\n const [{ openDb, readDbConfig }, { writeDigestState }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n ]);\n await writeDigestState(await openDb(readDbConfig()), next);\n}\n\n/**\n * Compute the cockpit's \"since a window\" roll-ups once, here, instead of on\n * every page load (MED-16 of the 2026-08-26 review).\n *\n * Both aggregates read the whole `submissions` table — the one unbounded-growth\n * table in the schema — and the cockpit root is the operator's most-loaded page.\n * Turso meters row scans, so recomputing them per request is a cost that grows\n * forever against numbers that move once a day.\n *\n * The trade is explicit: the figures become up to 24h old. That is why the\n * payload carries `computedAt` and the windows it used, and why an absent\n * roll-up reads as null rather than zero — a stale or unmeasured number\n * presented as live is worse than no number.\n *\n * Best-effort, like the snapshot write beside it: the digest has already been\n * sent by the time this runs, and a failed roll-up costs one strip on one page\n * until tomorrow, never the digest.\n */\nasync function writeCockpitRollupToDb(now: Date): Promise<\"written\" | \"absent\"> {\n // No Turso configured is a THIRD state, not a failure: it is what every unit\n // test looks like, and reporting it as `rollup=0` would train the eye to\n // ignore the number that is supposed to catch a dead writer. Same three-state\n // vocabulary the site mirrors already use (`mirrored=1|0|absent`).\n if (!process.env.TURSO_DATABASE_URL) return \"absent\";\n const [{ openDb, readDbConfig }, { writeCockpitRollup }, screenouts, submissions, collectors] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n import(\"../db/screenouts.js\"),\n import(\"../db/submissions.js\"),\n import(\"../alerts/digest-collectors.js\"),\n ]);\n const db = await openDb(readDbConfig());\n const SCREEN_OUT_WINDOW_DAYS = 30;\n const screenOutSince = screenouts.screenOutsSince(now, SCREEN_OUT_WINDOW_DAYS);\n const bounceSince = screenouts.screenOutsSince(now, collectors.NOTIFY_BOUNCE_WINDOW_DAYS);\n\n const totals = { honeypot: 0, tooFast: 0, markedSpam: 0 };\n for (const t of (await screenouts.listScreenOutsSince(db, screenOutSince)).values()) {\n totals.honeypot += t.honeypot;\n totals.tooFast += t.tooFast;\n totals.markedSpam += t.markedSpam;\n }\n const bounces = await submissions.countNotifyBouncedBySite(db, bounceSince);\n\n await writeCockpitRollup(db, {\n spamTotals: totals,\n notifyBounces: Object.fromEntries(bounces),\n windowDays: {\n screenOuts: SCREEN_OUT_WINDOW_DAYS,\n bounces: collectors.NOTIFY_BOUNCE_WINDOW_DAYS,\n },\n computedAt: now.toISOString(),\n });\n return \"written\";\n}\n\n/** Persist the next snapshot to BOTH stores (#609 dual-write).\n *\n * Turso is the read side, so a failure there is the one that actually costs\n * correct NEW badges tomorrow — but Airtable keeps being written so the move\n * stays reversible while Phase 5 is in flight. Both are best-effort: the\n * digest has already been sent (or deliberately skipped) by the time this\n * runs, and a snapshot write failure re-news at worst.\n *\n * The DIGEST_STATE_WRITE line exists because of #585: a dual-write that\n * silently stopped running looked identical to a healthy one for weeks. One\n * line, always emitted, naming both halves. */\nasync function persistDigestState(\n base: AirtableBase,\n next: DigestSnapshot,\n writeState?: (snap: DigestSnapshot) => Promise<void>,\n writeRollup?: () => Promise<\"written\" | \"absent\">,\n): Promise<void> {\n let turso = 0;\n let airtable = 0;\n let rollup: \"1\" | \"0\" | \"absent\" = \"0\";\n try {\n await (writeState ?? writeDigestStateToDb)(next);\n turso = 1;\n } catch (e) {\n console.warn(`⚠ digest state write failed (turso): ${(e as Error).message}`);\n }\n try {\n await writeAirtableDigestState(base, next);\n airtable = 1;\n } catch (e) {\n console.warn(`⚠ digest state write failed (airtable): ${(e as Error).message}`);\n }\n // The cockpit roll-up rides the same \"already sent, best-effort\" contract, and\n // gets its own counter on the line for the #585 reason: a dual-write that\n // silently stopped running looked identical to a healthy one for weeks. An\n // ABSENT `rollup=` is a different failure from `rollup=0`.\n if (writeRollup) {\n try {\n rollup = (await writeRollup()) === \"absent\" ? \"absent\" : \"1\";\n } catch (e) {\n rollup = \"0\";\n console.warn(`⚠ cockpit rollup write failed: ${(e as Error).message}`);\n }\n }\n console.log(`DIGEST_STATE_WRITE turso=${turso} airtable=${airtable} rollup=${rollup}`);\n}\n\n/** The \"Submissions (24h)\" telemetry window — a precise 24h ISO-timestamp compare\n * (unlike screenOutsSince's date-only strings). */\nconst SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/** Per-site submission counts for the digest telemetry section, read from libSQL.\n * Dynamic imports + fail-soft for the same reason as fetchNotifyBounceCounts:\n * kysely/libsql are devDependencies consuming fleet sites don't install, and a\n * Turso blip must drop just this section, never the digest. Returns null (not an\n * empty map) on failure so the renderer can omit the section rather than claim a\n * quiet fleet. */\nasync function fetchSubmissionsDigestCounts(\n now: Date,\n): Promise<ReadonlyMap<string, SiteSubmissionCounts> | null> {\n try {\n const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countSubmissionsSinceBySite(\n db,\n new Date(now.getTime() - SUBMISSIONS_DIGEST_WINDOW_MS).toISOString(),\n );\n } catch (e) {\n console.warn(`⚠ submissions telemetry unavailable (libSQL): ${(e as Error).message}`);\n return null;\n }\n}\n\n/** Assemble the \"Submissions (24h)\" section from the per-site counts. PURE (exported\n * for direct tests). Totals sum ALL map entries — an orphan site id still counts in\n * the fleet totals. bySite lists only ids that resolve in `sitesById` AND carry any\n * nonzero count, sorted by total volume desc then siteName A-Z. null counts (libSQL\n * unavailable) → null (section omitted). */\nexport function buildSubmissionsDigestSection(\n counts: ReadonlyMap<string, SiteSubmissionCounts> | null,\n sitesById: ReadonlyMap<string, WebsiteRow>,\n): SubmissionsDigestSection | null {\n if (counts === null) return null;\n let leads = 0;\n let signups = 0;\n let spamAuto = 0;\n const bySite: SubmissionsDigestSection[\"bySite\"] = [];\n for (const [siteId, c] of counts) {\n leads += c.leads;\n signups += c.signups;\n spamAuto += c.spamAuto;\n const site = sitesById.get(siteId);\n if (site && c.leads + c.signups + c.spamAuto > 0) {\n bySite.push({ siteName: site.name, ...c });\n }\n }\n bySite.sort(\n (a, b) =>\n b.leads + b.signups + b.spamAuto - (a.leads + a.signups + a.spamAuto) ||\n a.siteName.localeCompare(b.siteName),\n );\n return { leads, signups, spamAuto, bySite };\n}\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const notifyBounces = deps.notifyBounces ?? (await fetchNotifyBounceCounts(now));\n const deadLetters = deps.deadLetters ?? (await fetchDeadLetterCounts());\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ...runCollector(\"turnstile\", () =>\n collectTurnstileGuardrailAlerts(websites, deps.baseUrl, now),\n ),\n ...runCollector(\"notify-bounce\", () =>\n collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl),\n ),\n // #645. The dead-letter alarm. The digest is the SURFACE that matters for\n // this kind: the cockpit groups items onto site cards, and the worst flavour\n // — a slug that resolves to no fleet site — has no card by construction. The\n // digest renders every item flat by title, so it carries both flavours.\n ...runCollector(\"deadletter\", () =>\n collectDeadLetterAlerts(websites, deadLetters, deps.baseUrl),\n ),\n // Same collector the cockpit runs, over the same persisted `Prismic Models`\n // columns and the same keys — so the snapshot this digest writes is the one\n // the cockpit diffs NEW/WORSE against, and the two surfaces agree.\n ...runCollector(\"prismic-drift\", () => collectPrismicDriftAlerts(websites, deps.baseUrl, now)),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n /** Per-site submission counts for the \"Submissions (24h)\" section. undefined =\n * fetch from libSQL; null = simulate unavailable (section omitted); a Map =\n * injected (tests). */\n submissionCounts?: ReadonlyMap<string, SiteSubmissionCounts> | null;\n /**\n * The prior-run snapshot store (#609). Production omits it and gets Turso.\n *\n * Injectable because the read is deliberately NOT defensive: swallowing a\n * failure would badge every item NEW, which lands in the operator's inbox\n * reading as \"the whole fleet degraded overnight\". That makes Turso a hard\n * requirement of a real run — so tests, which have no libSQL, need a seam\n * rather than an env var.\n */\n digestState?: {\n read: () => Promise<DigestSnapshot>;\n write: (snap: DigestSnapshot) => Promise<void>;\n };\n /**\n * The cockpit roll-up writer (MED-16). Same seam and same reason as\n * `digestState`: production omits it and gets Turso, tests inject.\n *\n * Without a seam this would be untestable in exactly the way that matters —\n * `writeCockpitRollupToDb` opens a db from env, so in a test it throws and is\n * caught, and the run reports `rollup=0` forever while looking healthy. That\n * is the #585 shape the counter on the log line exists to expose.\n */\n cockpitRollup?: { write: (now: Date) => Promise<void> };\n /**\n * The run clock. Production omits it and gets `new Date()`.\n *\n * Injectable because several collectors this run feeds are AGE-SENSITIVE —\n * `collectPrismicDriftAlerts` alone changes an item's key and its wording once\n * a verdict crosses `PRISMIC_DRIFT_STALE_DAYS` — so a digest test that cannot\n * fix the clock is asserting on the day it happens to run. Two did, and both\n * went red four days after they were written, reporting the staleness wording\n * as if the drift wiring had broken. `collectAttention` already takes a `now`\n * for exactly this reason; this threads the same lever one layer out.\n */\n now?: Date;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = options.now ?? new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n // #609: Turso is the read side now. Airtable keeps being WRITTEN below\n // (dual-write, Phase 5's pattern everywhere else) so the move stays\n // reversible, but nothing reads it any more.\n const prior = await (options.digestState?.read ?? readDigestStateFromDb)();\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n // The operator only HEARS about a vuln once Renovate's auto-fix is exhausted\n // (tried and failed a couple of nightly cycles) — before that the fleet is\n // still self-patching and the cockpit's amber Watch band is the only surface.\n // Filter the EMAIL list only: `next` (written below) must keep every vuln key\n // so the cockpit's diff against this same snapshot stays consistent, and so\n // the exhausted-flip diffs as WORSE instead of arriving pre-badged-away.\n const needsAttention = tagged.filter(\n (it) => it.kind !== \"vuln\" || it.autoFixExhausted === true,\n );\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // Persist `next` even on a skip: a key that resolved on a quiet day clears so a\n // later recurrence diffs as NEW (spec §10), and — since the skip can now fire\n // with muted pre-exhaustion vulns collected — their keys keep snapshotting so\n // the eventual exhausted-flip diffs as WORSE. Wrapped: a write failure can't\n // fail the skip.\n await persistDigestState(base, next, options.digestState?.write, () =>\n writeRollupOnce(options, today),\n );\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n // Submissions telemetry rides only on a digest that is ALREADY sending — the\n // skip predicate above is deliberately unchanged (leads already fire their own\n // ingest-time notification; this is context, not an alarm), and fetching AFTER\n // the skip check keeps quiet days from touching libSQL at all.\n const counts =\n options.submissionCounts !== undefined\n ? options.submissionCounts\n : await fetchSubmissionsDigestCounts(today);\n const submissions = buildSubmissionsDigestSection(counts, sites);\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });\n const client = options.resend ?? defaultResendClient();\n const to = [operatorEmail()];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n await persistDigestState(base, next, options.digestState?.write, () =>\n writeRollupOnce(options, today),\n );\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n ${submissionsSection(sections.submissions)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":["writeDigestState"]}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
NOTIFY_BOUNCE_WINDOW_DAYS,
|
|
4
4
|
collectAnalyticsFailures,
|
|
5
5
|
collectCiAlerts,
|
|
6
|
+
collectDeadLetterAlerts,
|
|
6
7
|
collectDeliveryFailures,
|
|
7
8
|
collectLighthouseAlerts,
|
|
8
9
|
collectNotifyBounceAlerts,
|
|
@@ -12,24 +13,24 @@ import {
|
|
|
12
13
|
collectTurnstileGuardrailAlerts,
|
|
13
14
|
collectVulnAlerts,
|
|
14
15
|
prismicAckIsLive
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-62YWOFVN.js";
|
|
17
|
+
import "./chunk-OTEVKRAH.js";
|
|
17
18
|
import "./chunk-PZR4EBPY.js";
|
|
18
|
-
import "./chunk-
|
|
19
|
+
import "./chunk-P4A6D6HO.js";
|
|
19
20
|
import "./chunk-F3JWOPTG.js";
|
|
20
21
|
import "./chunk-VAD43MOR.js";
|
|
21
22
|
import "./chunk-IY6256GK.js";
|
|
22
23
|
import "./chunk-HN6I2XWI.js";
|
|
23
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-Y5BVR4HW.js";
|
|
24
25
|
import "./chunk-GECESMVS.js";
|
|
25
26
|
import "./chunk-KJGKZLCT.js";
|
|
26
27
|
import "./chunk-BGTYPVLT.js";
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
28
|
+
import "./chunk-MZZDMBDT.js";
|
|
29
|
+
import "./chunk-KN52PM63.js";
|
|
29
30
|
import "./chunk-WEKL2HYJ.js";
|
|
30
|
-
import "./chunk-
|
|
31
|
-
import "./chunk-
|
|
32
|
-
import "./chunk-
|
|
31
|
+
import "./chunk-EQQ4H6L3.js";
|
|
32
|
+
import "./chunk-6INOBNRO.js";
|
|
33
|
+
import "./chunk-T76MA2FH.js";
|
|
33
34
|
import "./chunk-RARREDQE.js";
|
|
34
35
|
import "./chunk-6VAL7XJT.js";
|
|
35
36
|
export {
|
|
@@ -37,6 +38,7 @@ export {
|
|
|
37
38
|
NOTIFY_BOUNCE_WINDOW_DAYS,
|
|
38
39
|
collectAnalyticsFailures,
|
|
39
40
|
collectCiAlerts,
|
|
41
|
+
collectDeadLetterAlerts,
|
|
40
42
|
collectDeliveryFailures,
|
|
41
43
|
collectLighthouseAlerts,
|
|
42
44
|
collectNotifyBounceAlerts,
|
|
@@ -47,4 +49,4 @@ export {
|
|
|
47
49
|
collectVulnAlerts,
|
|
48
50
|
prismicAckIsLive
|
|
49
51
|
};
|
|
50
|
-
//# sourceMappingURL=digest-collectors-
|
|
52
|
+
//# sourceMappingURL=digest-collectors-MC5NWEUA.js.map
|
|
@@ -18,6 +18,22 @@ async function writeDigestState(db, snap, updatedAt = (/* @__PURE__ */ new Date(
|
|
|
18
18
|
await db.insertInto("digest_state").values(row).onConflict((oc) => oc.column("id").doUpdateSet(row)).execute();
|
|
19
19
|
}
|
|
20
20
|
var COCKPIT_ROLLUP_ID = "cockpit_rollup";
|
|
21
|
+
function coerceBounces(raw) {
|
|
22
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [siteId, v] of Object.entries(raw)) {
|
|
25
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
26
|
+
out[siteId] = { total: v, permanent: 0 };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) continue;
|
|
30
|
+
const o = v;
|
|
31
|
+
if (typeof o.total === "number" && Number.isFinite(o.total) && typeof o.permanent === "number" && Number.isFinite(o.permanent)) {
|
|
32
|
+
out[siteId] = { total: o.total, permanent: o.permanent };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
21
37
|
async function readCockpitRollup(db) {
|
|
22
38
|
const row = await db.selectFrom("digest_state").select("snapshot").where("id", "=", COCKPIT_ROLLUP_ID).executeTakeFirst();
|
|
23
39
|
if (!row) return null;
|
|
@@ -26,7 +42,7 @@ async function readCockpitRollup(db) {
|
|
|
26
42
|
if (typeof parsed !== "object" || parsed === null || typeof parsed.computedAt !== "string" || typeof parsed.spamTotals !== "object" || parsed.spamTotals === null || typeof parsed.notifyBounces !== "object" || parsed.notifyBounces === null) {
|
|
27
43
|
return null;
|
|
28
44
|
}
|
|
29
|
-
return parsed;
|
|
45
|
+
return { ...parsed, notifyBounces: coerceBounces(parsed.notifyBounces) };
|
|
30
46
|
} catch {
|
|
31
47
|
return null;
|
|
32
48
|
}
|
|
@@ -47,4 +63,4 @@ export {
|
|
|
47
63
|
writeCockpitRollup,
|
|
48
64
|
writeDigestState
|
|
49
65
|
};
|
|
50
|
-
//# sourceMappingURL=digest-state-
|
|
66
|
+
//# sourceMappingURL=digest-state-ZDPKR7MW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/digest-state.ts"],"sourcesContent":["/** #609 (#539 Phase 5): the digest's prior-run snapshot, on Turso.\n *\n * Unlike every other Phase 5 slice this is a MIGRATION, not a mirror — there\n * was no Turso table to dual-write into, so these REPLACE the Airtable pair in\n * `src/alerts/digest-state.ts` rather than shadowing them. Nothing writes the\n * Airtable \"Digest State\" table after this, so parity does not cover the new\n * one: it has no counterpart to compare against.\n *\n * The pure half — `diffAttention`, and the `DigestSnapshot` type it is written\n * against — stays exactly where it is. Only the IO moved.\n *\n * Stored as ONE row holding the whole snapshot as JSON. Both readers\n * (`runDigest`'s diff and the fleet homepage's NEW badges) need the entire map,\n * so a keyed table would buy nothing on reads and its \"give me everything\"\n * query would be a raw scan needing a justified entry in the EXPLAIN gate's\n * allowlist. One row by primary key needs neither.\n *\n * Worth stating plainly: the homepage's read of this used to be an AIRTABLE\n * call on a request path — a Phase 2 leftover, since digest state was never in\n * that phase's scope. Moving it removes a live dependency, which is the same\n * failure class as the 2026-08-17 quota outage.\n */\nimport type { Db } from \"./client.js\";\nimport type { DigestSnapshot } from \"../alerts/digest-state.js\";\nimport type { NotifyBounceCounts } from \"./submissions.js\";\n\n/** The singleton row's key. A constant rather than a magic string at each call\n * site, so the \"there is exactly one row\" invariant is visible in one place. */\nexport const DIGEST_STATE_ID = \"default\";\n\n/**\n * The prior snapshot, or `{}` when there is none.\n *\n * A missing row and a malformed blob BOTH read as `{}` — the Airtable reader's\n * contract, kept verbatim. The digest runs unattended on a cron, so a bad blob\n * must cost accurate NEW badges for one run, never the whole send.\n */\nexport async function readDigestState(db: Db): Promise<DigestSnapshot> {\n const row = await db\n .selectFrom(\"digest_state\")\n .select(\"snapshot\")\n .where(\"id\", \"=\", DIGEST_STATE_ID)\n .executeTakeFirst();\n if (!row) return {};\n try {\n return JSON.parse(row.snapshot) as DigestSnapshot;\n } catch {\n return {};\n }\n}\n\n/**\n * Persist the next snapshot, replacing whatever was there.\n *\n * An upsert on the constant id rather than a get-then-create: the Airtable\n * version needed two round-trips to decide, and here the singleton invariant is\n * enforced by the primary key instead of by the code that happens to run.\n */\nexport async function writeDigestState(\n db: Db,\n snap: DigestSnapshot,\n updatedAt: string = new Date().toISOString(),\n): Promise<void> {\n const row = {\n id: DIGEST_STATE_ID,\n snapshot: JSON.stringify(snap),\n updated_at: updatedAt,\n };\n await db\n .insertInto(\"digest_state\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(row))\n .execute();\n}\n\n/**\n * A SECOND row in the same table, holding the cockpit's \"since a window\"\n * roll-ups. Keyed separately rather than folded into the snapshot: the snapshot\n * is the digest's own diff state and has one writer and one meaning; conflating\n * two unrelated payloads in one blob makes a partial write of either look like a\n * corruption of both.\n */\nexport const COCKPIT_ROLLUP_ID = \"cockpit_rollup\";\n\n/**\n * Fleet-wide counts the cockpit renders, computed once by the nightly digest\n * instead of aggregated on every page load.\n *\n * `computedAt` is not decoration. These figures are up to 24h old by\n * construction, and a stale number rendered as though it were live is worse than\n * no number — so the cockpit shows when it was taken.\n */\nexport type CockpitRollup = {\n spamTotals: { honeypot: number; tooFast: number; markedSpam: number };\n /** site id → unacknowledged bounce counts inside the bounce window, split by\n * whether Resend called any of them Permanent (#783). */\n notifyBounces: Record<string, NotifyBounceCounts>;\n /** The windows these were computed over, so a changed constant is visible\n * rather than silently re-labelling old numbers. */\n windowDays: { screenOuts: number; bounces: number };\n computedAt: string;\n};\n\n/**\n * The stored roll-up, or **null** when there is none.\n *\n * Null rather than zeros, deliberately. Every one of these numbers has a\n * legitimate zero, so a reader given `{honeypot: 0, …}` cannot tell \"nothing was\n * screened out\" from \"the digest has never run\". That is the same distinction\n * `FLEET_SMOKE_UNMEASURED` exists to preserve, and the cockpit renders the strip\n * as ABSENT for null rather than drawing a row of zeros.\n *\n * A malformed blob is also null: same reasoning, and it must never throw on a\n * request path.\n */\n/**\n * Normalize the stored per-site bounce map (#783).\n *\n * The shape changed from `{recA: 2}` to `{recA: {total, permanent}}`, and this\n * blob is written by the NIGHTLY digest — so after a deploy the stored payload\n * is the old shape until the next run. Rejecting it outright would blank the\n * whole strip, `spamTotals` included, for a number that is merely less detailed.\n *\n * An old numeric entry reads as `permanent: 0`, which is the honest translation:\n * that writer recorded no classification, and 0 is exactly \"nothing said the\n * address is dead\". It words the alarm as the mail-filter case for one night,\n * which is the safe direction. Anything else is dropped per SITE rather than\n * poisoning the map with NaN or discarding the roll-up.\n */\nfunction coerceBounces(raw: unknown): Record<string, NotifyBounceCounts> {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return {};\n const out: Record<string, NotifyBounceCounts> = {};\n for (const [siteId, v] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof v === \"number\" && Number.isFinite(v)) {\n out[siteId] = { total: v, permanent: 0 };\n continue;\n }\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) continue;\n const o = v as Record<string, unknown>;\n if (\n typeof o.total === \"number\" &&\n Number.isFinite(o.total) &&\n typeof o.permanent === \"number\" &&\n Number.isFinite(o.permanent)\n ) {\n out[siteId] = { total: o.total, permanent: o.permanent };\n }\n }\n return out;\n}\n\nexport async function readCockpitRollup(db: Db): Promise<CockpitRollup | null> {\n const row = await db\n .selectFrom(\"digest_state\")\n .select(\"snapshot\")\n .where(\"id\", \"=\", COCKPIT_ROLLUP_ID)\n .executeTakeFirst();\n if (!row) return null;\n try {\n const parsed = JSON.parse(row.snapshot) as Partial<CockpitRollup>;\n // Shape-check rather than trust: this blob is written by a different process\n // on a different schedule, so an older writer's payload can outlive a type\n // change here. Anything unrecognized reads as \"not measured\".\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof parsed.computedAt !== \"string\" ||\n typeof parsed.spamTotals !== \"object\" ||\n parsed.spamTotals === null ||\n typeof parsed.notifyBounces !== \"object\" ||\n parsed.notifyBounces === null\n ) {\n return null;\n }\n return { ...(parsed as CockpitRollup), notifyBounces: coerceBounces(parsed.notifyBounces) };\n } catch {\n return null;\n }\n}\n\n/** Upsert the roll-up, same singleton-by-PK shape as the snapshot. */\nexport async function writeCockpitRollup(db: Db, rollup: CockpitRollup): Promise<void> {\n const row = {\n id: COCKPIT_ROLLUP_ID,\n snapshot: JSON.stringify(rollup),\n updated_at: rollup.computedAt,\n };\n await db\n .insertInto(\"digest_state\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(row))\n .execute();\n}\n"],"mappings":";AA4BO,IAAM,kBAAkB;AAS/B,eAAsB,gBAAgB,IAAiC;AACrE,QAAM,MAAM,MAAM,GACf,WAAW,cAAc,EACzB,OAAO,UAAU,EACjB,MAAM,MAAM,KAAK,eAAe,EAChC,iBAAiB;AACpB,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI;AACF,WAAO,KAAK,MAAM,IAAI,QAAQ;AAAA,EAChC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AASA,eAAsB,iBACpB,IACA,MACA,aAAoB,oBAAI,KAAK,GAAE,YAAY,GAC5B;AACf,QAAM,MAAM;AAAA,IACV,IAAI;AAAA,IACJ,UAAU,KAAK,UAAU,IAAI;AAAA,IAC7B,YAAY;AAAA,EACd;AACA,QAAM,GACH,WAAW,cAAc,EACzB,OAAO,GAAG,EACV,WAAW,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,YAAY,GAAG,CAAC,EACnD,QAAQ;AACb;AASO,IAAM,oBAAoB;AA+CjC,SAAS,cAAc,KAAkD;AACvE,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AAC3E,QAAM,MAA0C,CAAC;AACjD,aAAW,CAAC,QAAQ,CAAC,KAAK,OAAO,QAAQ,GAA8B,GAAG;AACxE,QAAI,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,GAAG;AAC/C,UAAI,MAAM,IAAI,EAAE,OAAO,GAAG,WAAW,EAAE;AACvC;AAAA,IACF;AACA,QAAI,OAAO,MAAM,YAAY,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAG;AAC7D,UAAM,IAAI;AACV,QACE,OAAO,EAAE,UAAU,YACnB,OAAO,SAAS,EAAE,KAAK,KACvB,OAAO,EAAE,cAAc,YACvB,OAAO,SAAS,EAAE,SAAS,GAC3B;AACA,UAAI,MAAM,IAAI,EAAE,OAAO,EAAE,OAAO,WAAW,EAAE,UAAU;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,kBAAkB,IAAuC;AAC7E,QAAM,MAAM,MAAM,GACf,WAAW,cAAc,EACzB,OAAO,UAAU,EACjB,MAAM,MAAM,KAAK,iBAAiB,EAClC,iBAAiB;AACpB,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AAItC,QACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,eAAe,YAC7B,OAAO,eAAe,QACtB,OAAO,OAAO,kBAAkB,YAChC,OAAO,kBAAkB,MACzB;AACA,aAAO;AAAA,IACT;AACA,WAAO,EAAE,GAAI,QAA0B,eAAe,cAAc,OAAO,aAAa,EAAE;AAAA,EAC5F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,mBAAmB,IAAQ,QAAsC;AACrF,QAAM,MAAM;AAAA,IACV,IAAI;AAAA,IACJ,UAAU,KAAK,UAAU,MAAM;AAAA,IAC/B,YAAY,OAAO;AAAA,EACrB;AACA,QAAM,GACH,WAAW,cAAc,EACzB,OAAO,GAAG,EACV,WAAW,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,YAAY,GAAG,CAAC,EACnD,QAAQ;AACb;","names":[]}
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
} from "./chunk-HN6I2XWI.js";
|
|
5
5
|
import {
|
|
6
6
|
WEBSITES_TABLE,
|
|
7
|
-
|
|
7
|
+
mapRow,
|
|
8
8
|
siteSlug,
|
|
9
9
|
toAirtableStatus
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-KN52PM63.js";
|
|
11
11
|
import "./chunk-6VAL7XJT.js";
|
|
12
12
|
|
|
13
13
|
// src/reports/airtable/ensure-site.ts
|
|
@@ -23,8 +23,22 @@ async function ensureSite(base, input, mirror) {
|
|
|
23
23
|
throw new Error(
|
|
24
24
|
`ensure-site: display name '${input.displayName}' slugifies to '${siteSlug(input.displayName)}', not '${slug}' \u2014 the row would not be found on re-run`
|
|
25
25
|
);
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const raw = [];
|
|
27
|
+
await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
28
|
+
for (const rec of records) raw.push({ id: rec.id, fields: rec.fields });
|
|
29
|
+
fetchNextPage();
|
|
30
|
+
});
|
|
31
|
+
let existing;
|
|
32
|
+
let existingRaw;
|
|
33
|
+
for (const rec of raw) {
|
|
34
|
+
const row = mapRow(rec);
|
|
35
|
+
if (siteSlug(row.name) === slug) {
|
|
36
|
+
existing = row;
|
|
37
|
+
existingRaw = rec;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!existing || !existingRaw) {
|
|
28
42
|
const fields = {
|
|
29
43
|
Name: input.displayName ?? slug,
|
|
30
44
|
Status: toAirtableStatus("building"),
|
|
@@ -39,9 +53,17 @@ async function ensureSite(base, input, mirror) {
|
|
|
39
53
|
status: "created",
|
|
40
54
|
siteId: rec.id,
|
|
41
55
|
updatedFields: [],
|
|
42
|
-
skippedMismatches: []
|
|
56
|
+
skippedMismatches: [],
|
|
57
|
+
// The create path just inserted the row; there is nothing to heal, and a
|
|
58
|
+
// `hasRow` probe here would be a round-trip whose answer is already known.
|
|
59
|
+
healedDbRow: false
|
|
43
60
|
};
|
|
44
61
|
}
|
|
62
|
+
let healedDbRow = false;
|
|
63
|
+
if (mirror?.hasRow && !await mirror.hasRow(existingRaw.id)) {
|
|
64
|
+
await mirror.created(existingRaw);
|
|
65
|
+
healedDbRow = true;
|
|
66
|
+
}
|
|
45
67
|
const updates = {};
|
|
46
68
|
const skippedMismatches = [];
|
|
47
69
|
const blank = (v) => v === null || v === void 0 || v === "";
|
|
@@ -53,12 +75,15 @@ async function ensureSite(base, input, mirror) {
|
|
|
53
75
|
consider(COLS.url, input.url, existing.url || null);
|
|
54
76
|
consider(COLS.pointOfContact, input.pointOfContact, existing.pointOfContact);
|
|
55
77
|
consider(COLS.gitRepo, input.gitRepo, existing.gitRepo);
|
|
78
|
+
if (input.displayName && existing.name !== input.displayName) {
|
|
79
|
+
updates["Name"] = input.displayName;
|
|
80
|
+
}
|
|
56
81
|
const updatedFields = Object.keys(updates);
|
|
57
82
|
if (updatedFields.length > 0) {
|
|
58
83
|
await base(WEBSITES_TABLE).update([{ id: existing.id, fields: updates }]);
|
|
59
84
|
await mirror?.site(existing.id, updates);
|
|
60
85
|
}
|
|
61
|
-
return { status: "exists", siteId: existing.id, updatedFields, skippedMismatches };
|
|
86
|
+
return { status: "exists", siteId: existing.id, updatedFields, skippedMismatches, healedDbRow };
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
// src/cli/commands/ensure-site.ts
|
|
@@ -66,7 +91,7 @@ async function runEnsureSiteCommand(slug, opts) {
|
|
|
66
91
|
if (!slug) return { output: "Provide a <slug> (e.g. `ensure-site roalson`).", code: 2 };
|
|
67
92
|
try {
|
|
68
93
|
const base = openBase(readAirtableConfig());
|
|
69
|
-
const { makeSiteMirror } = await import("./site-mirror-
|
|
94
|
+
const { makeSiteMirror } = await import("./site-mirror-H2FYMN55.js");
|
|
70
95
|
const result = await ensureSite(
|
|
71
96
|
base,
|
|
72
97
|
{
|
|
@@ -78,11 +103,14 @@ async function runEnsureSiteCommand(slug, opts) {
|
|
|
78
103
|
},
|
|
79
104
|
await makeSiteMirror()
|
|
80
105
|
);
|
|
81
|
-
const
|
|
106
|
+
const renamed = result.updatedFields.includes("Name") ? ` \u2014 Name set to "${opts.name}"` : "";
|
|
107
|
+
const blanks = result.updatedFields.filter((f) => f !== "Name");
|
|
108
|
+
const filled = blanks.length > 0 ? ` \u2014 filled blank field(s): ${blanks.join(", ")}` : "";
|
|
82
109
|
const skipped = result.skippedMismatches.length > 0 ? ` \u2014 differs from existing, left untouched (edit in Airtable): ${result.skippedMismatches.join(", ")}` : "";
|
|
83
|
-
const
|
|
110
|
+
const healed = result.healedDbRow ? ` \u2014 HEALED: the Turso row was MISSING and has been inserted; leads for this slug were being dropped (run \`db replay-deadletters\`)` : "";
|
|
111
|
+
const nameNote = result.status === "created" && !opts.name ? ` \u2014 Name set to "${slug}"; retitle in Airtable before forms/announce go live (or re-run with --name)` : "";
|
|
84
112
|
return {
|
|
85
|
-
output: `[${slug}] ${result.status} (${result.siteId})${filled}${skipped}${nameNote}`,
|
|
113
|
+
output: `[${slug}] ${result.status} (${result.siteId})${healed}${renamed}${filled}${skipped}${nameNote}`,
|
|
86
114
|
code: 0
|
|
87
115
|
};
|
|
88
116
|
} catch (err) {
|
|
@@ -93,4 +121,4 @@ async function runEnsureSiteCommand(slug, opts) {
|
|
|
93
121
|
export {
|
|
94
122
|
runEnsureSiteCommand
|
|
95
123
|
};
|
|
96
|
-
//# sourceMappingURL=ensure-site-
|
|
124
|
+
//# sourceMappingURL=ensure-site-XESZHZEB.js.map
|