@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,24 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
operatorEmail
|
|
3
|
-
} from "./chunk-5ML7FMBD.js";
|
|
4
1
|
import {
|
|
5
2
|
buildReportDataForSite,
|
|
6
3
|
scoresFromRow
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import
|
|
9
|
-
|
|
4
|
+
} from "./chunk-PZZ6HM5T.js";
|
|
5
|
+
import {
|
|
6
|
+
operatorEmail
|
|
7
|
+
} from "./chunk-5ML7FMBD.js";
|
|
8
|
+
import "./chunk-DRNZVORX.js";
|
|
9
|
+
import "./chunk-UPE6D4AT.js";
|
|
10
10
|
import {
|
|
11
11
|
ELIGIBLE_STATUSES
|
|
12
12
|
} from "./chunk-PZR4EBPY.js";
|
|
13
13
|
import {
|
|
14
14
|
isProbablyEmail,
|
|
15
15
|
parseAddresses
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-LDHRIQUW.js";
|
|
17
|
+
import "./chunk-TEMQPSCM.js";
|
|
18
|
+
import "./chunk-ASLDCWBF.js";
|
|
18
19
|
import "./chunk-VAD43MOR.js";
|
|
19
20
|
import {
|
|
20
21
|
fetchAttachmentBytes
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-YJBSB4LP.js";
|
|
22
23
|
import {
|
|
23
24
|
openBase,
|
|
24
25
|
readAirtableConfig
|
|
@@ -26,23 +27,26 @@ import {
|
|
|
26
27
|
import {
|
|
27
28
|
prepareHeaderImage,
|
|
28
29
|
renderReportEmail
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-6THEZQLA.js";
|
|
30
31
|
import "./chunk-GECESMVS.js";
|
|
31
32
|
import "./chunk-KJGKZLCT.js";
|
|
32
33
|
import "./chunk-BGTYPVLT.js";
|
|
33
34
|
import {
|
|
34
35
|
applyReportTypeHeadline
|
|
35
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-LZMAG73U.js";
|
|
37
|
+
import {
|
|
38
|
+
listWebsites
|
|
39
|
+
} from "./chunk-TC4GANMX.js";
|
|
36
40
|
import {
|
|
37
|
-
listWebsites,
|
|
38
41
|
siteSlug
|
|
39
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ICSRWW5Q.js";
|
|
43
|
+
import "./chunk-RQJFGK3A.js";
|
|
40
44
|
import {
|
|
41
45
|
defaultResendClient
|
|
42
46
|
} from "./chunk-WEKL2HYJ.js";
|
|
43
|
-
import "./chunk-
|
|
44
|
-
import "./chunk-
|
|
45
|
-
import "./chunk-
|
|
47
|
+
import "./chunk-YXWBM26P.js";
|
|
48
|
+
import "./chunk-MMSMZC7S.js";
|
|
49
|
+
import "./chunk-IOTOOTII.js";
|
|
46
50
|
import "./chunk-RARREDQE.js";
|
|
47
51
|
import "./chunk-6VAL7XJT.js";
|
|
48
52
|
|
|
@@ -172,4 +176,4 @@ async function runSelftestCommand(kind, site, opts) {
|
|
|
172
176
|
export {
|
|
173
177
|
runSelftestCommand
|
|
174
178
|
};
|
|
175
|
-
//# sourceMappingURL=selftest-
|
|
179
|
+
//# sourceMappingURL=selftest-G4W3LIBQ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recipes/selftest-email.ts","../src/cli/commands/selftest.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug } from \"../reports/airtable/websites.js\";\nimport { ELIGIBLE_STATUSES } from \"../reports/due.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { fetchAttachmentBytes } from \"../reports/airtable/attachments.js\";\nimport { prepareHeaderImage } from \"../reports/maintenance-email/header-image.js\";\nimport { applyReportTypeHeadline } from \"../reports/header-image/index.js\";\nimport { buildReportDataForSite, scoresFromRow } from \"../reports/report-data.js\";\nimport { renderReportEmail } from \"../reports/send/render-email.js\";\nimport { defaultResendClient, type ResendClient } from \"../reports/send/resend.js\";\nimport { parseAddresses, isProbablyEmail } from \"../reports/send/orchestrate.js\";\nimport { operatorEmail } from \"../util/operator.js\";\nimport type { ReportType } from \"../reports/types.js\";\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\nconst REPLY_TO = \"info@reddoorla.com\";\n\nexport type SelftestEmailDeps = {\n /** Airtable handle (read-only here). Defaults to the live base from credentials. */\n base?: AirtableBase;\n /** Resend client. Defaults to the real client. */\n resend?: ResendClient;\n /** Single-site slug. Mutually exclusive with `all`. */\n site?: string;\n /** All `maintenance` sites (one email each). Mutually exclusive with `site`. */\n all?: boolean;\n /** Report type to preview. Default \"Announcement\". */\n type?: ReportType;\n /** Raw `--to` (comma- or newline-separated). Default: the operator inbox. */\n to?: string;\n /** Render only; write reports/<slug>/selftest-<type>.html, never send. */\n dryRun?: boolean;\n /** Single timestamp driving the window + completedOn. */\n now?: Date;\n};\n\nexport type SelftestEmailSiteResult =\n | { site: string; status: \"sent\" | \"dry-run\"; subject: string; recipients: string[] }\n | { site: string; status: \"skipped\"; reason: string }\n | { site: string; status: \"error\"; message: string };\n\nexport type SelftestEmailResult = { results: SelftestEmailSiteResult[] };\n\n/** Resolve the recipient list: explicit `--to` (validated) else the operator default. */\nfunction resolveRecipients(to: string | undefined): string[] {\n const operator = operatorEmail();\n const parsed = to ? parseAddresses(to) : null;\n const list = parsed ?? [operator];\n for (const addr of list) {\n if (!isProbablyEmail(addr)) {\n throw Object.assign(new Error(`--to has a malformed address: ${addr}`), { exitCode: 2 });\n }\n }\n return list;\n}\n\n/**\n * Send (or dry-render) a single report email per target site to the operator/`--to`, with NO\n * Airtable side effects (no draft, queue, or stamp). Mirrors the production render+send via the\n * shared `renderReportEmail` seam, so the preview matches a real send. One bad site never aborts\n * `--all` (per-site try/catch). Sites missing stored scores or a header image are skipped.\n */\nexport async function selftestEmail(deps: SelftestEmailDeps): Promise<SelftestEmailResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const resend = deps.resend ?? defaultResendClient();\n const type: ReportType = deps.type ?? \"Announcement\";\n const now = deps.now ?? new Date();\n const recipients = resolveRecipients(deps.to);\n\n const websites = await listWebsites(base);\n let targets: WebsiteRow[];\n if (deps.all) {\n // The report-eligible set (maintained + hosted-only), not a hard-coded \"maintained\" —\n // the latter silently excluded hosting sites and implied a type↔status coupling that\n // doesn't exist (the requested --type drives the rendered template; single-site mode\n // applies no status filter at all).\n targets = websites.filter((w) => w.status !== null && ELIGIBLE_STATUSES.has(w.status));\n } else if (deps.site) {\n const wanted = siteSlug(deps.site);\n targets = websites.filter((w) => siteSlug(w.name) === wanted);\n } else {\n throw Object.assign(new Error(\"Provide a site slug or --all\"), { exitCode: 2 });\n }\n\n const results: SelftestEmailSiteResult[] = [];\n for (const w of targets) {\n try {\n const scores = scoresFromRow(w);\n if (!scores) {\n results.push({ site: w.name, status: \"skipped\", reason: \"missing Lighthouse scores\" });\n continue;\n }\n if (!w.headerImage) {\n results.push({ site: w.name, status: \"skipped\", reason: \"no Header image\" });\n continue;\n }\n const original = await fetchAttachmentBytes(w.headerImage.url);\n // Mirror orchestrate.ts exactly: the stored header is the CLEAN plate, so the\n // report type's headline has to be stamped on before downscaling. Omitting this\n // step made every selftest preview ship a header with an EMPTY headline band —\n // the one thing a preview exists to catch, silently wrong in the preview itself.\n const withHeadline = await applyReportTypeHeadline(original.bytes, type);\n const header = await prepareHeaderImage(withHeadline);\n const slug = siteSlug(w.name);\n const reportData = await buildReportDataForSite(w, type, now, { scores, header });\n const { html, attachments, subject } = await renderReportEmail(reportData, {\n header,\n cidName: `${slug}-header`,\n });\n\n if (deps.dryRun) {\n const path = `reports/${slug}/selftest-${type.toLowerCase()}.html`;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, html, \"utf-8\");\n results.push({ site: w.name, status: \"dry-run\", subject, recipients });\n continue;\n }\n\n await resend.send({\n from: FROM_ADDRESS,\n to: recipients,\n replyTo: REPLY_TO,\n subject,\n html,\n attachments,\n });\n results.push({ site: w.name, status: \"sent\", subject, recipients });\n } catch (err) {\n results.push({\n site: w.name,\n status: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { results };\n}\n","import { selftestEmail, type SelftestEmailSiteResult } from \"../../recipes/selftest-email.js\";\nimport type { ReportType } from \"../../reports/types.js\";\n\nexport type SelftestCommandOptions = {\n type?: string;\n to?: string;\n all?: boolean;\n dryRun?: boolean;\n cwd?: string;\n};\n\nconst TYPES: Record<string, ReportType> = {\n announcement: \"Announcement\",\n maintenance: \"Maintenance\",\n testing: \"Testing\",\n launch: \"Launch\",\n};\n\nfunction formatResult(r: SelftestEmailSiteResult): string {\n if (r.status === \"skipped\") return `[${r.site}] skipped — ${r.reason}`;\n if (r.status === \"error\") return `[${r.site}] error: ${r.message}`;\n return `[${r.site}] ${r.status} — \"${r.subject}\" → ${r.recipients.join(\", \")}`;\n}\n\n/**\n * `selftest <kind> [site]` — operator self-tests. The only kind today is `email`: preview a\n * report email for one site (or `--all` maintenance sites) to the operator/`--to`, with no\n * Airtable side effects. Validates kind/type and the site-xor-all rule before doing any work.\n */\nexport async function runSelftestCommand(\n kind: string,\n site: string | undefined,\n opts: SelftestCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (kind !== \"email\") {\n return { output: `Unknown selftest kind '${kind}'. Supported: email`, code: 2 };\n }\n if (Boolean(site) === Boolean(opts.all)) {\n return { output: \"Provide exactly one of <site> or --all.\", code: 2 };\n }\n const typeKey = (opts.type ?? \"announcement\").toLowerCase();\n const type = TYPES[typeKey];\n if (!type) {\n return {\n output: `Unknown --type '${opts.type}'. Supported: ${Object.keys(TYPES).join(\", \")}`,\n code: 2,\n };\n }\n\n try {\n const { results } = await selftestEmail({\n ...(site ? { site } : {}),\n ...(opts.all ? { all: true } : {}),\n type,\n ...(opts.to ? { to: opts.to } : {}),\n ...(opts.dryRun ? { dryRun: true } : {}),\n });\n const output =\n results.length === 0 ? \"No matching sites.\" : results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"error\") ? 1 : 0;\n return { output, code };\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
|
+
{"version":3,"sources":["../src/recipes/selftest-email.ts","../src/cli/commands/selftest.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug } from \"../reports/airtable/websites.js\";\nimport { ELIGIBLE_STATUSES } from \"../reports/due.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { fetchAttachmentBytes } from \"../reports/airtable/attachments.js\";\nimport { prepareHeaderImage } from \"../reports/maintenance-email/header-image.js\";\nimport { applyReportTypeHeadline } from \"../reports/header-image/index.js\";\nimport { buildReportDataForSite, scoresFromRow } from \"../reports/report-data.js\";\nimport { renderReportEmail } from \"../reports/send/render-email.js\";\nimport { defaultResendClient, type ResendClient } from \"../reports/send/resend.js\";\nimport { parseAddresses, isProbablyEmail } from \"../reports/send/orchestrate.js\";\nimport { operatorEmail } from \"../util/operator.js\";\nimport type { ReportType } from \"../reports/types.js\";\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\nconst REPLY_TO = \"info@reddoorla.com\";\n\nexport type SelftestEmailDeps = {\n /** Airtable handle (read-only here). Defaults to the live base from credentials. */\n base?: AirtableBase;\n /** Resend client. Defaults to the real client. */\n resend?: ResendClient;\n /** Single-site slug. Mutually exclusive with `all`. */\n site?: string;\n /** All `maintenance` sites (one email each). Mutually exclusive with `site`. */\n all?: boolean;\n /** Report type to preview. Default \"Announcement\". */\n type?: ReportType;\n /** Raw `--to` (comma- or newline-separated). Default: the operator inbox. */\n to?: string;\n /** Render only; write reports/<slug>/selftest-<type>.html, never send. */\n dryRun?: boolean;\n /** Single timestamp driving the window + completedOn. */\n now?: Date;\n};\n\nexport type SelftestEmailSiteResult =\n | { site: string; status: \"sent\" | \"dry-run\"; subject: string; recipients: string[] }\n | { site: string; status: \"skipped\"; reason: string }\n | { site: string; status: \"error\"; message: string };\n\nexport type SelftestEmailResult = { results: SelftestEmailSiteResult[] };\n\n/** Resolve the recipient list: explicit `--to` (validated) else the operator default. */\nfunction resolveRecipients(to: string | undefined): string[] {\n const operator = operatorEmail();\n const parsed = to ? parseAddresses(to) : null;\n const list = parsed ?? [operator];\n for (const addr of list) {\n if (!isProbablyEmail(addr)) {\n throw Object.assign(new Error(`--to has a malformed address: ${addr}`), { exitCode: 2 });\n }\n }\n return list;\n}\n\n/**\n * Send (or dry-render) a single report email per target site to the operator/`--to`, with NO\n * Airtable side effects (no draft, queue, or stamp). Mirrors the production render+send via the\n * shared `renderReportEmail` seam, so the preview matches a real send. One bad site never aborts\n * `--all` (per-site try/catch). Sites missing stored scores or a header image are skipped.\n */\nexport async function selftestEmail(deps: SelftestEmailDeps): Promise<SelftestEmailResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const resend = deps.resend ?? defaultResendClient();\n const type: ReportType = deps.type ?? \"Announcement\";\n const now = deps.now ?? new Date();\n const recipients = resolveRecipients(deps.to);\n\n const websites = await listWebsites(base);\n let targets: WebsiteRow[];\n if (deps.all) {\n // The report-eligible set (maintained + hosted-only), not a hard-coded \"maintained\" —\n // the latter silently excluded hosting sites and implied a type↔status coupling that\n // doesn't exist (the requested --type drives the rendered template; single-site mode\n // applies no status filter at all).\n targets = websites.filter((w) => w.status !== null && ELIGIBLE_STATUSES.has(w.status));\n } else if (deps.site) {\n const wanted = siteSlug(deps.site);\n targets = websites.filter((w) => siteSlug(w.name) === wanted);\n } else {\n throw Object.assign(new Error(\"Provide a site slug or --all\"), { exitCode: 2 });\n }\n\n const results: SelftestEmailSiteResult[] = [];\n for (const w of targets) {\n try {\n const scores = scoresFromRow(w);\n if (!scores) {\n results.push({ site: w.name, status: \"skipped\", reason: \"missing Lighthouse scores\" });\n continue;\n }\n if (!w.headerImage) {\n results.push({ site: w.name, status: \"skipped\", reason: \"no Header image\" });\n continue;\n }\n const original = await fetchAttachmentBytes(w.headerImage.url);\n // Mirror orchestrate.ts exactly: the stored header is the CLEAN plate, so the\n // report type's headline has to be stamped on before downscaling. Omitting this\n // step made every selftest preview ship a header with an EMPTY headline band —\n // the one thing a preview exists to catch, silently wrong in the preview itself.\n const withHeadline = await applyReportTypeHeadline(original.bytes, type);\n const header = await prepareHeaderImage(withHeadline);\n const slug = siteSlug(w.name);\n const reportData = await buildReportDataForSite(w, type, now, { scores, header });\n const { html, attachments, subject } = await renderReportEmail(reportData, {\n header,\n cidName: `${slug}-header`,\n });\n\n if (deps.dryRun) {\n const path = `reports/${slug}/selftest-${type.toLowerCase()}.html`;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, html, \"utf-8\");\n results.push({ site: w.name, status: \"dry-run\", subject, recipients });\n continue;\n }\n\n await resend.send({\n from: FROM_ADDRESS,\n to: recipients,\n replyTo: REPLY_TO,\n subject,\n html,\n attachments,\n });\n results.push({ site: w.name, status: \"sent\", subject, recipients });\n } catch (err) {\n results.push({\n site: w.name,\n status: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { results };\n}\n","import { selftestEmail, type SelftestEmailSiteResult } from \"../../recipes/selftest-email.js\";\nimport type { ReportType } from \"../../reports/types.js\";\n\nexport type SelftestCommandOptions = {\n type?: string;\n to?: string;\n all?: boolean;\n dryRun?: boolean;\n cwd?: string;\n};\n\nconst TYPES: Record<string, ReportType> = {\n announcement: \"Announcement\",\n maintenance: \"Maintenance\",\n testing: \"Testing\",\n launch: \"Launch\",\n};\n\nfunction formatResult(r: SelftestEmailSiteResult): string {\n if (r.status === \"skipped\") return `[${r.site}] skipped — ${r.reason}`;\n if (r.status === \"error\") return `[${r.site}] error: ${r.message}`;\n return `[${r.site}] ${r.status} — \"${r.subject}\" → ${r.recipients.join(\", \")}`;\n}\n\n/**\n * `selftest <kind> [site]` — operator self-tests. The only kind today is `email`: preview a\n * report email for one site (or `--all` maintenance sites) to the operator/`--to`, with no\n * Airtable side effects. Validates kind/type and the site-xor-all rule before doing any work.\n */\nexport async function runSelftestCommand(\n kind: string,\n site: string | undefined,\n opts: SelftestCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (kind !== \"email\") {\n return { output: `Unknown selftest kind '${kind}'. Supported: email`, code: 2 };\n }\n if (Boolean(site) === Boolean(opts.all)) {\n return { output: \"Provide exactly one of <site> or --all.\", code: 2 };\n }\n const typeKey = (opts.type ?? \"announcement\").toLowerCase();\n const type = TYPES[typeKey];\n if (!type) {\n return {\n output: `Unknown --type '${opts.type}'. Supported: ${Object.keys(TYPES).join(\", \")}`,\n code: 2,\n };\n }\n\n try {\n const { results } = await selftestEmail({\n ...(site ? { site } : {}),\n ...(opts.all ? { all: true } : {}),\n type,\n ...(opts.to ? { to: opts.to } : {}),\n ...(opts.dryRun ? { dryRun: true } : {}),\n });\n const output =\n results.length === 0 ? \"No matching sites.\" : results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"error\") ? 1 : 0;\n return { output, code };\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":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSiteBySlug,
|
|
3
|
+
insertSiteRows,
|
|
4
|
+
updateSiteIdentity
|
|
5
|
+
} from "./chunk-BINFZQ2T.js";
|
|
6
|
+
import "./chunk-LF2TJN4X.js";
|
|
7
|
+
import "./chunk-ASLDCWBF.js";
|
|
8
|
+
import "./chunk-VAD43MOR.js";
|
|
9
|
+
import "./chunk-ICSRWW5Q.js";
|
|
10
|
+
|
|
11
|
+
// src/db/site-create.ts
|
|
12
|
+
var SLUG_TAKEN = /UNIQUE constraint failed: sites\.slug/;
|
|
13
|
+
function makeSiteStore(db, now = () => /* @__PURE__ */ new Date()) {
|
|
14
|
+
return {
|
|
15
|
+
findBySlug: async (slug) => {
|
|
16
|
+
const row = await getSiteBySlug(db, slug);
|
|
17
|
+
if (!row) return null;
|
|
18
|
+
return {
|
|
19
|
+
id: row.id,
|
|
20
|
+
name: row.name,
|
|
21
|
+
// `rowFromJoined` maps a null url to "" — undo that, so "blank" means one thing.
|
|
22
|
+
url: row.url === "" ? null : row.url,
|
|
23
|
+
pointOfContact: row.pointOfContact,
|
|
24
|
+
gitRepo: row.gitRepo
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
create: async (site) => {
|
|
28
|
+
try {
|
|
29
|
+
await db.transaction().execute((trx) => insertSiteRows(trx, site, now().toISOString()));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
const causeMessage = err instanceof Error && err.cause instanceof Error ? err.cause.message : "";
|
|
33
|
+
if (SLUG_TAKEN.test(message) || SLUG_TAKEN.test(causeMessage)) {
|
|
34
|
+
throw Object.assign(new Error(message, { cause: err }), { code: "SLUG_TAKEN" });
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
updateIdentity: (siteId, patch) => updateSiteIdentity(db, siteId, patch)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
makeSiteStore
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=site-create-ZU3ACBTO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/site-create.ts"],"sourcesContent":["/** #646 step 3: the Turso half of the Turso-native `ensure-site`.\n *\n * Turso is the source of truth for a new site, so this is NOT a mirror: nothing\n * shadows it and nothing converges it. It adapts `fleet-state`'s gated queries\n * to the `SiteStore` the creator (`src/fleet/ensure-site.ts`) drives, and adds\n * the one thing those queries cannot express on their own — the transaction.\n *\n * It lives in its own module, exempt from the EXPLAIN gate, for a concrete\n * reason: the gate runs every scenario against ONE shared `:memory:` libSQL\n * client, and `client.transaction()` on an in-memory client hands its connection\n * to the transaction and opens a fresh, EMPTY database for the next statement\n * (verified 2026-09-17: the statement after a committed transaction failed with\n * `no such table`). Every SQL statement this module issues is `insertSiteRows` /\n * `updateSiteIdentity` / `getSiteBySlug`, which the gate plans directly.\n *\n * The same hazard means any test driving `create` must use a `file:` database.\n */\nimport type { Db } from \"./client.js\";\nimport { getSiteBySlug, insertSiteRows, updateSiteIdentity } from \"./fleet-state.js\";\nimport type { SiteStore } from \"../fleet/ensure-site.js\";\n\n/** SQLite's message for the `sites.slug` UNIQUE index, as libSQL surfaces it. */\nconst SLUG_TAKEN = /UNIQUE constraint failed: sites\\.slug/;\n\nexport function makeSiteStore(db: Db, now: () => Date = () => new Date()): SiteStore {\n return {\n findBySlug: async (slug) => {\n const row = await getSiteBySlug(db, slug);\n if (!row) return null;\n return {\n id: row.id,\n name: row.name,\n // `rowFromJoined` maps a null url to \"\" — undo that, so \"blank\" means one thing.\n url: row.url === \"\" ? null : row.url,\n pointOfContact: row.pointOfContact,\n gitRepo: row.gitRepo,\n };\n },\n create: async (site) => {\n try {\n await db.transaction().execute((trx) => insertSiteRows(trx, site, now().toISOString()));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const causeMessage =\n err instanceof Error && err.cause instanceof Error ? err.cause.message : \"\";\n if (SLUG_TAKEN.test(message) || SLUG_TAKEN.test(causeMessage)) {\n throw Object.assign(new Error(message, { cause: err }), { code: \"SLUG_TAKEN\" });\n }\n throw err;\n }\n },\n updateIdentity: (siteId, patch) => updateSiteIdentity(db, siteId, patch),\n };\n}\n"],"mappings":";;;;;;;;;;;AAsBA,IAAM,aAAa;AAEZ,SAAS,cAAc,IAAQ,MAAkB,MAAM,oBAAI,KAAK,GAAc;AACnF,SAAO;AAAA,IACL,YAAY,OAAO,SAAS;AAC1B,YAAM,MAAM,MAAM,cAAc,IAAI,IAAI;AACxC,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO;AAAA,QACL,IAAI,IAAI;AAAA,QACR,MAAM,IAAI;AAAA;AAAA,QAEV,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI;AAAA,QACjC,gBAAgB,IAAI;AAAA,QACpB,SAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,SAAS;AACtB,UAAI;AACF,cAAM,GAAG,YAAY,EAAE,QAAQ,CAAC,QAAQ,eAAe,KAAK,MAAM,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,MACxF,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,cAAM,eACJ,eAAe,SAAS,IAAI,iBAAiB,QAAQ,IAAI,MAAM,UAAU;AAC3E,YAAI,WAAW,KAAK,OAAO,KAAK,WAAW,KAAK,YAAY,GAAG;AAC7D,gBAAM,OAAO,OAAO,IAAI,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,aAAa,CAAC;AAAA,QAChF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,gBAAgB,CAAC,QAAQ,UAAU,mBAAmB,IAAI,QAAQ,KAAK;AAAA,EACzE;AACF;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TURSO_IS_AUTHORITATIVE
|
|
3
|
+
} from "./chunk-WQLNUEZL.js";
|
|
4
|
+
|
|
5
|
+
// src/forms/site-lookup.ts
|
|
6
|
+
function makeSiteLookup(deps, strict = TURSO_IS_AUTHORITATIVE) {
|
|
7
|
+
return async (slug) => {
|
|
8
|
+
const hit = await deps.fromDb(slug);
|
|
9
|
+
if (hit) return hit;
|
|
10
|
+
if (strict) return null;
|
|
11
|
+
return deps.fromAirtable(slug);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeLazySiteLookup(deps, strict = TURSO_IS_AUTHORITATIVE) {
|
|
15
|
+
return makeSiteLookup(
|
|
16
|
+
{
|
|
17
|
+
fromDb: deps.fromDb,
|
|
18
|
+
fromAirtable: (slug) => deps.fromAirtable(deps.openAirtable(), slug)
|
|
19
|
+
},
|
|
20
|
+
strict
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
makeLazySiteLookup,
|
|
25
|
+
makeSiteLookup
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=site-lookup-DTOLJ4EA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/forms/site-lookup.ts"],"sourcesContent":["/** Phase 2 (#539): the Turso-primary site lookup for form ingest.\n *\n * The hot path reads Turso only — this is what retires the 08-17 outage class,\n * where an Airtable quota outage made `getWebsiteBySlug` throw while the lead\n * store itself was healthy. Airtable is consulted ONLY when Turso has no row\n * for the slug: the new-site window, where a site launched since the last\n * hourly sync exists in Airtable but not yet in Turso. Real traffic posts to\n * real sites' endpoints, so that fallback fires for brand-new sites and\n * garbage slugs — never per lead.\n *\n * Error semantics, both deliberate:\n * - A Turso failure propagates WITHOUT trying Airtable: the lead store is\n * Turso, so if it is down the submission cannot persist anyway — a fallback\n * row would only manufacture a lookup success the write path then wastes.\n * - An Airtable failure during the fallback propagates too, which hands the\n * lead to the dead-letter (Turso is up in that scenario) for replay once\n * the lookup recovers.\n */\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { TURSO_IS_AUTHORITATIVE } from \"../db/freeze.js\";\n\nexport type SiteLookupDeps = {\n fromDb: (slug: string) => Promise<WebsiteRow | null>;\n fromAirtable: (slug: string) => Promise<WebsiteRow | null>;\n};\n\nexport function makeSiteLookup(\n deps: SiteLookupDeps,\n /** #612. `true` = post-freeze: the Airtable fallback is not consulted at all,\n * so form ingest touches one store. Injected so both sides stay proven and\n * the freeze commit stays a one-line change. */\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): (slug: string) => Promise<WebsiteRow | null> {\n return async (slug) => {\n const hit = await deps.fromDb(slug);\n if (hit) return hit;\n // Post-freeze the fallback is not just unnecessary, it is WRONG: Airtable is\n // frozen, so a row it still holds and Turso does not is stale by definition,\n // and resolving a lead against it would attach that lead to a site the\n // system no longer believes in. A miss is an unknown slug, full stop.\n //\n // What made the fallback load-bearing was a site created directly in the\n // Airtable UI, before the next hourly import. Nothing hand-creates rows\n // after the freeze, and `ensure-site` inserts straight into Turso (#608),\n // so the window it covered no longer exists either.\n if (strict) return null;\n return deps.fromAirtable(slug);\n };\n}\n\n/**\n * #645. The same lookup, with the Airtable client built LAZILY inside the\n * fallback — the shape both real callers need, in one place so they cannot\n * drift.\n *\n * Laziness is load-bearing, not tidiness. `readAirtableConfig()` THROWS when the\n * PAT is unset, so constructing the base eagerly puts the whole Airtable layer\n * in front of a path that (under the freeze) never consults it. `form-ingest.mts`\n * learned this the expensive way — an eager base is what put Airtable in front of\n * every lead. `db replay-deadletters` had the same bug with a worse blast radius:\n * it opened the base at the top of the branch, so a missing Airtable credential\n * refused the recovery of leads that were already captured and waiting.\n *\n * Generic in the base type so this module stays the leaf it is — it imports no\n * Airtable client, only `WebsiteRow`.\n */\nexport function makeLazySiteLookup<TBase>(\n deps: {\n fromDb: (slug: string) => Promise<WebsiteRow | null>;\n /** Constructs the Airtable base. Called ONLY from inside the fallback, and\n * therefore never at all while `strict` is on. */\n openAirtable: () => TBase;\n fromAirtable: (base: TBase, slug: string) => Promise<WebsiteRow | null>;\n },\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): (slug: string) => Promise<WebsiteRow | null> {\n return makeSiteLookup(\n {\n fromDb: deps.fromDb,\n fromAirtable: (slug) => deps.fromAirtable(deps.openAirtable(), slug),\n },\n strict,\n );\n}\n"],"mappings":";;;;;AA0BO,SAAS,eACd,MAIA,SAAkB,wBAC4B;AAC9C,SAAO,OAAO,SAAS;AACrB,UAAM,MAAM,MAAM,KAAK,OAAO,IAAI;AAClC,QAAI,IAAK,QAAO;AAUhB,QAAI,OAAQ,QAAO;AACnB,WAAO,KAAK,aAAa,IAAI;AAAA,EAC/B;AACF;AAkBO,SAAS,mBACd,MAOA,SAAkB,wBAC4B;AAC9C,SAAO;AAAA,IACL;AAAA,MACE,QAAQ,KAAK;AAAA,MACb,cAAc,CAAC,SAAS,KAAK,aAAa,KAAK,aAAa,GAAG,IAAI;AAAA,IACrE;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TURSO_IS_AUTHORITATIVE
|
|
3
|
+
} from "./chunk-WQLNUEZL.js";
|
|
1
4
|
import {
|
|
2
5
|
mirrorHealthFields,
|
|
3
6
|
mirrorSiteFields,
|
|
4
|
-
mirrorSiteInsert
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
} from "./chunk-SVAAONYB.js";
|
|
10
|
-
import "./chunk-F3JWOPTG.js";
|
|
7
|
+
mirrorSiteInsert,
|
|
8
|
+
siteRowExists
|
|
9
|
+
} from "./chunk-BINFZQ2T.js";
|
|
10
|
+
import "./chunk-LF2TJN4X.js";
|
|
11
|
+
import "./chunk-ASLDCWBF.js";
|
|
11
12
|
import "./chunk-VAD43MOR.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-ICSRWW5Q.js";
|
|
13
14
|
import {
|
|
14
15
|
openDb,
|
|
15
16
|
readDbConfig
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-MMSMZC7S.js";
|
|
18
|
+
import "./chunk-IOTOOTII.js";
|
|
18
19
|
import "./chunk-6VAL7XJT.js";
|
|
19
20
|
|
|
20
21
|
// src/db/site-mirror.ts
|
|
@@ -53,10 +54,23 @@ async function makeSiteMirror(open = () => openDb(readDbConfig()), strict = TURS
|
|
|
53
54
|
return true;
|
|
54
55
|
}),
|
|
55
56
|
health: (siteId, fields) => run(siteId, "health", (d) => mirrorHealthFields(d, siteId, fields)),
|
|
56
|
-
site: (siteId, fields) => run(siteId, "site", (d) => mirrorSiteFields(d, siteId, fields))
|
|
57
|
+
site: (siteId, fields) => run(siteId, "site", (d) => mirrorSiteFields(d, siteId, fields)),
|
|
58
|
+
// Deliberately NOT routed through `run`: that helper logs a mirrored= line
|
|
59
|
+
// and, under strict, throws on anything but a clean write — both wrong for a
|
|
60
|
+
// read whose whole job is to answer a question. A failed probe answers
|
|
61
|
+
// "present" so the caller heals nothing it cannot see.
|
|
62
|
+
hasRow: async (siteId) => {
|
|
63
|
+
if (!db) return true;
|
|
64
|
+
try {
|
|
65
|
+
return await siteRowExists(db, siteId);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.log(`SITE_MIRROR site=${siteId} op=hasRow unknown error=${e.message}`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
};
|
|
58
72
|
}
|
|
59
73
|
export {
|
|
60
74
|
makeSiteMirror
|
|
61
75
|
};
|
|
62
|
-
//# sourceMappingURL=site-mirror-
|
|
76
|
+
//# sourceMappingURL=site-mirror-WR7ACXOV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/site-mirror.ts"],"sourcesContent":["/** #539 Phase 5: the Turso write-through for the ONE-OFF Websites writers.\n *\n * Phase 3 mirrored the nightly sweep — the audit write-back, github-signals and\n * the next-due dates. It did not touch the writers that run on their own\n * schedule or on demand: the analytics soft-fail stamp, the Renovate\n * auto-fix counter, the prismic-models verdict, `updateLaunched`, the\n * forms notify target, and the SINGLE-SITE audit write-back (only the fleet\n * path ever passed a mirror). Each of those reached Turso solely via the hourly\n * sync, which stops existing at the freeze.\n *\n * It lives in `src/db` rather than a feature folder because its callers span\n * audits, recipes, CLI commands and the send path — no one feature owns it.\n *\n * Deliberately UNLIKE `makeHealthMirrorBestEffort`, this never returns null.\n * #585 is the reason: that factory returned null without creds and the\n * dual-write silently no-opped in production for weeks, because a dead mirror\n * and a healthy one produced identical output — the only tell was an ABSENT log\n * suffix nobody was watching for. Here creds-absent is a state the mirror\n * REPORTS, so a missing SITE_MIRROR line means the wiring is gone.\n */\nimport { openDb, readDbConfig, type Db } from \"./client.js\";\nimport {\n mirrorHealthFields,\n mirrorSiteFields,\n mirrorSiteInsert,\n siteRowExists,\n} from \"./fleet-state.js\";\nimport { TURSO_IS_AUTHORITATIVE } from \"./freeze.js\";\n\n/** Two ops because the Websites row is split across two Turso tables. Callers\n * pass the EXACT FieldSet the Airtable writer returned, so the mirror can never\n * carry a different payload than the write it shadows. */\nexport type SiteMirror = {\n /** A row Airtable just CREATED, as Airtable echoed it back. `ensure-site` used\n * this until #646 step 3; it now creates sites in Turso directly\n * (`src/fleet/ensure-site.ts`), and neither this nor `hasRow` has a caller left\n * outside tests — both go with the Airtable layer in step 6.\n * Every other op is an UPDATE, which does nothing for a row that does not\n * exist yet — so without this a bootstrapped site is invisible to Turso and\n * every mirror the rest of the bootstrap fires reports `mirrored=missed`. */\n created: (rec: { id: string; fields: Record<string, unknown> }) => Promise<void>;\n /** Columns that live in `site_health`. */\n health: (siteId: string, fields: Record<string, unknown>) => Promise<void>;\n /** Columns that live in `sites`. */\n site: (siteId: string, fields: Record<string, unknown>) => Promise<void>;\n /** #645. Does Turso hold a row for this site id? A pure READ, so it is the one\n * op here that neither logs a SITE_MIRROR line nor throws under the freeze —\n * `ensure-site` uses it to decide whether to heal, and a probe that threw\n * would sink the very command that repairs the gap. Creds-absent answers\n * `true` (\"assume present\"), so a mirror that cannot read can never provoke\n * a blind insert. */\n hasRow: (siteId: string) => Promise<boolean>;\n};\n\n/** Build the one-off writers' mirror. Never throws and never returns null:\n * Airtable is still authoritative through Phase 5, so a mirror problem must not\n * cost the write it shadows — the hourly sync converges whatever this misses.\n * `open` is injectable for tests. */\nexport async function makeSiteMirror(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n /** #612. `true` = Turso is the store that must succeed, so every failure\n * throws instead of being logged and swallowed. Defaulted from the shipped\n * constant and injected by tests, so both sides stay proven. */\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<SiteMirror> {\n let db: Db | null = null;\n let why = \"\";\n try {\n db = await open();\n } catch (e) {\n why = (e as Error).message;\n }\n // Frozen: refuse to hand back a mirror that cannot write. Failing at\n // CONSTRUCTION rather than per write matters — a caller holding a\n // working-looking mirror would run its whole batch before anyone noticed that\n // nothing had persisted.\n if (strict && !db) throw new Error(`SITE_MIRROR unavailable: ${why}`);\n\n const run = async (siteId: string, op: string, work: (db: Db) => Promise<boolean>) => {\n if (!db) {\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=absent reason=${why}`);\n return;\n }\n // Every path below logs EXACTLY ONE line, then strict adds a throw. Logging\n // first matters: a frozen run that fails should still leave the same trace\n // in the run log that an unfrozen one would, not just a stack.\n let matched: boolean;\n try {\n matched = await work(db);\n } catch (e) {\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=0 error=${(e as Error).message}`);\n if (strict) throw e;\n return;\n }\n // `missed` is its own outcome: the UPDATE matched no row. Before the freeze\n // that means the hourly sync has not imported this site yet — a transient,\n // and reporting it as mirrored=1 would claim a write that never landed.\n // After the freeze no importer exists, so an absent row stays absent: it is\n // a bug, not a wait.\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=${matched ? \"1\" : \"missed\"}`);\n if (strict && !matched) {\n throw new Error(`SITE_MIRROR site=${siteId} op=${op}: no such row in Turso`);\n }\n };\n\n return {\n // An INSERT always lands, so it reports 1 rather than routing a row count\n // through `missed` — there was nothing to match in the first place.\n created: (rec) =>\n run(rec.id, \"created\", async (d) => {\n await mirrorSiteInsert(d, rec, new Date().toISOString());\n return true;\n }),\n health: (siteId, fields) => run(siteId, \"health\", (d) => mirrorHealthFields(d, siteId, fields)),\n site: (siteId, fields) => run(siteId, \"site\", (d) => mirrorSiteFields(d, siteId, fields)),\n // Deliberately NOT routed through `run`: that helper logs a mirrored= line\n // and, under strict, throws on anything but a clean write — both wrong for a\n // read whose whole job is to answer a question. A failed probe answers\n // \"present\" so the caller heals nothing it cannot see.\n hasRow: async (siteId) => {\n if (!db) return true;\n try {\n return await siteRowExists(db, siteId);\n } catch (e) {\n console.log(`SITE_MIRROR site=${siteId} op=hasRow unknown error=${(e as Error).message}`);\n return true;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0DA,eAAsB,eACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GAIrD,SAAkB,wBACG;AACrB,MAAI,KAAgB;AACpB,MAAI,MAAM;AACV,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,UAAO,EAAY;AAAA,EACrB;AAKA,MAAI,UAAU,CAAC,GAAI,OAAM,IAAI,MAAM,4BAA4B,GAAG,EAAE;AAEpE,QAAM,MAAM,OAAO,QAAgB,IAAY,SAAuC;AACpF,QAAI,CAAC,IAAI;AACP,cAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,2BAA2B,GAAG,EAAE;AAC/E;AAAA,IACF;AAIA,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,KAAK,EAAE;AAAA,IACzB,SAAS,GAAG;AACV,cAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,qBAAsB,EAAY,OAAO,EAAE;AAC1F,UAAI,OAAQ,OAAM;AAClB;AAAA,IACF;AAMA,YAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,aAAa,UAAU,MAAM,QAAQ,EAAE;AACtF,QAAI,UAAU,CAAC,SAAS;AACtB,YAAM,IAAI,MAAM,oBAAoB,MAAM,OAAO,EAAE,wBAAwB;AAAA,IAC7E;AAAA,EACF;AAEA,SAAO;AAAA;AAAA;AAAA,IAGL,SAAS,CAAC,QACR,IAAI,IAAI,IAAI,WAAW,OAAO,MAAM;AAClC,YAAM,iBAAiB,GAAG,MAAK,oBAAI,KAAK,GAAE,YAAY,CAAC;AACvD,aAAO;AAAA,IACT,CAAC;AAAA,IACH,QAAQ,CAAC,QAAQ,WAAW,IAAI,QAAQ,UAAU,CAAC,MAAM,mBAAmB,GAAG,QAAQ,MAAM,CAAC;AAAA,IAC9F,MAAM,CAAC,QAAQ,WAAW,IAAI,QAAQ,QAAQ,CAAC,MAAM,iBAAiB,GAAG,QAAQ,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKxF,QAAQ,OAAO,WAAW;AACxB,UAAI,CAAC,GAAI,QAAO;AAChB,UAAI;AACF,eAAO,MAAM,cAAc,IAAI,MAAM;AAAA,MACvC,SAAS,GAAG;AACV,gBAAQ,IAAI,oBAAoB,MAAM,4BAA6B,EAAY,OAAO,EAAE;AACxF,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -5,20 +5,21 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DYTYPKFN.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FP6HOW6J.js";
|
|
12
12
|
import {
|
|
13
13
|
smokeSuite
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-EN6CTBUN.js";
|
|
15
|
+
import "./chunk-TJLKWQOX.js";
|
|
16
|
+
import "./chunk-5CJVUPJU.js";
|
|
17
|
+
import "./chunk-LUF52EIY.js";
|
|
17
18
|
import "./chunk-Z3JXLULD.js";
|
|
18
19
|
import "./chunk-LBYOLBW7.js";
|
|
19
20
|
import "./chunk-RARREDQE.js";
|
|
20
21
|
import "./chunk-XXTZBPUY.js";
|
|
21
|
-
import "./chunk-
|
|
22
|
+
import "./chunk-WMF5S2MB.js";
|
|
22
23
|
|
|
23
24
|
// src/cli/commands/smoke-suite.ts
|
|
24
25
|
import { resolve } from "path";
|
|
@@ -51,4 +52,4 @@ async function runSmokeSuiteCommand(site, opts) {
|
|
|
51
52
|
export {
|
|
52
53
|
runSmokeSuiteCommand
|
|
53
54
|
};
|
|
54
|
-
//# sourceMappingURL=smoke-suite-
|
|
55
|
+
//# sourceMappingURL=smoke-suite-WAUPI6EJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/smoke-suite.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { smokeSuite } from \"../../recipes/smoke-suite/index.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type SmokeSuiteCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runSmokeSuiteCommand(\n site: string | undefined,\n opts: SmokeSuiteCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results = await runRecipeOverSites(\"smoke-suite\", sites, (s) => smokeSuite(s));\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/smoke-suite.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { smokeSuite } from \"../../recipes/smoke-suite/index.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type SmokeSuiteCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runSmokeSuiteCommand(\n site: string | undefined,\n opts: SmokeSuiteCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results = await runRecipeOverSites(\"smoke-suite\", sites, (s) => smokeSuite(s));\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAcxB,SAAS,aAAa,GAAyB;AAC7C,MAAI,EAAE,WAAW,OAAQ,QAAO,IAAI,EAAE,IAAI,WAAW,EAAE,SAAS,EAAE;AAClE,MAAI,EAAE,WAAW,SAAU,QAAO,IAAI,EAAE,IAAI,aAAa,EAAE,SAAS,EAAE;AACtE,SAAO,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,MAAM;AAAA,EAAe,EAAE,SAAS,EAAE;AAC7E;AAEA,eAAsB,qBACpB,MACA,MAC2C;AAC3C,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI,QAAQ,IAAI;AAEvD,MAAI,QAAQ,MAAM,aAAa;AAAA,IAC7B,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IACxD,GAAI,KAAK,YAAY,SAAY,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF,CAAC;AAED,MAAI,UAAyB,CAAC;AAC9B,MAAI,KAAK,OAAO;AACd,UAAM,UAAU,KAAK,WAAW,aAAa;AAC7C,UAAM,OAAO,MAAM,kBAAkB,OAAO,EAAE,QAAQ,CAAC;AACvD,YAAQ,KAAK;AACb,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,UAAU,MAAM,mBAAmB,eAAe,OAAO,CAAC,MAAM,WAAW,CAAC,CAAC;AAEnF,QAAM,SAAS,QAAQ,IAAI,YAAY,EAAE,KAAK,IAAI;AAClD,QAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ,IAAI,IAAI;AAC9D,SAAO,EAAE,QAAQ,iBAAiB,QAAQ,OAAO,GAAG,KAAK;AAC3D;","names":[]}
|
|
@@ -10,7 +10,7 @@ function parseExtraFields(raw) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
async function rescoreNewSubmissions(db, opts) {
|
|
13
|
-
const { listSubmissionsFiltered, rescoreSubmissionSpam } = await import("./submissions-
|
|
13
|
+
const { listSubmissionsFiltered, rescoreSubmissionSpam } = await import("./submissions-JDZP35JM.js");
|
|
14
14
|
const { classifySpam, SPAM_THRESHOLD } = await import("./spam-classifier-T5H4VTP6.js");
|
|
15
15
|
const rows = [];
|
|
16
16
|
const PAGE = 500;
|
|
@@ -40,10 +40,15 @@ async function rescoreNewSubmissions(db, opts) {
|
|
|
40
40
|
}
|
|
41
41
|
async function loadSiteNames() {
|
|
42
42
|
try {
|
|
43
|
-
const {
|
|
44
|
-
const {
|
|
45
|
-
const
|
|
46
|
-
|
|
43
|
+
const { openDb, readDbConfig } = await import("./client-DGTO65WB.js");
|
|
44
|
+
const { listSites } = await import("./fleet-state-RJROYXKO.js");
|
|
45
|
+
const db = await openDb(readDbConfig());
|
|
46
|
+
try {
|
|
47
|
+
const sites = await listSites(db);
|
|
48
|
+
return new Map(sites.map((s) => [s.id, s.name]));
|
|
49
|
+
} finally {
|
|
50
|
+
await db.destroy();
|
|
51
|
+
}
|
|
47
52
|
} catch (err) {
|
|
48
53
|
console.error(`[submissions] site names unavailable \u2014 printing raw site ids: ${String(err)}`);
|
|
49
54
|
return /* @__PURE__ */ new Map();
|
|
@@ -78,7 +83,7 @@ async function runSubmissionsCommand(action, opts) {
|
|
|
78
83
|
return { output: "--apply and --dry-run conflict. Pick one.", code: 2 };
|
|
79
84
|
}
|
|
80
85
|
const apply = opts.apply === true;
|
|
81
|
-
const { openDb, readDbConfig } = await import("./client-
|
|
86
|
+
const { openDb, readDbConfig } = await import("./client-DGTO65WB.js");
|
|
82
87
|
const { SPAM_THRESHOLD } = await import("./spam-classifier-T5H4VTP6.js");
|
|
83
88
|
const cfg = opts.url ? { url: opts.url } : readDbConfig();
|
|
84
89
|
const db = await openDb(cfg);
|
|
@@ -119,4 +124,4 @@ export {
|
|
|
119
124
|
rescoreNewSubmissions,
|
|
120
125
|
runSubmissionsCommand
|
|
121
126
|
};
|
|
122
|
-
//# sourceMappingURL=submissions-
|
|
127
|
+
//# sourceMappingURL=submissions-2SHCFOKD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/submissions.ts"],"sourcesContent":["import type { Db } from \"../../db/client.js\";\nimport type { SubmissionRow } from \"../../reports/submission-row.js\";\n\nexport type SubmissionsCommandOptions = {\n /** Write the re-buckets. Without it the command is a DRY RUN (deliberate default —\n * this rewrites operator-visible triage state, so the table prints first). */\n apply?: boolean;\n /** Explicit dry run (the default). Conflicts with --apply. */\n dryRun?: boolean;\n /** Override the libSQL url (tests use \":memory:\"); otherwise read from env. */\n url?: string;\n cwd?: string;\n verbose?: boolean;\n};\n\n/** Provenance marker appended to the NEW classifier reasons on every re-bucketed row,\n * so a retro'd row is always distinguishable from one the classifier caught at ingest. */\nexport const RESCORE_REASON_MARKER = \"retro-rescore\";\n\nexport type RescoreFlagged = {\n row: SubmissionRow;\n newScore: number;\n /** What --apply writes to spam_reason: the new verdict's reasons + RESCORE_REASON_MARKER. */\n newReason: string;\n /** True when --apply actually flipped the row (false in dry runs, or when the row\n * stopped being 'new' between the scan and the write). */\n written: boolean;\n};\n\n/** extraFields is stored as a raw JSON string; the classifier wants the object. A\n * null/garbage/non-object value degrades to {} — same blind-spot posture as ingest\n * (classification never throws on stored data). */\nfunction parseExtraFields(raw: string | null): Record<string, unknown> {\n if (!raw || raw.trim() === \"\") return {};\n try {\n const parsed: unknown = JSON.parse(raw);\n return parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Core of `submissions rescore`, separated from the command wrapper so tests can run it\n * against a seeded :memory: db. Scans EVERY status='new' row (the pre-2026-07-15-tuning\n * backlog these rows predate), re-runs the CURRENT classifySpam over each — turnstile\n * \"unverifiable\", exactly what ingest passes when no token reaches verification — and\n * flags rows at/above SPAM_THRESHOLD. With `apply` it re-buckets each flagged row to\n * spam_auto, REPLACING spam_score/spam_reason with the new verdict (+ marker). The\n * classifier is the single source of truth: no scoring logic is duplicated here.\n */\nexport async function rescoreNewSubmissions(\n db: Db,\n opts: { apply: boolean },\n): Promise<{ scanned: number; flagged: RescoreFlagged[] }> {\n const { listSubmissionsFiltered, rescoreSubmissionSpam } =\n await import(\"../../db/submissions.js\");\n const { classifySpam, SPAM_THRESHOLD } = await import(\"../../forms/spam-classifier.js\");\n\n // Collect the WHOLE backlog before any write: --apply mutates rows out of the\n // status='new' filter, so paging offset-wise while writing would skip rows.\n const rows: SubmissionRow[] = [];\n const PAGE = 500;\n for (let offset = 0; ; offset += PAGE) {\n const page = await listSubmissionsFiltered(db, { status: \"new\" }, { limit: PAGE, offset });\n rows.push(...page);\n if (page.length < PAGE) break;\n }\n\n const flagged: RescoreFlagged[] = [];\n for (const row of rows) {\n const verdict = classifySpam({\n name: row.name,\n email: row.email,\n // message is optional on the classifier input; exactOptionalPropertyTypes\n // forbids passing `undefined`, so spread only when present (form-ingest idiom).\n ...(row.message !== null ? { message: row.message } : {}),\n formType: row.formType,\n extraFields: parseExtraFields(row.extraFields),\n turnstile: \"unverifiable\",\n });\n if (verdict.score < SPAM_THRESHOLD) continue;\n const newReason = [...verdict.reasons, RESCORE_REASON_MARKER].join(\",\");\n const written = opts.apply\n ? await rescoreSubmissionSpam(db, row.id, verdict.score, newReason)\n : false;\n flagged.push({ row, newScore: verdict.score, newReason, written });\n }\n return { scanned: rows.length, flagged };\n}\n\n/** site_id → display name for the table. Reads the roster from Turso (#646 step 4),\n * which is also where the submissions themselves live — an Airtable read here could\n * not name a `site_<ULID>` site at all. Soft-fails to an empty map (raw ids still\n * print) so the re-score itself never depends on the name lookup. */\nasync function loadSiteNames(): Promise<Map<string, string>> {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { listSites } = await import(\"../../db/fleet-state.js\");\n const db = await openDb(readDbConfig());\n try {\n const sites = await listSites(db);\n return new Map(sites.map((s) => [s.id, s.name] as const));\n } finally {\n await db.destroy();\n }\n } catch (err) {\n console.error(`[submissions] site names unavailable — printing raw site ids: ${String(err)}`);\n return new Map();\n }\n}\n\nfunction pad(v: string, width: number): string {\n return v.length >= width ? v : v + \" \".repeat(width - v.length);\n}\n\nfunction formatTable(flagged: RescoreFlagged[], names: Map<string, string>): string {\n const rows = flagged.map((f) => ({\n site: names.get(f.row.siteId) ?? f.row.siteId,\n email: f.row.email || \"(no email)\",\n score: `${f.row.spamScore ?? \"—\"} → ${f.newScore}`,\n reasons: f.newReason,\n }));\n const siteW = Math.max(4, ...rows.map((r) => r.site.length));\n const emailW = Math.max(5, ...rows.map((r) => r.email.length));\n const scoreW = Math.max(5, ...rows.map((r) => r.score.length));\n const lines = [\n `${pad(\"site\", siteW)} ${pad(\"email\", emailW)} ${pad(\"score\", scoreW)} reasons`,\n ...rows.map(\n (r) =>\n `${pad(r.site, siteW)} ${pad(r.email, emailW)} ${pad(r.score, scoreW)} ${r.reasons}`,\n ),\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * `submissions <action>` — operate on stored form submissions. The only action today is\n * `rescore`: re-run the CURRENT spam classifier over the status='new' backlog. Dry-run\n * by default; `--apply` writes. Only status='new' rows are ever touched.\n */\nexport async function runSubmissionsCommand(\n action: string,\n opts: SubmissionsCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (action !== \"rescore\") {\n return { output: `unknown submissions action '${action}'. Use: rescore.`, code: 1 };\n }\n if (opts.apply && opts.dryRun) {\n return { output: \"--apply and --dry-run conflict. Pick one.\", code: 2 };\n }\n const apply = opts.apply === true;\n\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { SPAM_THRESHOLD } = await import(\"../../forms/spam-classifier.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const db = await openDb(cfg);\n\n const { scanned, flagged } = await rescoreNewSubmissions(db, { apply });\n if (flagged.length === 0) {\n return {\n output: `Scanned ${scanned} status='new' submission${scanned === 1 ? \"\" : \"s\"} — none score >= ${SPAM_THRESHOLD} under the current classifier. Nothing to re-bucket.`,\n code: 0,\n };\n }\n\n // Airtable is only consulted when there is a table to print.\n const table = formatTable(flagged, await loadSiteNames());\n const head = `Scanned ${scanned} status='new' submissions; ${flagged.length} score >= ${SPAM_THRESHOLD} under the current classifier:`;\n if (!apply) {\n return {\n output: `${head}\\n\\n${table}\\n\\nDRY RUN — nothing was written. Re-run with --apply to re-bucket these ${flagged.length} rows to spam_auto.`,\n code: 0,\n };\n }\n const written = flagged.filter((f) => f.written).length;\n const skipped = flagged.length - written;\n const skippedNote =\n skipped > 0\n ? `\\n${skipped} row${skipped === 1 ? \"\" : \"s\"} skipped — no longer status='new' at write time (operator/ingest got there first).`\n : \"\";\n return {\n output: `${head}\\n\\n${table}\\n\\nApplied: ${written} row${written === 1 ? \"\" : \"s\"} → spam_auto (new score + reasons written, '${RESCORE_REASON_MARKER}' marker appended).${skippedNote}`,\n code: 0,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
toFormType,
|
|
3
3
|
toNotifyStatus,
|
|
4
4
|
toStatus
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-YDZ7XGPZ.js";
|
|
6
6
|
import {
|
|
7
7
|
SIGNUP_FORM_TYPES
|
|
8
8
|
} from "./chunk-2OP4JKIQ.js";
|
|
@@ -28,7 +28,11 @@ function rowFromDb(r) {
|
|
|
28
28
|
resendMessageId: r.resend_message_id,
|
|
29
29
|
spamScore: typeof r.spam_score === "number" ? r.spam_score : null,
|
|
30
30
|
spamReason: r.spam_reason,
|
|
31
|
-
fanoutStatus: r.fanout_status
|
|
31
|
+
fanoutStatus: r.fanout_status,
|
|
32
|
+
bounceType: r.bounce_type,
|
|
33
|
+
bounceSubType: r.bounce_subtype,
|
|
34
|
+
bounceMessage: r.bounce_message,
|
|
35
|
+
bounceAckAt: r.bounce_ack_at
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
function newSubmissionId() {
|
|
@@ -62,7 +66,13 @@ async function createSubmission(db, input) {
|
|
|
62
66
|
spam_score: input.spamScore ?? null,
|
|
63
67
|
spam_reason: input.spamReason ?? null,
|
|
64
68
|
// Always null at insert: the fan-out runs after the row exists (stampFanout).
|
|
65
|
-
fanout_status: null
|
|
69
|
+
fanout_status: null,
|
|
70
|
+
// Always null at insert too: a bounce is reported by a webhook minutes
|
|
71
|
+
// later, and the ack is an operator gesture after that (#783).
|
|
72
|
+
bounce_type: null,
|
|
73
|
+
bounce_subtype: null,
|
|
74
|
+
bounce_message: null,
|
|
75
|
+
bounce_ack_at: null
|
|
66
76
|
}).execute();
|
|
67
77
|
const created = await getSubmissionById(db, id);
|
|
68
78
|
if (!created) throw new Error("createSubmission: row vanished after insert");
|
|
@@ -234,7 +244,11 @@ async function backfillSubmission(db, row) {
|
|
|
234
244
|
resend_message_id: row.resendMessageId,
|
|
235
245
|
spam_score: row.spamScore ?? null,
|
|
236
246
|
spam_reason: row.spamReason ?? null,
|
|
237
|
-
fanout_status: row.fanoutStatus ?? null
|
|
247
|
+
fanout_status: row.fanoutStatus ?? null,
|
|
248
|
+
bounce_type: row.bounceType ?? null,
|
|
249
|
+
bounce_subtype: row.bounceSubType ?? null,
|
|
250
|
+
bounce_message: row.bounceMessage ?? null,
|
|
251
|
+
bounce_ack_at: row.bounceAckAt ?? null
|
|
238
252
|
}).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
239
253
|
}
|
|
240
254
|
async function rescoreSubmissionSpam(db, id, score, reason) {
|
|
@@ -249,21 +263,47 @@ async function markFilteredAsRead(db, filter) {
|
|
|
249
263
|
).executeTakeFirst();
|
|
250
264
|
return Number(res.numUpdatedRows);
|
|
251
265
|
}
|
|
252
|
-
async function markNotifyBouncedByMessageId(db, messageId) {
|
|
266
|
+
async function markNotifyBouncedByMessageId(db, messageId, detail = null) {
|
|
253
267
|
if (messageId === "") return false;
|
|
254
|
-
const res = await db.updateTable("submissions").set(
|
|
268
|
+
const res = await db.updateTable("submissions").set(
|
|
269
|
+
detail === null ? { notify_status: "bounced" } : {
|
|
270
|
+
notify_status: "bounced",
|
|
271
|
+
bounce_type: detail.type,
|
|
272
|
+
bounce_subtype: detail.subType,
|
|
273
|
+
bounce_message: detail.message
|
|
274
|
+
}
|
|
275
|
+
).where("resend_message_id", "=", messageId).executeTakeFirst();
|
|
255
276
|
if (Number(res.numUpdatedRows) > 0) return true;
|
|
256
277
|
const existing = await db.selectFrom("submissions").select("id").where("resend_message_id", "=", messageId).executeTakeFirst();
|
|
257
278
|
return existing !== void 0;
|
|
258
279
|
}
|
|
280
|
+
async function ackNotifyBounce(db, id, now) {
|
|
281
|
+
const res = await db.updateTable("submissions").set({ bounce_ack_at: now.toISOString() }).where("id", "=", id).where("notify_status", "=", "bounced").where("bounce_ack_at", "is", null).executeTakeFirst();
|
|
282
|
+
return Number(res.numUpdatedRows) > 0;
|
|
283
|
+
}
|
|
259
284
|
async function countNotifyBouncedBySite(db, sinceDate) {
|
|
260
|
-
const rows = await db.selectFrom("submissions").select([
|
|
261
|
-
|
|
285
|
+
const rows = await db.selectFrom("submissions").select([
|
|
286
|
+
"site_id",
|
|
287
|
+
(eb) => eb.fn.countAll().as("n"),
|
|
288
|
+
// lower() is ASCII-only in SQLite, which is all this needs: the value is a
|
|
289
|
+
// Resend enum ("Permanent"/"Transient"/"Undetermined"). Matching it
|
|
290
|
+
// case-insensitively here keeps the SQL agreeing with isPermanentBounce.
|
|
291
|
+
sql`sum(case when lower(bounce_type) = 'permanent' then 1 else 0 end)`.as(
|
|
292
|
+
"permanent_n"
|
|
293
|
+
)
|
|
294
|
+
]).where("notify_status", "=", "bounced").where("submitted_at", ">=", sinceDate).where("bounce_ack_at", "is", null).groupBy("site_id").execute();
|
|
295
|
+
return new Map(
|
|
296
|
+
rows.map((r) => [
|
|
297
|
+
r.site_id,
|
|
298
|
+
{ total: Number(r.n) || 0, permanent: Number(r.permanent_n) || 0 }
|
|
299
|
+
])
|
|
300
|
+
);
|
|
262
301
|
}
|
|
263
302
|
export {
|
|
264
303
|
MIN_DUP_BODY_LEN,
|
|
265
304
|
MIN_SIMILAR_TOKENS,
|
|
266
305
|
SIMILARITY_THRESHOLD,
|
|
306
|
+
ackNotifyBounce,
|
|
267
307
|
backfillSubmission,
|
|
268
308
|
countAutoSpamSince,
|
|
269
309
|
countNotifyBouncedBySite,
|
|
@@ -286,4 +326,4 @@ export {
|
|
|
286
326
|
stampFanout,
|
|
287
327
|
stampNotified
|
|
288
328
|
};
|
|
289
|
-
//# sourceMappingURL=submissions-
|
|
329
|
+
//# sourceMappingURL=submissions-JDZP35JM.js.map
|