@reddoorla/maintenance 0.95.1 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
- package/dist/announce-5ZD2SBGD.js.map +1 -0
- package/dist/attachments-JFGFQRXQ.js +10 -0
- package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
- package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
- package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
- package/dist/{chunk-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
- package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/chunk-ASLDCWBF.js +41 -0
- package/dist/chunk-ASLDCWBF.js.map +1 -0
- package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
- package/dist/chunk-AYDQQBNG.js.map +1 -0
- package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
- package/dist/chunk-BINFZQ2T.js.map +1 -0
- package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
- package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
- package/dist/chunk-DRNZVORX.js.map +1 -0
- package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
- package/dist/chunk-DYTYPKFN.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
- package/dist/chunk-EN6CTBUN.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
- package/dist/chunk-FHFXJ3EM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
- package/dist/chunk-FP6HOW6J.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
- package/dist/chunk-ICSRWW5Q.js +148 -0
- package/dist/chunk-ICSRWW5Q.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
- package/dist/chunk-IDBPII6G.js.map +1 -0
- package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
- package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
- package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
- package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
- package/dist/chunk-L3S3QZXL.js.map +1 -0
- package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
- package/dist/chunk-LAG6IP7Y.js.map +1 -0
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
- package/dist/chunk-LDHRIQUW.js.map +1 -0
- package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
- package/dist/chunk-LF2TJN4X.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
- package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
- package/dist/chunk-LZMAG73U.js.map +1 -0
- package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
- package/dist/chunk-MVKNU4QD.js +37 -0
- package/dist/chunk-MVKNU4QD.js.map +1 -0
- package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
- package/dist/chunk-PARKXDXI.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/chunk-RQJFGK3A.js +51 -0
- package/dist/chunk-RQJFGK3A.js.map +1 -0
- package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
- package/dist/chunk-TC4GANMX.js.map +1 -0
- package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
- package/dist/chunk-TEMQPSCM.js.map +1 -0
- package/dist/chunk-TJLKWQOX.js +52 -0
- package/dist/chunk-TJLKWQOX.js.map +1 -0
- package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
- package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
- package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
- package/dist/chunk-U2ZI6G7H.js.map +1 -0
- package/dist/chunk-UPE6D4AT.js +69 -0
- package/dist/chunk-UPE6D4AT.js.map +1 -0
- package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
- package/dist/chunk-VMGHQUTV.js.map +1 -0
- package/dist/chunk-VQMAAQ5M.js +53 -0
- package/dist/chunk-VQMAAQ5M.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
- package/dist/chunk-WMF5S2MB.js.map +1 -0
- package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
- package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
- package/dist/{chunk-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
- package/dist/chunk-X56LAKPR.js.map +1 -0
- package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
- package/dist/chunk-Y6FCXRIC.js.map +1 -0
- package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
- package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
- package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
- package/dist/chunk-YJBSB4LP.js.map +1 -0
- package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/cli/bin.js +111 -51
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +33 -3
- package/dist/cli/commands/audit.js +28 -25
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-DGTO65WB.js +11 -0
- package/dist/configs/playwright-a11y.d.ts +11 -1
- package/dist/configs/playwright-a11y.js +3 -1
- package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-KLWA53N5.js} +6 -6
- package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
- package/dist/db-II5WAK4D.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
- package/dist/digest-CWKK2DMR.js.map +1 -0
- package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
- package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
- package/dist/digest-state-ZDPKR7MW.js.map +1 -0
- package/dist/ensure-site-2QZHFVQ3.js +70 -0
- package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
- package/dist/ensure-site-JEKV2ODE.js +105 -0
- package/dist/ensure-site-JEKV2ODE.js.map +1 -0
- package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
- package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
- package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
- package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
- package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
- package/dist/github-signals-QIRTHHET.js.map +1 -0
- package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
- package/dist/header-image-H7ZJAV26.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
- package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
- package/dist/index.d.ts +158 -419
- package/dist/index.js +91 -46
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
- package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
- package/dist/launch-7N3X2MXB.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
- package/dist/match-harness-GBCDSVPS.js +417 -0
- package/dist/match-harness-GBCDSVPS.js.map +1 -0
- package/dist/migrate-XZ7U5DFC.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
- package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
- package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
- package/dist/parity-2JGVUCZD.js +14 -0
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
- package/dist/preflight-O7Q6FWHU.js.map +1 -0
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
- package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
- package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
- package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
- package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
- package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
- package/dist/protection-audit-YXJKFU6O.js +413 -0
- package/dist/protection-audit-YXJKFU6O.js.map +1 -0
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
- package/dist/render-MHUUHPLN.js.map +1 -0
- package/dist/{render-from-row-IFSBHWDH.js → render-from-row-75JTED2Z.js} +6 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
- package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
- package/dist/report-IAHXN5CW.js.map +1 -0
- package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
- package/dist/report-mirror-4QM57LMS.js.map +1 -0
- package/dist/roster-GDNSIRAK.js +19 -0
- package/dist/roster-GDNSIRAK.js.map +1 -0
- package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
- package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
- package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
- package/dist/site-create-ZU3ACBTO.js +45 -0
- package/dist/site-create-ZU3ACBTO.js.map +1 -0
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
- package/dist/site-mirror-WR7ACXOV.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
- package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
- package/dist/submissions-2SHCFOKD.js.map +1 -0
- package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
- package/dist/submissions-JDZP35JM.js.map +1 -0
- package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-VOLOKKFI.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
- package/dist/turso-KQKBBQCK.js +24 -0
- package/dist/turso-KQKBBQCK.js.map +1 -0
- package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
- package/dist/util/git.d.ts +49 -10
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/announce-CLQYX5I4.js.map +0 -1
- package/dist/attachments-NLZIJ5NU.js +0 -9
- package/dist/chunk-2ZAZ7ETT.js.map +0 -1
- package/dist/chunk-3Z6X3BHU.js.map +0 -1
- package/dist/chunk-5EEMTJMT.js.map +0 -1
- package/dist/chunk-5GUJ36TL.js +0 -53
- package/dist/chunk-5GUJ36TL.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6R56OKJZ.js.map +0 -1
- package/dist/chunk-AVEQUR44.js.map +0 -1
- package/dist/chunk-BVEDKMNG.js.map +0 -1
- package/dist/chunk-BYGPUUCT.js.map +0 -1
- package/dist/chunk-CWXTNPNZ.js.map +0 -1
- package/dist/chunk-DFKBTBO3.js.map +0 -1
- package/dist/chunk-F3JWOPTG.js.map +0 -1
- package/dist/chunk-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.js.map +0 -1
- package/dist/chunk-IY6256GK.js.map +0 -1
- package/dist/chunk-KJRVMMM3.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-R5IMFAFR.js.map +0 -1
- package/dist/chunk-TO7DJYLZ.js.map +0 -1
- package/dist/chunk-TR3HFH6J.js.map +0 -1
- package/dist/chunk-WJMPCVOR.js.map +0 -1
- package/dist/chunk-WSJ7RO54.js.map +0 -1
- package/dist/chunk-WVNDEYXL.js.map +0 -1
- package/dist/chunk-XHHMM5HX.js.map +0 -1
- package/dist/chunk-XN37VTDB.js.map +0 -1
- package/dist/chunk-XPJQXSC4.js.map +0 -1
- package/dist/chunk-YFAXOD7Z.js +0 -42
- package/dist/chunk-YFAXOD7Z.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/chunk-Z2K4JPQ3.js.map +0 -1
- package/dist/chunk-ZCL7C5LH.js.map +0 -1
- package/dist/client-G2WGWPCA.js +0 -11
- package/dist/db-XKZYDCKM.js.map +0 -1
- package/dist/digest-7UBGUBRK.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-OAAE5YE2.js.map +0 -1
- package/dist/fleet-events-cF2Om88w.d.ts +0 -24
- package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
- package/dist/github-signals-TX5FPYP6.js.map +0 -1
- package/dist/header-image-PSPT7CUM.js.map +0 -1
- package/dist/launch-UD36AHUN.js.map +0 -1
- package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
- package/dist/match-harness-QRG6XZAR.js +0 -70
- package/dist/match-harness-QRG6XZAR.js.map +0 -1
- package/dist/migrate-Y7R4CESL.js +0 -7
- package/dist/parity-CAJ477PS.js +0 -14
- package/dist/preflight-RYIP54NY.js.map +0 -1
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prismic-models-P6FWWVRS.js.map +0 -1
- package/dist/prospect-audit-PRHU53HS.js.map +0 -1
- package/dist/protection-audit-SJO53MEQ.js +0 -168
- package/dist/protection-audit-SJO53MEQ.js.map +0 -1
- package/dist/render-YGNGTX3W.js.map +0 -1
- package/dist/replay-PCSQOGCZ.js.map +0 -1
- package/dist/report-LQS4ECTM.js.map +0 -1
- package/dist/report-mirror-HDTOJE64.js.map +0 -1
- package/dist/schema-A4WCPIF4.js.map +0 -1
- package/dist/site-mirror-WON4FTJ2.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- package/dist/submissions-OVZPVBYG.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
- /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
- /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
- /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
- /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
- /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
- /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
skipsAirtableShadow
|
|
3
|
+
} from "./chunk-RQJFGK3A.js";
|
|
4
|
+
|
|
1
5
|
// src/reports/airtable/attachments.ts
|
|
2
6
|
function looksLikeHtml(bytes) {
|
|
3
7
|
const start = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
@@ -23,6 +27,7 @@ async function fetchAttachmentBytes(url) {
|
|
|
23
27
|
return { bytes, contentType };
|
|
24
28
|
}
|
|
25
29
|
async function uploadAttachment(recordId, fieldName, body, filename, contentType, opts = {}) {
|
|
30
|
+
if (skipsAirtableShadow("uploadAttachment", recordId)) return;
|
|
26
31
|
const apiKey = process.env.AIRTABLE_PAT;
|
|
27
32
|
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
28
33
|
if (!apiKey || !baseId) {
|
|
@@ -79,4 +84,4 @@ export {
|
|
|
79
84
|
fetchAttachmentBytes,
|
|
80
85
|
uploadAttachment
|
|
81
86
|
};
|
|
82
|
-
//# sourceMappingURL=chunk-
|
|
87
|
+
//# sourceMappingURL=chunk-YJBSB4LP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reports/airtable/attachments.ts"],"sourcesContent":["import { skipsAirtableShadow } from \"../../fleet/site-id.js\";\n\n/** Cheap HTML sniff: an Airtable signed-URL \"200\" that is really a login/error page\n * starts with `<!doctype html`, `<html`, or `<head` after an optional UTF-8 BOM /\n * leading whitespace. We only need to catch the common error-page case, not parse\n * HTML. */\nfunction looksLikeHtml(bytes: Uint8Array): boolean {\n // Inspect the first ~64 bytes as ASCII (1 byte → 1 char; enough for a doctype /\n // opening tag). Skip a leading UTF-8 BOM (bytes EF BB BF) by index, then strip any\n // leading ASCII whitespace, and match the common HTML openers case-insensitively.\n const start = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;\n const head = Buffer.from(bytes.slice(start, start + 64))\n .toString(\"ascii\")\n .replace(/^[\\s]+/, \"\")\n .toLowerCase();\n return head.startsWith(\"<!doctype html\") || head.startsWith(\"<html\") || head.startsWith(\"<head\");\n}\n\nexport async function fetchAttachmentBytes(\n url: string,\n): Promise<{ bytes: Uint8Array; contentType: string }> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`,\n );\n }\n const contentType = res.headers.get(\"content-type\") ?? \"application/octet-stream\";\n const ab = await res.arrayBuffer();\n const bytes = new Uint8Array(ab);\n // Sanity-gate the body: a 200 that is actually an HTML error/login page (expired\n // signed URL, auth wall) would otherwise be attached as the \"image\" and ship a\n // broken header. Accept an explicit image/* content-type; otherwise reject anything\n // that sniffs as HTML — so the send fails loudly rather than emailing a broken image.\n const isImageType = contentType.toLowerCase().startsWith(\"image/\");\n if (!isImageType && looksLikeHtml(bytes)) {\n throw new Error(\n `Airtable attachment did not return image data (content-type=\"${contentType}\", ` +\n `body looks like an HTML page — the signed URL may have expired) (url=${url})`,\n );\n }\n return { bytes, contentType };\n}\n\n/**\n * Upload bytes (or a string) as an attachment to a specific record + field.\n * Uses Airtable's content.airtable.com upload endpoint (base64 body) because\n * the standard SDK only accepts public URLs for attachments, and we don't\n * host the generated content anywhere public.\n *\n * Docs: https://airtable.com/developers/web/api/upload-attachment\n *\n * Requires AIRTABLE_PAT + AIRTABLE_BASE_ID in env (same as the rest of the\n * reports module). The fieldName is URL-encoded for the request path.\n */\n/**\n * Upload one file into an Airtable attachment field.\n *\n * ⚠️ Airtable's uploadAttachment endpoint APPENDS — it never replaces. Every\n * re-upload therefore stacks another file in the field while readers\n * (`websites.ts`, `db/header-images.ts`) take attachment [0], the OLDEST. Left\n * alone that means a field silently serves a stale image forever: beachfront\n * accumulated four headers and kept sending the first, which is how a\n * pre-clean-plate header reached a 2026-08-24 announcement and got a second\n * headline printed over its baked one.\n *\n * Pass `{ replaceIn: \"<table>\" }` for a field that should hold exactly one\n * current file (the site header). Omit it where history is wanted (per-period\n * report previews, which are one row per period anyway).\n *\n * The table is part of the option rather than a separate flag because the prune\n * PATCHes `/v0/{baseId}/{table}/{recordId}` — an earlier version omitted the\n * table segment entirely and 403'd on every call, so `replace` cannot be asked\n * for without supplying it.\n */\nexport async function uploadAttachment(\n recordId: string,\n fieldName: string,\n body: Uint8Array | string,\n filename: string,\n contentType: string,\n opts: { replaceIn?: string } = {},\n): Promise<void> {\n // #646 step 3: a `site_<ULID>` site was created in Turso and has no Airtable\n // record to attach to. Checked BEFORE the credential gate — skipping needs no\n // creds, and a run without them must not fail on a write it was never going to make.\n if (skipsAirtableShadow(\"uploadAttachment\", recordId)) return;\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey || !baseId) {\n throw new Error(\"AIRTABLE_PAT and AIRTABLE_BASE_ID must be set\");\n }\n const base64 =\n typeof body === \"string\"\n ? Buffer.from(body, \"utf-8\").toString(\"base64\")\n : Buffer.from(body).toString(\"base64\");\n const payload = { contentType, file: base64, filename };\n const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n if (!res.ok) {\n throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);\n }\n if (!opts.replaceIn) return;\n\n // The upload response carries the record with the field's full, post-append\n // attachment list. Keep only the last entry (the one just uploaded) by\n // PATCHing the field to that id — Airtable preserves an attachment referenced\n // by id alone and drops any omitted. Best-effort: the file IS uploaded by this\n // point, so a prune failure must not fail the caller; it only leaves the\n // field over-full, which the next successful run tidies.\n try {\n const body = (await res.json()) as {\n fields?: Record<string, Array<{ id?: string }> | undefined>;\n };\n // Airtable keys this response's `fields` by FIELD ID, not field name — verified\n // live 2026-08-24: {\"id\":\"rec…\",\"createdTime\":…,\"fields\":{\"fldBUAW180p8MIpvh\":[…]}}.\n // Looking it up by NAME therefore yielded undefined, `list` was [], and the\n // `length <= 1` guard returned silently — so `replace` never pruned anything on a\n // real call, while a name-keyed test stub kept it green. Eleven site header fields\n // stacked before the discrepancy surfaced. The endpoint writes exactly ONE field,\n // so its response carries exactly one entry: take it whatever it is keyed by, and\n // fall back to the name in case Airtable ever switches.\n const lists = Object.values(body.fields ?? {}).filter((v) => Array.isArray(v));\n const list = body.fields?.[fieldName] ?? (lists.length === 1 ? lists[0] : undefined);\n if (!list) {\n // Never return quietly here: silence is what let the no-op ship.\n console.warn(\n `⚠ attachment prune skipped for \"${fieldName}\": upload response carried no ` +\n `attachment list (fields keys: ${Object.keys(body.fields ?? {}).join(\", \") || \"none\"})`,\n );\n return;\n }\n if (list.length <= 1) return;\n const newest = list[list.length - 1]?.id;\n if (!newest) {\n console.warn(`⚠ kept ${list.length} attachments in \"${fieldName}\": newest entry has no id`);\n return;\n }\n // /v0/{baseId}/{table}/{recordId} — the table segment is REQUIRED. Without it\n // Airtable answers 403 Forbidden (not 404), which reads like a token-scope\n // problem and sent this in the wrong direction once already.\n const patchUrl = `https://api.airtable.com/v0/${baseId}/${encodeURIComponent(opts.replaceIn)}/${recordId}`;\n const patch = await fetch(patchUrl, {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ fields: { [fieldName]: [{ id: newest }] } }),\n });\n if (!patch.ok) {\n console.warn(\n `⚠ kept ${list.length} attachments in \"${fieldName}\" — prune failed: ${patch.status} ${patch.statusText}`,\n );\n }\n } catch (e) {\n console.warn(`⚠ attachment prune skipped for \"${fieldName}\": ${(e as Error).message}`);\n }\n}\n"],"mappings":"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","names":["body"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openDb,
|
|
3
3
|
readDbConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MMSMZC7S.js";
|
|
5
5
|
|
|
6
6
|
// src/db/fleet-events.ts
|
|
7
7
|
async function recordFleetEvent(db, e) {
|
|
@@ -47,4 +47,4 @@ async function recordFleetEventsBestEffort(events, now, open = () => openDb(read
|
|
|
47
47
|
export {
|
|
48
48
|
recordFleetEventsBestEffort
|
|
49
49
|
};
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
50
|
+
//# sourceMappingURL=chunk-YXWBM26P.js.map
|
|
@@ -84,24 +84,30 @@ async function ingestSubmission(deps, slug, rawPayload, turnstileInput = "unveri
|
|
|
84
84
|
if (!normalized.ok) {
|
|
85
85
|
return { status: "rejected", reason: "invalid-payload", errors: normalized.errors };
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
site = await deps.getWebsiteBySlug(slug);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
if (!deps.deadLetter || isTestMode(rawPayload)) throw err;
|
|
87
|
+
const captureDeadLetter = async (error) => {
|
|
88
|
+
if (!deps.deadLetter || isTestMode(rawPayload)) return null;
|
|
92
89
|
const dl = await deps.deadLetter({
|
|
93
90
|
siteSlug: slug,
|
|
94
91
|
payload: rawPayload,
|
|
95
92
|
turnstile: verification,
|
|
96
|
-
error
|
|
93
|
+
error,
|
|
97
94
|
receivedAt: deps.now()
|
|
98
95
|
});
|
|
99
|
-
console.error(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
console.error(`[ingest] lead for '${slug}' dead-lettered as ${dl.id}: ${error}`);
|
|
97
|
+
return dl.id;
|
|
98
|
+
};
|
|
99
|
+
let site;
|
|
100
|
+
try {
|
|
101
|
+
site = await deps.getWebsiteBySlug(slug);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const id = await captureDeadLetter(String(err));
|
|
104
|
+
if (id === null) throw err;
|
|
105
|
+
return { status: "accepted", submissionId: id, notifyStatus: "skipped" };
|
|
106
|
+
}
|
|
107
|
+
if (!site) {
|
|
108
|
+
const id = await captureDeadLetter(`unknown-site: no fleet row for '${slug}'`);
|
|
109
|
+
return id === null ? { status: "unknown-site", slug } : { status: "unknown-site", slug, deadLetterId: id };
|
|
103
110
|
}
|
|
104
|
-
if (!site) return { status: "unknown-site", slug };
|
|
105
111
|
if (isTestMode(rawPayload)) {
|
|
106
112
|
return { status: "accepted", submissionId: "test-mode", notifyStatus: "skipped" };
|
|
107
113
|
}
|
|
@@ -287,4 +293,4 @@ export {
|
|
|
287
293
|
ingestSubmission,
|
|
288
294
|
hostsMatch
|
|
289
295
|
};
|
|
290
|
-
//# sourceMappingURL=chunk-
|
|
296
|
+
//# sourceMappingURL=chunk-Z7FDJ35L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/forms/payload.ts","../src/forms/ingest.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\nimport { parseReplyCopy, type ReplyCopy } from \"./reply-copy.js\";\n\n/** The JSON wire format a fleet site forwards to the ingest endpoint.\n *\n * Underscore-prefixed keys are RESERVED for transport concerns and are never\n * lead data: `_meta` (below) and `_screenOut` (the bot screen-out beacon —\n * a `{ _screenOut: \"honeypot\" | \"too-fast\" }` body is a counter ping, not a\n * submission; see ingest.ts `parseScreenOut`). Site field names must not\n * start with `_`. */\nexport type SubmissionPayload = {\n formType?: string;\n name?: string;\n firstName?: string;\n lastName?: string;\n email?: string;\n phone?: string;\n message?: string;\n sourceUrl?: string;\n utm?: string;\n /** Any additional, site-specific fields. */\n extra?: Record<string, unknown>;\n /** Reserved transport envelope (token/IP/UA); stripped by normalizeSubmission, never persisted. */\n _meta?: SubmissionMeta;\n /** Reserved: confirmation-email copy the SITE resolved from its own CMS.\n * Validated by parseReplyCopy, then stored under the same key in extraFields\n * so a replayed submission sends the same email it would have sent live. */\n _reply?: ReplyCopy;\n};\n\nexport type NormalizedSubmission = {\n formType: FormType;\n name: string;\n email: string;\n phone?: string;\n message?: string;\n sourceUrl?: string;\n utm?: string;\n extraFields: Record<string, unknown>;\n};\n\nexport type NormalizeResult =\n { ok: true; value: NormalizedSubmission } | { ok: false; errors: string[] };\n\nconst KNOWN_KEYS = new Set([\n \"formType\",\n \"name\",\n \"firstName\",\n \"lastName\",\n \"email\",\n \"phone\",\n \"message\",\n \"sourceUrl\",\n \"utm\",\n \"extra\",\n \"_meta\",\n \"_reply\",\n]);\n\n// Keys that, copied as own-properties into the captured object, could surprise a\n// downstream consumer (template/DB layer). This is an untrusted boundary, so the\n// extraFields merge drops them rather than trusting the caller.\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\nconst EMAIL_RE = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/;\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Resolve the submission's formType. An ABSENT/blank value defaults to \"contact\"\n * (the long-standing minimal-form contract). A PRESENT but unrecognized value\n * (e.g. a typo'd \"news\") returns null → the caller rejects it, rather than\n * silently storing it as contact — which would, for a newsletter signup, drop the\n * Mailchimp fan-out. Mirrors createIngestEndpoint's reject-invalid behavior.\n */\nfunction resolveFormType(raw: unknown): FormType | null {\n const s = str(raw);\n if (s === \"\") return \"contact\";\n return (SUBMISSION_FORM_TYPES as readonly string[]).includes(s) ? (s as FormType) : null;\n}\n\n/**\n * Defensively normalize an untrusted ingest payload into typed fields. Folds\n * name/first+last, lowercases email, and dumps every unclaimed key into\n * extraFields so no site-specific data is lost. Rejects only when there's\n * nothing to act on (no email AND no message) or a present email is malformed.\n */\nexport function normalizeSubmission(payload: unknown): NormalizeResult {\n if (typeof payload !== \"object\" || payload === null) {\n return { ok: false, errors: [\"payload must be a JSON object\"] };\n }\n const p = payload as Record<string, unknown>;\n const name = str(p.name) || [str(p.firstName), str(p.lastName)].filter(Boolean).join(\" \");\n const email = str(p.email).toLowerCase();\n const message = str(p.message);\n\n const formType = resolveFormType(p.formType);\n const errors: string[] = [];\n if (!email && !message) errors.push(\"at least one of email or message is required\");\n if (email && !EMAIL_RE.test(email)) errors.push(\"email is not a valid address\");\n if (formType === null) errors.push(\"formType is not a recognized form type\");\n // The `|| formType === null` is redundant for control flow (the push above\n // already made errors non-empty) but narrows formType to FormType past this line.\n if (errors.length > 0 || formType === null) return { ok: false, errors };\n\n const extraFields: Record<string, unknown> = {};\n const extra = p.extra;\n if (typeof extra === \"object\" && extra !== null) {\n for (const [k, v] of Object.entries(extra)) {\n if (!DANGEROUS_KEYS.has(k) && !k.startsWith(\"_\")) extraFields[k] = v;\n }\n }\n for (const [k, v] of Object.entries(p)) {\n if (!KNOWN_KEYS.has(k) && !DANGEROUS_KEYS.has(k) && !k.startsWith(\"_\")) extraFields[k] = v;\n }\n // The ONE underscore key a caller may set. Both loops above dropped the rest,\n // including any `_reply` smuggled in through `extra` — this is the only way a\n // value reaches that key, and it is validated on the way through. Without it,\n // a bot could POST its own copy and dictate the text of an email we send from\n // a domain with real sending reputation.\n const reply = parseReplyCopy(p._reply);\n if (reply) extraFields._reply = reply;\n\n const value: NormalizedSubmission = {\n formType,\n name,\n email,\n extraFields,\n };\n const phone = str(p.phone);\n if (phone) value.phone = phone;\n if (message) value.message = message;\n const sourceUrl = str(p.sourceUrl);\n if (sourceUrl) value.sourceUrl = sourceUrl;\n const utm = str(p.utm);\n if (utm) value.utm = utm;\n return { ok: true, value };\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport type {\n SubmissionRow,\n SubmissionInput,\n NotifyStatus,\n SubmissionStatus,\n} from \"../reports/submission-row.js\";\nimport { normalizeSubmission, type NormalizedSubmission } from \"./payload.js\";\nimport type { TurnstileOutcome, TurnstileVerification } from \"./turnstile.js\";\nimport { SPAM_THRESHOLD, type SpamVerdict } from \"./spam-classifier.js\";\n\n/** How far back the duplicate-spray AND repeat-sender lookups scan. Sprays arrive in\n * bursts but bots also re-run for weeks; 30 days catches repeats without an unbounded\n * table scan. */\nconst DUPLICATE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport type IngestDeps = {\n getWebsiteBySlug: (slug: string) => Promise<WebsiteRow | null>;\n createSubmission: (input: SubmissionInput) => Promise<SubmissionRow>;\n /** Optional last-resort capture for a lead whose site lookup THREW (a store\n * outage, not an unknown slug). When present, the lead is persisted here and\n * the visitor still gets \"accepted\"; `db replay-deadletters` re-runs it once\n * the lookup recovers. Absent → the error propagates exactly as before, so\n * callers that never wired it are byte-for-byte unchanged. */\n deadLetter?: (input: {\n siteSlug: string;\n payload: unknown;\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: Date;\n }) => Promise<{ id: string }>;\n notify: (\n site: WebsiteRow,\n submission: SubmissionRow,\n ) => Promise<{ status: NotifyStatus; messageId: string | null }>;\n stampNotified: (id: string, status: NotifyStatus, messageId: string | null) => Promise<void>;\n now: () => Date;\n /** Optional spam classifier. When present, its verdict drives the stored\n * status/score/reason; absent → every submission scores 0 (fail-open, clean).\n * Injected so the classifier stays a pure, transport-agnostic leaf. */\n classifySpam?: (n: NormalizedSubmission, turnstile: TurnstileOutcome) => SpamVerdict;\n /** Optional: fleet-wide duplicate/near-duplicate body lookup since `since`. Drives\n * the duplicate-spray signal — the same pitch blasted to many sites shows up as\n * identical (`exact`) or template-substituted (`similar`, token-set Jaccard) bodies.\n * Statuses let the retro re-bucket pick still-'new' prior copies; siteId+email let\n * the caller exempt a genuine same-sender resubmission on the same site. Absent →\n * the check is skipped (fail-open clean). */\n findRecentDuplicates?: (\n message: string,\n since: Date,\n ) => Promise<{\n exact: Array<{ id: string; status: string; siteId: string; email: string }>;\n similar: Array<{ id: string; status: string; siteId: string; email: string }>;\n }>;\n /** Optional: recent non-newsletter submissions from the same email since `since`.\n * Drives the cross-site repeat-sender signal — the fleet's sites are unrelated\n * businesses, so one address contacting 2+ of them within the window is a\n * solicitation tell. Absent → the check is skipped (fail-open clean). */\n listRecentSubmissionsForEmail?: (\n email: string,\n since: Date,\n ) => Promise<Array<{ id: string; siteId: string; status: string }>>;\n /** Optional: retroactively re-bucket prior still-'new' rows once a later copy\n * identifies a spray (the first copy is delivered by design). Implementations\n * must only touch status='new' rows. Best-effort — failures are swallowed. */\n retroBucket?: (ids: string[], reason: string) => Promise<void>;\n /** Optional: POST a newsletter submission to the site's configured webhook\n * (best-effort). Omitted in tests/handlers that don't need it. */\n forwardNewsletter?: (\n webhookUrl: string,\n submission: SubmissionRow,\n site: WebsiteRow,\n ) => Promise<{ ok: boolean; status: number }>;\n /** Optional: add a newsletter submitter to the site's Mailchimp audience\n * (best-effort). Omitted where unused. `tagged:false` reports a member who was\n * upserted but could NOT be tagged — recorded separately, see FANOUT below. */\n addToMailchimp?: (\n site: WebsiteRow,\n submission: SubmissionRow,\n ) => Promise<{ ok: boolean; status: number; tagged?: boolean }>;\n /** Optional: persist the newsletter fan-out outcome on the row (see FANOUT below).\n * Best-effort — failures are swallowed. Absent → the fan-out still runs, just\n * unrecorded (older callers and most tests). */\n stampFanout?: (id: string, fanoutStatus: string) => Promise<void>;\n /** Optional: run the best-effort tail (notify → stamp → fan-out) AFTER the\n * response instead of before it. Production passes Netlify's\n * `context.waitUntil`, which keeps the invocation alive until the promise\n * settles. Absent → the tail runs inline, exactly as it always did; this is a\n * latency change, never a behavioural one.\n *\n * Why it matters: the submitting site waits on this response under an abort\n * budget (forms/client.ts `INGEST_TIMEOUT_MS`). Once the row is written the\n * lead is captured, so every further second spent on email/webhook providers\n * is time in which a captured lead can still be reported to the visitor as a\n * failed submission — which is exactly what happened to 1836dig on\n * 2026-08-03. */\n defer?: (work: Promise<unknown>) => void;\n};\n\nexport type IngestResult =\n | { status: \"accepted\"; submissionId: string; notifyStatus: NotifyStatus | \"deferred\" }\n | { status: \"rejected\"; reason: \"invalid-payload\"; errors: string[] }\n /** #645. `deadLetterId` is present whenever the lead was captured on its way\n * out — i.e. `deadLetter` was wired and the payload was not a testMode probe.\n * The STATUS is unchanged on purpose: the handler still answers 404, so the\n * sending site still learns its slug does not resolve. */\n | { status: \"unknown-site\"; slug: string; deadLetterId?: string };\n\nexport type ScreenOutDeps = {\n getWebsiteBySlug: (slug: string) => Promise<WebsiteRow | null>;\n recordScreenOut: (siteId: string, reason: \"honeypot\" | \"too-fast\") => Promise<void>;\n};\n\nexport type ScreenOutResult =\n { status: \"recorded\"; slug: string } | { status: \"unknown-site\"; slug: string };\n\n/** Extract the screen-out reason from a beacon body, or null if it isn't one.\n * The beacon key is the reserved `_screenOut` (underscore-namespaced like `_meta`,\n * see payload.ts). The bare `screenOut` key is the DEPRECATED pre-namespacing wire\n * shape — sites run older package versions for a while, so the central receiver\n * keeps accepting it. `_screenOut` wins when both are present. */\nexport function parseScreenOut(payload: unknown): \"honeypot\" | \"too-fast\" | null {\n if (!payload || typeof payload !== \"object\") return null;\n const body = payload as Record<string, unknown>;\n const v = \"_screenOut\" in body ? body[\"_screenOut\"] : body[\"screenOut\"];\n return v === \"honeypot\" || v === \"too-fast\" ? v : null;\n}\n\n/** Resolve the site and record a caught screen-out. Best-effort: a record failure is\n * the caller's to swallow — a missed count must never error a screened bot. */\nexport async function ingestScreenOut(\n deps: ScreenOutDeps,\n slug: string,\n reason: \"honeypot\" | \"too-fast\",\n): Promise<ScreenOutResult> {\n const site = await deps.getWebsiteBySlug(slug);\n if (!site) return { status: \"unknown-site\", slug };\n await deps.recordScreenOut(site.id, reason);\n return { status: \"recorded\", slug };\n}\n\n/**\n * Normalize → resolve site → persist → notify → stamp. The order is load-bearing:\n * the row is written BEFORE notify, and notify/stamp failures are swallowed (logged)\n * so a Resend or Airtable-write-back hiccup can never turn an accepted lead into a 502.\n */\nexport async function ingestSubmission(\n deps: IngestDeps,\n slug: string,\n rawPayload: unknown,\n turnstileInput: TurnstileOutcome | TurnstileVerification = \"unverifiable\",\n): Promise<IngestResult> {\n // Accept either the full verification (the production handler) or a bare outcome\n // string (older callers and virtually every test) — a string simply has no solved-\n // hostname to check. Normalized once so the body has a single shape to reason about.\n const verification: TurnstileVerification =\n typeof turnstileInput === \"string\"\n ? { outcome: turnstileInput, hostname: null }\n : turnstileInput;\n const turnstile = verification.outcome;\n const normalized = normalizeSubmission(rawPayload);\n if (!normalized.ok) {\n return { status: \"rejected\", reason: \"invalid-payload\", errors: normalized.errors };\n }\n // Persist before enrich (#539 Phase 0). The lookup THROWING is a store outage,\n // not an answer — and until 2026-08-23 it was the one await that could cost a\n // lead: the row write comes later, so a thrown lookup 502'd the visitor with\n // nothing recorded anywhere (the 2026-08-17 Airtable quota outage did exactly\n // that, while the submissions store itself was healthy the whole time). With\n // `deadLetter` wired, the lead lands there and the visitor gets an honest\n // \"accepted\"; `db replay-deadletters` runs it through this same function once\n // the lookup recovers, producing a normal row with real classification+notify.\n //\n // Three deliberate boundaries:\n // - A testMode probe rethrows: it persists nothing by design, so there is\n // nothing to save, and swallowing the outage would green the form-e2e audit\n // precisely when central ingest is degraded.\n // - `deadLetter` itself throwing propagates: both stores are down, and the 502\n // is honest — there is nowhere left to put the lead.\n // - `deadLetter` absent → unchanged behaviour for every caller that never\n // wired it.\n //\n // #645: the ONE writer, used by both ways a lookup can fail to place a lead.\n // Returns null when there is nothing to capture (no writer, or a probe).\n const captureDeadLetter = async (error: string): Promise<string | null> => {\n if (!deps.deadLetter || isTestMode(rawPayload)) return null;\n const dl = await deps.deadLetter({\n siteSlug: slug,\n payload: rawPayload,\n turnstile: verification,\n error,\n receivedAt: deps.now(),\n });\n console.error(`[ingest] lead for '${slug}' dead-lettered as ${dl.id}: ${error}`);\n return dl.id;\n };\n\n let site: WebsiteRow | null;\n try {\n site = await deps.getWebsiteBySlug(slug);\n } catch (err) {\n const id = await captureDeadLetter(String(err));\n if (id === null) throw err;\n return { status: \"accepted\", submissionId: id, notifyStatus: \"skipped\" };\n }\n // #645. A lookup that RESOLVES to null used to return empty-handed — no row,\n // no alarm, no way back. The comment that justified it (\"a junk slug is a\n // rejection, not a lead to save\") predates the flip, and two facts retire it:\n //\n // 1. `/api/forms/:slug` is TOKEN-GATED before this function runs, so the\n // slugs that reach here belong to fleet sites, not to bots guessing.\n // 2. Post-#643 the lookup reads Turso only, so a real client site whose Turso\n // row is missing — a half-finished `ensure-site`, a deleted row, Phase 6 —\n // is indistinguishable here from a typo, and dropping it loses a paying\n // client's lead permanently and silently. It is the only failure in the\n // fleet that does that.\n //\n // The STATUS stays `unknown-site`, so the handler still answers 404 and the\n // sending site still learns its slug does not resolve. What changes is that\n // the lead now exists somewhere: the row is the record, the `deadletter`\n // attention item is the alarm, `db replay-deadletters` is the recovery.\n //\n // Unlike the throw path, a dead-letter failure here PROPAGATES rather than\n // degrading: the lookup just succeeded, so Turso is up, and a write failure on\n // a healthy store is a real error the sending site should see.\n if (!site) {\n const id = await captureDeadLetter(`unknown-site: no fleet row for '${slug}'`);\n return id === null\n ? { status: \"unknown-site\", slug }\n : { status: \"unknown-site\", slug, deadLetterId: id };\n }\n\n // Synthetic end-to-end probe (the `form-e2e` fleet audit). A central-only marker\n // on the payload routes the submission away from EVERY real sink: no row is\n // persisted, no spam classification, no operator/autoresponder email, no\n // newsletter fan-out — and Turnstile enforcement is bypassed (the short-circuit\n // sits before that check). The marker therefore grants a bot NO benefit: it\n // reaches no inbox/DB/webhook, so skipping Turnstile costs nothing. This\n // suppression MUST be central — the submitting site alone cannot stop the real\n // inbox firing. Validity + site resolution are still enforced above (a junk body\n // is rejected, an unknown slug is unknown-site), so the marker can't smuggle\n // anything through. Return accepted+skipped so the probe asserts success.\n if (isTestMode(rawPayload)) {\n return { status: \"accepted\", submissionId: \"test-mode\", notifyStatus: \"skipped\" };\n }\n\n const n = normalized.value;\n\n // A site still under construction is where the operator tests their OWN forms:\n // one address, several unrelated sites, minutes apart. That is byte-for-byte the\n // cross-site repeat-sender signature below, so a site in development reliably\n // auto-spams its own builder's test submissions — the row lands `spam_auto`,\n // notify is skipped and the cockpit hides it, which reads exactly like \"the form\n // is broken\" (it did, on 2026-08-17). Spam handling on a site with no real\n // visitors protects nothing, so skip ALL of it: content scoring, the Turnstile\n // escalation, the repeat-sender scan and the duplicate/spray scan — including\n // their retroactive re-bucketing of OTHER sites' rows, which a test submission\n // has no business triggering. Re-enabled the moment the site leaves development.\n const spamHandlingEnabled = site.status !== \"building\";\n\n // Fold the content signals + the Turnstile verdict into ONE spam decision.\n // Absent classifier → treat as clean (fail-open). A throwing classifier is\n // swallowed the same way — a bug in the heuristic must never turn an\n // otherwise-good lead into a 500; it just scores clean. On a `requireTurnstile`\n // site, both an ACTUAL \"fail\" (forged token) AND an \"absent\" token escalate to\n // auto-spam regardless of score — a real browser that renders the widget ALWAYS\n // sends a token, so a completely missing one is the direct-POST-bot signature. A\n // present-but-\"unverifiable\" token (expired/duplicate — a real browser DID render\n // the widget) stays neutral, as does anything on a site that hasn't opted in.\n let verdict: SpamVerdict = { score: 0, reasons: [] };\n if (spamHandlingEnabled && deps.classifySpam) {\n try {\n verdict = deps.classifySpam(n, turnstile);\n } catch (err) {\n console.error(`[ingest] classifySpam threw: ${String(err)}`);\n }\n }\n const reasons = [...verdict.reasons];\n let status: SubmissionStatus = verdict.score >= SPAM_THRESHOLD ? \"spam_auto\" : \"new\";\n if (\n spamHandlingEnabled &&\n site.requireTurnstile &&\n (turnstile === \"fail\" || turnstile === \"absent\")\n ) {\n status = \"spam_auto\";\n const reason = turnstile === \"fail\" ? \"turnstile-required-failed\" : \"turnstile-required-absent\";\n if (!reasons.includes(reason)) reasons.push(reason);\n } else if (\n spamHandlingEnabled &&\n site.requireTurnstile &&\n turnstile === \"pass\" &&\n verification.hostname !== null &&\n !turnstileHostnameAcceptable(verification.hostname, site.url)\n ) {\n // Defense-in-depth vs token farming: the token PASSED, but Cloudflare says it was\n // solved on a host unrelated to this site. Cloudflare domain-binds sitekeys, so\n // this only trips on a loose widget allowlist — still, a passing token from a\n // foreign host accompanying a gated site's submission is not a real visitor. A\n // null hostname (older responses) or an unparseable site.url skips the check\n // entirely (fail-open); subdomains of the site's host (www., previews) match.\n status = \"spam_auto\";\n if (!reasons.includes(\"turnstile-required-hostname\"))\n reasons.push(\"turnstile-required-hostname\");\n }\n\n // Cross-site repeat-sender signal: the fleet's sites are UNRELATED businesses\n // (art gallery, realtor, home builder…), so the same email writing to 2+ different\n // sites within the window is a solicitation tell no single-message scan can see.\n // Same-site repeats alone must NOT trigger — those are genuine follow-ups. The\n // operator explicitly accepts overblocking here (spam_auto is recoverable). Prior\n // still-'new' rows on OTHER sites are retro-bucketed too: the first copy of a\n // spray is delivered by design, so the copy that identifies it also cleans the\n // queue. The scan runs EVEN WHEN the incoming copy is already spam_auto — after the\n // classifier began catching whole spray families, an already-bucketed copy was\n // skipping this scan and the retro cleanup never fired for exactly the sprays it\n // was built for; escalation/reason still only applies when not already spam.\n // Best-effort — a lookup failure never blocks a lead.\n if (spamHandlingEnabled && n.formType !== \"newsletter\" && deps.listRecentSubmissionsForEmail) {\n try {\n const since = new Date(deps.now().getTime() - DUPLICATE_WINDOW_MS);\n const prior = await deps.listRecentSubmissionsForEmail(n.email, since);\n const otherSites = prior.filter((p) => p.siteId !== site.id);\n if (otherSites.length > 0) {\n if (status !== \"spam_auto\") {\n status = \"spam_auto\";\n if (!reasons.includes(\"repeat-sender\")) reasons.push(\"repeat-sender\");\n }\n const retroIds = otherSites.filter((p) => p.status === \"new\").map((p) => p.id);\n if (deps.retroBucket && retroIds.length > 0) {\n try {\n await deps.retroBucket(retroIds, \"retro:repeat-sender\");\n } catch (err) {\n console.error(`[ingest] retroBucket (repeat-sender) threw: ${String(err)}`);\n }\n }\n }\n } catch (err) {\n console.error(`[ingest] listRecentSubmissionsForEmail threw: ${String(err)}`);\n }\n }\n\n // Duplicate/spray signal: the same pitch blasted across the fleet (or repeated) is\n // a bot tell a lone content scan can't see. An identical body ('exact') OR a\n // template-substituted near-copy ('similar' — the live dog-harness spray differed\n // only in greeting; SEO sprays swap the target domain per site) → auto-spam\n // (recoverable). Prior still-'new' copies are retro-bucketed for the same reason\n // as above, and the scan runs even when the incoming copy is already spam_auto\n // (see the repeat-sender note). GENUINE-RESUBMIT EXEMPTION: a match from the SAME\n // sender on the SAME site is a real visitor double-submitting or resending after\n // silence, not spray evidence — without this exemption the resend was silently\n // bucketed AND retro-flipped the delivered original, vanishing an active lead\n // with no signal. Only cross-site or different-sender copies count. Guarded:\n // non-newsletter forms with a real body; the db helper ignores short bodies /\n // small token sets. Best-effort — a lookup failure never blocks a lead.\n if (\n spamHandlingEnabled &&\n n.formType !== \"newsletter\" &&\n n.message !== undefined &&\n deps.findRecentDuplicates\n ) {\n try {\n const since = new Date(deps.now().getTime() - DUPLICATE_WINDOW_MS);\n const dupes = await deps.findRecentDuplicates(n.message, since);\n const senderEmail = n.email.trim().toLowerCase();\n const isOwnResend = (m: { siteId: string; email: string }) =>\n m.siteId === site.id && m.email.trim().toLowerCase() === senderEmail;\n const exact = dupes.exact.filter((m) => !isOwnResend(m));\n const similar = dupes.similar.filter((m) => !isOwnResend(m));\n const reason =\n exact.length > 0 ? \"duplicate-body\" : similar.length > 0 ? \"similar-body\" : null;\n if (reason !== null) {\n if (status !== \"spam_auto\") {\n status = \"spam_auto\";\n if (!reasons.includes(reason)) reasons.push(reason);\n }\n const retroIds = [...exact, ...similar].filter((m) => m.status === \"new\").map((m) => m.id);\n if (deps.retroBucket && retroIds.length > 0) {\n try {\n await deps.retroBucket(retroIds, \"retro:duplicate-body\");\n } catch (err) {\n console.error(`[ingest] retroBucket (duplicate-body) threw: ${String(err)}`);\n }\n }\n }\n } catch (err) {\n console.error(`[ingest] findRecentDuplicates threw: ${String(err)}`);\n }\n }\n const spamReason = reasons.length > 0 ? reasons.join(\",\") : null;\n\n const row = await deps.createSubmission({\n siteId: site.id,\n formType: n.formType,\n name: n.name,\n email: n.email,\n extraFields: n.extraFields,\n status,\n spamScore: verdict.score,\n spamReason,\n // Optional fields spread only when present — exactOptionalPropertyTypes\n // forbids assigning `undefined` to an optional `phone?: string` etc.\n ...(n.phone !== undefined ? { phone: n.phone } : {}),\n ...(n.message !== undefined ? { message: n.message } : {}),\n ...(n.sourceUrl !== undefined ? { sourceUrl: n.sourceUrl } : {}),\n ...(n.utm !== undefined ? { utm: n.utm } : {}),\n submittedAt: deps.now(),\n });\n\n // Auto-spam (and operator-marked spam) is captured but silent: no operator\n // email, no autoresponder, no newsletter fan-out. Skip notify entirely and\n // record the honest \"skipped\" stamp. notify.ts also nulls both builders for\n // these statuses (defense in depth), but short-circuiting here means the\n // injected notify dep is never even invoked for a spam row.\n const isSpam = row.status === \"spam_auto\" || row.status === \"spam\";\n\n // ── best-effort tail ───────────────────────────────────────────────────────\n // notify → stamp → newsletter fan-out. NOTHING below can cost the lead: the\n // row is already durable, and every step here is swallowed+logged rather than\n // allowed to turn an accepted lead into a 502. That is precisely why it does\n // not belong on the visitor's critical path — the submitting site waits on\n // this response under an abort budget (forms/client.ts INGEST_TIMEOUT_MS), so\n // Resend latency used to be able to make a captured lead read as a failed\n // submission in the visitor's browser (1836dig, 2026-08-03).\n //\n // `deps.defer` (Netlify's `context.waitUntil` in production) runs the tail\n // AFTER the response goes out. Absent — every test, and any runtime without\n // post-response execution — it runs inline exactly as before, so this is a\n // latency change and never a behavioural one.\n const runTail = async (): Promise<NotifyStatus> => {\n let notify: { status: NotifyStatus; messageId: string | null };\n if (isSpam) {\n notify = { status: \"skipped\", messageId: null };\n } else {\n try {\n notify = await deps.notify(site, row);\n } catch (err) {\n console.error(`[ingest] notify threw: ${String(err)}`);\n notify = { status: \"failed\", messageId: null };\n }\n }\n try {\n await deps.stampNotified(row.id, notify.status, notify.messageId);\n } catch (err) {\n console.error(`[ingest] stampNotified failed: ${String(err)}`);\n }\n\n await runFanout();\n return notify.status;\n };\n\n // Newsletter fan-out: each configured destination fires best-effort and is\n // swallowed+logged — the lead is already persisted; never turn it into a 502.\n // Guarded on the row status so a spam signup is never forwarded to a site\n // webhook or added to a Mailchimp audience.\n //\n // FANOUT provenance (2026-07-31): the outcome is ALSO recorded on the row, one\n // `<destination>:<outcome>` token per attempt, comma-joined in attempt order —\n // `webhook:ok,mailchimp:401`. Outcome is `ok`, the HTTP status of a non-2xx\n // (`0` for a network error / misconfiguration, see mailchimp.ts), or `threw`.\n // `mailchimp-tags:failed` is appended when the member was upserted but the tag\n // write did not land. Until this existed the results were console.error-only, so\n // an expired Mailchimp key stopped signups reaching the audience while the row\n // still read `notify=sent` — healthy-looking, with the failure visible nowhere the\n // operator ever looks. Nothing attempted → nothing stamped (null stays \"no\n // destination configured\", NOT \"we tried and lost the answer\"). The stamp is\n // best-effort like everything else here: a provenance write must never cost a lead.\n // A const arrow, not a hoisted declaration: `site` is narrowed non-null above,\n // and that narrowing only survives into a closure created after it. Declared\n // below runTail (which calls it) but before either call site, so there is no TDZ.\n const runFanout = async (): Promise<void> => {\n const fanout: string[] = [];\n if (n.formType === \"newsletter\" && !isSpam) {\n if (site.newsletterWebhook && deps.forwardNewsletter) {\n try {\n const fwd = await deps.forwardNewsletter(site.newsletterWebhook, row, site);\n if (!fwd.ok)\n console.error(`[ingest] newsletter webhook → ${fwd.status} for ${site.name}`);\n fanout.push(fwd.ok ? \"webhook:ok\" : `webhook:${fwd.status}`);\n } catch (err) {\n console.error(`[ingest] newsletter webhook threw: ${String(err)}`);\n fanout.push(\"webhook:threw\");\n }\n }\n if (site.mailchimpApiKey && site.mailchimpAudienceId && deps.addToMailchimp) {\n try {\n const mc = await deps.addToMailchimp(site, row);\n if (!mc.ok) console.error(`[ingest] mailchimp add → ${mc.status} for ${site.name}`);\n fanout.push(mc.ok ? \"mailchimp:ok\" : `mailchimp:${mc.status}`);\n // Only meaningful alongside a successful add — a failed add was never tagged\n // either, and one token per real problem keeps the row readable.\n if (mc.ok && mc.tagged === false) {\n console.error(`[ingest] mailchimp tags not applied for ${site.name}`);\n fanout.push(\"mailchimp-tags:failed\");\n }\n } catch (err) {\n console.error(`[ingest] mailchimp add threw: ${String(err)}`);\n fanout.push(\"mailchimp:threw\");\n }\n }\n }\n if (fanout.length > 0 && deps.stampFanout) {\n try {\n await deps.stampFanout(row.id, fanout.join(\",\"));\n } catch (err) {\n console.error(`[ingest] stampFanout failed: ${String(err)}`);\n }\n }\n };\n\n if (deps.defer) {\n // A rejection escaping here would land as an unhandled rejection in the\n // platform's post-response context, where nothing is left to catch it — so\n // the boundary is made unrejectable. Everything inside is already swallowed;\n // this guards the shape, not a known throw.\n deps.defer(\n runTail().catch((err) => {\n console.error(`[ingest] deferred tail threw: ${String(err)}`);\n }),\n );\n // The lead is durable and the response can go out now. The real notify\n // outcome lands on the ROW via stampNotified moments later — this field is\n // only ever read by callers wanting the in-request outcome, which by\n // definition does not exist once the tail is deferred.\n return { status: \"accepted\", submissionId: row.id, notifyStatus: \"deferred\" };\n }\n return { status: \"accepted\", submissionId: row.id, notifyStatus: await runTail() };\n}\n\n/** True when `a` and `b` are the same host or one is a subdomain of the other\n * (case-insensitive): `www.reddoorla.com` vs `reddoorla.com` matches both ways.\n * Exported for tests. PURE. */\nexport function hostsMatch(a: string, b: string): boolean {\n const ha = a.trim().toLowerCase();\n const hb = b.trim().toLowerCase();\n if (ha.length === 0 || hb.length === 0) return false;\n return ha === hb || ha.endsWith(`.${hb}`) || hb.endsWith(`.${ha}`);\n}\n\n/** Whether a passing token's solved-hostname is acceptable for the site at `siteUrl`.\n * An unparseable/hostless `siteUrl` returns TRUE — the check self-disables rather\n * than punishing a possibly-real visitor for an operator data problem (fail-open,\n * same philosophy as verifyTurnstile). PURE. */\nexport function turnstileHostnameAcceptable(tokenHostname: string, siteUrl: string): boolean {\n let siteHost: string;\n try {\n siteHost = new URL(siteUrl).hostname;\n } catch {\n return true;\n }\n if (!siteHost) return true;\n return hostsMatch(tokenHostname, siteHost);\n}\n\n/** True when an untrusted ingest payload carries the synthetic-probe marker\n * (top-level `testMode: true`). Read from the RAW payload so the branch never\n * depends on normalization internals; any non-`true` value is ignored (a real\n * visitor's form never sets it — the starter only forwards it when the submitted\n * form field `testMode` equals \"true\"). */\nexport function isTestMode(rawPayload: unknown): boolean {\n if (!rawPayload || typeof rawPayload !== \"object\") return false;\n return (rawPayload as Record<string, unknown>).testMode === true;\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli/bin.js
CHANGED
|
@@ -8,6 +8,36 @@ import { dirname as dirname2 } from "path";
|
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { cac } from "cac";
|
|
10
10
|
|
|
11
|
+
// src/cli/retired-flags.ts
|
|
12
|
+
var RETIRED_FLAGS = {
|
|
13
|
+
"--write-airtable": "--write-back"
|
|
14
|
+
};
|
|
15
|
+
function rewriteRetiredFlags(argv, note = (message) => console.error(message)) {
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
const out = argv.slice(0, 2);
|
|
18
|
+
let terminated = false;
|
|
19
|
+
for (const token of argv.slice(2)) {
|
|
20
|
+
if (terminated || token === "--") {
|
|
21
|
+
terminated = true;
|
|
22
|
+
out.push(token);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const eq = token.indexOf("=");
|
|
26
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
27
|
+
const next = Object.hasOwn(RETIRED_FLAGS, name) ? RETIRED_FLAGS[name] : void 0;
|
|
28
|
+
if (next === void 0) {
|
|
29
|
+
out.push(token);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!seen.has(name)) {
|
|
33
|
+
seen.add(name);
|
|
34
|
+
note(`note: ${name} is now ${next}; the old spelling still works.`);
|
|
35
|
+
}
|
|
36
|
+
out.push(eq === -1 ? next : `${next}${token.slice(eq)}`);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
// src/cli/version.ts
|
|
12
42
|
import { readFileSync, existsSync } from "fs";
|
|
13
43
|
import { dirname, join } from "path";
|
|
@@ -91,9 +121,9 @@ cli.command("list-recipes", "Print the available recipes.").action(() => {
|
|
|
91
121
|
});
|
|
92
122
|
cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option(
|
|
93
123
|
"--fleet <inventory>",
|
|
94
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
124
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
95
125
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").option(
|
|
96
|
-
"--write-
|
|
126
|
+
"--write-back [slug]",
|
|
97
127
|
"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name."
|
|
98
128
|
).option("--fail-on-violations", "Exit non-zero if any a11y violations are found (for CI gates)").option(
|
|
99
129
|
"--url <url>",
|
|
@@ -109,37 +139,43 @@ cli.command("audit [site]", "Run audits against a site (default: cwd).").option(
|
|
|
109
139
|
);
|
|
110
140
|
cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option(
|
|
111
141
|
"--fleet <inventory>",
|
|
112
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
142
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
113
143
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
114
144
|
async (site, opts) => runOrExit(
|
|
115
|
-
async () => (await import("../sync-configs-
|
|
145
|
+
async () => (await import("../sync-configs-OXSWKVZ2.js")).runSyncConfigsCommand(site, opts),
|
|
116
146
|
opts
|
|
117
147
|
)
|
|
118
148
|
);
|
|
119
149
|
cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option(
|
|
120
150
|
"--fleet <inventory>",
|
|
121
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
151
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
122
152
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
123
153
|
async (site, opts) => runOrExit(
|
|
124
|
-
async () => (await import("../bump-deps-
|
|
154
|
+
async () => (await import("../bump-deps-775VLDYB.js")).runBumpDepsCommand(site, opts),
|
|
125
155
|
opts
|
|
126
156
|
)
|
|
127
157
|
);
|
|
128
158
|
cli.command(
|
|
129
159
|
"self-updating [site]",
|
|
130
160
|
"Bootstrap a repo to keep itself updated (Renovate + protection; platform auto-merge off; ci.yml is the starter's, not managed here)."
|
|
131
|
-
).option("--dry", "List what would be enabled without writing or opening PRs").option(
|
|
161
|
+
).option("--dry", "List what would be enabled without writing or opening PRs").option(
|
|
162
|
+
"--fleet <inventory>",
|
|
163
|
+
'Inventory file (.json or .mjs/.js), or "turso" ("airtable" is a deprecated alias)'
|
|
164
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
132
165
|
async (site, opts) => runOrExit(
|
|
133
|
-
async () => (await import("../self-updating-
|
|
166
|
+
async () => (await import("../self-updating-ZUWPGYUP.js")).runSelfUpdatingCommand(site, opts),
|
|
134
167
|
opts
|
|
135
168
|
)
|
|
136
169
|
);
|
|
137
170
|
cli.command(
|
|
138
171
|
"prismic-ci [site]",
|
|
139
172
|
"Add the Prismic model delivery workflow to a site via PR (one PR per repo; never pushes to main)."
|
|
140
|
-
).option("--dry", "List the sites that would be offered the workflow, without opening any PR").option(
|
|
173
|
+
).option("--dry", "List the sites that would be offered the workflow, without opening any PR").option(
|
|
174
|
+
"--fleet <inventory>",
|
|
175
|
+
'Inventory file (.json or .mjs/.js), or "turso" ("airtable" is a deprecated alias)'
|
|
176
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
141
177
|
async (site, opts) => runOrExit(
|
|
142
|
-
async () => (await import("../prismic-ci-
|
|
178
|
+
async () => (await import("../prismic-ci-NAPWOXXD.js")).runPrismicCiCommand(site, opts),
|
|
143
179
|
opts
|
|
144
180
|
)
|
|
145
181
|
);
|
|
@@ -149,21 +185,24 @@ cli.command(
|
|
|
149
185
|
).option(
|
|
150
186
|
"--apply",
|
|
151
187
|
"Push local models to Prismic. Never deletes \u2014 remote-only models are reported."
|
|
152
|
-
).option("--pull", "Write remote-only models into the repo (single-site; review + PR the result).").option("--tokens", "Print the per-site write-token doctor: which env var, present?, reads?").option(
|
|
188
|
+
).option("--pull", "Write remote-only models into the repo (single-site; review + PR the result).").option("--tokens", "Print the per-site write-token doctor: which env var, present?, reads?").option(
|
|
189
|
+
"--fleet <inventory>",
|
|
190
|
+
'Inventory file (.json or .mjs/.js), or "turso" ("airtable" is a deprecated alias). Read-only.'
|
|
191
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").option("--write-back", "Fleet mode: persist each site's verdict to its Websites row").option(
|
|
153
192
|
"--comment-file <path>",
|
|
154
193
|
"Write the report to this file (the CI workflow posts it as a PR comment)"
|
|
155
194
|
).action(
|
|
156
195
|
async (site, opts) => runOrExit(
|
|
157
|
-
async () => (await import("../prismic-models-
|
|
196
|
+
async () => (await import("../prismic-models-LL6ORTQ2.js")).runPrismicModelsCommand(site, opts),
|
|
158
197
|
opts
|
|
159
198
|
)
|
|
160
199
|
);
|
|
161
200
|
cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option(
|
|
162
201
|
"--fleet <inventory>",
|
|
163
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
202
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
164
203
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
165
204
|
async (upgrade, site, opts) => runOrExit(
|
|
166
|
-
async () => (await import("../upgrade-
|
|
205
|
+
async () => (await import("../upgrade-EVAL3JTO.js")).runUpgradeCommand(upgrade, site, opts),
|
|
167
206
|
opts
|
|
168
207
|
)
|
|
169
208
|
);
|
|
@@ -172,19 +211,19 @@ cli.command(
|
|
|
172
211
|
"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts)."
|
|
173
212
|
).option(
|
|
174
213
|
"--fleet <inventory>",
|
|
175
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
214
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
176
215
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
177
216
|
async (site, opts) => runOrExit(
|
|
178
|
-
async () => (await import("../convert-to-pnpm-
|
|
217
|
+
async () => (await import("../convert-to-pnpm-KLWA53N5.js")).runConvertToPnpmCommand(site, opts),
|
|
179
218
|
opts
|
|
180
219
|
)
|
|
181
220
|
);
|
|
182
221
|
cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option(
|
|
183
222
|
"--fleet <inventory>",
|
|
184
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
223
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
185
224
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
186
225
|
async (site, opts) => runOrExit(
|
|
187
|
-
async () => (await import("../svelte-codemods-
|
|
226
|
+
async () => (await import("../svelte-codemods-VOLOKKFI.js")).runSvelteCodemodsCommand(site, opts),
|
|
188
227
|
opts
|
|
189
228
|
)
|
|
190
229
|
);
|
|
@@ -193,10 +232,10 @@ cli.command(
|
|
|
193
232
|
"Write src/routes/health/+server.ts (function-health probe for the report gate)."
|
|
194
233
|
).option(
|
|
195
234
|
"--fleet <inventory>",
|
|
196
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
235
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
197
236
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
198
237
|
async (site, opts) => runOrExit(
|
|
199
|
-
async () => (await import("../health-endpoint-
|
|
238
|
+
async () => (await import("../health-endpoint-BJ6BKRHF.js")).runHealthEndpointCommand(site, opts),
|
|
200
239
|
opts
|
|
201
240
|
)
|
|
202
241
|
);
|
|
@@ -205,10 +244,10 @@ cli.command(
|
|
|
205
244
|
"Install the matching-a-page gate harness (dev-guarded /dev/match/[uid] route + matching/ scripts) for a live-reference rebuild."
|
|
206
245
|
).option("--ref <url>", "Reference (live) site URL the harness gates against \u2014 required").option("--cand <url>", "Candidate origin (default http://localhost:5173)").option("--matrix <list>", "Breakpoint matrix, comma-separated (default 1440,834,390)").option(
|
|
207
246
|
"--fleet <inventory>",
|
|
208
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
247
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
209
248
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
210
249
|
async (site, opts) => runOrExit(
|
|
211
|
-
async () => (await import("../match-harness-
|
|
250
|
+
async () => (await import("../match-harness-GBCDSVPS.js")).runMatchHarnessCommand(site, opts),
|
|
212
251
|
opts
|
|
213
252
|
)
|
|
214
253
|
);
|
|
@@ -217,10 +256,10 @@ cli.command(
|
|
|
217
256
|
"Add the smoke suite (test:smoke + playwright config + /health smoke routes)."
|
|
218
257
|
).option(
|
|
219
258
|
"--fleet <inventory>",
|
|
220
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
259
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
221
260
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
222
261
|
async (site, opts) => runOrExit(
|
|
223
|
-
async () => (await import("../smoke-suite-
|
|
262
|
+
async () => (await import("../smoke-suite-WAUPI6EJ.js")).runSmokeSuiteCommand(site, opts),
|
|
224
263
|
opts
|
|
225
264
|
)
|
|
226
265
|
);
|
|
@@ -229,10 +268,10 @@ cli.command(
|
|
|
229
268
|
"Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm)."
|
|
230
269
|
).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option(
|
|
231
270
|
"--fleet <inventory>",
|
|
232
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
271
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
233
272
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
234
273
|
async (site, opts) => runOrExit(
|
|
235
|
-
async () => (await import("../onboard-
|
|
274
|
+
async () => (await import("../onboard-JQQIMQW7.js")).runOnboardCommand(site, opts),
|
|
236
275
|
opts
|
|
237
276
|
)
|
|
238
277
|
);
|
|
@@ -241,22 +280,25 @@ cli.command(
|
|
|
241
280
|
"One-shot guided onboarding: convert-to-pnpm \u2192 onboard \u2192 sync-configs \u2192 svelte-codemods \u2192 a11y-fixtures-page \u2192 health-endpoint \u2192 smoke-suite \u2192 audit."
|
|
242
281
|
).option(
|
|
243
282
|
"--fleet <inventory>",
|
|
244
|
-
'Inventory file (.json or .mjs/.js), or "
|
|
283
|
+
'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
|
|
245
284
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
246
|
-
async (site, opts) => runOrExit(async () => (await import("../init-
|
|
285
|
+
async (site, opts) => runOrExit(async () => (await import("../init-GOWDA5QS.js")).runInitCommand(site, opts), opts)
|
|
247
286
|
);
|
|
248
287
|
cli.command(
|
|
249
288
|
"launch <site>",
|
|
250
289
|
"Bootstrap + first-audit a site, then draft its launch email for approval."
|
|
251
290
|
).action(
|
|
252
291
|
async (site, opts) => runOrExit(
|
|
253
|
-
async () => (await import("../launch-
|
|
292
|
+
async () => (await import("../launch-7N3X2MXB.js")).runLaunchCommand(site, opts),
|
|
254
293
|
opts
|
|
255
294
|
)
|
|
256
295
|
);
|
|
257
|
-
cli.command("header-image [site]", "Generate a site's report header image from its live homepage.").option("--all", "Every live site with no Header image yet (backfill)").option("--force", "With --all, regenerate sites that already have one").option("--write-
|
|
296
|
+
cli.command("header-image [site]", "Generate a site's report header image from its live homepage.").option("--all", "Every live site with no Header image yet (backfill)").option("--force", "With --all, regenerate sites that already have one").option("--write-back", "Upload to the Websites row instead of writing a local file").option("--out-dir <path>", "Directory for local output (default: reports/)").option("--settle-ms <ms>", "Override the post-load settle delay for slow/animated homepages").option(
|
|
297
|
+
"--consent-selector <css>",
|
|
298
|
+
"Extra CSS selector(s) to hide before the shutter (consent UI the heuristic misses)"
|
|
299
|
+
).action(
|
|
258
300
|
async (site, opts) => runOrExit(
|
|
259
|
-
async () => (await import("../header-image-
|
|
301
|
+
async () => (await import("../header-image-H7ZJAV26.js")).runHeaderImageCommand(site, opts),
|
|
260
302
|
opts
|
|
261
303
|
)
|
|
262
304
|
);
|
|
@@ -265,7 +307,7 @@ cli.command(
|
|
|
265
307
|
"Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
|
|
266
308
|
).action(
|
|
267
309
|
async (site, opts) => runOrExit(
|
|
268
|
-
async () => (await import("../announce-
|
|
310
|
+
async () => (await import("../announce-5ZD2SBGD.js")).runAnnounceCommand(site, opts),
|
|
269
311
|
opts
|
|
270
312
|
)
|
|
271
313
|
);
|
|
@@ -274,7 +316,7 @@ cli.command(
|
|
|
274
316
|
"Show who a form submission would email, and optionally flip the pre-launch guard (read-back confirmed)."
|
|
275
317
|
).option("--set <on|off>", "Flip the guard: on = route to operator, off = restore.").option("--restore <status>", "Status to restore with --set off. Required; never inferred.").action(
|
|
276
318
|
async (site, opts) => runOrExit(
|
|
277
|
-
async () => (await import("../forms-notify-target-
|
|
319
|
+
async () => (await import("../forms-notify-target-X6UEOI2M.js")).runFormsNotifyTargetCommand(
|
|
278
320
|
site,
|
|
279
321
|
opts
|
|
280
322
|
),
|
|
@@ -286,7 +328,7 @@ cli.command(
|
|
|
286
328
|
"Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself."
|
|
287
329
|
).option("--type <type>", "Report type: announcement (default) | maintenance | testing | launch").option("--to <addr>", "Recipient(s), comma-separated. Default: OPERATOR_EMAIL.").option("--all", "Send a preview for every maintenance site (to --to/operator).").option("--dry-run", "Render only; write reports/<slug>/selftest-<type>.html; do not send.").action(
|
|
288
330
|
async (kind, site, opts) => runOrExit(
|
|
289
|
-
async () => (await import("../selftest-
|
|
331
|
+
async () => (await import("../selftest-G4W3LIBQ.js")).runSelftestCommand(kind, site, opts),
|
|
290
332
|
opts
|
|
291
333
|
)
|
|
292
334
|
);
|
|
@@ -298,13 +340,16 @@ cli.command(
|
|
|
298
340
|
"Fleet mode: announcement\u2192maintenance sites; maintenance/testing\u2192all schedulable sites."
|
|
299
341
|
).action(
|
|
300
342
|
async (site, opts) => runOrExit(
|
|
301
|
-
async () => (await import("../preflight-
|
|
343
|
+
async () => (await import("../preflight-O7Q6FWHU.js")).runPreflightCommand(site, opts),
|
|
302
344
|
opts
|
|
303
345
|
)
|
|
304
346
|
);
|
|
305
|
-
cli.command(
|
|
347
|
+
cli.command(
|
|
348
|
+
"ensure-site <slug>",
|
|
349
|
+
"Create/verify a site's fleet row in Turso (new sites get a site_<ULID> id)."
|
|
350
|
+
).option("--name <display name>", "Human Name for the row (client-facing copy uses it verbatim).").option("--url <url>", "Deployed URL (e.g. the Netlify site URL).").option("--contact <email>", "point of contact \u2014 the client address reports resolve to.").option("--git-repo <owner/repo>", "GitHub identity. Default on create: reddoorla/<slug>.").action(
|
|
306
351
|
async (slug, opts) => runOrExit(
|
|
307
|
-
async () => (await import("../ensure-site-
|
|
352
|
+
async () => (await import("../ensure-site-2QZHFVQ3.js")).runEnsureSiteCommand(slug, opts),
|
|
308
353
|
opts
|
|
309
354
|
)
|
|
310
355
|
);
|
|
@@ -323,9 +368,15 @@ cli.command(
|
|
|
323
368
|
).option("--no-probes", "Skip the live AI-visibility probes (tier 3).").option("--out <file>", "Also write the rendered report to this file.").option("--json", "Print the raw result JSON instead of the report summary.").option(
|
|
324
369
|
"--email",
|
|
325
370
|
"Email the internal sheet (scores, what wasn't measured, top fixes, the link) to PROSPECT_AUDIT_RECIPIENTS."
|
|
371
|
+
).option(
|
|
372
|
+
"--terms <list>",
|
|
373
|
+
"Comma-separated search terms for the visibility section, chosen by hand. Blank generates them from the site, as before."
|
|
374
|
+
).option(
|
|
375
|
+
"--questions <list>",
|
|
376
|
+
"Comma-separated buyer questions, chosen by hand. Blank asks the goal's fixed set. A chosen set gets its own version key, so comparisons never mix sets."
|
|
326
377
|
).action(
|
|
327
378
|
async (url, opts) => runOrExit(
|
|
328
|
-
async () => (await import("../prospect-audit-
|
|
379
|
+
async () => (await import("../prospect-audit-FMPGQIRQ.js")).runProspectAuditCommand(url, opts),
|
|
329
380
|
opts
|
|
330
381
|
)
|
|
331
382
|
);
|
|
@@ -346,18 +397,18 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
|
|
|
346
397
|
"Regenerate one UNSENT report's stored HTML from its current row, so the console preview shows edited commentary. Refuses a sent report."
|
|
347
398
|
).action(
|
|
348
399
|
async (site, opts) => runOrExit(
|
|
349
|
-
async () => (await import("../report-
|
|
400
|
+
async () => (await import("../report-IAHXN5CW.js")).runReportCommand(site, opts),
|
|
350
401
|
opts
|
|
351
402
|
)
|
|
352
403
|
);
|
|
353
404
|
cli.command(
|
|
354
405
|
"github-signals",
|
|
355
|
-
"Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write
|
|
356
|
-
).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-
|
|
406
|
+
"Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write each site's row."
|
|
407
|
+
).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-back", "Write each site's signals back to its Websites row.").action(
|
|
357
408
|
async (opts) => runOrExit(
|
|
358
|
-
async () => (await import("../github-signals-
|
|
409
|
+
async () => (await import("../github-signals-QIRTHHET.js")).runGitHubSignalsCommand({
|
|
359
410
|
fleet: opts.fleet,
|
|
360
|
-
|
|
411
|
+
writeBack: opts.writeBack
|
|
361
412
|
}),
|
|
362
413
|
opts
|
|
363
414
|
)
|
|
@@ -367,7 +418,7 @@ cli.command(
|
|
|
367
418
|
"Verify every public org repo carries a sound branch ruleset (empty bypass, PR-required)."
|
|
368
419
|
).option("--org <org>", "GitHub org to sweep (enumerated from the API, never a hand list).").action(
|
|
369
420
|
async (opts) => runOrExit(
|
|
370
|
-
async () => (await import("../protection-audit-
|
|
421
|
+
async () => (await import("../protection-audit-YXJKFU6O.js")).runProtectionAuditCommand({
|
|
371
422
|
org: opts.org
|
|
372
423
|
}),
|
|
373
424
|
opts
|
|
@@ -385,8 +436,17 @@ cli.command(
|
|
|
385
436
|
).option(
|
|
386
437
|
"--force",
|
|
387
438
|
"import-airtable / sync: run despite the freeze (#643) \u2014 a deliberate rollback-window converge from the frozen Airtable shadow. Without it both refuse while Turso is authoritative, because an import overwrites authoritative rows."
|
|
439
|
+
).option(
|
|
440
|
+
"--abandon <slug-or-id>",
|
|
441
|
+
"replay-deadletters: mark a slug's queued leads (or one dl_\u2026 row) abandoned \u2014 terminal by decision, dropped from the replay queue and the cockpit alarm, row and payload kept."
|
|
442
|
+
).option(
|
|
443
|
+
"--reason <text>",
|
|
444
|
+
"replay-deadletters --abandon: why the leads are being written off. Required."
|
|
445
|
+
).option(
|
|
446
|
+
"--by <who>",
|
|
447
|
+
"replay-deadletters --abandon: who decided (default: OPERATOR_EMAIL, else 'operator')."
|
|
388
448
|
).action(
|
|
389
|
-
async (action, opts) => runOrExit(async () => (await import("../db-
|
|
449
|
+
async (action, opts) => runOrExit(async () => (await import("../db-II5WAK4D.js")).runDbCommand(action, opts), opts)
|
|
390
450
|
);
|
|
391
451
|
cli.command(
|
|
392
452
|
"submissions <action>",
|
|
@@ -396,7 +456,7 @@ cli.command(
|
|
|
396
456
|
"Write the re-buckets: status \u2192 spam_auto, spam_score/spam_reason replaced with the new verdict + 'retro-rescore' marker."
|
|
397
457
|
).action(
|
|
398
458
|
async (action, opts) => runOrExit(
|
|
399
|
-
async () => (await import("../submissions-
|
|
459
|
+
async () => (await import("../submissions-2SHCFOKD.js")).runSubmissionsCommand(action, opts),
|
|
400
460
|
opts
|
|
401
461
|
)
|
|
402
462
|
);
|
|
@@ -405,10 +465,10 @@ cli.command(
|
|
|
405
465
|
"Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns."
|
|
406
466
|
).option("--fleet", "Run across every active, repo-backed site in the Airtable inventory.").action(
|
|
407
467
|
async (opts) => runOrExit(
|
|
408
|
-
async () => (await import("../renovate-dispatch-
|
|
468
|
+
async () => (await import("../renovate-dispatch-2IHAZDVA.js")).runRenovateDispatchCommand({
|
|
409
469
|
fleet: opts.fleet,
|
|
410
470
|
// #539 Phase 5: mirror the auto-fix counter into site_health.
|
|
411
|
-
siteMirror: await (await import("../site-mirror-
|
|
471
|
+
siteMirror: await (await import("../site-mirror-WR7ACXOV.js")).makeSiteMirror()
|
|
412
472
|
}),
|
|
413
473
|
opts
|
|
414
474
|
)
|
|
@@ -433,7 +493,7 @@ cli.command(
|
|
|
433
493
|
"freeze: JSON declaring slots the template has no token for (video poster, a panel rebuilt from a baked image). Keys must start with 'x.'"
|
|
434
494
|
).action(
|
|
435
495
|
async (action, dir, opts) => runOrExit(
|
|
436
|
-
async () => (await import("../blux-
|
|
496
|
+
async () => (await import("../blux-NXQLMS4W.js")).runBluxCommand(action, dir, opts),
|
|
437
497
|
opts
|
|
438
498
|
)
|
|
439
499
|
);
|
|
@@ -445,7 +505,7 @@ cli.command(
|
|
|
445
505
|
"Output directory for capture (default: webflow-out) / docs (default: dirname of the ir.json)"
|
|
446
506
|
).action(
|
|
447
507
|
async (action, target, opts) => runOrExit(
|
|
448
|
-
async () => (await import("../webflow-
|
|
508
|
+
async () => (await import("../webflow-624FEBFD.js")).runWebflowCommand(action, target, opts),
|
|
449
509
|
opts
|
|
450
510
|
)
|
|
451
511
|
);
|
|
@@ -458,7 +518,7 @@ cli.on("command:*", () => {
|
|
|
458
518
|
);
|
|
459
519
|
process.exit(1);
|
|
460
520
|
});
|
|
461
|
-
cli.parse();
|
|
521
|
+
cli.parse(rewriteRetiredFlags(process.argv));
|
|
462
522
|
export {
|
|
463
523
|
runOrExit
|
|
464
524
|
};
|