@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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
|
|
2
|
+
|
|
3
|
+
type AirtableConfig = {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseId: string;
|
|
6
|
+
};
|
|
7
|
+
type AirtableBase = ReturnType<typeof openBase>;
|
|
8
|
+
declare function openBase(cfg: AirtableConfig): airtable_lib_airtable_base_js.AirtableBase;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The site-status vocabulary — ONE module owns it (#539 Phase 4).
|
|
12
|
+
*
|
|
13
|
+
* It lived at `src/reports/airtable/site-status.ts` until #539 Phase 6 step 1
|
|
14
|
+
* (#646) moved it here: `canonicalizeStatus` runs on every Turso lead read, so
|
|
15
|
+
* it cannot sit in the directory Phase 6 deletes. The Airtable write seam
|
|
16
|
+
* `toAirtableStatus`, discussed below, stayed behind in that file, which also
|
|
17
|
+
* re-exports everything here.
|
|
18
|
+
*
|
|
19
|
+
* The fleet's lifecycle names were inherited from an Airtable single-select
|
|
20
|
+
* written by hand years ago ("probably not our problem"). The operator approved
|
|
21
|
+
* a canonical vocabulary, and the migration is now COMPLETE in both stores:
|
|
22
|
+
* Airtable's "Status" single-select carries exactly the six canonical options
|
|
23
|
+
* and nothing else. This module no longer knows the old names at all.
|
|
24
|
+
*
|
|
25
|
+
* The retired mapping, kept as the record of what was merged into what:
|
|
26
|
+
*
|
|
27
|
+
* old (retired 2026-08-25) new (canonical)
|
|
28
|
+
* ───────────────────────── ───────────────
|
|
29
|
+
* in development building
|
|
30
|
+
* launch period launching
|
|
31
|
+
* maintenance maintained
|
|
32
|
+
* hosting hosted-only
|
|
33
|
+
* probably not our problem external
|
|
34
|
+
* legacy ┐
|
|
35
|
+
* deprecated ┴ archived ← approved MERGE, many-to-one
|
|
36
|
+
*
|
|
37
|
+
* `legacy` and `deprecated` were already treated identically by every PREDICATE
|
|
38
|
+
* (ARCHIVED_STATUSES held both), so collapsing them changes no *selection* — no
|
|
39
|
+
* fleet op gains or loses a site. It is NOT true that collapsing them changes
|
|
40
|
+
* nothing at all, and one surface proved it: the cockpit's Archived lane LABELS
|
|
41
|
+
* each row with its Status cell, and canonicalizing there rendered all 12 live
|
|
42
|
+
* archived rows as "archived". That lane now labels from `statusRaw`, which is
|
|
43
|
+
* exactly why `WebsiteRow.statusRaw` exists. The rule this leaves behind:
|
|
44
|
+
* canonical values decide BEHAVIOUR; anything DISPLAYING a Status cell, or
|
|
45
|
+
* writing one back, uses the raw cell.
|
|
46
|
+
*
|
|
47
|
+
* The merge had NO clean reverse map while both vocabularies coexisted, which
|
|
48
|
+
* is why `toAirtableStatus` had to pick one archived name deliberately. With the
|
|
49
|
+
* old names retired there is nothing left to pick between, and no code path
|
|
50
|
+
* feeds operator-supplied text through it anyway (see
|
|
51
|
+
* `src/recipes/forms-notify-target.ts`, which writes `--restore` verbatim).
|
|
52
|
+
*
|
|
53
|
+
* THE SHAPE OF THE TRANSITION (three stages):
|
|
54
|
+
*
|
|
55
|
+
* stage 1 (DONE) code speaks the NEW vocabulary internally; reads canonicalize
|
|
56
|
+
* at the two Airtable/Turso seams; writes still emit the OLD
|
|
57
|
+
* Airtable option names. Airtable untouched. No SELECTION
|
|
58
|
+
* change — every predicate picks exactly the sites it picked
|
|
59
|
+
* before (pinned row-by-row in tests/reports/airtable/
|
|
60
|
+
* site-status.test.ts). Displayed labels come from `statusRaw`.
|
|
61
|
+
* stage 2 (DONE) the Airtable "Status" single-select gained the 6 new options
|
|
62
|
+
* and all 44 cells were migrated (verified live: maintained=13,
|
|
63
|
+
* archived=12, external=9, building=6, hosted-only=2,
|
|
64
|
+
* launching=2 — zero old values, zero unrecognized), `db sync`
|
|
65
|
+
* ran clean (mismatches=0), and AIRTABLE_USES_NEW_VOCABULARY
|
|
66
|
+
* flipped to true. Writers now emit canonical names verbatim.
|
|
67
|
+
* The alias map STAYS: the 7 old options still exist in the
|
|
68
|
+
* field, so a human can still pick one in the Airtable UI.
|
|
69
|
+
* stage 3 (DONE) the operator deleted the 7 old options from the Airtable
|
|
70
|
+
* "Status" field on 2026-08-25, which is what made this safe:
|
|
71
|
+
* the gate was always "no old value CAN BE ENTERED", not
|
|
72
|
+
* merely "none is stored". Verified against the live base
|
|
73
|
+
* before any code changed — the field carries exactly the six
|
|
74
|
+
* canonical choices, byte-exact, and all 44 cells still hold a
|
|
75
|
+
* canonical value with ZERO blanks (deleting a single-select
|
|
76
|
+
* option CLEARS the cells using it, so "no cell was blanked"
|
|
77
|
+
* was checked, not assumed). AIRTABLE_STATUS_ALIASES,
|
|
78
|
+
* AIRTABLE_OLD_NAMES and AIRTABLE_USES_NEW_VOCABULARY are
|
|
79
|
+
* gone; both functions reduce to the identity.
|
|
80
|
+
*
|
|
81
|
+
* WHAT CHANGED IN BEHAVIOUR AT STAGE 3, and it is the only thing that did: an
|
|
82
|
+
* old name is no longer translated. `canonicalizeStatus("maintenance")` used to
|
|
83
|
+
* yield `maintained`; it now yields `"maintenance"` verbatim, which
|
|
84
|
+
* `isUnrecognizedStatus` flags and the cockpit surfaces as a watch row. That is
|
|
85
|
+
* intended. The option cannot be selected any more, so a stored old value would
|
|
86
|
+
* mean something went wrong — a restored backup, a scripted write, an API
|
|
87
|
+
* caller with a stale constant — and the fleet should say so rather than absorb
|
|
88
|
+
* it into a status nobody chose.
|
|
89
|
+
*
|
|
90
|
+
* Canonicalization happens on READ, never at rest: `src/db/import-airtable.ts`
|
|
91
|
+
* still stores the raw Airtable cell verbatim in `sites.status`, so the hourly
|
|
92
|
+
* parity check keeps comparing raw-to-raw and the importer stays honest to its
|
|
93
|
+
* source. Both readers — `mapRow` (Airtable) and `rowFromJoined` (Turso) — run
|
|
94
|
+
* the raw value through `canonicalizeStatus`, which is what keeps the #558
|
|
95
|
+
* reader-equivalence instrument green.
|
|
96
|
+
*/
|
|
97
|
+
/** The canonical site lifecycle vocabulary. */
|
|
98
|
+
type Status = "building" | "launching" | "maintained" | "hosted-only" | "external" | "archived";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The fleet's site-row model and its pure, vendor-neutral coercers.
|
|
102
|
+
*
|
|
103
|
+
* `WebsiteRow` is what BOTH readers return — `mapRow` over an Airtable record
|
|
104
|
+
* (`src/reports/airtable/websites.ts`) and `rowFromJoined` over Turso
|
|
105
|
+
* (`src/db/fleet-state.ts`) — and the coercers below are the ONE truth each of
|
|
106
|
+
* them applies to a raw cell/column. They lived in the Airtable module until
|
|
107
|
+
* #539 Phase 6 step 1 (#646) moved them here: they run on every Turso lead read,
|
|
108
|
+
* so they could not stay in the directory Phase 6 deletes. Nothing here touches
|
|
109
|
+
* a store; the doc comments still name Airtable columns because that is where
|
|
110
|
+
* each value was first defined, and the Turso columns carry the same raw values.
|
|
111
|
+
*
|
|
112
|
+
* `src/reports/airtable/websites.ts` re-exports every name, so existing imports
|
|
113
|
+
* resolve unchanged.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
type Frequency = "None" | "Monthly" | "Quarterly" | "Yearly";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Per-site notification routing. When present on a `maintained` site, the form
|
|
120
|
+
* notification is addressed by the value of a submission field (`field`, read from
|
|
121
|
+
* `extraFields`) — e.g. route a contact form's `interest` to a different recipient
|
|
122
|
+
* per option, always CC-ing a shared address. Absent (`null`) → the site keeps the
|
|
123
|
+
* default single-POC behavior. Recipients live HERE (server-side Airtable config),
|
|
124
|
+
* never supplied by the submitting site, so the ingest can't be turned into an open
|
|
125
|
+
* relay.
|
|
126
|
+
*/
|
|
127
|
+
type NotifyRouting = {
|
|
128
|
+
/** The `extraFields` key whose value selects a route, e.g. "interest". */
|
|
129
|
+
field: string;
|
|
130
|
+
/** Field-value → recipient address(es). */
|
|
131
|
+
routes: Record<string, string | string[]>;
|
|
132
|
+
/** Recipient(s) when the value matches no route. */
|
|
133
|
+
default?: string | string[];
|
|
134
|
+
/** Address(es) CC'd on every routed (maintenance) send. */
|
|
135
|
+
cc?: string[];
|
|
136
|
+
};
|
|
137
|
+
type WebsiteRow = {
|
|
138
|
+
id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
url: string;
|
|
141
|
+
/** Canonical lifecycle status (see ./site-status.ts). Canonicalized at the read
|
|
142
|
+
* boundary, so an old-vocabulary Airtable cell and a new one read identically.
|
|
143
|
+
* A present-but-unrecognized cell survives VERBATIM (blind-cast) rather than
|
|
144
|
+
* becoming null — `isUnrecognizedStatus` flags it, and null would make
|
|
145
|
+
* due.ts/preflight.ts treat the row as eligible-by-default. */
|
|
146
|
+
status: Status | null;
|
|
147
|
+
/** The literal Airtable Status cell behind `status` — same pattern as
|
|
148
|
+
* `maintenanceFreqRaw`. The dashboard status editor round-trips THIS, so its
|
|
149
|
+
* dropdown offers and preselects the values Airtable actually holds while the
|
|
150
|
+
* two vocabularies coexist. Null = blank cell. */
|
|
151
|
+
statusRaw: string | null;
|
|
152
|
+
pointOfContact: string | null;
|
|
153
|
+
maintenanceFreq: Frequency;
|
|
154
|
+
testingFreq: Frequency;
|
|
155
|
+
/** The literal Airtable cell values behind the coerced frequencies. `toFrequency`
|
|
156
|
+
* trims whitespace ("Monthly " reads as Monthly), then maps any still-unrecognized
|
|
157
|
+
* value ("Quaterly") to "None" with a LOUD console.warn so nothing bogus reaches a
|
|
158
|
+
* client email and the drop is never invisible. Preflight validates THESE to surface
|
|
159
|
+
* the same failure as a structured finding. Null = blank cell. */
|
|
160
|
+
maintenanceFreqRaw: string | null;
|
|
161
|
+
testingFreqRaw: string | null;
|
|
162
|
+
/** Last manually-recorded maintenance day (used as fallback when no Reports row exists). */
|
|
163
|
+
maintenanceDay: string | null;
|
|
164
|
+
testingDay: string | null;
|
|
165
|
+
ga4PropertyId: string | null;
|
|
166
|
+
/** Operator-supplied query for the Google search-presence check (e.g. the business name).
|
|
167
|
+
* Null = no query set → the check is skipped for this site. */
|
|
168
|
+
searchQuery: string | null;
|
|
169
|
+
/** Explicit Search Console property for this site (`sc-domain:...` or `https://.../`).
|
|
170
|
+
* Null = auto-resolve from the SA's visible properties by host. */
|
|
171
|
+
searchConsoleProperty: string | null;
|
|
172
|
+
/** ISO timestamp of the last draft run where THIS site's GA/Search enrichment
|
|
173
|
+
* ERRORED (vs a legitimate "not configured" skip). Set by drafting on a soft-fail,
|
|
174
|
+
* cleared (null) on a clean enrichment, so the per-site analytics-failure signal
|
|
175
|
+
* self-heals. Null when the operator-added `Analytics soft-fail at` column is absent. */
|
|
176
|
+
analyticsSoftFailAt: string | null;
|
|
177
|
+
/** GitHub repo identity as `owner/repo`. Null = no git wiring → self-update ops skip
|
|
178
|
+
* (or, for local runs, fall back to the checkout's origin remote). */
|
|
179
|
+
gitRepo: string | null;
|
|
180
|
+
reportRecipientsTo: string | null;
|
|
181
|
+
reportRecipientsCc: string | null;
|
|
182
|
+
acceptedWatchConditions: string[];
|
|
183
|
+
/** First attachment in the Header image field (Airtable's signed URL — fetch before expiry). */
|
|
184
|
+
headerImage: {
|
|
185
|
+
url: string;
|
|
186
|
+
filename: string;
|
|
187
|
+
type: string;
|
|
188
|
+
} | null;
|
|
189
|
+
/** Lighthouse "current state" snapshot, kept fresh by `audit lighthouse --write-back`. */
|
|
190
|
+
pScore: number | null;
|
|
191
|
+
rScore: number | null;
|
|
192
|
+
bpScore: number | null;
|
|
193
|
+
seoScore: number | null;
|
|
194
|
+
/** ISO timestamp set by `audit lighthouse --write-back` when scores were last refreshed. */
|
|
195
|
+
lastLighthouseAuditAt: string | null;
|
|
196
|
+
/** Last-known counts from non-lighthouse audits, written by
|
|
197
|
+
* `audit --write-back`. `null` = never audited (or this audit
|
|
198
|
+
* type was skipped on the last run). 0 = audited, clean. */
|
|
199
|
+
a11yViolations: number | null;
|
|
200
|
+
/** Declared-range drift vs the Reddoor baseline (what package.json asks for). */
|
|
201
|
+
depsDrifted: number | null;
|
|
202
|
+
depsMajorBehind: number | null;
|
|
203
|
+
/** Real installed-version drift: deps behind the registry's latest, from the
|
|
204
|
+
* committed lockfile (`pnpm outdated`). Null = not determined this run. */
|
|
205
|
+
depsOutdated: number | null;
|
|
206
|
+
/** Of {@link depsOutdated}, how many are a *major* version behind the
|
|
207
|
+
* registry's latest — the "majors available on npm" signal, distinct from the
|
|
208
|
+
* baseline-drift {@link depsMajorBehind}. Null = not determined this run. */
|
|
209
|
+
depsMajorOutdated: number | null;
|
|
210
|
+
securityVulnsCritical: number | null;
|
|
211
|
+
securityVulnsHigh: number | null;
|
|
212
|
+
securityVulnsModerate: number | null;
|
|
213
|
+
securityVulnsLow: number | null;
|
|
214
|
+
/** Count of consecutive nightly Renovate auto-fix dispatches for the CURRENT
|
|
215
|
+
* critical/high vuln episode that have NOT yet cleared it. Owned by
|
|
216
|
+
* `renovate-dispatch`: +1 per real dispatch, reset to 0 when vulns clear.
|
|
217
|
+
* Null = field absent / never dispatched → reads as 0. At/above
|
|
218
|
+
* AUTO_FIX_EXHAUSTED_CYCLES the vuln renders as "auto-fix failed". */
|
|
219
|
+
securityAutoFixAttempts: number | null;
|
|
220
|
+
/** ISO timestamp the security audit last ran — gates freshness of the Security Updates auto-tick
|
|
221
|
+
* (clean counts only auto-tick when recent). */
|
|
222
|
+
lastSecurityAuditAt: string | null;
|
|
223
|
+
/** The known advisories behind the counts (severity-sorted, capped), so the dashboard can show
|
|
224
|
+
* WHICH packages are vulnerable, not just the totals. null = never audited / unparseable;
|
|
225
|
+
* empty array = audited clean. Written alongside the counts by the security audit. */
|
|
226
|
+
securityAdvisories: SecurityAdvisory[] | null;
|
|
227
|
+
/** Domain/DNS/SSL probe (the `domain` audit). `certDaysRemaining` is days until the TLS cert
|
|
228
|
+
* expires (null = unresolved or no usable cert); `domainCheckedAt` is when it last ran. */
|
|
229
|
+
certDaysRemaining: number | null;
|
|
230
|
+
domainCheckedAt: string | null;
|
|
231
|
+
/** Netlify site id — the IDENTITY the `netlify-deploy` audit needs to query the
|
|
232
|
+
* Netlify API. Read-only input (operator-set in Airtable); null = not on Netlify
|
|
233
|
+
* (or not wired) → that audit skips. Never derived from the URL. */
|
|
234
|
+
netlifyId: string | null;
|
|
235
|
+
/** Latest PRODUCTION deploy health (the `netlify-deploy` audit). `deployStatus` is
|
|
236
|
+
* Netlify's deploy state lower-cased (`ready`/`error`/`building`/…), null = none / not
|
|
237
|
+
* checked; `lastDeployAt` is when it deployed; `deployLogUrl` links to the deploy/log. */
|
|
238
|
+
deployStatus: string | null;
|
|
239
|
+
lastDeployAt: string | null;
|
|
240
|
+
deployLogUrl: string | null;
|
|
241
|
+
/** When the `netlify-deploy` audit last RAN (freshness stamp for `deployStatus`). The audit
|
|
242
|
+
* already writes "Deploy checked at"; this read-back is the Plan-2 fix so `deployEvidence`
|
|
243
|
+
* (Plan 4) can gate on check time — NOT on `lastDeployAt`, which is deploy time, not check time. */
|
|
244
|
+
deployCheckedAt: string | null;
|
|
245
|
+
/** Function-health verdict (the `function-health` audit): the deployed `/health` function
|
|
246
|
+
* answered `ok:true` (pass) or `ok:false` (fail). Single-select `pass`/`fail`; null = never ran
|
|
247
|
+
* / unreachable (→ Plan 4 maps to unknown/amber). Kept SEPARATE from `deployStatus` so
|
|
248
|
+
* `isFailedDeployStatus` keeps meaning "the build failed". */
|
|
249
|
+
functionHealth: "pass" | "fail" | null;
|
|
250
|
+
/** CMS reachability (server-side), derived from the same `/health` body's `details.prismic ===
|
|
251
|
+
* "ok"`. Single-select `pass`/`fail`; null = never ran. No per-site Prismic token or identity
|
|
252
|
+
* column is ever built — this rides `/health`. */
|
|
253
|
+
cmsReachable: "pass" | "fail" | null;
|
|
254
|
+
/** Does the deployed site's Turnstile widget actually work? Single-select `pass`/`fail`;
|
|
255
|
+
* null = **unverified**, which is the normal state — see below. Freshness gated by
|
|
256
|
+
* `formE2eCheckedAt`, the clock of the audit that writes it. Powers the cockpit Require-Turnstile guardrail: a site with
|
|
257
|
+
* `requireTurnstile` ON but no working widget silently buckets 100% of its real leads.
|
|
258
|
+
*
|
|
259
|
+
* One of the two `"fail"` arms is derived from `/health` (the other is a browser seeing
|
|
260
|
+
* 110200 on the live hostname), whose `forms.turnstile` is a truthiness check
|
|
261
|
+
* on `PUBLIC_TURNSTILE_SITE_KEY` that never contacts Cloudflare (see
|
|
262
|
+
* audits/function-health-airtable.ts for the incident). No key IS proof the widget can't
|
|
263
|
+
* work; a key is NOT proof that it can — a sitekey whose widget is full at Cloudflare's
|
|
264
|
+
* 10-hostname cap sets the var and still mints no token. So a `"pass"` here has to be
|
|
265
|
+
* earned by a real browser rendering the real widget (form-e2e), and until it is, null
|
|
266
|
+
* keeps the site in the cockpit's amber "can't verify" watch rather than claiming health
|
|
267
|
+
* nobody measured. */
|
|
268
|
+
turnstileWidget: "pass" | "fail" | null;
|
|
269
|
+
/** When the `function-health` audit last ran — the freshness gate for `functionHealth`,
|
|
270
|
+
* `cmsReachable`, and `turnstileWidget`. Null = never ran. */
|
|
271
|
+
functionHealthCheckedAt: string | null;
|
|
272
|
+
/** Deployed-URL browser probe (the `browser` audit): cross-engine render OK, mobile render OK,
|
|
273
|
+
* internal-links OK + broken count, and when it last ran (one timestamp gates all three). */
|
|
274
|
+
crossbrowserOk: boolean | null;
|
|
275
|
+
mobileOk: boolean | null;
|
|
276
|
+
linksOk: boolean | null;
|
|
277
|
+
brokenLinks: number | null;
|
|
278
|
+
browserCheckedAt: string | null;
|
|
279
|
+
/** Uptime-reachable verdict (browser audit): every sampled route returned 2xx/3xx. Single-select
|
|
280
|
+
* `pass`/`fail`; null = never ran. Point-in-time. Freshness-gated by `browserCheckedAt`. */
|
|
281
|
+
reachableOk: "pass" | "fail" | null;
|
|
282
|
+
/** Titles & meta verdict (browser audit, chromium): every sampled route has a non-empty `<title>`
|
|
283
|
+
* ≤ 70 chars + a non-empty meta description, and no duplicate titles across the sample.
|
|
284
|
+
* Single-select `pass`/`fail`; null = never ran. Freshness-gated by `browserCheckedAt`. */
|
|
285
|
+
titleMetaOk: "pass" | "fail" | null;
|
|
286
|
+
/** Per-site copy overrides (M6a). Blank → null → the DEFAULT_COPY value. */
|
|
287
|
+
copyIntro: string | null;
|
|
288
|
+
copyContact: string | null;
|
|
289
|
+
copyFooter: string | null;
|
|
290
|
+
/** Go-live timestamp, stamped when a Launch report sends (M6b). Null = not yet launched. */
|
|
291
|
+
launchedAt: string | null;
|
|
292
|
+
/** Optional per-site webhook (e.g. Zapier Catch Hook). When set, the ingest
|
|
293
|
+
* POSTs newsletter-formType submissions here (best-effort). Blank → null. */
|
|
294
|
+
newsletterWebhook: string | null;
|
|
295
|
+
/** Per-site Mailchimp (newsletter). Both must be set for the direct add;
|
|
296
|
+
* blank → skipped. The API key is `key-dc` format; dc is derived from it. */
|
|
297
|
+
mailchimpApiKey: string | null;
|
|
298
|
+
mailchimpAudienceId: string | null;
|
|
299
|
+
/** Per-site Cloudflare Turnstile gate (Airtable checkbox). When true, a submission
|
|
300
|
+
* whose Turnstile token verifies as "fail" (forged) OR "absent" (secret configured
|
|
301
|
+
* centrally but NO token forwarded — the direct-POST-bot signature) is escalated to
|
|
302
|
+
* auto-spam regardless of content score. A present-but-expired token stays
|
|
303
|
+
* "unverifiable" and neutral (a real browser DID render the widget — fail-open).
|
|
304
|
+
* ROLLOUT PRECONDITION: only enable on a site whose DEPLOYED package forwards
|
|
305
|
+
* `_meta.turnstileToken` from EVERY form (widget rendered + `cf-turnstile-response`
|
|
306
|
+
* posted; check the site's `/health` → forms.turnstile:true). A site that never
|
|
307
|
+
* forwards tokens would silently bucket 100% of its real leads — and the form-e2e
|
|
308
|
+
* probe cannot catch that state (testMode bypasses the gate). */
|
|
309
|
+
requireTurnstile: boolean;
|
|
310
|
+
/** GitHub-signals sweep (slice 2a), written nightly by `github-signals --fleet`. */
|
|
311
|
+
renovateFailingCis: number | null;
|
|
312
|
+
defaultBranchCi: string | null;
|
|
313
|
+
lastCommitAt: string | null;
|
|
314
|
+
githubSignalsAt: string | null;
|
|
315
|
+
/** Per-site smoke-suite verdict (the `smoke` audit runs `pnpm test:smoke`).
|
|
316
|
+
* Single-select pass/fail; null = never ran. `lastSmokeAt` gates freshness. */
|
|
317
|
+
smokeOk: "pass" | "fail" | null;
|
|
318
|
+
lastSmokeAt: string | null;
|
|
319
|
+
/** Synthetic form end-to-end verdict (the `form-e2e` audit submits the real prod
|
|
320
|
+
* contact form in test-mode). Single-select pass/fail; null = never ran OR (with
|
|
321
|
+
* a fresh `formE2eCheckedAt`) no contact form → n/a. `formE2eCheckedAt` gates
|
|
322
|
+
* freshness AND encodes the n/a-vs-never-ran distinction. */
|
|
323
|
+
formE2eOk: "pass" | "fail" | null;
|
|
324
|
+
formE2eCheckedAt: string | null;
|
|
325
|
+
/**
|
|
326
|
+
* Nightly Prismic model drift sweep (`prismic-models --fleet --write-back`).
|
|
327
|
+
*
|
|
328
|
+
* FOUR-VALUED, unlike every other verdict column in this table, and the extra
|
|
329
|
+
* state is the point:
|
|
330
|
+
*
|
|
331
|
+
* - `pass` — the repo and its Prismic repository were compared and agree.
|
|
332
|
+
* - `fail` — they were compared and DIVERGE (a model to create/update, or
|
|
333
|
+
* one that exists only in Prismic). A finding for a human.
|
|
334
|
+
* - `unknown` — THE CHECK ITSELF FAILED. Unreadable checkout, broken config,
|
|
335
|
+
* missing/expired token, unreadable remote. Nothing at all was
|
|
336
|
+
* established about this site's models.
|
|
337
|
+
* - `null` — no verdict to hold: the sweep has never run for this site, or
|
|
338
|
+
* it ran and found no Prismic config at all (a repo that simply
|
|
339
|
+
* is not a Prismic site). `prismicModelsCheckedAt` separates
|
|
340
|
+
* those two — fresh timestamp, blank verdict = "checked, and
|
|
341
|
+
* there is nothing here to check".
|
|
342
|
+
*
|
|
343
|
+
* `unknown` exists because the alternatives are both wrong in the dangerous
|
|
344
|
+
* direction. Writing `pass` for a site nobody could read is a green tick over
|
|
345
|
+
* an outage; writing NOTHING (the obvious "leave the last known value alone")
|
|
346
|
+
* leaves yesterday's `pass` standing for a site that has been failing every
|
|
347
|
+
* night since — and nothing ages a stale `pass` out, because the digest's
|
|
348
|
+
* freshness gate only ever examines failures. Both make "I could not read this
|
|
349
|
+
* site" indistinguishable from "this site is fine", in the record the cockpit
|
|
350
|
+
* and the digest read.
|
|
351
|
+
*
|
|
352
|
+
* OPERATOR PRECONDITION: `Prismic Models` is a single select whose options are
|
|
353
|
+
* `pass`, `fail`, `unknown`. Until the three columns exist the sweep's writes
|
|
354
|
+
* fail with UNKNOWN_FIELD_NAME and are collected as soft failures — the feature
|
|
355
|
+
* ships dark rather than reddening the nightly.
|
|
356
|
+
*/
|
|
357
|
+
prismicModels: PrismicModelsVerdict | null;
|
|
358
|
+
/** When the sweep last ran for this site — written on EVERY outcome, including
|
|
359
|
+
* `unknown` and blank, so the age of the row is the age of the answer. */
|
|
360
|
+
prismicModelsCheckedAt: string | null;
|
|
361
|
+
/** What the sweep found, verbatim: the drift report for a `fail`, the reason
|
|
362
|
+
* nothing could be established for an `unknown`, the skip reason for a blank,
|
|
363
|
+
* and null for a `pass` (which clears yesterday's finding). */
|
|
364
|
+
prismicModelsDrift: string | null;
|
|
365
|
+
/**
|
|
366
|
+
* An expiry the operator sets to accept a KNOWN, EXPECTED `fail` — most often
|
|
367
|
+
* "I am modelling in Prismic on a branch that has not landed, so Prismic is
|
|
368
|
+
* legitimately ahead of `main`". While this timestamp is in the future the
|
|
369
|
+
* drift item is not raised.
|
|
370
|
+
*
|
|
371
|
+
* It expires on purpose, and nothing renews it automatically. A permanent ack
|
|
372
|
+
* would reproduce this column's own failure mode one step later: once the
|
|
373
|
+
* branch lands, the same acked cell would swallow real drift and "nobody is
|
|
374
|
+
* looking at this" would render as "this is fine". An expiry means the worst
|
|
375
|
+
* case is a silence that ends by itself.
|
|
376
|
+
*
|
|
377
|
+
* Narrow by construction — it mutes only `fail`, the finding the operator
|
|
378
|
+
* actually reviewed. `unknown` (the check could not run) and a verdict too old
|
|
379
|
+
* to be current are never muted by it; see `collectPrismicDriftAlerts`.
|
|
380
|
+
*
|
|
381
|
+
* OPERATOR PRECONDITION: `Prismic Ack Until` is a dateTime column. Until it
|
|
382
|
+
* exists this reads null and nothing is acked, which is the safe direction.
|
|
383
|
+
*/
|
|
384
|
+
prismicAckUntil: string | null;
|
|
385
|
+
notifyRouting: NotifyRouting | null;
|
|
386
|
+
/** The RAW `Notify Routing` cell, verbatim. Same reason `statusRaw` exists: the
|
|
387
|
+
* dashboard editor round-trips this JSON, and re-serializing the PARSED object
|
|
388
|
+
* would drop any key the parser ignores and reformat what the operator typed —
|
|
389
|
+
* a silent rewrite of a cell they only opened to look at. */
|
|
390
|
+
notifyRoutingRaw: string | null;
|
|
391
|
+
/** Read-back of the code-owned next-due dates (date-only, written by the
|
|
392
|
+
* nightly `writeNextDueDates`). These exist so the writer can DIFF the
|
|
393
|
+
* freshly-computed dates against what the row already holds and skip the
|
|
394
|
+
* ~31 no-op writes a night the fleet was paying for (#539 Phase 3). Null =
|
|
395
|
+
* cell blank or the operator-added `Next … at` column absent. */
|
|
396
|
+
nextMaintenanceAt: string | null;
|
|
397
|
+
nextTestingAt: string | null;
|
|
398
|
+
};
|
|
399
|
+
/** The `Prismic Models` cell — see {@link WebsiteRow.prismicModels} for why it
|
|
400
|
+
* carries a third state that no other verdict column in this table has. */
|
|
401
|
+
type PrismicModelsVerdict = "pass" | "fail" | "unknown";
|
|
402
|
+
type Severity = "low" | "moderate" | "high" | "critical";
|
|
403
|
+
/** One known vulnerability behind the security counts, as persisted/rendered. */
|
|
404
|
+
type SecurityAdvisory = {
|
|
405
|
+
module: string;
|
|
406
|
+
severity: Severity;
|
|
407
|
+
title: string;
|
|
408
|
+
cves: string[];
|
|
409
|
+
url: string | null;
|
|
410
|
+
/** GitHub Dependabot dependency scope ("runtime" | "development"), when known. Lets the
|
|
411
|
+
* dashboard flag build-time-only ("development") vulns. Omitted for advisories from the
|
|
412
|
+
* lockfile `pnpm audit` fallback, which carries no per-package graph scope. */
|
|
413
|
+
scope?: "runtime" | "development";
|
|
414
|
+
/** GitHub Dependabot dependency-graph relationship, when known. "transitive" means no
|
|
415
|
+
* direct-dep bump can fix it — Renovate's vuln alerts have no fix vehicle until the weekly
|
|
416
|
+
* lockfile-maintenance window. Omitted when GitHub reports "unknown" or for the
|
|
417
|
+
* `pnpm audit` fallback. Drives {@link allActionableVulnsTransitive}. */
|
|
418
|
+
relationship?: "direct" | "transitive";
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
type FleetEventType = "pr_automerged" | "vuln_cleared" | "ci_recovered" | "site_launched" | "fleet_swept" | "cert_renewed" | "report_sent_with_override";
|
|
422
|
+
/** One recorded fleet activity event. `id` is deterministic (see the detectors)
|
|
423
|
+
* so re-runs INSERT OR IGNORE without duplicating. `siteId`/`siteName` are null
|
|
424
|
+
* for fleet-wide rollups; `data` is optional structured detail (e.g. the PR url). */
|
|
425
|
+
type FleetEvent = {
|
|
426
|
+
id: string;
|
|
427
|
+
ts: string;
|
|
428
|
+
type: FleetEventType;
|
|
429
|
+
siteId: string | null;
|
|
430
|
+
siteName: string | null;
|
|
431
|
+
summary: string;
|
|
432
|
+
data: unknown | null;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export type { AirtableBase as A, FleetEvent as F, WebsiteRow as W, FleetEventType as a };
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
REPORT_LIST_COLUMNS,
|
|
3
|
+
findReportByMessageId,
|
|
3
4
|
getReportById,
|
|
4
5
|
getReportHtml,
|
|
5
6
|
getSiteById,
|
|
6
7
|
getSiteBySlug,
|
|
8
|
+
insertReportRow,
|
|
9
|
+
insertSiteRows,
|
|
7
10
|
listAllReports,
|
|
8
11
|
listReportsForSite,
|
|
12
|
+
listSendableReports,
|
|
9
13
|
listSites,
|
|
10
14
|
mirrorHealthFields,
|
|
11
15
|
mirrorReportInsert,
|
|
@@ -14,20 +18,26 @@ import {
|
|
|
14
18
|
mirrorSiteField,
|
|
15
19
|
mirrorSiteFields,
|
|
16
20
|
mirrorSiteInsert,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
siteRowExists,
|
|
22
|
+
storeRenderedHtml,
|
|
23
|
+
updateSiteIdentity
|
|
24
|
+
} from "./chunk-BINFZQ2T.js";
|
|
25
|
+
import "./chunk-LF2TJN4X.js";
|
|
26
|
+
import "./chunk-ASLDCWBF.js";
|
|
21
27
|
import "./chunk-VAD43MOR.js";
|
|
22
|
-
import "./chunk-
|
|
28
|
+
import "./chunk-ICSRWW5Q.js";
|
|
23
29
|
export {
|
|
24
30
|
REPORT_LIST_COLUMNS,
|
|
31
|
+
findReportByMessageId,
|
|
25
32
|
getReportById,
|
|
26
33
|
getReportHtml,
|
|
27
34
|
getSiteById,
|
|
28
35
|
getSiteBySlug,
|
|
36
|
+
insertReportRow,
|
|
37
|
+
insertSiteRows,
|
|
29
38
|
listAllReports,
|
|
30
39
|
listReportsForSite,
|
|
40
|
+
listSendableReports,
|
|
31
41
|
listSites,
|
|
32
42
|
mirrorHealthFields,
|
|
33
43
|
mirrorReportInsert,
|
|
@@ -36,6 +46,8 @@ export {
|
|
|
36
46
|
mirrorSiteField,
|
|
37
47
|
mirrorSiteFields,
|
|
38
48
|
mirrorSiteInsert,
|
|
39
|
-
|
|
49
|
+
siteRowExists,
|
|
50
|
+
storeRenderedHtml,
|
|
51
|
+
updateSiteIdentity
|
|
40
52
|
};
|
|
41
|
-
//# sourceMappingURL=fleet-state-
|
|
53
|
+
//# sourceMappingURL=fleet-state-RJROYXKO.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
describeNotifyTarget
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-5ML7FMBD.js";
|
|
3
|
+
} from "./chunk-PXRN2MER.js";
|
|
4
|
+
import "./chunk-Z7FDJ35L.js";
|
|
6
5
|
import "./chunk-NO74DXDP.js";
|
|
6
|
+
import "./chunk-5ML7FMBD.js";
|
|
7
7
|
import "./chunk-I7NHLVVI.js";
|
|
8
8
|
import {
|
|
9
9
|
openBase,
|
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
} from "./chunk-HN6I2XWI.js";
|
|
12
12
|
import "./chunk-BGTYPVLT.js";
|
|
13
13
|
import {
|
|
14
|
-
canonicalizeStatus,
|
|
15
|
-
listWebsites,
|
|
16
|
-
siteSlug,
|
|
17
14
|
toAirtableStatus,
|
|
18
15
|
updateSiteField
|
|
19
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-TC4GANMX.js";
|
|
17
|
+
import {
|
|
18
|
+
canonicalizeStatus,
|
|
19
|
+
siteSlug
|
|
20
|
+
} from "./chunk-ICSRWW5Q.js";
|
|
21
|
+
import "./chunk-RQJFGK3A.js";
|
|
20
22
|
import "./chunk-2OP4JKIQ.js";
|
|
21
23
|
import "./chunk-6VAL7XJT.js";
|
|
22
24
|
|
|
@@ -38,14 +40,14 @@ function findSite(rows, site) {
|
|
|
38
40
|
}
|
|
39
41
|
async function formsNotifyTarget(deps) {
|
|
40
42
|
const base = deps.base ?? openBase(readAirtableConfig());
|
|
41
|
-
const rows = await
|
|
43
|
+
const rows = await deps.roster();
|
|
42
44
|
const row = findSite(rows, deps.site);
|
|
43
45
|
if (!row) {
|
|
44
46
|
const needle = siteSlug(deps.site);
|
|
45
47
|
const near = rows.map((r) => r.name).filter((n) => siteSlug(n).includes(needle) || needle.includes(siteSlug(n)));
|
|
46
48
|
throw Object.assign(
|
|
47
49
|
new Error(
|
|
48
|
-
`No
|
|
50
|
+
`No site matches '${deps.site}'. The stored NAME is not always the repo slug (Sonder, not gallerysonder).` + (near.length > 0 ? ` Did you mean: ${near.join(", ")}?` : "")
|
|
49
51
|
),
|
|
50
52
|
{ exitCode: 2 }
|
|
51
53
|
);
|
|
@@ -73,10 +75,10 @@ async function formsNotifyTarget(deps) {
|
|
|
73
75
|
}
|
|
74
76
|
const cell = deps.set === "on" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw);
|
|
75
77
|
await updateSiteField(base, row.id, STATUS_COLUMN, cell);
|
|
76
|
-
await deps.siteMirror
|
|
77
|
-
const after = findSite(await
|
|
78
|
+
await deps.siteMirror.site(row.id, { [STATUS_COLUMN]: cell });
|
|
79
|
+
const after = findSite(await deps.roster(), deps.site);
|
|
78
80
|
if (!after) {
|
|
79
|
-
throw Object.assign(new Error(`${row.name} vanished from
|
|
81
|
+
throw Object.assign(new Error(`${row.name} vanished from the fleet during the flip.`), {
|
|
80
82
|
exitCode: 1
|
|
81
83
|
});
|
|
82
84
|
}
|
|
@@ -137,15 +139,17 @@ function formatNotifyTarget(r) {
|
|
|
137
139
|
}
|
|
138
140
|
async function runFormsNotifyTargetCommand(site, opts) {
|
|
139
141
|
if (!site?.trim()) {
|
|
140
|
-
return { output: "forms-notify-target requires <site> (slug or
|
|
142
|
+
return { output: "forms-notify-target requires <site> (slug or site name)", code: 2 };
|
|
141
143
|
}
|
|
142
144
|
const set = opts.set?.trim().toLowerCase();
|
|
143
145
|
if (set !== void 0 && set !== "on" && set !== "off") {
|
|
144
146
|
return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };
|
|
145
147
|
}
|
|
146
148
|
try {
|
|
147
|
-
const { makeSiteMirror } = await import("./site-mirror-
|
|
149
|
+
const { makeSiteMirror } = await import("./site-mirror-WR7ACXOV.js");
|
|
150
|
+
const { readFleetRoster } = await import("./roster-GDNSIRAK.js");
|
|
148
151
|
const result = await formsNotifyTarget({
|
|
152
|
+
roster: () => readFleetRoster(),
|
|
149
153
|
siteMirror: await makeSiteMirror(),
|
|
150
154
|
site: site.trim(),
|
|
151
155
|
...set ? { set } : {},
|
|
@@ -164,4 +168,4 @@ export {
|
|
|
164
168
|
formatNotifyTarget,
|
|
165
169
|
runFormsNotifyTargetCommand
|
|
166
170
|
};
|
|
167
|
-
//# sourceMappingURL=forms-notify-target-
|
|
171
|
+
//# sourceMappingURL=forms-notify-target-X6UEOI2M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/recipes/forms-notify-target.ts","../src/cli/commands/forms-notify-target.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport {\n siteSlug,\n updateSiteField,\n type Status,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus, toAirtableStatus } from \"../reports/airtable/site-status.js\";\nimport { describeNotifyTarget, type NotifyTarget } from \"../forms/notify.js\";\n\n/** The Airtable column the pre-launch guard actually lives in. */\nexport const STATUS_COLUMN = \"Status\";\n\n/** The two ends of the verify flip. Deliberately the ONLY transition this\n * performs: a site in any other status is already guarded (or is deliberately\n * something else, like \"hosted-only\"), and silently rewriting that would be the\n * same class of unseen change as the incident. */\nexport const LIVE_STATUS: Status = \"maintained\";\nexport const VERIFY_STATUS: Status = \"launching\";\n\nexport type FormsNotifyTargetDeps = {\n base?: AirtableBase;\n /** Every site in the fleet, read from TURSO (#646 step 4). Required, not\n * defaulted: the unit suite drives this with a fake fleet, and a default would\n * open a real libSQL handle from inside it. The CLI wires `readFleetRoster`.\n *\n * It is called TWICE on a flip — once to find the site, once to read the cell\n * back — and both reads matter: Turso is what `/api/forms/:slug` consults to\n * decide who a submission emails (`getSiteBySlug`, #643), so confirming the\n * Airtable cell would confirm the wrong store, and an Airtable roster cannot\n * see a `site_<ULID>` site at all. */\n roster: () => Promise<WebsiteRow[]>;\n /** Site slug or the stored site NAME (both accepted). */\n site: string;\n /** `on` routes notifications to the operator; `off` restores. Omit to read. */\n set?: \"on\" | \"off\";\n /** Status to restore with `--set off`. Required, never inferred. */\n restore?: string;\n /** The Status write itself (#646 step 4): Turso is the store the form ingest\n * reads, so this is the write the read-back below confirms — the Airtable\n * `updateSiteField` beside it is the shadow, and it skips a `site_` id.\n * Required, and injected at the CLI root for the same reason `roster` is. */\n siteMirror: SiteMirror;\n};\n\nexport type FormsNotifyTargetResult = {\n site: string;\n status: Status | null;\n target: NotifyTarget;\n /** Present only when a flip was attempted. `confirmed` is the whole point:\n * it comes from RE-READING the row, not from the write call returning. */\n flip?: { from: Status | null; to: Status; confirmed: boolean };\n};\n\n/**\n * The exact Airtable cell to write for an operator-supplied `--restore` value:\n * the operator's own string, verbatim.\n *\n * This is the ONE non-revertible surface in the #539 Phase 4 stage-1 rename —\n * every other change is code, and `git revert` undoes code. It cannot undo a\n * rewritten Airtable cell. So the rule here is stricter than everywhere else:\n * substitute only when the substitution is provably lossless, i.e. when\n * canonicalizing and mapping back ROUND-TRIPS to the operator's own string.\n *\n * That condition is, today, never false in a way that changes the answer — which\n * is the point. `toAirtableStatus(canonicalizeStatus(raw))` either equals `raw`\n * (so writing it is writing `raw`) or it does not (so we must write `raw`). The\n * function therefore reduces to \"write raw\", and it is written this way so the\n * reduction is visible rather than assumed. The case that made it matter:\n * `--restore legacy` canonicalizes to `archived`, which maps back to\n * \"deprecated\" — a different, real, operator-visible Airtable option that nobody\n * asked for. `hosting` → `hosted-only` → \"hosting\" round-trips and is safe.\n *\n * Airtable, not this module, is the authority on which option strings the\n * \"Status\" single-select accepts. Writing verbatim delegates to it: a typo is\n * rejected loudly at the API, exactly as it was before the rename.\n */\nexport function restoreCell(raw: string): string {\n const canonical = canonicalizeStatus(raw);\n if (canonical === null) return raw;\n const roundTripped = toAirtableStatus(canonical);\n return roundTripped === raw ? roundTripped : raw;\n}\n\nfunction findSite(rows: WebsiteRow[], site: string): WebsiteRow | undefined {\n const wanted = site.trim().toLowerCase();\n return rows.find(\n (r) => siteSlug(r.name) === siteSlug(site) || r.name.trim().toLowerCase() === wanted,\n );\n}\n\n/**\n * Answer \"who would a form submission on this site email?\" — and optionally\n * flip the pre-launch guard, confirming the flip by reading it back.\n *\n * The guard is a single Airtable `Status` cell. Nothing between \"I intended to\n * flip it\" and \"the client received a test lead\" reported the current state, so\n * on 2026-08-03 a flip that never landed sent a real client a test submission.\n * The fix is not a better intention, it is feedback: this reads the row back\n * after every write and refuses to call the flip confirmed on anything less.\n */\nexport async function formsNotifyTarget(\n deps: FormsNotifyTargetDeps,\n): Promise<FormsNotifyTargetResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const rows = await deps.roster();\n const row = findSite(rows, deps.site);\n if (!row) {\n // The Websites NAME is not the repo slug (\"Sonder\", not \"gallerysonder\"),\n // and that mismatch has cost time before — so name the near misses rather\n // than making the operator go read Airtable to find the spelling.\n const needle = siteSlug(deps.site);\n const near = rows\n .map((r) => r.name)\n .filter((n) => siteSlug(n).includes(needle) || needle.includes(siteSlug(n)));\n throw Object.assign(\n new Error(\n `No site matches '${deps.site}'. The stored NAME is not always the repo ` +\n `slug (Sonder, not gallerysonder).` +\n (near.length > 0 ? ` Did you mean: ${near.join(\", \")}?` : \"\"),\n ),\n { exitCode: 2 },\n );\n }\n\n if (!deps.set) {\n return { site: row.name, status: row.status, target: describeNotifyTarget(row) };\n }\n\n // `--restore` is operator free text. It is canonicalized for what this command\n // REPORTS (`flip.to`, and the predicates downstream), but NEVER for what it\n // WRITES — see `restoreCell` below.\n const restoreRaw = deps.restore?.trim();\n const to = deps.set === \"on\" ? VERIFY_STATUS : (canonicalizeStatus(restoreRaw) ?? undefined);\n if (deps.set === \"off\" && !to) {\n throw Object.assign(\n new Error(\n `--set off needs --restore <status>: the status to return to is never inferred. ` +\n `Guessing \"${LIVE_STATUS}\" for a site that was \"hosted-only\" or \"archived\" would start ` +\n `sending real client notifications — the inverse of the failure this command exists ` +\n `to prevent.`,\n ),\n { exitCode: 2 },\n );\n }\n // Only ever flip a LIVE site into verify mode. A site already outside\n // \"maintained\" is guarded already, and rewriting its status would destroy a\n // real value nobody asked us to touch.\n if (deps.set === \"on\" && row.status !== LIVE_STATUS) {\n throw Object.assign(\n new Error(\n `${row.name} is \"${row.status ?? \"blank\"}\", not \"${LIVE_STATUS}\" — notifications ` +\n `already go to the operator only, so there is nothing to flip. Leaving the status ` +\n `untouched.`,\n ),\n { exitCode: 2 },\n );\n }\n\n // `--set on` writes a status this MODULE owns (VERIFY_STATUS), so mapping it to\n // the current Airtable vocabulary is correct. `--set off` writes the operator's\n // own string — see restoreCell.\n const cell = deps.set === \"on\" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw!);\n // The Airtable shadow first, still allowed to fail loudly while it is kept\n // trustworthy; it skips a site id Airtable cannot hold (#646 step 3).\n await updateSiteField(base, row.id, STATUS_COLUMN, cell);\n // The write that DECIDES: `/api/forms/:slug` reads this cell from Turso, and\n // the console reads it there too.\n await deps.siteMirror.site(row.id, { [STATUS_COLUMN]: cell });\n\n // Read it back — from Turso, the store just confirmed to be the one that\n // matters. The write returning is NOT evidence the field changed.\n const after = findSite(await deps.roster(), deps.site);\n if (!after) {\n throw Object.assign(new Error(`${row.name} vanished from the fleet during the flip.`), {\n exitCode: 1,\n });\n }\n return {\n site: after.name,\n status: after.status,\n target: describeNotifyTarget(after),\n // `confirmed` compares the RAW cell against the exact string written, not\n // canonical-to-canonical. Canonical equality is too weak to be a read-back\n // guard here: `legacy` and `deprecated` are the same canonical status, so a\n // cell holding either would confirm a flip to the other. Comparing raw is the\n // only comparison that can actually catch \"the cell is not what I sent\". The\n // canonical check rides along so a reader that stopped canonicalizing is\n // caught too.\n flip: {\n from: row.status,\n to: to!,\n confirmed: after.statusRaw === cell && after.status === to,\n },\n };\n}\n","import {\n formsNotifyTarget,\n LIVE_STATUS,\n VERIFY_STATUS,\n type FormsNotifyTargetResult,\n} from \"../../recipes/forms-notify-target.js\";\n\nexport type FormsNotifyTargetCommandOptions = {\n set?: string;\n restore?: string;\n cwd?: string;\n};\n\nconst AUDIENCE_LABEL = {\n client: \"THE CLIENT\",\n operator: \"OPERATOR ONLY\",\n nobody: \"NOBODY\",\n} as const;\n\n/** The line that decides whether it is safe to test-submit. Deliberately the\n * loudest thing on screen: the incident happened because the answer was\n * invisible, not because it was subtle. */\nexport function formatNotifyTarget(r: FormsNotifyTargetResult): string {\n const lines: string[] = [];\n if (r.flip) {\n const arrow = `${r.flip.from ?? \"blank\"} → ${r.flip.to}`;\n lines.push(\n r.flip.confirmed\n ? `${r.site} Status: ${arrow} ✓ confirmed by read-back`\n : `${r.site} Status: ${arrow} ✗ NOT CONFIRMED — the row still reads \"${r.status ?? \"blank\"}\". Nothing was verified; do not test-submit.`,\n );\n } else {\n lines.push(`${r.site} Status: ${r.status ?? \"blank\"}`);\n }\n\n lines.push(`A submission right now would notify: ${AUDIENCE_LABEL[r.target.audience]}`);\n if (r.target.to.length > 0) lines.push(` to: ${r.target.to.join(\", \")}`);\n if (r.target.cc.length > 0) lines.push(` cc: ${r.target.cc.join(\", \")}`);\n lines.push(` ${r.target.reason}`);\n\n if (r.target.audience === \"client\") {\n lines.push(\n \"\",\n `⚠️ A test submission WILL email the client, and email cannot be recalled.`,\n ` Route it to yourself first: reddoor forms-notify-target ${r.site} --set on`,\n );\n }\n if (r.flip?.confirmed && r.flip.to === VERIFY_STATUS) {\n lines.push(\n \"\",\n `Safe to test. When you are done, restore it:`,\n ` reddoor forms-notify-target ${r.site} --set off --restore ${r.flip.from ?? LIVE_STATUS}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * `forms-notify-target <site>` — show who a form submission would email, and\n * optionally flip the pre-launch guard with a read-back confirmation.\n *\n * Read-only by default: answering the question must never be riskier than not\n * asking it.\n */\nexport async function runFormsNotifyTargetCommand(\n site: string | undefined,\n opts: FormsNotifyTargetCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!site?.trim()) {\n return { output: \"forms-notify-target requires <site> (slug or site name)\", code: 2 };\n }\n const set = opts.set?.trim().toLowerCase();\n if (set !== undefined && set !== \"on\" && set !== \"off\") {\n return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };\n }\n try {\n // #539 Phase 5: the flip writes Status on the Websites row; mirror it so the\n // console (which reads Turso) shows the new routing immediately.\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n // #646 step 4: the fleet roster — and the flip's read-back — come from Turso,\n // which is the store `/api/forms/:slug` reads to decide who a submission\n // emails, and the only one that can see a `site_<ULID>` site.\n const { readFleetRoster } = await import(\"../../fleet/roster.js\");\n const result = await formsNotifyTarget({\n roster: () => readFleetRoster(),\n siteMirror: await makeSiteMirror(),\n site: site.trim(),\n ...(set ? { set: set as \"on\" | \"off\" } : {}),\n ...(opts.restore ? { restore: opts.restore } : {}),\n });\n // An unconfirmed flip must not exit 0: a script (or a person skimming) that\n // reads exit status would otherwise take \"I flipped it\" on faith, which is\n // exactly the assumption that sent a client a test lead.\n return {\n output: formatNotifyTarget(result),\n code: result.flip && !result.flip.confirmed ? 1 : 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TURSO_IS_AUTHORITATIVE,
|
|
3
3
|
mirrorWrite
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WQLNUEZL.js";
|
|
5
5
|
export {
|
|
6
6
|
TURSO_IS_AUTHORITATIVE,
|
|
7
7
|
mirrorWrite
|
|
8
8
|
};
|
|
9
|
-
//# sourceMappingURL=freeze-
|
|
9
|
+
//# sourceMappingURL=freeze-GUQ43EAU.js.map
|