@reddoorla/maintenance 0.86.0 → 0.88.2
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/dist/airtable-QG5ICI3Y.js +9 -0
- package/dist/{announce-MXTUQ6C7.js → announce-GKD5YJNB.js} +49 -28
- package/dist/announce-GKD5YJNB.js.map +1 -0
- package/dist/attachments-NLZIJ5NU.js +9 -0
- package/dist/{blux-ZGDSHI3U.js → blux-DN4MI532.js} +5 -5
- package/dist/{bump-deps-EC74CPLS.js → bump-deps-KGH5UYSF.js} +4 -4
- package/dist/{chunk-N45M3FDK.js → chunk-6I6L3CMX.js} +4 -4
- package/dist/{chunk-KTH5ZODA.js → chunk-7BZUOZPU.js} +2 -2
- package/dist/{chunk-2EVEFOZY.js → chunk-7FFUBF4I.js} +6 -3
- package/dist/chunk-7FFUBF4I.js.map +1 -0
- package/dist/{chunk-LDWZSLSU.js → chunk-7NYN2P6I.js} +7 -4
- package/dist/{chunk-LDWZSLSU.js.map → chunk-7NYN2P6I.js.map} +1 -1
- package/dist/{chunk-X3B723J4.js → chunk-7TMWBXX7.js} +184 -25
- package/dist/chunk-7TMWBXX7.js.map +1 -0
- package/dist/{chunk-3VQWSE3F.js → chunk-7UAYMAKR.js} +4 -4
- package/dist/{chunk-W7Z3BGCI.js → chunk-A2UNJIKH.js} +15 -7
- package/dist/chunk-A2UNJIKH.js.map +1 -0
- package/dist/{chunk-F5KM4LEQ.js → chunk-ANJNTAMM.js} +4 -70
- package/dist/chunk-ANJNTAMM.js.map +1 -0
- package/dist/{chunk-2ZPT7GLV.js → chunk-B23FFWU4.js} +4 -4
- package/dist/chunk-B23FFWU4.js.map +1 -0
- package/dist/{chunk-DJZWYIPP.js → chunk-BGTYPVLT.js} +1 -1
- package/dist/chunk-BGTYPVLT.js.map +1 -0
- package/dist/chunk-BRYXFH77.js +70 -0
- package/dist/chunk-BRYXFH77.js.map +1 -0
- package/dist/{chunk-2NDRHXMR.js → chunk-DGZWFI2G.js} +18 -141
- package/dist/chunk-DGZWFI2G.js.map +1 -0
- package/dist/{chunk-ACBRLLFT.js → chunk-F3JWOPTG.js} +7 -3
- package/dist/chunk-F3JWOPTG.js.map +1 -0
- package/dist/chunk-GQDDWYU2.js +372 -0
- package/dist/chunk-GQDDWYU2.js.map +1 -0
- package/dist/{chunk-Z7AXYKQB.js → chunk-IY6256GK.js} +1 -1
- package/dist/{chunk-BYK66EWO.js → chunk-J3WEZGIN.js} +52 -46
- package/dist/chunk-J3WEZGIN.js.map +1 -0
- package/dist/{chunk-BNM5POSJ.js → chunk-JRHIFA5R.js} +2 -2
- package/dist/{chunk-ZKXSX4LO.js → chunk-K6SLF6GI.js} +6 -6
- package/dist/{chunk-CQR4LIM7.js → chunk-KJGKZLCT.js} +7 -3
- package/dist/{chunk-CQR4LIM7.js.map → chunk-KJGKZLCT.js.map} +1 -1
- package/dist/{chunk-PPE5KZJK.js → chunk-KMCZA7CH.js} +13 -3
- package/dist/chunk-KMCZA7CH.js.map +1 -0
- package/dist/{chunk-3SF7D4DY.js → chunk-MFHS7IQ2.js} +36 -1
- package/dist/{chunk-3SF7D4DY.js.map → chunk-MFHS7IQ2.js.map} +1 -1
- package/dist/{chunk-SNSTJLQW.js → chunk-NO74DXDP.js} +48 -2
- package/dist/chunk-NO74DXDP.js.map +1 -0
- package/dist/{chunk-C6B4RHXR.js → chunk-O3GT46R2.js} +8 -2
- package/dist/chunk-O3GT46R2.js.map +1 -0
- package/dist/{chunk-ITIOFN7A.js → chunk-P6AORG32.js} +29 -10
- package/dist/chunk-P6AORG32.js.map +1 -0
- package/dist/{chunk-SLUQ7R6I.js → chunk-PR343K7F.js} +5 -5
- package/dist/{chunk-P2FE5AI6.js → chunk-Q47DU255.js} +4 -4
- package/dist/{chunk-EVBCK2KC.js → chunk-RARREDQE.js} +2 -1
- package/dist/{chunk-EVBCK2KC.js.map → chunk-RARREDQE.js.map} +1 -1
- package/dist/chunk-RUPPWAD6.js +7 -0
- package/dist/chunk-RUPPWAD6.js.map +1 -0
- package/dist/{chunk-VSBFM7DS.js → chunk-SJOXWJ5Y.js} +3 -3
- package/dist/{chunk-TERE5SDF.js → chunk-ULHPALI5.js} +4 -4
- package/dist/{chunk-XUGSZP7Z.js → chunk-W45FZEGE.js} +2 -2
- package/dist/{chunk-JHGHLKFS.js → chunk-WS6NU475.js} +2 -2
- package/dist/chunk-XP5WM2LV.js +146 -0
- package/dist/chunk-XP5WM2LV.js.map +1 -0
- package/dist/{chunk-ILUNZHYV.js → chunk-YFAXOD7Z.js} +10 -6
- package/dist/chunk-YFAXOD7Z.js.map +1 -0
- package/dist/{chunk-6TWWY7ZU.js → chunk-Z3JXLULD.js} +2 -2
- package/dist/cli/bin.js +34 -29
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +4 -0
- package/dist/cli/commands/audit.js +25 -18
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-JYKPVCJX.js +11 -0
- package/dist/configs/playwright-a11y.js +1 -1
- package/dist/{convert-to-pnpm-6L6P74WR.js → convert-to-pnpm-NWB5FCQ2.js} +4 -4
- package/dist/{db-B35OGV3G.js → db-V6UIS433.js} +124 -29
- package/dist/db-V6UIS433.js.map +1 -0
- package/dist/{digest-BL6UJYKH.js → digest-2JEAEJK6.js} +60 -35
- package/dist/digest-2JEAEJK6.js.map +1 -0
- package/dist/digest-state-CRZRNGDI.js +25 -0
- package/dist/digest-state-CRZRNGDI.js.map +1 -0
- package/dist/digest-state-F37XUCS5.js +13 -0
- package/dist/{dump-P6EBSIOH.js → dump-EPL7AZFR.js} +38 -13
- package/dist/dump-EPL7AZFR.js.map +1 -0
- package/dist/{email-VTTTKKL5.js → email-SL7464YI.js} +16 -6
- package/dist/{email-VTTTKKL5.js.map → email-SL7464YI.js.map} +1 -1
- package/dist/{ensure-site-DFTB7MN3.js → ensure-site-FU6527OU.js} +19 -11
- package/dist/ensure-site-FU6527OU.js.map +1 -0
- package/dist/fleet-state-R5LU7IC3.js +41 -0
- package/dist/{forms-notify-target-75U6OU7L.js → forms-notify-target-SGDGXXK5.js} +10 -7
- package/dist/forms-notify-target-SGDGXXK5.js.map +1 -0
- package/dist/{github-signals-IJUPGCUX.js → github-signals-GWGCKEE2.js} +13 -12
- package/dist/{github-signals-IJUPGCUX.js.map → github-signals-GWGCKEE2.js.map} +1 -1
- package/dist/{header-image-AWLLQA4L.js → header-image-7A7XLVEU.js} +8 -8
- package/dist/{header-images-335L2AKB.js → header-images-SQBCE5OD.js} +21 -1
- package/dist/header-images-SQBCE5OD.js.map +1 -0
- package/dist/{health-endpoint-WJC2UMYS.js → health-endpoint-UVX6R5N5.js} +4 -4
- package/dist/health-mirror-GCDN7BJL.js +46 -0
- package/dist/health-mirror-GCDN7BJL.js.map +1 -0
- package/dist/{import-airtable-HE43FB4H.js → import-airtable-OHVY344G.js} +7 -5
- package/dist/index.d.ts +149 -0
- package/dist/index.js +245 -108
- package/dist/index.js.map +1 -1
- package/dist/{init-NJEOEO7I.js → init-FGTYNGUI.js} +11 -11
- package/dist/{launch-XNAX427U.js → launch-5H7LPEGP.js} +59 -29
- package/dist/launch-5H7LPEGP.js.map +1 -0
- package/dist/{lighthouse-airtable-LBVIYYP5.js → lighthouse-airtable-3GUWHETW.js} +3 -3
- package/dist/migrate-YITCXBLS.js +7 -0
- package/dist/{notify-C645PGD5.js → notify-4HP4GZ2H.js} +5 -5
- package/dist/{onboard-GK4PHPNI.js → onboard-BKCYBAVW.js} +4 -4
- package/dist/{orchestrate-77UO4BYJ.js → orchestrate-JNKCWU5K.js} +14 -13
- package/dist/parity-NBYZUBFU.js +14 -0
- package/dist/pdf-N5QLQ5ZQ.js +38 -0
- package/dist/pdf-N5QLQ5ZQ.js.map +1 -0
- package/dist/{pipeline-S6CVHDGB.js → pipeline-WYTXSLHX.js} +3 -3
- package/dist/{preflight-6WBNU2GZ.js → preflight-IMTYDDD4.js} +15 -14
- package/dist/{preflight-6WBNU2GZ.js.map → preflight-IMTYDDD4.js.map} +1 -1
- package/dist/{prismic-ci-3RHOZUUT.js → prismic-ci-MY2U6NN7.js} +7 -7
- package/dist/{prismic-models-QBGUT2Q3.js → prismic-models-NJN5JORF.js} +15 -10
- package/dist/prismic-models-NJN5JORF.js.map +1 -0
- package/dist/prospect/types.d.ts +281 -0
- package/dist/prospect/types.js +1 -0
- package/dist/{prospect-audit-IUNCJ2LV.js → prospect-audit-JMBRVXSG.js} +32 -13
- package/dist/prospect-audit-JMBRVXSG.js.map +1 -0
- package/dist/recipes/sync-configs.js +2 -2
- package/dist/render-VX72AESY.js +10 -0
- package/dist/render-VX72AESY.js.map +1 -0
- package/dist/render-from-row-DOYJDO2L.js +17 -0
- package/dist/render-from-row-DOYJDO2L.js.map +1 -0
- package/dist/{renovate-dispatch-QJY75ODR.js → renovate-dispatch-6GQWTRKX.js} +10 -10
- package/dist/{renovate-dispatch-QJY75ODR.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
- package/dist/{replay-C2LXMJZP.js → replay-4S6ENYCH.js} +3 -3
- package/dist/{report-4EVNDHOZ.js → report-TALY5P3L.js} +63 -21
- package/dist/report-TALY5P3L.js.map +1 -0
- package/dist/report-mirror-AAK3MXHY.js +55 -0
- package/dist/report-mirror-AAK3MXHY.js.map +1 -0
- package/dist/rerender-3FM6DNRH.js +32 -0
- package/dist/rerender-3FM6DNRH.js.map +1 -0
- package/dist/schema-75PJECAE.js +18 -0
- package/dist/schema-75PJECAE.js.map +1 -0
- package/dist/{self-updating-6DWI2Z56.js → self-updating-NTCCWJYZ.js} +4 -4
- package/dist/{selftest-RC36MTKP.js → selftest-NZTR4SM2.js} +26 -24
- package/dist/{selftest-RC36MTKP.js.map → selftest-NZTR4SM2.js.map} +1 -1
- package/dist/site-mirror-Q763PLGG.js +62 -0
- package/dist/site-mirror-Q763PLGG.js.map +1 -0
- package/dist/{smoke-suite-VYY2LXM2.js → smoke-suite-4JFZZYVW.js} +4 -4
- package/dist/{spam-classifier-XLBFCO7G.js → spam-classifier-T5H4VTP6.js} +4 -2
- package/dist/spam-classifier-T5H4VTP6.js.map +1 -0
- package/dist/{submissions-Q7DRFOCD.js → submissions-TEHPBSZJ.js} +5 -5
- package/dist/{svelte-codemods-25J2FO22.js → svelte-codemods-QAPDZC6F.js} +4 -4
- package/dist/{sync-34HNZZ56.js → sync-GBFV45WX.js} +6 -4
- package/dist/{sync-34HNZZ56.js.map → sync-GBFV45WX.js.map} +1 -1
- package/dist/{sync-configs-3SE6PSCO.js → sync-configs-34JIA6KZ.js} +6 -6
- package/dist/{upgrade-WBRQ62FX.js → upgrade-IR2OO3KP.js} +4 -4
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{webhook-VOGOW3W6.js → webhook-XM6EV4XT.js} +2 -2
- package/dist/{websites-SXWTP72S.js → websites-D6WYFAES.js} +2 -2
- package/dist/websites-D6WYFAES.js.map +1 -0
- package/dist/write-audits-to-airtable-OBHGR7DQ.js +17 -0
- package/dist/write-audits-to-airtable-OBHGR7DQ.js.map +1 -0
- package/package.json +5 -1
- package/dist/airtable-G7WBQQMI.js +0 -9
- package/dist/announce-MXTUQ6C7.js.map +0 -1
- package/dist/chunk-2EVEFOZY.js.map +0 -1
- package/dist/chunk-2NDRHXMR.js.map +0 -1
- package/dist/chunk-2ZPT7GLV.js.map +0 -1
- package/dist/chunk-ACBRLLFT.js.map +0 -1
- package/dist/chunk-BYK66EWO.js.map +0 -1
- package/dist/chunk-C6B4RHXR.js.map +0 -1
- package/dist/chunk-DJZWYIPP.js.map +0 -1
- package/dist/chunk-F5KM4LEQ.js.map +0 -1
- package/dist/chunk-ILUNZHYV.js.map +0 -1
- package/dist/chunk-ITIOFN7A.js.map +0 -1
- package/dist/chunk-PPE5KZJK.js.map +0 -1
- package/dist/chunk-SNSTJLQW.js.map +0 -1
- package/dist/chunk-W7Z3BGCI.js.map +0 -1
- package/dist/chunk-X3B723J4.js.map +0 -1
- package/dist/client-AUE3XUPO.js +0 -11
- package/dist/db-B35OGV3G.js.map +0 -1
- package/dist/digest-BL6UJYKH.js.map +0 -1
- package/dist/dump-P6EBSIOH.js.map +0 -1
- package/dist/ensure-site-DFTB7MN3.js.map +0 -1
- package/dist/forms-notify-target-75U6OU7L.js.map +0 -1
- package/dist/header-images-335L2AKB.js.map +0 -1
- package/dist/health-mirror-4B2L7T2Y.js +0 -75
- package/dist/health-mirror-4B2L7T2Y.js.map +0 -1
- package/dist/launch-XNAX427U.js.map +0 -1
- package/dist/migrate-GQH237A6.js +0 -7
- package/dist/parity-MWS24A3H.js +0 -12
- package/dist/prismic-models-QBGUT2Q3.js.map +0 -1
- package/dist/prospect-audit-IUNCJ2LV.js.map +0 -1
- package/dist/render-AQAVQDPZ.js +0 -10
- package/dist/report-4EVNDHOZ.js.map +0 -1
- package/dist/write-audits-to-airtable-MBEILLPU.js +0 -14
- /package/dist/{airtable-G7WBQQMI.js.map → airtable-QG5ICI3Y.js.map} +0 -0
- /package/dist/{client-AUE3XUPO.js.map → attachments-NLZIJ5NU.js.map} +0 -0
- /package/dist/{blux-ZGDSHI3U.js.map → blux-DN4MI532.js.map} +0 -0
- /package/dist/{bump-deps-EC74CPLS.js.map → bump-deps-KGH5UYSF.js.map} +0 -0
- /package/dist/{chunk-N45M3FDK.js.map → chunk-6I6L3CMX.js.map} +0 -0
- /package/dist/{chunk-KTH5ZODA.js.map → chunk-7BZUOZPU.js.map} +0 -0
- /package/dist/{chunk-3VQWSE3F.js.map → chunk-7UAYMAKR.js.map} +0 -0
- /package/dist/{chunk-Z7AXYKQB.js.map → chunk-IY6256GK.js.map} +0 -0
- /package/dist/{chunk-BNM5POSJ.js.map → chunk-JRHIFA5R.js.map} +0 -0
- /package/dist/{chunk-ZKXSX4LO.js.map → chunk-K6SLF6GI.js.map} +0 -0
- /package/dist/{chunk-SLUQ7R6I.js.map → chunk-PR343K7F.js.map} +0 -0
- /package/dist/{chunk-P2FE5AI6.js.map → chunk-Q47DU255.js.map} +0 -0
- /package/dist/{chunk-VSBFM7DS.js.map → chunk-SJOXWJ5Y.js.map} +0 -0
- /package/dist/{chunk-TERE5SDF.js.map → chunk-ULHPALI5.js.map} +0 -0
- /package/dist/{chunk-XUGSZP7Z.js.map → chunk-W45FZEGE.js.map} +0 -0
- /package/dist/{chunk-JHGHLKFS.js.map → chunk-WS6NU475.js.map} +0 -0
- /package/dist/{chunk-6TWWY7ZU.js.map → chunk-Z3JXLULD.js.map} +0 -0
- /package/dist/{import-airtable-HE43FB4H.js.map → client-JYKPVCJX.js.map} +0 -0
- /package/dist/{convert-to-pnpm-6L6P74WR.js.map → convert-to-pnpm-NWB5FCQ2.js.map} +0 -0
- /package/dist/{lighthouse-airtable-LBVIYYP5.js.map → digest-state-F37XUCS5.js.map} +0 -0
- /package/dist/{migrate-GQH237A6.js.map → fleet-state-R5LU7IC3.js.map} +0 -0
- /package/dist/{header-image-AWLLQA4L.js.map → header-image-7A7XLVEU.js.map} +0 -0
- /package/dist/{health-endpoint-WJC2UMYS.js.map → health-endpoint-UVX6R5N5.js.map} +0 -0
- /package/dist/{notify-C645PGD5.js.map → import-airtable-OHVY344G.js.map} +0 -0
- /package/dist/{init-NJEOEO7I.js.map → init-FGTYNGUI.js.map} +0 -0
- /package/dist/{orchestrate-77UO4BYJ.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
- /package/dist/{parity-MWS24A3H.js.map → migrate-YITCXBLS.js.map} +0 -0
- /package/dist/{pipeline-S6CVHDGB.js.map → notify-4HP4GZ2H.js.map} +0 -0
- /package/dist/{onboard-GK4PHPNI.js.map → onboard-BKCYBAVW.js.map} +0 -0
- /package/dist/{render-AQAVQDPZ.js.map → orchestrate-JNKCWU5K.js.map} +0 -0
- /package/dist/{spam-classifier-XLBFCO7G.js.map → parity-NBYZUBFU.js.map} +0 -0
- /package/dist/{websites-SXWTP72S.js.map → pipeline-WYTXSLHX.js.map} +0 -0
- /package/dist/{prismic-ci-3RHOZUUT.js.map → prismic-ci-MY2U6NN7.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-MBEILLPU.js.map → prospect/types.js.map} +0 -0
- /package/dist/{replay-C2LXMJZP.js.map → replay-4S6ENYCH.js.map} +0 -0
- /package/dist/{self-updating-6DWI2Z56.js.map → self-updating-NTCCWJYZ.js.map} +0 -0
- /package/dist/{smoke-suite-VYY2LXM2.js.map → smoke-suite-4JFZZYVW.js.map} +0 -0
- /package/dist/{submissions-Q7DRFOCD.js.map → submissions-TEHPBSZJ.js.map} +0 -0
- /package/dist/{svelte-codemods-25J2FO22.js.map → svelte-codemods-QAPDZC6F.js.map} +0 -0
- /package/dist/{sync-configs-3SE6PSCO.js.map → sync-configs-34JIA6KZ.js.map} +0 -0
- /package/dist/{upgrade-WBRQ62FX.js.map → upgrade-IR2OO3KP.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{webhook-VOGOW3W6.js.map → webhook-XM6EV4XT.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audits/deps.ts","../src/audits/deps-outdated.ts","../src/audits/lint.ts","../src/github/gh-rest.ts","../src/audits/security.ts","../src/audits/a11y.ts","../src/audits/domain.ts","../src/audits/route-discovery.ts","../src/audits/browser.ts","../src/audits/netlify-deploy.ts","../src/audits/function-health.ts","../src/audits/form-e2e.ts","../src/audits/index.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport { scanOutdated, type OutdatedCounts } from \"./deps-outdated.js\";\n\nexport type Drift = \"same\" | \"patch\" | \"minor\" | \"major\" | \"newer\";\n\nexport type DepsDriftEntry = {\n pkg: string;\n baseline: string;\n actual: string;\n drift: Drift;\n};\n\n/** The deps audit reports TWO signals:\n * - `entries`: declared-range drift vs the canonical baseline (what the\n * package.json *asks for*, caret-stripped) — the long-standing signal.\n * - `outdated`: real installed-version drift vs the registry's latest, from\n * the committed lockfile (null when it can't be determined). Added so the\n * \"Deps Drifted\" dashboard number stops being the only — and misleading —\n * deps signal. */\nexport type DepsDetails = {\n entries: DepsDriftEntry[];\n outdated: OutdatedCounts | null;\n};\n\nfunction stripCaret(range: string): string {\n return range.replace(/^[\\^~]/, \"\");\n}\n\n/** A spec we can drift-compare against a semver baseline: a plain version or\n * caret/tilde range like \"5.55.10\", \"^5.55.10\", \"~5.0.0\". Excludes \"*\",\n * \"latest\", \"workspace:*\", \"npm:\"-aliases, and git/URL/file specs — those used\n * to parse to NaN and produce bogus drift, so they're skipped instead. */\nfunction isComparableRange(spec: string): boolean {\n return /^[\\^~]?\\d/.test(spec.trim());\n}\n\nfunction parseSemver(v: string): [number, number, number] {\n const cleaned = stripCaret(v).split(\"-\")[0] ?? \"0.0.0\";\n const parts = cleaned.split(\".\").map((n) => Number.parseInt(n, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nfunction compareSemver(actual: string, baseline: string): Drift {\n const [aMajor, aMinor, aPatch] = parseSemver(actual);\n const [bMajor, bMinor, bPatch] = parseSemver(baseline);\n if (aMajor > bMajor) return \"newer\";\n if (aMajor < bMajor) return \"major\";\n if (aMinor > bMinor) return \"newer\";\n if (aMinor < bMinor) return \"minor\";\n if (aPatch > bPatch) return \"newer\";\n if (aPatch < bPatch) return \"patch\";\n return \"same\";\n}\n\nexport async function depsAudit(ctx: AuditContext): Promise<AuditResult> {\n const pkgPath = join(ctx.site.path, \"package.json\");\n let pkgRaw: string;\n try {\n pkgRaw = await readFile(pkgPath, \"utf-8\");\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"skip\",\n summary: `no package.json at ${pkgPath}`,\n details: { error: String(err) },\n };\n }\n\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"fail\",\n summary: `package.json is not valid JSON: ${(err as Error).message}`,\n details: { error: String(err) },\n };\n }\n const installed: Record<string, string> = {\n ...(pkg.dependencies ?? {}),\n ...(pkg.devDependencies ?? {}),\n };\n\n const entries: DepsDriftEntry[] = [];\n for (const [name, baseline] of Object.entries(baselineVersions)) {\n const actual = installed[name];\n if (!actual) continue;\n // Skip non-semver specs (\"*\", \"workspace:*\", \"npm:\"-aliases, git/URL): they\n // can't be drift-compared and used to yield NaN-driven bogus drift (LOW-3).\n if (!isComparableRange(actual)) continue;\n entries.push({\n pkg: name,\n baseline,\n actual,\n drift: compareSemver(actual, baseline),\n });\n }\n\n const anyMajor = entries.some((d) => d.drift === \"major\");\n const anyMinor = entries.some((d) => d.drift === \"minor\");\n const anyNewer = entries.some((d) => d.drift === \"newer\");\n\n // Status stays driven by the declared-range baseline drift (unchanged\n // behavior). The outdated count is an independent, informational signal.\n const status: AuditResult[\"status\"] = anyMajor ? \"fail\" : anyMinor || anyNewer ? \"warn\" : \"pass\";\n\n const driftSummary =\n status === \"pass\"\n ? `all ${entries.length} tracked deps in line with baseline`\n : status === \"warn\"\n ? `${entries.filter((d) => d.drift !== \"same\").length} of ${entries.length} tracked deps drifted`\n : `${entries.filter((d) => d.drift === \"major\").length} deps lagging by a major version`;\n\n const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);\n const summary = outdated\n ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)`\n : driftSummary;\n\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status,\n summary,\n details: { entries, outdated } satisfies DepsDetails,\n };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { SpawnFn } from \"./util/spawn.js\";\n\n/** Real installed-version drift, distinct from the declared-range \"drift\" the\n * deps audit computes against the baseline: how many dependencies are behind\n * the registry's latest, per the committed lockfile. */\nexport type OutdatedCounts = { outdated: number; major: number };\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction majorOf(version: string): number {\n const head = version.replace(/^[\\^~]/, \"\").split(\".\")[0] ?? \"0\";\n const n = Number.parseInt(head, 10);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/**\n * Count outdated dependencies for a site, using its committed lockfile as the\n * source of truth for \"what's installed/deployed\". Returns `null` (skip — the\n * caller degrades gracefully) when it can't determine this:\n * - no `pnpm-lock.yaml` (not a pnpm site, or never installed)\n * - the lockfile is stale vs package.json (`--frozen-lockfile` install fails)\n * - `pnpm outdated` output isn't parseable\n *\n * `pnpm outdated` exits non-zero precisely WHEN there are outdated packages, so\n * its exit code is ignored and only its JSON is parsed. `--frozen-lockfile`\n * never mutates the lockfile, so this stays read-only with respect to the repo.\n */\nexport async function scanOutdated(\n sitePath: string,\n spawn: SpawnFn,\n): Promise<OutdatedCounts | null> {\n if (!(await exists(join(sitePath, \"pnpm-lock.yaml\")))) return null;\n\n // Everything below is best-effort: a thrown spawn (timeout, `pnpm` not on\n // PATH, spawn error) must degrade to a skip (null), NOT bubble up and flip the\n // whole deps audit to a hard fail — the declared-range drift is independent of\n // pnpm and must still report. (Mirrors securityAudit's try/catch.)\n try {\n // Materialize node_modules from the lockfile, but only when it's missing —\n // an already-installed checkout skips the cold install. `--frozen-lockfile`\n // never rewrites the lockfile (read-only wrt the repo) and fails fast on a\n // lockfile out of sync with package.json → skip.\n if (!(await exists(join(sitePath, \"node_modules\")))) {\n const install = await spawn(\"pnpm\", [\"install\", \"--frozen-lockfile\"], {\n cwd: sitePath,\n timeoutMs: 180_000,\n });\n if (install.code !== 0) return null;\n }\n\n // `pnpm outdated` exits non-zero precisely WHEN there are outdated packages,\n // so its exit code is ignored and only its JSON is parsed.\n const res = await spawn(\"pnpm\", [\"outdated\", \"--json\"], {\n cwd: sitePath,\n timeoutMs: 60_000,\n });\n const parsed = JSON.parse(res.stdout || \"{}\") as Record<\n string,\n { current?: string; latest?: string }\n >;\n const entries = Object.values(parsed);\n return {\n outdated: entries.length,\n major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current))\n .length,\n };\n } catch {\n return null;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { ESLint } from \"eslint\";\nimport { check as prettierCheck, resolveConfig as prettierResolveConfig } from \"prettier\";\nimport { glob } from \"tinyglobby\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\nconst TARGET_GLOBS = [\"**/*.{ts,js,svelte}\"];\nconst IGNORE = [\"node_modules/**\", \"dist/**\", \".svelte-kit/**\", \"build/**\", \".netlify/**\"];\n\nasync function listFiles(cwd: string): Promise<string[]> {\n return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n}\n\nexport async function lintAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const configPath = join(site.path, \"eslint.config.js\");\n\n if (!existsSync(configPath)) {\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status: \"skip\",\n summary: \"no eslint config at site root\",\n };\n }\n\n const eslint = new ESLint({\n cwd: site.path,\n overrideConfigFile: configPath,\n errorOnUnmatchedPattern: false,\n });\n\n const relFiles = await listFiles(site.path);\n\n // Pass relative paths to ESLint; its cwd is already site.path. Avoids\n // dereferencing symlinks on pnpm workspaces.\n const eslintResults = await eslint.lintFiles(relFiles);\n const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);\n const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);\n\n const prettierUnformatted: string[] = [];\n for (const rel of relFiles) {\n const absForResolve = join(site.path, rel);\n const source = await readFile(absForResolve, \"utf-8\");\n const options = (await prettierResolveConfig(absForResolve)) ?? {};\n const ok = await prettierCheck(source, { ...options, filepath: absForResolve });\n if (!ok) prettierUnformatted.push(rel);\n }\n\n const status: AuditResult[\"status\"] =\n eslintErrors > 0 || prettierUnformatted.length > 0\n ? \"fail\"\n : eslintWarnings > 0\n ? \"warn\"\n : \"pass\";\n\n const summary =\n status === \"pass\"\n ? `lint clean across ${relFiles.length} files`\n : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;\n\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status,\n summary,\n details: {\n eslintErrors,\n eslintWarnings,\n prettierUnformatted,\n files: relFiles.length,\n },\n };\n}\n","import { assertUrlSegment } from \"./gh.js\";\n\n/**\n * A `fetch`-based GitHub client covering the two operations the dashboard's\n * request-path Renovate trigger needs: resolve a repo's default branch and fire\n * a `workflow_dispatch`.\n *\n * Why not reuse `makeGitHub` (in gh.ts)? That client shells out to the `gh` CLI\n * (`spawn(\"gh\", …)`), which is the right tool inside GitHub Actions / a dev box\n * but is ABSENT in the Netlify Functions (AWS Lambda) runtime where this code\n * runs — so every `gh` call there throws `ENOENT`. The button's first live\n * dispatch surfaced exactly that as a 502. The Lambda runtime has a global\n * `fetch`, so we talk to the REST API directly.\n *\n * Mirrors the relevant subset of the `GitHub` interface so the `.mts` handler's\n * dispatch adapter is a drop-in swap. `fetch` is injectable for tests.\n */\n/** A subset of a GitHub Actions workflow run, mapped to camelCase. */\nexport type WorkflowRun = {\n id: number;\n status: string; // \"queued\" | \"in_progress\" | \"completed\" | \"requested\" | \"waiting\" | ...\n conclusion: string | null; // \"success\" | \"failure\" | \"cancelled\" | \"timed_out\" | ... | null\n createdAt: string; // ISO\n htmlUrl: string;\n};\n\n/** One GitHub Dependabot alert, mapped to the fields the security audit needs. */\nexport type DependabotAlert = {\n /** The vulnerable package name (`dependency.package.name`). */\n package: string;\n /** GitHub's raw severity vocabulary: \"low\" | \"medium\" | \"high\" | \"critical\". The audit layer\n * maps \"medium\" → the app's \"moderate\"; the rest pass through. */\n severity: string;\n /** Human advisory summary (`security_advisory.summary`). */\n summary: string;\n /** CVE id from the advisory's `cve_id`, as a 0- or 1-element array. Empty for GHSA-only\n * advisories (no CVE assigned) — those have no inline identifier here, but `url` still links\n * to the GHSA advisory page. (The GitHub `identifiers[]` array is intentionally not surfaced.) */\n cves: string[];\n /** Link to the alert (`html_url`), or null. */\n url: string | null;\n /** Dependency graph scope, or null when GitHub doesn't classify it. \"development\" marks a\n * build-time-only dependency (lower live-exploit surface for a static site). */\n scope: \"runtime\" | \"development\" | null;\n /** Dependency graph relationship (`dependency.relationship`), or null when GitHub reports\n * \"unknown\"/nothing. \"transitive\" means no direct-dep bump can fix it — Renovate's vuln\n * alerts have no fix vehicle until the weekly lockfile-maintenance window. */\n relationship: \"direct\" | \"transitive\" | null;\n};\n\nexport type GitHubRest = {\n defaultBranch: (repo: string) => Promise<string>;\n /** Fire a `workflow_dispatch` for `<workflow>` (a filename like `renovate.yml`)\n * on `ref`. Needs the token's `actions:write` scope; a non-2xx (404 no such\n * workflow / 403 missing scope) surfaces as a thrown error carrying the status. */\n dispatchWorkflow: (\n repo: string,\n workflow: string,\n ref: string,\n /** Declared `workflow_dispatch` inputs. Omitted from the body entirely when\n * absent — a workflow that declares none rejects an `inputs` key. */\n inputs?: Record<string, string>,\n ) => Promise<void>;\n /** List a workflow's runs, newest first, created on/after `opts.since` (ISO).\n * Used to re-find the run a prior `workflow_dispatch` started (dispatch returns\n * 204 with no id). Non-2xx surfaces as a thrown error carrying the status. */\n listWorkflowRuns: (\n repo: string,\n workflow: string,\n opts: { since: string; event?: string; perPage?: number },\n ) => Promise<WorkflowRun[]>;\n /** The name of the in-progress step of a run's in-progress job, or null if none\n * is currently running (between steps / completed). Used to show a coarse \"phase\"\n * in the refresh spinner. Non-2xx throws (callers treat it best-effort). */\n currentRunStep: (repo: string, runId: number) => Promise<string | null>;\n /** List a repo's Dependabot alerts, following pagination. `opts.state` defaults to \"open\".\n * Needs the token's Dependabot-alerts read permission (fine-grained PAT) or `repo`/\n * `security_events` (classic PAT); a non-2xx (403 missing scope / 404 alerts disabled)\n * throws carrying the status so the caller can fall back. */\n listDependabotAlerts: (\n repo: string,\n opts?: { state?: \"open\" | \"dismissed\" | \"fixed\" | \"auto_dismissed\" },\n ) => Promise<DependabotAlert[]>;\n};\n\nconst GITHUB_API = \"https://api.github.com\";\n\n/** Per-request timeout for the Dependabot fetch. It runs inside the nightly fleet sweep at\n * `--concurrency 1`, so a hung connection (half-open TCP / accept-but-no-response) would otherwise\n * stall the whole sweep — `fetch` has no default overall timeout. With this signal a hang rejects\n * with an AbortError, which the audit's try/catch converts to the pnpm-audit fallback. */\nconst DEPENDABOT_FETCH_TIMEOUT_MS = 15_000;\n\n/** Extract the `rel=\"next\"` URL from a GitHub `Link` header, or null when absent. GitHub's\n * Dependabot-alerts endpoint paginates ONLY by opaque `after` cursor exposed via this header —\n * there is no numeric `page` param (it is silently ignored) — so following rel=\"next\" is the only\n * correct way to advance pages. */\nfunction nextLink(link: string | null): string | null {\n if (!link) return null;\n for (const part of link.split(\",\")) {\n const m = part.match(/<([^>]+)>\\s*;\\s*rel=\"next\"/);\n if (m) return m[1] ?? null;\n }\n return null;\n}\n\n/** Split + validate an `owner/repo`, mirroring the shape check in gh.ts's methods. */\nfunction splitRepo(repo: string): { owner: string; name: string } {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`expected \"owner/repo\", got ${JSON.stringify(repo)}`);\n }\n return { owner, name };\n}\n\n/** Best-effort response body for an error message; capped so a huge body can't bloat logs. */\nasync function bodyText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 300);\n } catch {\n return \"<no body>\";\n }\n}\n\n/** Map one raw Dependabot-alert object to {@link DependabotAlert}, or null if it carries no\n * usable package name (fail-soft — a malformed alert is dropped rather than propagated). */\nfunction mapDependabotAlert(raw: Record<string, unknown>): DependabotAlert | null {\n const dependency = (raw[\"dependency\"] ?? {}) as Record<string, unknown>;\n const pkg = (dependency[\"package\"] ?? {}) as Record<string, unknown>;\n const packageName = typeof pkg[\"name\"] === \"string\" ? (pkg[\"name\"] as string) : null;\n if (!packageName) return null;\n\n const adv = (raw[\"security_advisory\"] ?? {}) as Record<string, unknown>;\n const cveId = typeof adv[\"cve_id\"] === \"string\" ? (adv[\"cve_id\"] as string) : null;\n const rawScope = dependency[\"scope\"];\n const rawRelationship = dependency[\"relationship\"];\n return {\n package: packageName,\n severity: typeof adv[\"severity\"] === \"string\" ? (adv[\"severity\"] as string) : \"\",\n summary: typeof adv[\"summary\"] === \"string\" ? (adv[\"summary\"] as string) : \"\",\n cves: cveId ? [cveId] : [],\n url: typeof raw[\"html_url\"] === \"string\" ? (raw[\"html_url\"] as string) : null,\n scope: rawScope === \"runtime\" || rawScope === \"development\" ? rawScope : null,\n relationship:\n rawRelationship === \"direct\" || rawRelationship === \"transitive\" ? rawRelationship : null,\n };\n}\n\nexport function makeGitHubRest(deps: { token: string; fetch?: typeof fetch }): GitHubRest {\n const doFetch = deps.fetch ?? fetch;\n // GitHub requires a User-Agent; the rest are the documented JSON-API headers.\n const baseHeaders: Record<string, string> = {\n authorization: `Bearer ${deps.token}`,\n accept: \"application/vnd.github+json\",\n \"x-github-api-version\": \"2022-11-28\",\n \"user-agent\": \"reddoor-maintenance-dashboard\",\n };\n\n return {\n async defaultBranch(repo) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n const res = await doFetch(`${GITHUB_API}/repos/${owner}/${name}`, { headers: baseHeaders });\n if (!res.ok) {\n throw new Error(\n `GitHub GET repos/${owner}/${name} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: { default_branch?: string };\n try {\n body = (await res.json()) as { default_branch?: string };\n } catch {\n throw new Error(`GitHub repos/${owner}/${name}: 200 with a non-JSON body`);\n }\n if (!body.default_branch) {\n throw new Error(`GitHub repos/${owner}/${name}: response had no default_branch`);\n }\n return body.default_branch;\n },\n\n async dispatchWorkflow(repo, workflow, ref, inputs) {\n const { owner, name } = splitRepo(repo);\n // Every segment interpolates into the API path — guard them all (defense in\n // depth), matching gh.ts's dispatchWorkflow. `ref` is repo-sourced.\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n assertUrlSegment(\"branch\", ref);\n const res = await doFetch(\n `${GITHUB_API}/repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,\n {\n method: \"POST\",\n headers: { ...baseHeaders, \"content-type\": \"application/json\" },\n body: JSON.stringify(inputs ? { ref, inputs } : { ref }),\n },\n );\n // 204 No Content on success.\n if (!res.ok) {\n throw new Error(\n `GitHub workflow_dispatch ${owner}/${name}/${workflow} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n },\n\n async listWorkflowRuns(repo, workflow, opts) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n const qs = new URLSearchParams({\n created: `>=${opts.since}`,\n per_page: String(opts.perPage ?? 1),\n });\n if (opts.event) qs.set(\"event\", opts.event);\n const res = await doFetch(\n `${GITHUB_API}/repos/${owner}/${name}/actions/workflows/${workflow}/runs?${qs.toString()}`,\n { headers: baseHeaders },\n );\n if (!res.ok) {\n throw new Error(\n `GitHub GET runs ${owner}/${name}/${workflow} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: { workflow_runs?: Array<Record<string, unknown>> };\n try {\n body = (await res.json()) as { workflow_runs?: Array<Record<string, unknown>> };\n } catch {\n throw new Error(`GitHub runs ${owner}/${name}/${workflow}: 200 with a non-JSON body`);\n }\n return (\n (body.workflow_runs ?? [])\n .map((r) => ({\n id: Number(r.id),\n status: String(r.status ?? \"\"),\n conclusion: (r.conclusion as string | null) ?? null,\n createdAt: String(r.created_at ?? \"\"),\n htmlUrl: String(r.html_url ?? \"\"),\n }))\n // Fail-soft like the other fields: a record with a missing/garbage id\n // (Number(...) → NaN) is dropped rather than propagated.\n .filter((r) => Number.isFinite(r.id))\n );\n },\n\n async currentRunStep(repo, runId) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n if (!Number.isInteger(runId) || runId < 0) {\n throw new Error(`currentRunStep: expected a non-negative integer runId, got ${runId}`);\n }\n const res = await doFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs`, {\n headers: baseHeaders,\n });\n if (!res.ok) {\n throw new Error(\n `GitHub GET run ${owner}/${name}/${runId} jobs failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: {\n jobs?: Array<{ status?: string; steps?: Array<{ name?: string; status?: string }> }>;\n };\n try {\n body = (await res.json()) as typeof body;\n } catch {\n throw new Error(`GitHub run ${owner}/${name}/${runId} jobs: 200 with a non-JSON body`);\n }\n const jobs = body.jobs ?? [];\n // Prefer the in-progress job; fall back to the first job (single-job workflows).\n const job = jobs.find((j) => j.status === \"in_progress\") ?? jobs[0];\n const step = (job?.steps ?? []).find((s) => s.status === \"in_progress\");\n return step?.name ?? null;\n },\n\n async listDependabotAlerts(repo, opts) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n const state = opts?.state ?? \"open\";\n const out: DependabotAlert[] = [];\n // Follow the Link rel=\"next\" cursor (NOT a numeric `page` param — that is silently ignored\n // and would re-fetch page 1 forever, duplicating counts). The iteration cap is a runaway\n // backstop. Each request carries an abort timeout so a hung connection rejects (→ the\n // caller's try/catch → pnpm fallback) instead of stalling the whole sequential fleet sweep.\n let url: string | null =\n `${GITHUB_API}/repos/${owner}/${name}/dependabot/alerts?` +\n new URLSearchParams({ state, per_page: \"100\" }).toString();\n for (let i = 0; i < 100 && url; i++) {\n const res = await doFetch(url, {\n headers: baseHeaders,\n signal: AbortSignal.timeout(DEPENDABOT_FETCH_TIMEOUT_MS),\n });\n if (!res.ok) {\n throw new Error(\n `GitHub GET dependabot/alerts ${owner}/${name} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n throw new Error(`GitHub dependabot/alerts ${owner}/${name}: 200 with a non-JSON body`);\n }\n // The alerts endpoint returns a bare array; anything else means we're done.\n if (!Array.isArray(body)) break;\n for (const raw of body as Array<Record<string, unknown>>) {\n const mapped = mapDependabotAlert(raw);\n if (mapped) out.push(mapped);\n }\n url = nextLink(res.headers.get(\"link\"));\n }\n return out;\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { defaultSpawn, type SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { makeGitHubRest, type DependabotAlert } from \"../github/gh-rest.js\";\nimport { readGitHubConfig } from \"../github/config.js\";\n\ntype Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n\ntype Counts = { low: number; moderate: number; high: number; critical: number };\n\ntype AdvisoryEntry = {\n module: string;\n severity: Severity;\n title: string;\n cves?: string[];\n url?: string;\n /** Dependency graph scope from Dependabot (\"runtime\" | \"development\"); absent for the\n * lockfile `pnpm audit` fallback, which carries no per-package scope. Display-only. */\n scope?: \"runtime\" | \"development\";\n /** Dependency graph relationship from Dependabot; absent when GitHub reports \"unknown\"\n * or for the `pnpm audit` fallback. \"transitive\" = no direct-dep fix vehicle, so the\n * auto-fix-attempts counter and the digest treat nightly dispatches as no-ops. */\n relationship?: \"direct\" | \"transitive\";\n};\n\n// pnpm audit output (npm-compat with extra advisories map keyed by ID).\ntype PnpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n advisories?: Record<\n string,\n {\n id?: number;\n title?: string;\n module_name?: string;\n severity?: string;\n cves?: string[];\n url?: string;\n }\n >;\n};\n\n// npm v7+ shape (vulnerabilities keyed by package name).\ntype NpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n vulnerabilities?: Record<\n string,\n {\n name?: string;\n severity?: string;\n via?: unknown;\n url?: string;\n }\n >;\n};\n\nfunction classify(v: Counts) {\n if (v.critical > 0 || v.high > 0) return \"fail\" as const;\n if (v.moderate > 0 || v.low > 0) return \"warn\" as const;\n return \"pass\" as const;\n}\n\nfunction normalizeSeverity(s: unknown): Severity {\n if (s === \"low\" || s === \"moderate\" || s === \"high\" || s === \"critical\") return s;\n // npm/pnpm sometimes emit \"info\" for informational advisories. Map down\n // rather than defaulting to \"moderate\" (which would inflate severity).\n return \"low\";\n}\n\nfunction extractAdvisoriesFromPnpm(parsed: PnpmAuditJson): AdvisoryEntry[] {\n const out: AdvisoryEntry[] = [];\n for (const a of Object.values(parsed.advisories ?? {})) {\n if (!a) continue;\n out.push({\n module: a.module_name ?? \"unknown\",\n severity: normalizeSeverity(a.severity),\n title: a.title ?? \"(no title)\",\n ...(a.cves ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n });\n }\n return out;\n}\n\n/** Walk an npm v7+ `via` chain to find the root entry whose `via` array\n * contains a real advisory object (rather than another package name string).\n * Returns the package name at the root and the advisory detail. */\nfunction resolveNpmAdvisoryRoot(\n startName: string,\n vulnerabilities: NonNullable<NpmAuditJson[\"vulnerabilities\"]>,\n): { rootName: string; detail?: { title?: string; url?: string } } {\n const seen = new Set<string>();\n let current = startName;\n while (!seen.has(current)) {\n seen.add(current);\n const entry = vulnerabilities[current];\n if (!entry || !Array.isArray(entry.via)) return { rootName: current };\n\n const detailed = entry.via.find(\n (e): e is { title?: string; url?: string } => typeof e === \"object\" && e !== null,\n );\n if (detailed) return { rootName: current, detail: detailed };\n\n const next = entry.via.find((e): e is string => typeof e === \"string\");\n if (!next || next === current) return { rootName: current };\n current = next;\n }\n return { rootName: current };\n}\n\nfunction extractAdvisoriesFromNpm(parsed: NpmAuditJson): AdvisoryEntry[] {\n const vulnerabilities = parsed.vulnerabilities ?? {};\n const roots = new Map<string, AdvisoryEntry>();\n\n for (const [name, v] of Object.entries(vulnerabilities)) {\n if (!v) continue;\n const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);\n if (roots.has(rootName)) continue; // already surfaced via another transitive entry\n\n const rootEntry = vulnerabilities[rootName];\n const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);\n const title = detail?.title ?? rootName;\n const url = detail?.url;\n\n roots.set(rootName, {\n module: rootEntry?.name ?? rootName,\n severity,\n title,\n ...(url ? { url } : {}),\n });\n }\n\n return [...roots.values()];\n}\n\ntype ToolResult =\n | { kind: \"missing\" }\n | { kind: \"error\"; reason: string }\n | { kind: \"ok\"; parsed: PnpmAuditJson & NpmAuditJson };\n\nasync function runAuditTool(\n spawn: (cmd: string, args: readonly string[], opts?: { cwd?: string }) => Promise<SpawnResult>,\n cmd: string,\n args: readonly string[],\n cwd: string,\n): Promise<ToolResult> {\n let raw: SpawnResult;\n try {\n raw = await spawn(cmd, args, { cwd });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) return { kind: \"missing\" };\n return { kind: \"error\", reason: `spawn failed: ${String(err).slice(0, 200)}` };\n }\n\n // 0 = clean, 1 = vulns found. Anything else is a real error.\n if (raw.code !== 0 && raw.code !== 1) {\n return {\n kind: \"error\",\n reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : \"\"}`,\n };\n }\n\n let parsed: PnpmAuditJson & NpmAuditJson;\n try {\n parsed = JSON.parse(raw.stdout || \"{}\") as PnpmAuditJson & NpmAuditJson;\n } catch (err) {\n return { kind: \"error\", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };\n }\n\n // pnpm error envelope: { error: { code, message } }. npm sometimes emits\n // a top-level error too. Either means the audit didn't actually run.\n const errEnvelope = (parsed as unknown as { error?: { code?: string } }).error;\n if (errEnvelope && typeof errEnvelope === \"object\") {\n return { kind: \"error\", reason: errEnvelope.code ?? \"error envelope returned\" };\n }\n\n // Without metadata.vulnerabilities there are no counts to report and we\n // can't trust the result. An empty `{}` is just as suspect as a missing\n // key — counts default to 0 and we'd silently report \"pass\". Treat both\n // as a tool failure so the caller can fall through to the other audit.\n const vulnsMeta = parsed.metadata?.vulnerabilities;\n if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {\n return { kind: \"error\", reason: \"no metadata.vulnerabilities in output\" };\n }\n\n return { kind: \"ok\", parsed };\n}\n\n/** The Dependabot fetch the security audit needs, injectable for tests. The real default is\n * built from GITHUB_TOKEN; absent token (or a site with no gitRepo) → pnpm/npm audit fallback. */\nexport type DependabotDeps = {\n listAlerts: (repo: string) => Promise<DependabotAlert[]>;\n};\n\nfunction defaultDependabotDeps(): DependabotDeps | null {\n const cfg = readGitHubConfig();\n if (!cfg) return null;\n const gh = makeGitHubRest({ token: cfg.token });\n return { listAlerts: (repo) => gh.listDependabotAlerts(repo, { state: \"open\" }) };\n}\n\n/** Map GitHub's severity vocabulary onto the app's. \"medium\" → \"moderate\"; an unrecognized\n * value maps DOWN to \"low\" rather than inflating (mirrors `normalizeSeverity`). */\nfunction ghSeverityToApp(s: string): Severity {\n if (s === \"critical\" || s === \"high\" || s === \"low\") return s;\n if (s === \"medium\" || s === \"moderate\") return \"moderate\";\n return \"low\";\n}\n\n/** Build a security AuditResult from GitHub Dependabot alerts, or null if the fetch failed so\n * the caller falls back to `pnpm audit`. ALL open alerts count toward the severity tallies —\n * the cockpit's auto-patching (amber) vs Renovate-exhausted (red) bands decide urgency, so the\n * prod/dev scope is carried on each advisory for display only, not used to weight the counts. */\nasync function dependabotAudit(\n deps: DependabotDeps,\n repo: string,\n label: string,\n): Promise<AuditResult | null> {\n let alerts: DependabotAlert[];\n try {\n alerts = await deps.listAlerts(repo);\n } catch (e) {\n // Loud, greppable degradation marker: the caller silently falls back to the\n // weaker lockfile `pnpm audit`, so a systematic token/permission regression\n // would otherwise be invisible across the whole fleet.\n console.error(\n `[security] ${label}: dependabot API failed — degraded to pnpm audit: ` +\n `${e instanceof Error ? e.message : String(e)}`,\n );\n return null;\n }\n\n const counts: Counts = { low: 0, moderate: 0, high: 0, critical: 0 };\n const advisories: AdvisoryEntry[] = [];\n for (const a of alerts) {\n const severity = ghSeverityToApp(a.severity);\n counts[severity] += 1;\n advisories.push({\n module: a.package,\n severity,\n title: a.summary || \"(no title)\",\n ...(a.cves.length > 0 ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n ...(a.scope ? { scope: a.scope } : {}),\n ...(a.relationship ? { relationship: a.relationship } : {}),\n });\n }\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? \"Dependabot: 0 alerts\"\n : `Dependabot: ${total} alert(s) (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return { audit: \"security\", site: label, status, summary, details: { counts, advisories } };\n}\n\nexport async function securityAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // Prefer GitHub Dependabot alerts (ground truth: prod+dev, GitHub Advisory DB) for repo-backed\n // sites. The lockfile `pnpm audit` below stays the fallback for sites with no GitHub repo, no\n // token, or a transient API failure — a Dependabot hiccup must never fail an otherwise-fine site.\n if (site.gitRepo) {\n const dependabot = ctx.dependabotDeps ?? defaultDependabotDeps();\n if (dependabot) {\n const viaDependabot = await dependabotAudit(dependabot, site.gitRepo, label);\n if (viaDependabot) return viaDependabot;\n }\n }\n\n let used: \"pnpm audit\" | \"npm audit\" = \"pnpm audit\";\n let result = await runAuditTool(spawn, \"pnpm\", [\"audit\", \"--json\", \"--prod\"], site.path);\n\n // Fall through to npm if pnpm is missing OR pnpm couldn't actually\n // audit the project (e.g., no pnpm-lock.yaml). Previously we only fell\n // through on ENOENT, which meant npm-using sites silently reported \"pass\"\n // because pnpm returned an error envelope with no metadata.\n if (result.kind !== \"ok\") {\n const pnpmReason = result.kind === \"missing\" ? \"not installed\" : result.reason;\n const npmResult = await runAuditTool(\n spawn,\n \"npm\",\n [\"audit\", \"--json\", \"--omit=dev\"],\n site.path,\n );\n if (npmResult.kind === \"ok\") {\n result = npmResult;\n used = \"npm audit\";\n } else {\n const npmReason = npmResult.kind === \"missing\" ? \"not installed\" : npmResult.reason;\n return {\n audit: \"security\",\n site: label,\n status: \"skip\",\n summary: `cannot run audit — pnpm: ${pnpmReason}; npm: ${npmReason}`,\n };\n }\n }\n\n const parsed = result.parsed;\n\n const counts: Counts = {\n low: parsed.metadata?.vulnerabilities?.low ?? 0,\n moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,\n high: parsed.metadata?.vulnerabilities?.high ?? 0,\n critical: parsed.metadata?.vulnerabilities?.critical ?? 0,\n };\n\n const advisories =\n used === \"pnpm audit\" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? `${used}: 0 vulnerabilities`\n : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return {\n audit: \"security\",\n site: label,\n status,\n summary,\n details: { counts, advisories },\n };\n}\n","import { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { a11yRoutes, smokeRoutes, type A11yRoute } from \"../configs/playwright-a11y.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { findFreePort } from \"../util/free-port.js\";\n\ntype Impact = \"minor\" | \"moderate\" | \"serious\" | \"critical\";\n\ntype AxeViolation = {\n id: string;\n impact: Impact;\n route: string;\n help?: string;\n helpUrl?: string;\n nodes?: Array<{ html?: string; target?: string[] }>;\n};\n\ntype NormalizedA11y = {\n totalViolations: number;\n byImpact: Partial<Record<Impact, number>>;\n violations: AxeViolation[];\n};\n\nconst RESULTS_REL = \".reddoor-a11y/results.json\";\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n// The audit-controlled playwright config. We synthesize it (rather than\n// rely on the site's playwright.config.ts) so we can pin the dev server\n// port + force `--strictPort` — same fix as the lighthouse audit, same\n// reason (zombie vite processes squatting on 5173 would otherwise eat\n// the audit's request and return stale 404s).\nfunction buildPlaywrightConfig(port: number, sitePath: string): string {\n return `import { defineConfig } from \"@playwright/test\";\n\nexport default defineConfig({\n testDir: \".\",\n testMatch: /.*\\\\.spec\\\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:${port}\",\n trace: \"on-first-retry\",\n },\n webServer: {\n // --strictPort: refuse to bump to a different port if ours is taken,\n // so the audit fails loudly instead of probing a zombie.\n // reuseExistingServer:false: never reuse — we control the lifecycle.\n // cwd: playwright's default webServer.cwd is the config file's\n // directory. Our config lives in /tmp so without this override,\n // \"npm run vite:dev\" tries to read /tmp/.../package.json and\n // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).\n command: \"npm run vite:dev -- --port ${port} --strictPort\",\n url: \"http://localhost:${port}/dev/a11y-fixtures\",\n cwd: ${JSON.stringify(sitePath)},\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n`;\n}\n\n// The spec the audit writes runs all configured routes through axe in a single\n// test (so worker isolation doesn't fragment the collected violations) and\n// writes the structured result to <cwd>/.reddoor-a11y/results.json before\n// asserting. That way, the audit can read real axe details even when the\n// expect(...).toEqual([]) assertion fails.\nfunction buildSpec(axePages: A11yRoute[]): string {\n return `import { test, expect } from \"@playwright/test\";\nimport AxeBuilder from \"@axe-core/playwright\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nconst pages = ${JSON.stringify(axePages)};\nconst smokePages = ${JSON.stringify(smokeRoutes)};\nconst OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;\n\n// Playwright's default per-test timeout is 30s. We loop through every\n// configured route in a single test, so the budget needs to scale.\ntest.setTimeout(5 * 60_000);\n\ntest(\"a11y + hydration across configured routes\", async ({ page }) => {\n const violations = [];\n\n // Capture uncaught client-side exceptions across every route we visit. A page\n // that builds + SSRs cleanly can still throw on hydrate and blank itself\n // (data-dynamiq: a Svelte 4->5 run() referenced a $state declared after it) --\n // axe never sees that, so we listen for it directly and tag the route in scope.\n let currentRoute = \"\";\n page.on(\"pageerror\", (err) => {\n violations.push({\n id: \"client-error\",\n impact: \"critical\",\n route: currentRoute,\n help: String(err && err.message ? err.message : err),\n });\n });\n\n for (const { path, name } of pages) {\n currentRoute = name;\n await page.goto(path);\n // Snap CSS transitions/animations to their resting state before axe runs.\n // AnimateIn-style fixtures transition opacity 0->1; sampling mid-transition\n // makes axe compute color-contrast against semi-transparent text, yielding a\n // flaky \"serious\" color-contrast violation (~1/3 of runs on /dev/animate-in).\n // Disabling transitions/animations forces the final, rendered state\n // deterministically -- which is also what users (and prefers-reduced-motion\n // users) actually see, so it's the correct thing to assert.\n await page.addStyleTag({\n content: \"*,*::before,*::after{transition:none!important;animation:none!important;}\",\n });\n const results = await new AxeBuilder({ page })\n .withTags([\"wcag2a\",\"wcag2aa\",\"wcag21a\",\"wcag21aa\",\"wcag22aa\"])\n .analyze();\n for (const v of results.violations) {\n violations.push({\n id: v.id,\n impact: v.impact ?? \"moderate\",\n route: name,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),\n });\n }\n }\n\n // Hydration smoke check: load real routes (the homepage) and fail on any\n // uncaught client-side error. No axe here -- real routes carry pre-existing\n // a11y debt we don't gate on; we only assert they don't crash on hydrate.\n // HTTP/SSR errors don't fire 'pageerror', so a data-less CI homepage that\n // renders empty-but-valid won't false-fail -- only a real client crash does.\n for (const { path, name } of smokePages) {\n currentRoute = name;\n await page.goto(path);\n // Let hydration + first effects run so a TDZ/ReferenceError surfaces.\n await page.waitForTimeout(2000);\n }\n\n const byImpact = {};\n for (const v of violations) {\n byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;\n }\n if (OUTPUT) {\n await mkdir(dirname(OUTPUT), { recursive: true });\n await writeFile(\n OUTPUT,\n JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),\n );\n }\n expect(violations).toEqual([]);\n});\n`;\n}\n\nexport async function a11yAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // specDir lives INSIDE site.path (not /tmp) so the spec's\n // `import AxeBuilder from \"@axe-core/playwright\"` resolves via Node's\n // walk-up — the site's node_modules is the nearest one. A spec written\n // to /tmp ENOENTs at module resolution before any test runs. Caltex\n // 2026-05-28 (0.10.6 dogfood), third layer of the same class as the\n // webServer.cwd bug.\n const specDir = await mkdtemp(join(site.path, \".reddoor-a11y-spec-\"));\n // Everything past mkdtemp is wrapped so the transient specDir is removed on\n // EVERY catchable exit — success, skip-return, or any throw (a failed\n // writeFile/findFreePort used to orphan it). A timeout-SIGKILL of the parent\n // can't be caught here; `.reddoor-a11y-spec-*/` is fleet-gitignored as the\n // backstop for that. (2026-06-10 MEDIUM-D; recurred from 06-05 M3.)\n try {\n // Real routes the site has opted in to (package.json#reddoor.a11yRoutes),\n // appended to the fixtures rather than replacing them: the fixtures exercise\n // design-system components in isolation, which no real page covers. Absent\n // key → the fixtures alone, exactly as before. This exists because scanning\n // only fixtures let a critical `image-alt` violation ship to five production\n // pages with CI green; it is opt-in because the audit runs with\n // --fail-on-violations and most of the fleet has pre-existing debt.\n // The route path doubles as its name — the spec tags each violation with\n // `route: name`, so it has to identify the page.\n const { a11yRoutes: siteRoutes } = await readSiteConfig(site.path);\n const axePages: A11yRoute[] = [\n ...a11yRoutes,\n ...(siteRoutes ?? []).map((path) => ({ path, name: path })),\n ];\n\n const specPath = join(specDir, \"a11y.spec.ts\");\n await writeFile(specPath, buildSpec(axePages), \"utf-8\");\n\n const port = await findFreePort();\n const configPath = join(specDir, \"playwright.config.ts\");\n await writeFile(configPath, buildPlaywrightConfig(port, site.path), \"utf-8\");\n\n const resultsPath = join(site.path, RESULTS_REL);\n // Clear stale artifacts so a failed spawn never reports old data.\n await rm(join(site.path, \".reddoor-a11y\"), { recursive: true, force: true });\n\n let raw;\n try {\n raw = await spawn(\n \"npx\",\n [\"--yes\", \"playwright\", \"test\", `--config=${configPath}`, \"--reporter=line\", specPath],\n {\n cwd: site.path,\n env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },\n // playwright on a cold tree downloads Chrome, boots the site's dev\n // server, and runs axe over every configured route. The shared 30 s\n // default in runAudits is fine for deps/lint/security but starves\n // playwright (mirrors the lighthouse fix shipped earlier).\n timeoutMs: 5 * 60_000,\n },\n );\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"skip\",\n summary: \"npx/playwright not available\",\n };\n }\n throw err;\n }\n\n const artifact = await readJsonMaybe<NormalizedA11y>(resultsPath);\n\n if (!artifact) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"fail\",\n summary: `a11y: no results written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const hasSerious =\n (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;\n const hasAny = artifact.totalViolations > 0;\n\n const status: AuditResult[\"status\"] = hasSerious ? \"fail\" : hasAny ? \"warn\" : \"pass\";\n const summary =\n status === \"pass\"\n ? `a11y: 0 violations across ${a11yRoutes.length} routes (+${smokeRoutes.length} hydration smoke)`\n : `a11y: ${artifact.totalViolations} violations`;\n\n return {\n audit: \"a11y\",\n site: label,\n status,\n summary,\n details: artifact,\n };\n } finally {\n await rm(specDir, { recursive: true, force: true });\n }\n}\n","import { promises as dnsPromises } from \"node:dns\";\nimport tls from \"node:tls\";\nimport type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Result of probing a site's domain: did DNS resolve, and how many days until the TLS cert\n * expires (null when unresolved or no usable cert). The auto-tick verdict is recomputed from\n * the persisted fields — this is just the raw signal. */\nexport type DomainCheck = {\n resolved: boolean;\n certDaysRemaining: number | null;\n};\n\n/** Injected IO so the check is unit-testable without real DNS/TLS. `lookup` throws when the host\n * doesn't resolve; `certValidTo` returns the cert's notAfter date, or null when there's none. */\nexport type DomainDeps = {\n lookup: (host: string) => Promise<void>;\n certValidTo: (host: string) => Promise<Date | null>;\n now: Date;\n};\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/**\n * Probe one URL's domain: resolve DNS, then read the TLS certificate expiry. PURE given its deps.\n * Any failure degrades safely — an unresolvable host or a missing/unreadable cert yields\n * `certDaysRemaining: null` (which the auto-tick rule treats as \"not provable\", never a pass).\n */\nexport async function checkDomain(url: string, deps: DomainDeps): Promise<DomainCheck> {\n let host: string;\n try {\n host = new URL(url).hostname;\n } catch {\n return { resolved: false, certDaysRemaining: null };\n }\n try {\n await deps.lookup(host);\n } catch {\n return { resolved: false, certDaysRemaining: null };\n }\n let validTo: Date | null;\n try {\n validTo = await deps.certValidTo(host);\n } catch {\n validTo = null;\n }\n if (!validTo || Number.isNaN(validTo.getTime()))\n return { resolved: true, certDaysRemaining: null };\n return {\n resolved: true,\n certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY),\n };\n}\n\n/** Real DNS + TLS. `certValidTo` opens a TLS socket (SNI = host), reads the peer cert's\n * `valid_to`, and closes immediately — no HTTP request. */\nexport function defaultDomainDeps(now: Date): DomainDeps {\n return {\n lookup: async (host) => {\n await dnsPromises.lookup(host);\n },\n certValidTo: (host) =>\n new Promise<Date | null>((resolvePromise) => {\n // `rejectUnauthorized: true` is what makes this check HONEST and the green box truthful:\n // the secureConnect callback only fires on a trusted, host-matching (SNI), unexpired\n // chain — an expired / self-signed / untrusted-CA / SAN-mismatched cert fires \"error\"\n // → null → fail. The whole \"valid cert\" claim depends on this. NEVER set it false.\n // `socket.authorized` is asserted belt-and-suspenders before trusting `valid_to`.\n const socket = tls.connect(\n { host, port: 443, servername: host, timeout: 10_000, rejectUnauthorized: true },\n () => {\n const cert = socket.authorized ? socket.getPeerCertificate() : null;\n socket.end();\n const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;\n resolvePromise(validTo);\n },\n );\n socket.on(\"error\", () => resolvePromise(null));\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolvePromise(null);\n });\n }),\n now,\n };\n}\n\n/**\n * Audit a site's domain (DNS + TLS) against its deployed URL. Checkout-free — needs only\n * `site.deployedUrl`. Skips a site with no deployed URL. Writes `certDaysRemaining` + a checked-at\n * timestamp via the Airtable layer; the auto-tick rule decides pass/fail from those.\n */\nexport async function domainAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"domain\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n const now = ctx.now ?? new Date();\n const deps = ctx.domainDeps ?? defaultDomainDeps(now);\n const check = await checkDomain(site.deployedUrl, deps);\n const checkedAt = now.toISOString();\n const status: AuditResult[\"status\"] =\n check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14\n ? \"pass\"\n : \"warn\";\n const summary = !check.resolved\n ? \"did not resolve\"\n : check.certDaysRemaining === null\n ? \"resolved, no usable TLS cert\"\n : `resolved, cert ${check.certDaysRemaining}d remaining`;\n return {\n audit: \"domain\",\n site: label,\n status,\n summary,\n details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt },\n };\n}\n","/**\n * Route discovery for the deployed-URL browser audit. Pulls the site's sitemap, then samples a\n * REPRESENTATIVE set of paths — bucketed by path family (first segment) and sampled round-robin —\n * so every page *type* is covered, including the dynamic CMS-generated templates (Prismic\n * `[uid]`/`[slug]` detail pages: blog posts, projects, portfolio items) where broken\n * images / overflowing galleries / dead links hide. Taking the first N sitemap entries would skew\n * to top-level static pages and miss them. Pure functions + one fetch-injected entry point.\n */\n\nexport type DiscoverDeps = {\n /** Fetch a URL, returning its text body, or null on any non-2xx / network error. */\n fetchText: (url: string) => Promise<string | null>;\n};\n\nexport type DiscoveredRoutes = {\n /** Absolute URLs (rebuilt on the deployed origin), always including the homepage. */\n routes: string[];\n source: \"sitemap\" | \"homepage-links\" | \"root-only\";\n /** Path-family → count, for the evidence note (\"/, /work ×4, /blog ×4\"). */\n familyCounts: Record<string, number>;\n};\n\nconst DEFAULT_CAP = 15;\n\n/** Extract `<loc>` URLs from sitemap XML. Tolerant of namespaces/whitespace; ignores junk. */\nexport function parseSitemapUrls(xml: string): string[] {\n const out: string[] = [];\n const re = /<loc>\\s*([^<\\s]+)\\s*<\\/loc>/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(xml)) !== null) {\n const url = m[1];\n if (url) out.push(url.trim());\n }\n return out;\n}\n\n/** Same-origin `<a href>` paths from an HTML body (homepage fallback). Returns root-relative\n * pathnames; off-origin / anchor / mailto / tel links are dropped. */\nexport function parseHtmlLinks(html: string, baseUrl: string): string[] {\n const out = new Set<string>();\n const re = /<a\\b[^>]*\\bhref\\s*=\\s*[\"']([^\"']+)[\"']/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(html)) !== null) {\n const href = m[1];\n if (!href || href.startsWith(\"#\") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;\n try {\n const u = new URL(href, baseUrl);\n if (u.origin !== new URL(baseUrl).origin) continue;\n out.add(u.pathname);\n } catch {\n /* skip unparseable */\n }\n }\n return [...out];\n}\n\n/** First non-empty path segment (\"/\" → \"\", \"/work/x\" → \"work\"). The \"family\" we bucket by. */\nfunction family(pathname: string): string {\n return pathname.split(\"/\").filter(Boolean)[0] ?? \"\";\n}\n\n/** Extensions that mark a path as a downloadable asset/document, NOT a navigable page. Probing\n * one in a browser can never yield a `<title>`/meta description, and a download makes\n * `page.goto` throw — so a single homepage-linked PDF used to poison the reachability AND\n * titles-meta verdicts with certain fails (2026-07-16: MSOT's capabilities PDF). Blocklist (not\n * an html/htm allowlist) so dotted page slugs like `/blog/v2.0-release` are kept. */\nconst ASSET_EXTENSIONS = new Set([\n \"pdf\",\n \"doc\",\n \"docx\",\n \"xls\",\n \"xlsx\",\n \"ppt\",\n \"pptx\",\n \"csv\",\n \"txt\",\n \"rtf\",\n \"jpg\",\n \"jpeg\",\n \"png\",\n \"gif\",\n \"webp\",\n \"avif\",\n \"svg\",\n \"ico\",\n \"bmp\",\n \"tif\",\n \"tiff\",\n \"css\",\n \"js\",\n \"mjs\",\n \"map\",\n \"json\",\n \"xml\",\n \"webmanifest\",\n \"rss\",\n \"atom\",\n \"zip\",\n \"gz\",\n \"tar\",\n \"rar\",\n \"7z\",\n \"dmg\",\n \"exe\",\n \"mp3\",\n \"mp4\",\n \"webm\",\n \"mov\",\n \"avi\",\n \"wav\",\n \"ogg\",\n \"m4a\",\n \"m4v\",\n \"woff\",\n \"woff2\",\n \"ttf\",\n \"otf\",\n \"eot\",\n]);\n\n/** True when a pathname looks like a navigable PAGE (extensionless, .html/.htm, or a dotted slug\n * whose \"extension\" isn't a known asset type). PURE; exported for tests. */\nexport function isPageRoutePath(pathname: string): boolean {\n const last = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n const dot = last.lastIndexOf(\".\");\n if (dot <= 0) return true; // no extension (or a dotfile-style segment) → page\n const ext = last.slice(dot + 1).toLowerCase();\n if (ext === \"html\" || ext === \"htm\") return true;\n return !ASSET_EXTENSIONS.has(ext);\n}\n\n/**\n * Sample a representative set of PATHNAMES from a list of URLs/paths. Always includes \"/\". Buckets\n * the rest by path family and takes them round-robin (one per family per pass), so every family\n * is represented before any family gets a second page — guaranteeing CMS templates aren't skipped.\n * Caps the total. Asset/file URLs are dropped (only real pages are probed), and trailing slashes\n * are normalized away so `/a` and `/a/` — the same page — can't be sampled twice (2026-07-16:\n * revogen.com linked both forms, which guaranteed a bogus \"duplicate title\" titles-meta fail). PURE.\n */\nexport function sampleRoutePaths(urlsOrPaths: string[], cap: number = DEFAULT_CAP): string[] {\n const seen = new Set<string>([\"/\"]);\n const buckets = new Map<string, string[]>();\n for (const raw of urlsOrPaths) {\n let pathname: string;\n try {\n pathname = raw.startsWith(\"/\")\n ? new URL(raw, \"https://x.invalid\").pathname\n : new URL(raw).pathname;\n } catch {\n continue;\n }\n pathname = pathname.replace(/\\/+$/, \"\"); // `/a/` ≡ `/a` — same page, never two routes\n if (pathname === \"\") continue; // root (\"/\", \"//\", …) already guaranteed\n if (seen.has(pathname)) continue;\n seen.add(pathname);\n if (!isPageRoutePath(pathname)) continue;\n const fam = family(pathname);\n const arr = buckets.get(fam) ?? [];\n arr.push(pathname);\n buckets.set(fam, arr);\n }\n const result = [\"/\"];\n const families = [...buckets.values()];\n let guard = 0;\n while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 10_000) {\n for (const fam of families) {\n if (result.length >= cap) break;\n const next = fam.shift();\n if (next) result.push(next);\n }\n }\n return result;\n}\n\n/** Per-family counts of a sampled path list, for the evidence note. */\nexport function familyCountsOf(paths: string[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const p of paths) {\n const key = p === \"/\" ? \"/\" : `/${family(p)}`;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/**\n * Discover a representative route set for `deployedUrl`. Tries `<origin>/sitemap.xml` first\n * (family-sampled); falls back to the homepage's same-origin links; worst case the homepage only.\n * Returns absolute URLs rebuilt on the deployed origin.\n */\nexport async function discoverRoutes(\n deployedUrl: string,\n deps: DiscoverDeps,\n cap: number = DEFAULT_CAP,\n): Promise<DiscoveredRoutes> {\n const origin = new URL(deployedUrl).origin;\n const abs = (paths: string[]) => paths.map((p) => new URL(p, origin).href);\n\n const sitemapXml = await deps.fetchText(new URL(\"/sitemap.xml\", origin).href);\n if (sitemapXml) {\n const urls = parseSitemapUrls(sitemapXml);\n if (urls.length > 0) {\n const paths = sampleRoutePaths(urls, cap);\n return { routes: abs(paths), source: \"sitemap\", familyCounts: familyCountsOf(paths) };\n }\n }\n\n const homeHtml = await deps.fetchText(origin);\n if (homeHtml) {\n const links = parseHtmlLinks(homeHtml, origin);\n if (links.length > 0) {\n const paths = sampleRoutePaths(links, cap);\n return { routes: abs(paths), source: \"homepage-links\", familyCounts: familyCountsOf(paths) };\n }\n }\n\n return { routes: [new URL(\"/\", origin).href], source: \"root-only\", familyCounts: { \"/\": 1 } };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport {\n discoverRoutes,\n familyCountsOf,\n type DiscoverDeps,\n type DiscoveredRoutes,\n} from \"./route-discovery.js\";\n\n/** One route probed across desktop engines + mobile devices, plus the internal links found on it,\n * plus the chromium-derived reachability + SEO signals (captured on the page chromium already\n * opened — no extra navigation). */\nexport type RouteResult = {\n url: string;\n /** Per desktop engine (chromium/firefox/webkit): loaded with no JS error + a visible main\n * landmark. `status` = the HTTP status of THAT engine's navigation (null = the nav threw:\n * timeout/crash); optional because it wasn't always captured — undefined is never excusable.\n * NOTE: `ok` may be an EXCUSAL, not proof of a render — excuseChallengedEngineChecks flips a\n * WAF-challenged entry to ok:true on a reachable route (see details.excusedEngineChecks). */\n desktop: Array<{ engine: string; ok: boolean; status?: number | null }>;\n /** Per mobile device: loaded with no JS error and no horizontal overflow. Same `status`\n * semantics as desktop, and the same excusal caveat on `ok`. */\n mobile: Array<{ device: string; ok: boolean; status?: number | null }>;\n /** Same-origin links discovered on the page (absolute URLs), for the Links check. */\n links: string[];\n /** HTTP status of the chromium navigation (2xx/3xx = reachable). null = the nav failed/threw. */\n status: number | null;\n /** The chromium `<title>` (trimmed by the reducer), or null when not captured. */\n title: string | null;\n /** The chromium `meta[name=\"description\"]` content, or null when absent/not captured. */\n metaDescription: string | null;\n /** Unsubstituted SvelteKit template placeholders (e.g. \"%sveltekit.body%\") the SERVED HTML\n * carried — a broken app.html (a placeholder token trapped in a comment, a malformed template)\n * ships these literally. Empty = clean. Optional: a runner that didn't capture it is treated as\n * clean (undefined ≠ failure), but a token that IS seen is unambiguous corruption. */\n leakedTemplateTokens?: string[];\n};\n\nexport type LinkResult = { url: string; status: number | null };\n\n/** Injected browser IO. The real impl drives Playwright; tests pass a fake. */\nexport type BrowserRunner = {\n probe: (urls: string[]) => Promise<RouteResult[]>;\n checkLinks: (urls: string[]) => Promise<LinkResult[]>;\n close?: () => Promise<void>;\n};\n\n/** What a plain (non-browser) GET of a route observed. `status: null` = network error/timeout. */\nexport type PageFetch = {\n status: number | null;\n title: string | null;\n metaDescription: string | null;\n};\n\n/** What a no-follow GET of the base URL observed: the raw status (null = network error/timeout)\n * and the raw Location header (null when absent). See canonicalizeBaseUrl. */\nexport type RedirectFetch = { status: number | null; location: string | null };\n\n/** Injected plain-fetch re-verification IO (see reverifyRoutes). Tests pass a fake. */\nexport type VerifyDeps = {\n fetchPage: (url: string) => Promise<PageFetch>;\n /** Optional single `redirect:\"manual\"` GET, for canonicalizeBaseUrl's base-URL preflight.\n * Optional so existing fakes stay valid; absent → canonicalization is a no-op. */\n fetchRedirect?: (url: string) => Promise<RedirectFetch>;\n};\n\n/** True when two hostnames belong to the same site: equal, or one is a subdomain of the other\n * (apex ↔ www), case-insensitive. Mirrors forms/ingest's hostsMatch — kept local so the audit\n * layer doesn't grow an import on the forms surface. */\nfunction sameSiteHost(a: string, b: string): boolean {\n const ha = a.toLowerCase();\n const hb = b.toLowerCase();\n return ha === hb || ha.endsWith(`.${hb}`) || hb.endsWith(`.${ha}`);\n}\n\n/** canonicalizeBaseUrl's answer. At most one of redirectedFrom / offsiteLocation is set. */\nexport type CanonicalBase = {\n url: string;\n /** Set when the configured URL 30x'd to its canonical same-site host and we followed it. */\n redirectedFrom?: string;\n /** Set when the configured URL redirects OFF-SITE. Deliberately NOT followed — auto-probing a\n * foreign host could bless a parked/squatted target with our green verdicts; the operator\n * should update the site's url instead. */\n offsiteLocation?: string;\n};\n\n/**\n * Resolve the audit's effective base URL by following the configured URL's own redirect ONCE.\n * WHY (2026-07-17, Vineyard): an apex-configured Airtable url (vineyardconstruction.com) that\n * 301s to www adds a redirect hop to EVERY probed route; under the audit's own probe burst that\n * hop tipped two heavy gallery pages over the nav timeout → \"no response\" false alarms on a\n * perfectly healthy site. Probing the canonical host removes the hop at the source, so the class\n * can't recur when a future site is onboarded apex-first. Conservative by design:\n * - ONE hop only — no loop-chasing; the canonical host answering directly is the normal case,\n * and if IT also redirects, the per-route probes/reverify still follow redirects as before.\n * - Same-site hosts only (apex ↔ www / subdomain); an off-site redirect is surfaced, not followed.\n * - Any error / non-redirect status / missing or unparseable Location / non-http(s) target →\n * the configured URL stands unchanged (fail-open, like every other audit fetch).\n */\nexport async function canonicalizeBaseUrl(\n baseUrl: string,\n verify: VerifyDeps,\n): Promise<CanonicalBase> {\n if (!verify.fetchRedirect) return { url: baseUrl };\n let origin: URL;\n try {\n origin = new URL(baseUrl);\n } catch {\n return { url: baseUrl };\n }\n const seen = await verify.fetchRedirect(baseUrl);\n const isRedirect =\n seen.status !== null &&\n [301, 302, 307, 308].includes(seen.status) &&\n (seen.location ?? \"\") !== \"\";\n if (!isRedirect) return { url: baseUrl };\n let target: URL;\n try {\n // Location may be relative (spec-legal) — resolve against the configured base.\n target = new URL(seen.location!, baseUrl);\n } catch {\n return { url: baseUrl };\n }\n if (target.protocol !== \"http:\" && target.protocol !== \"https:\") return { url: baseUrl };\n if (!sameSiteHost(origin.hostname, target.hostname)) {\n return { url: baseUrl, offsiteLocation: target.href };\n }\n return { url: target.href, redirectedFrom: baseUrl };\n}\n\nexport type BrowserSummary = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n /** Every sampled route returned a 2xx/3xx status (point-in-time uptime). */\n reachableOk: boolean;\n /** Every sampled route has a non-empty `<title>` ≤ 70 chars + a non-empty meta description, and\n * no two routes share a title. */\n titleMetaOk: boolean;\n /** The confirmed-failing routes behind a reachableOk=false, as \"url → status\" strings, so the\n * operator sees WHICH url failed, not just \"fail\". Empty when reachableOk is true. */\n unreachableUrls: string[];\n /** The per-route findings behind a titleMetaOk=false (\"url: missing meta description\",\n * 'duplicate title \"X\": urlA + urlB', …). Empty when titleMetaOk is true. */\n titleMetaProblems: string[];\n /** No sampled route served an unsubstituted SvelteKit placeholder. False = a broken app.html\n * shipped a literal \"%sveltekit.*%\" to the browser (blank/corrupt render — the failure that\n * can slip past desktop/mobile when the page still renders a `<main>` but leaks the token). */\n templateOk: boolean;\n /** The per-route findings behind templateOk=false (\"url: unsubstituted %sveltekit.body%\").\n * Empty when templateOk is true. */\n templateProblems: string[];\n /** Confirmed-failing desktop checks as \"url [engine]\" (+\" → status\" when a definite non-2xx/3xx\n * nav status was captured), or \"url: no desktop observations\". Empty when desktopOk. */\n desktopFailures: string[];\n /** Same for mobile devices. */\n mobileFailures: string[];\n /** The broken links behind brokenLinks, as \"url → status|no response\" (mirrors unreachableUrls). */\n brokenLinkUrls: string[];\n brokenLinks: number;\n routesChecked: number;\n note: string;\n};\n\n/** A link status counts as broken when it didn't return a 2xx/3xx (null = unreachable). */\nfunction isBroken(status: number | null): boolean {\n return status === null || status >= 400;\n}\n\n/** True for a 2xx/3xx observation (the \"reachable\" bar routes must clear). */\nfunction isOkStatus(status: number | null): status is number {\n return status !== null && status >= 200 && status < 400;\n}\n\n/** Decode the handful of HTML entities that show up in real `<title>`s so lengths/duplicate\n * comparisons match what `page.title()` (which decodes) would have reported. */\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&/gi, \"&\")\n .replace(/</gi, \"<\")\n .replace(/>/gi, \">\")\n .replace(/"/gi, '\"')\n .replace(/�?39;|'/gi, \"'\")\n .replace(/ /gi, \" \");\n}\n\n/** Pull `<title>` + `meta[name=\"description\"]` out of raw HTML (SSR truth — what crawlers see).\n * Tolerant regex parse: attribute order-independent for the meta tag. PURE; exported for tests. */\nexport function extractTitleMeta(html: string): {\n title: string | null;\n metaDescription: string | null;\n} {\n const t = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html);\n const title = t?.[1] ? decodeEntities(t[1]).trim() || null : null;\n let metaDescription: string | null = null;\n const metaTags = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const tag of metaTags) {\n if (!/\\bname\\s*=\\s*[\"']description[\"']/i.test(tag)) continue;\n const content = /\\bcontent\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/i.exec(tag);\n const raw = content?.[2] ?? content?.[3] ?? \"\";\n metaDescription = decodeEntities(raw).trim() || null;\n break;\n }\n return { title, metaDescription };\n}\n\n/** One plain-fetch re-check of a browser-probed route: what the browser saw vs what a plain GET\n * (no browser) saw. Kept in the audit details so a WAF-challenged run is visible, not silent. */\nexport type Reverification = {\n url: string;\n browserStatus: number | null;\n fetchedStatus: number | null;\n};\n\n/** Cooldown-retry schedule for the reachability re-check. Live measurement 2026-07-16: right\n * after the ~75-navigation probe burst, the WAF rate-flags the runner's IP so even the plain\n * fetch briefly 403s — but a plain fetch ~60s later is 200 again. A fail only persists when the\n * URL stays bad across these waits (total ~60s). Definitive answers (404/410) skip the retries. */\nconst REVERIFY_RETRY_DELAYS_MS = [15_000, 45_000];\n\ntype SleepFn = (ms: number) => Promise<void>;\nconst defaultSleep: SleepFn = (ms) => new Promise((r) => setTimeout(r, ms));\n\n/** A status worth a cooldown retry: WAF-challenge/rate-limit shapes (403/408/429), server errors,\n * and null (timeout / network error) can all be transient. 404/410 are definitive. */\nfunction isRetryableStatus(status: number | null): boolean {\n return status === null || status === 403 || status === 408 || status === 429 || status >= 500;\n}\n\n/** One plain fetch per unique url, then cooldown retries (REVERIFY_RETRY_DELAYS_MS) for any\n * whose answer is still transient-shaped (isRetryableStatus). Definitive answers (2xx/3xx,\n * 404/410, other 4xx) return immediately. Shared by reverifyRoutes + reverifyLinks — the ONE\n * copy of the WAF-cooldown discipline. */\nasync function fetchStatusesWithCooldown(\n urls: string[],\n verify: VerifyDeps,\n sleep: SleepFn,\n): Promise<Map<string, PageFetch>> {\n const byUrl = new Map<string, PageFetch>();\n for (const url of new Set(urls)) byUrl.set(url, await verify.fetchPage(url));\n for (const delayMs of REVERIFY_RETRY_DELAYS_MS) {\n const pending = [...byUrl.entries()]\n .filter(([, f]) => !isOkStatus(f.status) && isRetryableStatus(f.status))\n .map(([u]) => u);\n if (pending.length === 0) break;\n await sleep(delayMs);\n for (const url of pending) byUrl.set(url, await verify.fetchPage(url));\n }\n return byUrl;\n}\n\n/**\n * Re-verify browser observations with a plain fetch BEFORE any fail verdict can persist.\n * WHY (2026-07-16): hosts' bot protection (Netlify WAF et al.) intermittently serves 403\n * challenge interstitials to the headless-browser probe burst — status 403, `<title>` = the bare\n * domain, no meta — while the site is perfectly up for real visitors (plain fetch → 200). That\n * poisoned Uptime-Reachable AND Titles-&-Meta on most of the fleet. So:\n * - a route whose browser status was non-2xx/3xx (or null) is re-fetched plainly, with cooldown\n * retries for transient shapes; only a CONFIRMED non-2xx/timeout keeps the bad status. A\n * confirmed-ok route also takes its title/meta from the served HTML (the browser DOM was a\n * challenge page — worthless).\n * - a route that loaded fine but is missing title/meta (capture flake / challenge variants)\n * gets the missing fields re-read from the served HTML; a genuinely absent meta stays absent.\n * Never mutates its input; returns fresh RouteResults + the reachability re-checks performed.\n */\nexport async function reverifyRoutes(\n routes: RouteResult[],\n verify: VerifyDeps,\n sleep: (ms: number) => Promise<void> = defaultSleep,\n): Promise<{ routes: RouteResult[]; reverified: Reverification[] }> {\n const statusIdx: number[] = [];\n const titleMetaIdx: number[] = [];\n routes.forEach((r, i) => {\n if (!isOkStatus(r.status)) statusIdx.push(i);\n else if (!(r.title ?? \"\").trim() || !(r.metaDescription ?? \"\").trim()) titleMetaIdx.push(i);\n });\n\n // Reachability re-checks get the shared cooldown discipline (the IP may still be WAF-flagged\n // from the probe burst itself); route urls are unique by construction (discoverRoutes dedupes).\n const statusFetches = await fetchStatusesWithCooldown(\n statusIdx.map((i) => routes[i]!.url),\n verify,\n sleep,\n );\n // Title/meta recovery stays a single un-retried fetch — the route already answered ok, so\n // there's nothing transient to wait out.\n const fetchedByIdx = new Map<number, PageFetch>();\n for (const i of titleMetaIdx) fetchedByIdx.set(i, await verify.fetchPage(routes[i]!.url));\n\n const out = [...routes];\n const reverified: Reverification[] = [];\n for (const i of statusIdx) {\n const r = routes[i]!;\n const fetched = statusFetches.get(r.url)!;\n reverified.push({ url: r.url, browserStatus: r.status, fetchedStatus: fetched.status });\n // The plain fetch is the reachability truth. When it's ok, the challenge-page title/meta\n // the browser captured are discarded for the served HTML's; when the fail is CONFIRMED,\n // title/meta go null (a dead route has no page — don't let a 404 page's \"Not found\" title\n // masquerade as content).\n const ok = isOkStatus(fetched.status);\n out[i] = {\n ...r,\n status: fetched.status,\n title: ok ? fetched.title : null,\n metaDescription: ok ? fetched.metaDescription : null,\n };\n }\n for (const i of titleMetaIdx) {\n const r = routes[i]!;\n const fetched = fetchedByIdx.get(i)!;\n // Browser loaded the page but captured no title/meta: trust the served HTML for the\n // MISSING fields only (never overwrite what the browser did capture). A failed plain\n // fetch changes nothing — the browser observation stands.\n if (!isOkStatus(fetched.status)) continue;\n out[i] = {\n ...r,\n title: (r.title ?? \"\").trim() ? r.title : fetched.title,\n metaDescription: (r.metaDescription ?? \"\").trim()\n ? r.metaDescription\n : fetched.metaDescription,\n };\n }\n return { routes: out, reverified };\n}\n\n/** After reverifyRoutes has settled each route's TRUE reachability (plain-fetch verified), void\n * the desktop/mobile observations that failed only because the engine was served a WAF\n * challenge / transient rejection. Behind a challenge interstitial the engine never saw the\n * real page, so its JS-error/landmark/overflow signals measured the WAF page — worthless for\n * the crossbrowser/mobile verdicts (server status is owned by reachableOk). The headline\n * 2026-07-16 case: chromium+firefox+webkit+both mobiles all 403-challenged mid-burst — #428\n * cleared reachableOk via plain fetch while desktopOk/mobileOk still false-alarmed on the very\n * same challenge. What still stands: fails on a LOADED page (2xx/3xx entry status = genuine\n * rendering breakage); null/undefined entry status (a hung/crashed nav can be a genuine\n * engine-specific problem, and undefined means the status was never captured); definitive\n * 404/410; and every entry on a route whose unreachability was CONFIRMED. The route-level\n * `status` here is already the plain-fetch-verified truth (chromium's own 2xx or reverifyRoutes'\n * cooled-down fetch), so no additional fetches are needed — this function is PURE and reuses\n * reverifyRoutes' output rather than forking its retry logic. Never mutates its input.\n * KNOWN BLIND SPOT: unlike reverifyLinks (which re-fetches and only clears a 403 that turns\n * 2xx), this applies NO persistence test — a challenge-shaped engine status is excused every\n * run. So a genuinely engine-specific 403 (e.g. an edge rule that blocks only one UA while the\n * route stays reachable to plain fetch + other engines) is silently excused nightly, visible\n * only as an excusedEngineChecks note. Accepted because it's undecidable with today's signals:\n * a headless re-probe of that engine would just be re-challenged by the same WAF. */\nexport function excuseChallengedEngineChecks(routes: RouteResult[]): {\n routes: RouteResult[];\n excused: string[];\n} {\n const excused: string[] = [];\n // Excusable: failed, with a captured numeric status that is challenge-shaped. The\n // isRetryableStatus gate deliberately excludes 404/410 (an engine that got a definitive dead\n // answer on an otherwise-reachable route stays a named failure) and the typeof-number gate\n // deliberately excludes null (nav timeout) and undefined (status never captured), so genuine\n // detection is never weakened.\n const excusable = (e: { ok: boolean; status?: number | null }): boolean =>\n !e.ok && typeof e.status === \"number\" && !isOkStatus(e.status) && isRetryableStatus(e.status);\n const out = routes.map((route) => {\n // Confirmed-dead route: everything fails honestly, consistent with reachableOk.\n if (!isOkStatus(route.status)) return route;\n const desktop = route.desktop.map((d) => {\n if (!excusable(d)) return d;\n excused.push(\n `${route.url} [${d.engine}]: browser saw ${d.status}, route re-verified reachable (${route.status})`,\n );\n return { ...d, ok: true };\n });\n const mobile = route.mobile.map((m) => {\n if (!excusable(m)) return m;\n excused.push(\n `${route.url} [${m.device}]: browser saw ${m.status}, route re-verified reachable (${route.status})`,\n );\n return { ...m, ok: true };\n });\n return { ...route, desktop, mobile };\n });\n return { routes: out, excused };\n}\n\n/** One re-check of a link the first pass called broken with a transient-shaped status. */\nexport type LinkReverification = {\n url: string;\n firstStatus: number | null;\n fetchedStatus: number | null;\n};\n\n/**\n * Re-check any link whose first result was challenge-shaped (403/408/429/5xx/null) with a plain\n * GET plus the shared cooldown schedule BEFORE it can count broken — the probe burst itself\n * rate-flags the runner's IP for ~60s, so checkLinks' plain fetches 403 right after it.\n * Definitive answers (404/410, other 4xx incl. 401) NEVER re-verify: one observation stands.\n * Every link checked here is SAME-ORIGIN by construction (probe() filters on the deployed\n * origin), so any 403 is from our own fleet host — where a challenge of our own probe burst is\n * the overwhelmingly likely cause and a plain re-fetch after cooldown is the truth. A 403 that\n * SURVIVES the cooldown counts broken: on our own host that means real visitors can hit it\n * (password-protected/preview leakage is a genuine content problem). If external third-party\n * link checking is ever added, a confirmed 403 from a foreign host must downgrade to a warn —\n * many sites legitimately 403 non-browser clients — while 404/410 stay broken.\n * Incidental fix: fetchPage is a GET, so hosts that reject HEAD with 403 (checkLinks probes\n * HEAD-first) get an honest second look too. Reusing VerifyDeps.fetchPage means the ok-path\n * reads the body to parse title/meta we discard — accepted (small N, one injected dep).\n * Returns the ORIGINAL array shape (same length/order — the linksOk \"zero links checked = not\n * proven\" fail-safe depends on it) + every re-check performed, cleared or confirmed.\n */\nexport async function reverifyLinks(\n links: LinkResult[],\n verify: VerifyDeps,\n sleep: (ms: number) => Promise<void> = defaultSleep,\n): Promise<{ links: LinkResult[]; reverified: LinkReverification[] }> {\n const suspects = links.filter((l) => isBroken(l.status) && isRetryableStatus(l.status));\n if (suspects.length === 0) return { links, reverified: [] };\n const fetched = await fetchStatusesWithCooldown(\n suspects.map((l) => l.url),\n verify,\n sleep,\n );\n const suspectUrls = new Set(suspects.map((l) => l.url));\n const reverified: LinkReverification[] = [];\n const out = links.map((l) => {\n if (!suspectUrls.has(l.url)) return l;\n const finalStatus = fetched.get(l.url)!.status;\n reverified.push({ url: l.url, firstStatus: l.status, fetchedStatus: finalStatus });\n return { url: l.url, status: finalStatus };\n });\n return { links: out, reverified };\n}\n\n/** \" → status\" suffix for a failing desktop/mobile entry, only when a definite non-2xx/3xx nav\n * status was captured (2xx/3xx = the page loaded, the failure was rendering; undefined = the\n * status was never captured — neither adds signal to the offender string). */\nconst fmtEntryStatus = (s?: number | null): string =>\n typeof s === \"number\" && !isOkStatus(s) ? ` → ${s}` : \"\";\n\n/**\n * Reduce raw per-route observations to the three checklist verdicts. PURE.\n * - desktopOk: EVERY route loaded cleanly in EVERY desktop engine (a WAF-challenged entry\n * excused by excuseChallengedEngineChecks counts as clean — see its caveat below).\n * - mobileOk: EVERY route loaded cleanly (no overflow / error) on EVERY mobile device.\n * - linksOk: NO internal link is broken.\n * Empty observations (a probe that produced nothing) → not ok (fail-safe: prove, don't assume).\n * Every fail names its offenders (desktopFailures/mobileFailures/brokenLinkUrls, mirroring\n * unreachableUrls) so a red box is actionable, never a bare \"fail\".\n */\nexport function summarizeBrowser(\n routes: RouteResult[],\n links: LinkResult[],\n familyCounts: Record<string, number>,\n): BrowserSummary {\n const desktopChecks = routes.flatMap((r) => r.desktop);\n const mobileChecks = routes.flatMap((r) => r.mobile);\n // Per-route completeness: EVERY route must carry observations AND every observation must be ok.\n // (Guarding only the flattened array would excuse a route that produced no observations at all —\n // a false green.) A route with an empty desktop[]/mobile[] (never rendered) → not ok.\n const desktopOk =\n routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));\n const mobileOk =\n routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));\n // The named offenders behind desktopOk/mobileOk=false, one string per failing engine/device\n // check (or per observation-less route — the fail-safe above already failed it).\n const desktopFailures = routes.flatMap((r) =>\n r.desktop.length === 0\n ? [`${r.url}: no desktop observations`]\n : r.desktop\n .filter((d) => !d.ok)\n .map((d) => `${r.url} [${d.engine}]${fmtEntryStatus(d.status)}`),\n );\n const mobileFailures = routes.flatMap((r) =>\n r.mobile.length === 0\n ? [`${r.url}: no mobile observations`]\n : r.mobile\n .filter((m) => !m.ok)\n .map((m) => `${r.url} [${m.device}]${fmtEntryStatus(m.status)}`),\n );\n const brokenLinkUrls = links\n .filter((l) => isBroken(l.status))\n .map((l) => `${l.url} → ${l.status ?? \"no response\"}`);\n const brokenLinks = brokenLinkUrls.length;\n // linksOk requires links to have ACTUALLY been checked — zero links checked is \"not proven\",\n // never a pass (e.g. a JS-rendered page whose hrefs weren't in the DOM, or a chromium evaluate\n // that threw → []). Otherwise the box would assert \"all links resolve\" having checked nothing.\n const linksOk = links.length > 0 && brokenLinks === 0;\n\n // reachableOk: every sampled route returned 2xx/3xx. Empty observations → false (fail-safe).\n // The failing routes are NAMED (url → status) so a fail is actionable, never a bare \"fail\".\n const unreachableUrls = routes\n .filter((r) => !isOkStatus(r.status))\n .map((r) => `${r.url} → ${r.status ?? \"no response\"}`);\n const reachableOk = routes.length > 0 && unreachableUrls.length === 0;\n\n // titleMetaOk (chromium-only signals): every route has a non-empty title ≤ 70 chars + a non-empty\n // meta description, AND no two routes share a title. Empty observations → false (fail-safe).\n // Each violated sub-check is recorded per-URL; duplicate detection runs on NON-empty titles only\n // (an empty title is already its own finding — two blank routes aren't a \"duplicate\" insight).\n const titleMetaProblems: string[] = [];\n for (const r of routes) {\n const t = (r.title ?? \"\").trim();\n const desc = (r.metaDescription ?? \"\").trim();\n if (t.length === 0) titleMetaProblems.push(`${r.url}: empty title`);\n else if (t.length > 70) titleMetaProblems.push(`${r.url}: title ${t.length} chars (max 70)`);\n if (desc.length === 0) titleMetaProblems.push(`${r.url}: missing meta description`);\n }\n const byTitle = new Map<string, string[]>();\n for (const r of routes) {\n const t = (r.title ?? \"\").trim();\n if (t.length === 0) continue;\n byTitle.set(t, [...(byTitle.get(t) ?? []), r.url]);\n }\n for (const [t, urls] of byTitle) {\n if (urls.length > 1) titleMetaProblems.push(`duplicate title \"${t}\": ${urls.join(\" + \")}`);\n }\n const titleMetaOk = routes.length > 0 && titleMetaProblems.length === 0;\n\n // templateOk: no route served an unsubstituted SvelteKit placeholder. A literal \"%sveltekit.*%\"\n // in shipped HTML is always corruption (a token trapped in an app.html comment, a malformed\n // template) and never a transient WAF challenge, so — unlike desktop/mobile — it needs no excusal\n // path: a single hit is a definite warn. This catches the failure mode where the page still\n // renders a `<main>` (desktop/mobile pass) but leaks the token as visible text.\n const templateProblems = routes.flatMap((r) =>\n (r.leakedTemplateTokens ?? []).map((t) => `${r.url}: unsubstituted ${t}`),\n );\n const templateOk = templateProblems.length === 0;\n\n const engines = [...new Set(desktopChecks.map((d) => d.engine))];\n const devices = [...new Set(mobileChecks.map((m) => m.device))];\n const families = Object.entries(familyCounts)\n .map(([f, n]) => (f === \"/\" ? \"/\" : `${f} ×${n}`))\n .join(\", \");\n // Surface the first offenders right in the note (full lists ride in details/unreachableUrls +\n // details/titleMetaProblems) so the evidence line answers \"which url?\" without a JSON dig.\n const firstOf = (xs: string[], n: number) =>\n xs.slice(0, n).join(\"; \") + (xs.length > n ? ` (+${xs.length - n} more)` : \"\");\n const note =\n `${routes.length} routes (${families}); ` +\n `desktop ${engines.join(\"/\") || \"—\"}; mobile ${devices.join(\"/\") || \"—\"}; ` +\n `${links.length} links, ${brokenLinks} broken` +\n (unreachableUrls.length > 0 ? `; unreachable: ${firstOf(unreachableUrls, 3)}` : \"\") +\n (titleMetaProblems.length > 0 ? `; title/meta: ${firstOf(titleMetaProblems, 3)}` : \"\") +\n (templateProblems.length > 0 ? `; template: ${firstOf(templateProblems, 3)}` : \"\") +\n (desktopFailures.length > 0 ? `; desktop failing: ${firstOf(desktopFailures, 3)}` : \"\") +\n (mobileFailures.length > 0 ? `; mobile failing: ${firstOf(mobileFailures, 3)}` : \"\") +\n (brokenLinkUrls.length > 0 ? `; broken: ${firstOf(brokenLinkUrls, 3)}` : \"\");\n\n return {\n desktopOk,\n mobileOk,\n linksOk,\n reachableOk,\n titleMetaOk,\n templateOk,\n unreachableUrls,\n titleMetaProblems,\n templateProblems,\n desktopFailures,\n mobileFailures,\n brokenLinkUrls,\n brokenLinks,\n routesChecked: routes.length,\n note,\n };\n}\n\n/**\n * Deployed-URL browser audit: discovers a representative route set (incl. CMS templates), probes\n * each across desktop engines + mobile devices, and checks internal links — all against the LIVE\n * url (checkout-free). Skips a site with no deployed URL. Produces ONE AuditResult whose details\n * carry the three verdicts (Crossbrowser / Mobile / Links); the Airtable layer fans them out and\n * the auto-tick rule gates each on freshness.\n */\nexport async function browserAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"browser\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n const now = ctx.now ?? new Date();\n const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();\n const verifyDeps = ctx.verifyDeps ?? defaultVerifyDeps();\n const runner = ctx.browserRunner ?? (await defaultBrowserRunner());\n try {\n // Probe the CANONICAL host: follow the configured URL's own redirect once (apex → www) so\n // every route doesn't pay a redirect hop that can tip a heavy page over the nav timeout\n // under the probe burst (the 2026-07-17 Vineyard false-alarm class) — see canonicalizeBaseUrl.\n const canonical = await canonicalizeBaseUrl(site.deployedUrl, verifyDeps);\n const discovered: DiscoveredRoutes = await discoverRoutes(canonical.url, discoverDeps);\n const probed = await runner.probe(discovered.routes);\n // Plain-fetch re-verification BEFORE any verdict: a browser-side 4xx/timeout (WAF challenge,\n // engine flake) must not persist a fail the site doesn't deserve — see reverifyRoutes.\n const { routes: reverifiedRoutes, reverified } = await reverifyRoutes(probed, verifyDeps);\n // Engine/device checks poisoned by the same challenge are voided AGAINST the verified\n // reachability — reuses reverifyRoutes' output, no second copy of the retry logic.\n const { routes: routeResults, excused } = excuseChallengedEngineChecks(reverifiedRoutes);\n const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];\n const rawLinks = await runner.checkLinks(internalLinks);\n // A link the burst-flagged IP saw as 403 gets a plain-fetch cooldown re-check before it can\n // count broken (the probe burst itself rate-flags the runner for ~60s — see reverifyLinks).\n const { links: linkResults, reverified: linkReverified } = await reverifyLinks(\n rawLinks,\n verifyDeps,\n );\n const summary = summarizeBrowser(\n routeResults,\n linkResults,\n discovered.familyCounts ?? familyCountsOf(discovered.routes),\n );\n // Cleared re-checks are surfaced (note + details) so a challenge-heavy run is visible even\n // when every verdict lands green; confirmed fails are already named in unreachableUrls.\n const cleared = reverified.filter((v) => isOkStatus(v.fetchedStatus));\n const reverifiedUrls = cleared.map(\n (v) =>\n `${v.url}: browser saw ${v.browserStatus ?? \"no response\"}, plain fetch ${v.fetchedStatus}`,\n );\n const clearedLinks = linkReverified.filter((v) => isOkStatus(v.fetchedStatus));\n const linkReverifiedUrls = clearedLinks.map(\n (v) =>\n `${v.url}: first check saw ${v.firstStatus ?? \"no response\"}, plain re-fetch ${v.fetchedStatus}`,\n );\n const note =\n summary.note +\n (cleared.length > 0\n ? `; ${cleared.length} route(s) re-verified reachable by plain fetch after a browser-side challenge/flake`\n : \"\") +\n (excused.length > 0\n ? `; ${excused.length} engine/device check(s) excused (challenge on a re-verified-reachable route)`\n : \"\") +\n (clearedLinks.length > 0\n ? `; ${clearedLinks.length} link(s) re-verified ok by plain fetch after cooldown`\n : \"\") +\n (canonical.redirectedFrom\n ? `; base url redirects to its canonical host — probed ${canonical.url}`\n : \"\") +\n (canonical.offsiteLocation\n ? `; base url redirects OFF-SITE to ${canonical.offsiteLocation} — not followed; update the site's url`\n : \"\");\n const status: AuditResult[\"status\"] =\n summary.desktopOk && summary.mobileOk && summary.linksOk && summary.templateOk\n ? \"pass\"\n : \"warn\";\n return {\n audit: \"browser\",\n site: label,\n status,\n summary: note,\n details: {\n ...summary,\n note,\n reverifiedUrls,\n excusedEngineChecks: excused,\n linkReverifiedUrls,\n // Canonicalization evidence (spread-conditional — exactOptionalPropertyTypes idiom):\n // which base was actually probed, and any off-site redirect the operator must act on.\n ...(canonical.redirectedFrom\n ? { canonicalBaseUrl: canonical.url, canonicalizedFrom: canonical.redirectedFrom }\n : {}),\n ...(canonical.offsiteLocation ? { offsiteRedirect: canonical.offsiteLocation } : {}),\n checkedAt: now.toISOString(),\n },\n };\n } finally {\n await runner.close?.();\n }\n}\n\n/** Bound the plain fetch()es (route-discovery GET + link HEAD/GET) so a host that hangs\n * WITHOUT erroring can't stall the sequential fleet audit indefinitely. Playwright's\n * page.goto already has PAGE_TIMEOUT_MS; these fetches had no ceiling. On abort the existing\n * catch path treats it exactly like a network error (→ null), so a slow host degrades, never throws. */\nconst FETCH_TIMEOUT_MS = 10_000;\n\n/** Real route-discovery fetch: GET text, null on any non-2xx / error / timeout. */\nexport function defaultDiscoverDeps(): DiscoverDeps {\n return {\n fetchText: async (url) => {\n try {\n const res = await fetch(url, {\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n if (!res.ok) return null;\n return await res.text();\n } catch {\n return null;\n }\n },\n };\n}\n\n/** Real re-verification fetch: one plain GET (no browser — precisely the point: WAFs that\n * challenge headless browsers leave a plain fetch alone). Parses title/meta out of the served\n * HTML when the response is ok + HTML; any error/timeout degrades to a null status (= the fail\n * is CONFIRMED without a browser to blame). */\nexport function defaultVerifyDeps(): VerifyDeps {\n return {\n fetchPage: async (url) => {\n try {\n const res = await fetch(url, {\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n const isHtml = (res.headers.get(\"content-type\") ?? \"\").includes(\"html\");\n if (!res.ok || !isHtml) {\n return { status: res.status, title: null, metaDescription: null };\n }\n return { status: res.status, ...extractTitleMeta(await res.text()) };\n } catch {\n return { status: null, title: null, metaDescription: null };\n }\n },\n // Base-URL preflight for canonicalizeBaseUrl: one no-follow GET so the redirect target is\n // observable (redirect:\"follow\" would hide it). Errors degrade to null = \"no redirect seen\".\n fetchRedirect: async (url) => {\n try {\n const res = await fetch(url, {\n redirect: \"manual\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n return { status: res.status, location: res.headers.get(\"location\") };\n } catch {\n return { status: null, location: null };\n }\n },\n };\n}\n\nconst DESKTOP_VIEWPORT = { width: 1366, height: 900 };\nconst PAGE_TIMEOUT_MS = 30_000;\n\n/**\n * Real Playwright runner. Launches chromium/firefox/webkit (desktop) + mobile-emulated chromium &\n * webkit, probes each route, and HEAD/GET-checks internal links. Every failure degrades to a\n * `false`/`null` (never throws past the audit), so a flaky run yields a non-pass (→ the box stays\n * manual) rather than a false green. Imported lazily so unit tests (which inject a fake runner)\n * never load Playwright.\n */\nexport async function defaultBrowserRunner(): Promise<BrowserRunner> {\n const { chromium, firefox, webkit, devices } = await import(\"@playwright/test\");\n const desktopEngines = [\n { engine: \"chromium\", type: chromium },\n { engine: \"firefox\", type: firefox },\n { engine: \"webkit\", type: webkit },\n ];\n const mobileTargets = [\n { device: \"Pixel 7\", descriptor: devices[\"Pixel 7\"] },\n { device: \"iPhone 14\", descriptor: devices[\"iPhone 14\"] },\n ];\n\n return {\n async probe(urls) {\n const results: RouteResult[] = [];\n const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));\n const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));\n try {\n for (const url of urls) {\n const desktop: RouteResult[\"desktop\"] = [];\n const linkSet = new Set<string>();\n let status: number | null = null;\n let title: string | null = null;\n let metaDescription: string | null = null;\n let leakedTemplateTokens: string[] = [];\n for (let i = 0; i < desktopEngines.length; i++) {\n const engine = desktopEngines[i]!.engine;\n const browser = browsers[i]!;\n const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });\n const page = await ctx.newPage();\n const errors: string[] = [];\n page.on(\"pageerror\", (e) => errors.push(String(e)));\n let ok = false;\n let navStatus: number | null = null;\n try {\n const resp = await page.goto(url, {\n waitUntil: \"domcontentloaded\",\n timeout: PAGE_TIMEOUT_MS,\n });\n navStatus = resp ? resp.status() : null;\n const hasMain = await page\n .locator(\"main, [role=main]\")\n .first()\n .isVisible()\n .catch(() => false);\n ok = !!resp && resp.ok() && errors.length === 0 && hasMain;\n if (engine === \"chromium\") {\n status = resp ? resp.status() : null;\n title = ((await page.title().catch(() => \"\")) as string).trim() || null;\n metaDescription =\n (\n ((await page\n .evaluate(\n \"document.querySelector('meta[name=\\\"description\\\"]')?.getAttribute('content') || ''\",\n )\n .catch(() => \"\")) as string) || \"\"\n ).trim() || null;\n // Link discovery runs on chromium only (cost). Links coverage therefore depends on\n // chromium loading this route + the evaluate succeeding; if it yields nothing, the\n // Links verdict is \"not proven\" (links.length===0 ⇒ linksOk false), never a pass.\n // String-form evaluate so the browser-context code isn't type-checked against the\n // Node lib (no DOM globals in this project's tsconfig). Returns absolute hrefs.\n const hrefs = (await page\n .evaluate(\"Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)\")\n .catch(() => [])) as string[];\n const origin = new URL(url).origin;\n for (const h of hrefs) {\n try {\n if (new URL(h).origin === origin) linkSet.add(new URL(h).href);\n } catch {\n /* skip */\n }\n }\n // Detect a broken app.html that shipped an unsubstituted SvelteKit placeholder\n // (e.g. a \"%sveltekit.body%\" named in a comment BEFORE the real one, so the naive\n // first-match template substitution fills the comment and leaves the real token\n // literal). A \"%sveltekit.*%\" never appears in a correctly-built page — one in the\n // shipped HTML is corruption the rendered checks can miss (the page may still show a\n // <main>). Read the RAW response body, not the parsed DOM, so a placeholder the\n // browser swallowed into a comment is still caught.\n const html = resp ? await resp.text().catch(() => \"\") : \"\";\n leakedTemplateTokens = [\n ...new Set(html.match(/%sveltekit\\.[a-zA-Z0-9_.]+%/g) ?? []),\n ];\n }\n } catch {\n ok = false;\n } finally {\n await ctx.close().catch(() => {});\n }\n desktop.push({ engine, ok, status: navStatus });\n }\n\n const mobile: RouteResult[\"mobile\"] = [];\n for (let i = 0; i < mobileTargets.length; i++) {\n const { device, descriptor } = mobileTargets[i]!;\n const browser = mobileBrowsers[i]!;\n const ctx = await browser.newContext({ ...descriptor });\n const page = await ctx.newPage();\n const errors: string[] = [];\n page.on(\"pageerror\", (e) => errors.push(String(e)));\n let ok = false;\n let navStatus: number | null = null;\n try {\n const resp = await page.goto(url, {\n waitUntil: \"domcontentloaded\",\n timeout: PAGE_TIMEOUT_MS,\n });\n navStatus = resp ? resp.status() : null;\n // Require a visible main landmark on mobile too (mirrors desktop) — otherwise a\n // fully-blank render passes mobile on status + no-overflow alone (exactly how a\n // broken app.html slipped past the mobile check while desktop caught it).\n const hasMain = await page\n .locator(\"main, [role=main]\")\n .first()\n .isVisible()\n .catch(() => false);\n const overflow = (await page\n .evaluate(\"document.documentElement.scrollWidth > window.innerWidth + 2\")\n .catch(() => true)) as boolean;\n ok = !!resp && resp.ok() && errors.length === 0 && !overflow && hasMain;\n } catch {\n ok = false;\n } finally {\n await ctx.close().catch(() => {});\n }\n mobile.push({ device, ok, status: navStatus });\n }\n\n results.push({\n url,\n desktop,\n mobile,\n links: [...linkSet],\n status,\n title,\n metaDescription,\n leakedTemplateTokens,\n });\n }\n } finally {\n await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {})));\n }\n return results;\n },\n\n async checkLinks(urls) {\n const out: LinkResult[] = [];\n for (const url of urls) {\n let status: number | null;\n try {\n let res = await fetch(url, {\n method: \"HEAD\",\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n // Some servers reject HEAD (405/501) — retry GET before declaring it broken.\n if (res.status === 405 || res.status === 501) {\n res = await fetch(url, {\n method: \"GET\",\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n }\n status = res.status;\n } catch {\n status = null;\n }\n out.push({ url, status });\n }\n return out;\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Bound the deploy read so a half-open TCP can't stall the sequential fleet sweep\n * (parity with the Dependabot fetch). A timeout aborts into the same catch that\n * maps every read failure to `{ ok: false }` — i.e. \"couldn't read, keep prior status\". */\nconst NETLIFY_FETCH_TIMEOUT_MS = 15_000;\n\n/** Result of probing a site's latest PRODUCTION Netlify deploy. Every field is\n * nullable: a site with no production deploy yet degrades to all-nulls (a real\n * \"none\" verdict). A read FAILURE is NOT represented here — it surfaces as\n * `{ ok: false }` on the enclosing `NetlifyDeployFetch` so it can't be mistaken\n * for a verdict. See `NetlifyDeployFetch`.\n * - `state` is Netlify's raw deploy state (`ready`/`error`/`building`/…), lower-cased,\n * or null when the site has no production deploy.\n * - `deployedAt` is the deploy's publish/create time (ISO), or null.\n * - `logUrl` links to the deploy (its live URL or the Netlify admin/build-log page).\n * - `errorMessage` is Netlify's `error_message` for a failed build, or null. */\nexport type NetlifyDeployCheck = {\n state: string | null;\n deployedAt: string | null;\n logUrl: string | null;\n errorMessage: string | null;\n};\n\n/** Outcome of probing Netlify, distinguishing \"couldn't read\" from \"read OK\".\n * This distinction is load-bearing: `Deploy status` drives the cockpit's Broken\n * band (an ALARM), so a transient read failure must NOT be allowed to clear a\n * real `error` — unlike a degraded \"no deploy yet\", which is a real verdict.\n * - `{ ok: false }` — the API was unreachable/unreadable (network error, non-2xx,\n * malformed body). The caller persists nothing, leaving the prior status intact.\n * - `{ ok: true, deploy }` — the API answered. `deploy` is the latest production\n * deploy, or all-nulls when the site genuinely has no production deploy yet\n * (a real verdict, safe to persist). */\nexport type NetlifyDeployFetch = { ok: false } | { ok: true; deploy: NetlifyDeployCheck };\n\n/** Injected IO so the check is unit-testable without hitting the Netlify API.\n * `fetchLatestProductionDeploy` returns a `NetlifyDeployFetch` — `{ ok: false }`\n * when the call couldn't be read, else `{ ok: true, deploy }` with the latest\n * production deploy (or all-nulls for a site with none). `now` is the clock for\n * the checked-at stamp. */\nexport type NetlifyDeployDeps = {\n fetchLatestProductionDeploy: (siteId: string) => Promise<NetlifyDeployFetch>;\n now: Date;\n};\n\n/** The Netlify deploy states we treat as a successful production build. */\nconst READY_STATES: ReadonlySet<string> = new Set([\"ready\"]);\n/** States that mean the build actually failed — the \"needs attention\" case. */\nconst FAILED_STATES: ReadonlySet<string> = new Set([\"error\", \"failed\", \"rejected\"]);\n/** States that mean a build is still in flight — neutral, not yet a verdict. */\nconst IN_PROGRESS_STATES: ReadonlySet<string> = new Set([\n \"building\",\n \"enqueued\",\n \"new\",\n \"processing\",\n \"uploading\",\n \"preparing\",\n]);\n\nexport function isReadyState(state: string | null): boolean {\n return state !== null && READY_STATES.has(state);\n}\nexport function isFailedState(state: string | null): boolean {\n return state !== null && FAILED_STATES.has(state);\n}\nexport function isInProgressState(state: string | null): boolean {\n return state !== null && IN_PROGRESS_STATES.has(state);\n}\n\n/**\n * Probe one Netlify site's latest production deploy. PURE given its deps — the\n * real network/parse work lives in `defaultNetlifyDeployDeps`. Propagates the\n * read/no-read distinction: a deps throw is treated as `{ ok: false }` (couldn't\n * read), so the caller preserves the prior status rather than clearing it.\n */\nexport async function checkNetlifyDeploy(\n siteId: string,\n deps: NetlifyDeployDeps,\n): Promise<NetlifyDeployFetch> {\n return deps\n .fetchLatestProductionDeploy(siteId)\n .catch(() => ({ ok: false }) as NetlifyDeployFetch);\n}\n\n/** Coerce an untrusted value to a trimmed non-empty string, or null. */\nfunction str(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Real Netlify API deps. Fetches the latest production deploy for `siteId` and\n * maps the raw JSON into a `NetlifyDeployFetch`. A read failure — network error,\n * non-2xx, malformed/unexpected body — resolves to `{ ok: false }` (the caller\n * preserves the prior status); an empty deploy list resolves to a real all-null\n * `{ ok: true }` verdict. NEVER throws: a fleet sweep must not red an unrelated\n * site because one site's Netlify call hiccuped.\n *\n * `fetchImpl` is injected (defaults to the global `fetch`) only so the default\n * deps themselves stay testable; production callers pass nothing.\n */\nexport function defaultNetlifyDeployDeps(\n token: string,\n now: Date,\n fetchImpl: typeof fetch = fetch,\n): NetlifyDeployDeps {\n return {\n now,\n fetchLatestProductionDeploy: async (siteId): Promise<NetlifyDeployFetch> => {\n // `production=true` + `per_page=1` asks Netlify for ONLY the newest\n // production deploy, so we read `[0]` without paging. siteId is the\n // Airtable-supplied site identity; encode it so it can't break the path.\n const url =\n `https://api.netlify.com/api/v1/sites/${encodeURIComponent(siteId)}/deploys` +\n `?per_page=1&production=true`;\n let res: Response;\n try {\n res = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: AbortSignal.timeout(NETLIFY_FETCH_TIMEOUT_MS),\n });\n } catch {\n return { ok: false }; // network error — couldn't read; preserve prior status\n }\n if (!res.ok) return { ok: false }; // non-2xx — couldn't read\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n return { ok: false }; // malformed JSON — couldn't read\n }\n if (!Array.isArray(body)) return { ok: false }; // unexpected shape — couldn't read\n if (body.length === 0) {\n // Read OK; the site genuinely has no production deploy yet. An all-null\n // verdict is real (not a read failure), so it's safe to persist.\n return {\n ok: true,\n deploy: { state: null, deployedAt: null, logUrl: null, errorMessage: null },\n };\n }\n const d = body[0] as Record<string, unknown>;\n const state = str(d[\"state\"]);\n // Prefer the publish time; fall back to create time for an in-progress deploy.\n const deployedAt = str(d[\"published_at\"]) ?? str(d[\"created_at\"]);\n // Prefer the live deploy URL, then the Netlify admin page, then a raw log link.\n const logUrl = str(d[\"deploy_ssl_url\"]) ?? str(d[\"admin_url\"]) ?? str(d[\"deploy_url\"]);\n const errorMessage = str(d[\"error_message\"]);\n return {\n ok: true,\n deploy: {\n state: state ? state.toLowerCase() : null,\n deployedAt,\n logUrl,\n errorMessage,\n },\n };\n },\n };\n}\n\n/**\n * Audit a site's latest PRODUCTION Netlify deploy health. Checkout-free — needs\n * only the site's Netlify id. Degrades gracefully fleet-wide:\n * - no `site.netlifyId` → skip (\"no netlify id\"); the site simply isn't on Netlify\n * (or hasn't been wired up) so there's nothing to check.\n * - no `NETLIFY_PAT` in the env → skip (\"no NETLIFY_PAT\"); the whole audit is\n * unconfigured, which must not red every site.\n *\n * Status mapping: `ready` → pass; a failed build (`error`/`failed`/`rejected`) →\n * fail (the \"needs attention\" status — it lands the site in the cockpit's attention\n * tier, mirroring a sub-floor Lighthouse score); an in-flight build or any\n * unknown/unreadable state → warn (neutral, no verdict yet). `details` carries\n * `{ state, deployedAt, logUrl, checkedAt }` for the Airtable writer + dashboard.\n */\nexport async function netlifyDeployAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.netlifyId) {\n return { audit: \"netlify-deploy\", site: label, status: \"skip\", summary: \"no netlify id\" };\n }\n const token = process.env.NETLIFY_PAT;\n if (!token) {\n return { audit: \"netlify-deploy\", site: label, status: \"skip\", summary: \"no NETLIFY_PAT\" };\n }\n\n const now = ctx.now ?? new Date();\n const deps = ctx.netlifyDeployDeps ?? defaultNetlifyDeployDeps(token, now);\n const fetched = await checkNetlifyDeploy(site.netlifyId, deps);\n\n // Couldn't read Netlify (network error / non-2xx / malformed). Return WITHOUT\n // details so `hasNetlifyDeployResult` is false and the Airtable writer skips the\n // write — leaving the prior `Deploy status` intact. A `Deploy status` of `error`\n // drives the cockpit's Broken band, and a transient API hiccup during the nightly\n // sweep must NEVER silently clear that alarm to \"all clear\" while prod is down.\n if (!fetched.ok) {\n return {\n audit: \"netlify-deploy\",\n site: label,\n status: \"warn\",\n summary: \"deploy status unavailable (Netlify API unreachable)\",\n };\n }\n const check = fetched.deploy;\n const checkedAt = now.toISOString();\n\n const status: AuditResult[\"status\"] = isReadyState(check.state)\n ? \"pass\"\n : isFailedState(check.state)\n ? \"fail\"\n : \"warn\";\n\n const summary =\n check.state === null\n ? \"no deploy found\"\n : isFailedState(check.state)\n ? `deploy ${check.state}${check.errorMessage ? ` — ${check.errorMessage}` : \"\"}`\n : `deploy ${check.state}`;\n\n return {\n audit: \"netlify-deploy\",\n site: label,\n status,\n summary,\n details: {\n state: check.state,\n deployedAt: check.deployedAt,\n logUrl: check.logUrl,\n checkedAt,\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** The parts of the deployed `/health` body this audit reads. `ok` is the endpoint's self-rollup\n * (`functionRan && prismic !== \"error\"`); `prismic` is the server-side CMS probe status; `forms`\n * are booleans recorded for the dashboard only. A public endpoint can return anything, so every\n * field is validated/coerced by `parseHealthBody`. */\nexport type HealthBody = {\n ok: boolean;\n prismic: \"ok\" | \"error\" | \"skipped\" | null;\n forms: { ingestUrl: boolean; ingestToken: boolean; turnstile: boolean } | null;\n};\n\n/** Outcome of fetching `{deployedUrl}/health`, distinguishing \"no usable report\" (`present:false`\n * → self-skip, the writer preserves the prior verdict) from \"read a body\" (`present:true`\n * → a real pass/fail). `present:true` with a real `HealthBody` is a 200 JSON response (`ok`\n * drives pass/fail); the fetch layer also synthesizes an all-`ok:false` body for \"deployed but\n * erroring\" responses (a non-404 non-2xx, or a 200 that isn't valid JSON) so those fail loudly\n * instead of silently skipping. Mirrors `netlify-deploy`'s `NetlifyDeployFetch` read/no-read split. */\nexport type HealthFetch = { present: false } | { present: true; body: HealthBody };\n\n/** Injected IO so the audit is unit-testable without a network. `fetchHealth` returns a\n * `HealthFetch`; `now` is the clock for the checked-at stamp. */\nexport type FunctionHealthDeps = {\n fetchHealth: (deployedUrl: string) => Promise<HealthFetch>;\n now: Date;\n};\n\nconst HEALTH_TIMEOUT_MS = 10_000;\n\n/** A synthetic \"deployed but erroring\" body for responses that carry no real health data — a\n * non-404 non-2xx status, or a 200 whose body isn't valid JSON. `ok:false` drives a `fail`\n * verdict; the null fields just mean \"nothing more to report\" (not a real CMS/forms read). */\nconst ERRORING_BODY: HealthBody = { ok: false, prismic: null, forms: null };\n\n/** Coerce an untrusted `/health` JSON payload into a `HealthFetch`. PURE. A non-object, or a body\n * without a boolean `ok`, is `{present:false}` (not a usable report — the endpoint hasn't\n * adopted the health-check contract, distinct from a deployed-but-erroring response). An\n * unrecognized `prismic` or a missing/!object `forms` degrades that field to null but keeps the\n * body present. */\nexport function parseHealthBody(raw: unknown): HealthFetch {\n if (!raw || typeof raw !== \"object\") return { present: false };\n const o = raw as Record<string, unknown>;\n if (typeof o[\"ok\"] !== \"boolean\") return { present: false };\n const prismic =\n o[\"prismic\"] === \"ok\" || o[\"prismic\"] === \"error\" || o[\"prismic\"] === \"skipped\"\n ? (o[\"prismic\"] as \"ok\" | \"error\" | \"skipped\")\n : null;\n const f = o[\"forms\"];\n const forms =\n f && typeof f === \"object\"\n ? {\n ingestUrl: (f as Record<string, unknown>)[\"ingestUrl\"] === true,\n ingestToken: (f as Record<string, unknown>)[\"ingestToken\"] === true,\n turnstile: (f as Record<string, unknown>)[\"turnstile\"] === true,\n }\n : null;\n return { present: true, body: { ok: o[\"ok\"], prismic, forms } };\n}\n\n/** Real fetch deps. GETs `{deployedUrl}/health` with a 10s abort timeout. Maps the response\n * precisely so a _deployed-but-erroring_ function fails loudly while a _not-yet-adopted_\n * `/health` stays a self-skip (spec R2.1):\n * - unparseable deployed URL, network error/timeout, or HTTP 404 → `{present:false}` (not\n * adopted / unreachable — self-skip, writer preserves the prior verdict).\n * - any OTHER non-2xx (5xx, 403, …), or a 200 whose body isn't valid JSON → `{present:true,\n * body:ERRORING_BODY}` (deployed but erroring — a real `fail`).\n * - a 200 JSON body → `parseHealthBody` (usually a real pass/fail; an unrecognized shape still\n * degrades to `{present:false}`, i.e. not-yet-adopted).\n * NEVER throws, so one site's dead endpoint can't red an unrelated site in the fleet sweep.\n * `fetchImpl` is injected only so the default deps stay testable; production callers pass\n * nothing. */\nexport function defaultFunctionHealthDeps(\n now: Date,\n fetchImpl: typeof fetch = fetch,\n): FunctionHealthDeps {\n return {\n now,\n fetchHealth: async (deployedUrl): Promise<HealthFetch> => {\n let url: string;\n try {\n // Root `/health` — an absolute path replaces any path on the deployed URL.\n url = new URL(\"/health\", deployedUrl).toString();\n } catch {\n return { present: false }; // unparseable deployed URL — nothing to probe\n }\n let res: Response;\n try {\n res = await fetchImpl(url, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });\n } catch {\n return { present: false }; // network error / timeout — unreachable, self-skip\n }\n if (res.status === 404) return { present: false }; // not adopted yet — self-skip\n if (!res.ok) return { present: true, body: ERRORING_BODY }; // deployed but erroring — fail\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n return { present: true, body: ERRORING_BODY }; // 200 non-JSON — deployed but erroring, fail\n }\n return parseHealthBody(body);\n },\n };\n}\n\n/**\n * Audit a site's deployed `/health` function. Checkout-free — needs only `site.deployedUrl`. Skips\n * a site with no deployed URL. Status mapping (spec Phase 2, R2.1):\n * - no usable report (unreachable, timeout, or `/health` 404 — not yet adopted) → `skip` WITHOUT\n * details → the Airtable writer preserves the prior verdict (a site that hasn't adopted\n * `/health`, or a transient outage, stays \"never ran\"; Plan 4 maps that to unknown/amber, which\n * blocks).\n * - deployed but erroring — any other non-2xx, a 200 non-JSON body, or a 200 JSON body with\n * `ok:false` → `fail` (records details so the fail persists).\n * - a 200 JSON body with `ok:true` → `pass` (records details).\n * `details` carries `{ ok, prismic, forms, checkedAt }`; the Airtable layer derives the\n * `Function health` + `CMS Reachable` verdicts (CMS from `prismic === \"ok\"`) + the checked-at stamp.\n * It must NEVER write `Deploy status` — that stays the Netlify build state.\n */\nexport async function functionHealthAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"function-health\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n const now = ctx.now ?? new Date();\n const deps = ctx.functionHealthDeps ?? defaultFunctionHealthDeps(now);\n const fetched: HealthFetch = await deps\n .fetchHealth(site.deployedUrl)\n .catch(() => ({ present: false }) as HealthFetch);\n\n if (!fetched.present) {\n return {\n audit: \"function-health\",\n site: label,\n status: \"skip\",\n summary: \"health endpoint unreachable / not adopted\",\n };\n }\n const checkedAt = now.toISOString();\n const status: AuditResult[\"status\"] = fetched.body.ok ? \"pass\" : \"fail\";\n const summary = `health ${fetched.body.ok ? \"ok\" : \"not ok\"} (prismic ${fetched.body.prismic ?? \"?\"})`;\n return {\n audit: \"function-health\",\n site: label,\n status,\n summary,\n details: {\n ok: fetched.body.ok,\n prismic: fetched.body.prismic,\n forms: fetched.body.forms,\n checkedAt,\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { INGEST_TIMEOUT_MS } from \"../forms/client.js\";\n\n/** Cloudflare's PUBLIC test sitekey — always issues a passing client token with no\n * real challenge, so the probe can satisfy a site's Turnstile widget without any\n * secret. The central `testMode` ingest branch (src/forms/ingest.ts, health-gate\n * plan3 Task 5) skips Turnstile enforcement for testMode submissions, making the\n * token's validity moot — it exists to get past the CLIENT widget only. */\nexport const CF_TEST_SITEKEY = \"1x00000000000000000000AA\";\n\n/** Routes probed, in order, for a submittable contact form. `/contact` is the\n * canonical reddoor-starter route and stays first so the common case still costs a\n * single navigation. `/` catches one-page sites whose only form lives on the\n * homepage (1836dig) — before this, `/contact` 404'd there and the site was\n * silently recorded as \"no contact form\", so its only conversion path went\n * unmonitored while the cockpit looked clean. */\nexport const CONTACT_PATHS: readonly string[] = [\"/contact\", \"/\"];\n\n/** Persisted form-e2e verdict. `ok` is the single-select value: \"pass\"/\"fail\" when a\n * form was found + submitted; null when NO contact form exists (n/a — paired with a\n * fresh checkedAt so the writer stores \"checked, no form\" distinctly from \"never ran\"). */\nexport type FormE2eDetails = {\n ok: \"pass\" | \"fail\" | null;\n formPresent: boolean;\n checkedAt: string;\n};\n\n/** Outcome of driving one site's contact form. `formPresent:false` ⇒ n/a\n * (persisted). `testModeUndeclared` ⇒ the site's /health does not declare\n * `forms.testMode`, so the probe refused to submit — a plain skip, prior\n * verdict preserved. `elapsedMs` times the submit itself (click → success\n * banner) and feeds the budget-headroom check; absent means \"not measured\",\n * which never manufactures a verdict. */\nexport type FormSubmitOutcome =\n | { formPresent: false }\n | {\n formPresent: true;\n success: boolean;\n detail?: string;\n /** Click → success banner VISIBLE. A user-experience number: it includes the\n * browser's own render of the banner, which no server budget governs. */\n elapsedMs?: number;\n /** Click → the action's POST response arrives. This is the span the site's\n * `INGEST_TIMEOUT_MS` abort budget actually governs (it wraps the\n * site→central fetch, which is nested inside this POST), so it — not\n * `elapsedMs` — is what the budget check must compare against. Undefined\n * when no POST was observed. */\n postElapsedMs?: number;\n }\n | { testModeUndeclared: true };\n\n/**\n * Work a REAL submission does that a `testMode` probe never reaches. The marker\n * short-circuits in `ingestSubmission` right after site resolution, BEFORE the\n * spam classifier, the repeat-sender/duplicate scans and the row insert — so a\n * probe's elapsed time is a LOWER BOUND on what a visitor's submission costs,\n * and a green probe says nothing about the rest.\n *\n * That gap is exactly how 1836dig read `Form E2E OK: pass` at 13:24 on\n * 2026-08-03 while real submissions at 18:23 were being reported to the visitor\n * as failures: the probe never paid the sink work that pushed the real call\n * past the site's abort budget.\n *\n * Sized from the live fleet on 2026-08-03: scans ~0.4s + insert ~0.3s. Notify\n * (~0.8s) and the stamp (~0.3s) are NOT counted — `ingestSubmission` now hands\n * that tail to `deps.defer` (the handler's `context.waitUntil`), so it lands\n * after the response and costs the visitor nothing. Should the deferral ever be\n * removed, this constant has to grow back with it, or the projection\n * under-reports.\n *\n * Deliberately an ESTIMATE rather than a measurement: making testMode do the\n * real work would either persist bot-triggerable rows or send real email, which\n * is the whole reason the short-circuit sits where it does.\n */\nexport const TESTMODE_SKIPPED_WORK_MS = 1_000;\n\n/** Fraction of the site's abort budget a projected real submission may consume\n * before the probe warns. Half leaves a 2x margin for cold starts and provider\n * jitter — this is a leading indicator, so it must fire while submissions still\n * succeed, not once visitors are already seeing errors. */\nexport const BUDGET_WARN_RATIO = 0.5;\n\n/** What a real submission would have cost, given a testMode probe's elapsed time. PURE. */\nexport function projectRealSubmissionMs(probeElapsedMs: number): number {\n return probeElapsedMs + TESTMODE_SKIPPED_WORK_MS;\n}\n\n/** Whether a probe of this duration leaves too little of the client abort budget\n * (`INGEST_TIMEOUT_MS`, the site-side budget in forms/client.ts) for the real\n * submission it stands in for. PURE — `budgetMs` is injectable for tests. */\nexport function isIngestBudgetThin(\n probeElapsedMs: number,\n budgetMs: number = INGEST_TIMEOUT_MS,\n): boolean {\n return projectRealSubmissionMs(probeElapsedMs) > budgetMs * BUDGET_WARN_RATIO;\n}\n\n/** Operator-facing budget line. `BUDGET_THIN` is a stable grep token — the\n * nightly fleet-form-e2e workflow raises it as a GitHub warning. */\nfunction budgetThinSummary(probeElapsedMs: number): string {\n const s = (ms: number) => `${(ms / 1000).toFixed(1)}s`;\n return (\n `BUDGET_THIN: probe ${s(probeElapsedMs)} + ~${s(TESTMODE_SKIPPED_WORK_MS)} of sink work ` +\n `the probe skips ≈ ${s(projectRealSubmissionMs(probeElapsedMs))} projected against a ` +\n `${s(INGEST_TIMEOUT_MS)} abort budget — a real submission may be reported to the ` +\n `visitor as failed while central still captures the lead`\n );\n}\n\n/** Injected browser IO. The real impl drives Playwright; tests pass a fake. */\nexport type FormRunner = {\n submit: (opts: {\n baseUrl: string;\n testMode: boolean;\n testSitekey: string;\n }) => Promise<FormSubmitOutcome>;\n close?: () => Promise<void>;\n};\n\n/** Opt-in gate for the live Playwright fallback. `defaultFormRunner` drives a REAL\n * browser against `site.deployedUrl` and submits the REAL production contact form.\n * Both central prerequisites HAVE landed: ingest's `testMode` short-circuit\n * (health-gate plan3 Task 5, `src/forms/ingest.ts`) and the starter's\n * `buildPayload` forwarding (Task 6, `reddoor-starter/src/routes/contact/\n * +page.server.ts`). But forwarding is PER-SITE — a site built before Task 6\n * ignores the injected marker and would deliver the probe as a REAL lead. Two\n * layers keep that impossible: (1) this env gate keeps the runner inert in any\n * unfiltered `runAudits` call (e.g. the `init` recipe's final step) unless the\n * operator/producer explicitly arms it (`REDDOOR_FORM_E2E_LIVE=1` — the nightly\n * fleet-form-e2e workflow does); (2) even armed, the runner preflights the\n * site's /health and refuses to submit unless it DECLARES `forms.testMode`\n * (see `declaresTestModeForwarding`) — the declaration ships in the same deploy\n * as the forwarding, so it is always truthful. */\nconst LIVE_ENV_VAR = \"REDDOOR_FORM_E2E_LIVE\";\n\nfunction liveRunnerEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return env[LIVE_ENV_VAR] === \"1\" || env[LIVE_ENV_VAR] === \"true\";\n}\n\n/**\n * Submit the REAL production contact form against `site.deployedUrl` in test-mode\n * and reduce the outcome to a verdict. Checkout-free (drives the deployed URL, like\n * browser.ts). The submission carries a `testMode` marker that central ingest is\n * INTENDED to recognize and route away from every real sink (no inbox/DB/webhook,\n * Turnstile enforcement bypassed) once that branch exists — see `liveRunnerEnabled`\n * above for why the live Playwright fallback stays gated off until then. Tests that\n * inject `ctx.formRunner` bypass the gate entirely (it only guards the real,\n * dynamically-imported Playwright runner).\n *\n * - no deployedUrl → skip, NO details → writer preserves the prior verdict.\n * - no injected runner + live gate off → skip, NO details (not yet safe to run live).\n * - no contact form → skip WITH details (ok:null + fresh checkedAt) → persisted as n/a.\n * - form submitted, success → pass (ok:\"pass\"); not success → warn (ok:\"fail\").\n */\nexport async function formE2eAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"form-e2e\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n if (!ctx.formRunner && !liveRunnerEnabled()) {\n return {\n audit: \"form-e2e\",\n site: label,\n status: \"skip\",\n summary:\n \"live form-e2e disabled (central testMode ingest suppression not yet wired — \" +\n `set ${LIVE_ENV_VAR}=1 once it is)`,\n };\n }\n const now = ctx.now ?? new Date();\n const checkedAt = now.toISOString();\n const runner = ctx.formRunner ?? (await defaultFormRunner());\n try {\n const outcome = await runner.submit({\n baseUrl: site.deployedUrl,\n testMode: true,\n testSitekey: CF_TEST_SITEKEY,\n });\n if (\"testModeUndeclared\" in outcome) {\n // Not n/a — the site may well have a form; it just hasn't rolled out\n // testMode forwarding, so probing it would submit a real lead. Plain\n // skip with NO details preserves the prior verdict.\n return {\n audit: \"form-e2e\",\n site: label,\n status: \"skip\",\n summary:\n \"site /health does not declare forms.testMode — probe refused \" +\n \"(testMode forwarding not yet rolled out here)\",\n };\n }\n if (!outcome.formPresent) {\n return {\n audit: \"form-e2e\",\n site: label,\n status: \"skip\",\n summary: \"no contact form (n/a)\",\n details: { ok: null, formPresent: false, checkedAt } satisfies FormE2eDetails,\n };\n }\n const ok: \"pass\" | \"fail\" = outcome.success ? \"pass\" : \"fail\";\n const details = { ok, formPresent: true, checkedAt } satisfies FormE2eDetails;\n if (!outcome.success) {\n return {\n audit: \"form-e2e\",\n site: label,\n status: \"warn\",\n summary: `form-e2e: synthetic submission failed${outcome.detail ? ` — ${outcome.detail}` : \"\"}`,\n details,\n };\n }\n // The submission worked, but a probe that already eats the abort budget means\n // real submissions — which pay the sink work this one skipped — are close to\n // being reported as failures. Warn on the RUN while leaving the persisted\n // verdict at \"pass\": the form does work, and flipping the cockpit to \"fail\"\n // would report a working form as broken.\n // Measured against the POST span, NOT click→banner. `INGEST_TIMEOUT_MS` aborts\n // the site→central fetch and nothing else, so comparing it to a window that\n // also contains Turnstile's token round-trip and the browser's render of the\n // success banner over-warns: the claim \"may be reported to the visitor as\n // failed\" is only true if the FETCH overruns. On 2026-08-17 vineyard-custom-homes\n // warned at 16.9s click→banner while its own function answered in 0.25s warm\n // / 2.0s cold — the warning was reporting page-render time as abort risk.\n // No POST observed → no claim to make.\n const thin =\n typeof outcome.postElapsedMs === \"number\" && isIngestBudgetThin(outcome.postElapsedMs)\n ? budgetThinSummary(outcome.postElapsedMs)\n : null;\n return {\n audit: \"form-e2e\",\n site: label,\n status: thin ? \"warn\" : \"pass\",\n summary: thin\n ? `form-e2e: synthetic submission succeeded — ${thin}`\n : \"form-e2e: synthetic submission succeeded\",\n details,\n };\n } finally {\n await runner.close?.();\n }\n}\n\n/** Minimum plausible fill time the site's bot-timing screen enforces (client.ts\n * MIN_FILL_MS = 800). A too-fast submit is silently dropped (success shown, ingest\n * never reached), so the probe waits past this before submitting. */\nconst FILL_SETTLE_MS = 1200;\nconst PAGE_TIMEOUT_MS = 30_000;\nconst HEALTH_TIMEOUT_MS = 10_000;\n\n/** The slice of a Playwright page the route probe needs. Declared structurally so\n * the discovery loop is unit-testable without launching a browser; the real\n * `Page` satisfies it. */\nexport type FormProbePage = {\n goto: (\n url: string,\n opts?: { waitUntil?: string; timeout?: number },\n ) => Promise<{ ok: () => boolean } | null>;\n locator: (selector: string) => { count: () => Promise<number> };\n};\n\n/**\n * Navigate `paths` in order and return the URL of the first that renders a contact\n * form — a `<form>` carrying an email field. `null` means no candidate route has\n * one, which the caller reports as n/a (never a failure).\n *\n * A form without an email input does not count: several sites put a search or\n * newsletter `<form>` on the homepage, and submitting one as a \"contact form\"\n * would report a false pass. Navigation errors are misses, not aborts, so one dead\n * route cannot mask a form on the next.\n */\nexport async function findFormPath(\n page: FormProbePage,\n baseUrl: string,\n paths: readonly string[] = CONTACT_PATHS,\n): Promise<string | null> {\n for (const path of paths) {\n const url = new URL(path, baseUrl).toString();\n const resp = await page\n .goto(url, { waitUntil: \"domcontentloaded\", timeout: PAGE_TIMEOUT_MS })\n .catch(() => null);\n if (!resp || !resp.ok()) continue;\n const forms = await page\n .locator(\"form\")\n .count()\n .catch(() => 0);\n if (forms === 0) continue;\n const emails = await page\n .locator('input[name=\"email\"], input[type=\"email\"]')\n .count()\n .catch(() => 0);\n if (emails === 0) continue;\n return url;\n }\n return null;\n}\n\n/**\n * GET `{baseUrl}/health` and report whether the site DECLARES that its contact\n * form forwards the `testMode` marker (`forms.testMode === true`, strict\n * boolean). The starter sets the flag in the same deploy whose `buildPayload`\n * forwards the marker, so a declaration is proof the injected field round-trips\n * to central ingest's short-circuit instead of landing as a real lead.\n * Fail-closed: unreachable /health, non-2xx, unparseable body, or a missing/\n * non-boolean flag all return false — the probe then refuses to submit.\n */\nexport async function declaresTestModeForwarding(baseUrl: string): Promise<boolean> {\n try {\n const res = await fetch(new URL(\"/health\", baseUrl), {\n signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),\n });\n if (!res.ok) return false;\n const body: unknown = await res.json();\n if (typeof body !== \"object\" || body === null) return false;\n const forms = (body as Record<string, unknown>).forms;\n if (typeof forms !== \"object\" || forms === null) return false;\n return (forms as Record<string, unknown>).testMode === true;\n } catch {\n return false;\n }\n}\n\n/**\n * Real Playwright form runner. Lazily imports @playwright/test so unit tests (which\n * inject a fake runner) never load it — and so the audit's static import graph stays\n * central-dep-free for `test:dist`. Every failure degrades to `success:false` (never\n * throws past the audit), so a flaky run yields a non-pass (box stays manual), not a\n * false green.\n */\nexport async function defaultFormRunner(): Promise<FormRunner> {\n const { chromium } = await import(\"@playwright/test\");\n return {\n async submit({ baseUrl, testSitekey }) {\n // Per-site safety preflight — refuse before a browser even launches.\n if (!(await declaresTestModeForwarding(baseUrl))) {\n return { testModeUndeclared: true };\n }\n const browser = await chromium.launch();\n try {\n const ctx = await browser.newContext();\n const page = await ctx.newPage();\n // Probe each candidate route; none carrying a form ⇒ no contact form (n/a).\n // findFormPath leaves the page on the route it selected, so the fills below\n // act on the form it found.\n const found = await findFormPath(page as unknown as FormProbePage, baseUrl);\n if (!found) return { formPresent: false };\n\n await page.fill('[name=\"name\"]', \"Reddoor Monitor\").catch(() => {});\n await page.fill('[name=\"email\"]', \"monitor+e2e@reddoorla.com\").catch(() => {});\n await page.fill('[name=\"phone\"]', \"5555550123\").catch(() => {});\n await page\n .fill('[name=\"message\"]', \"Synthetic end-to-end health check — please ignore.\")\n .catch(() => {});\n\n // Inject the testMode marker + a Turnstile token into the submitted form.\n // The site declared forwarding (preflight above), so the marker routes the\n // submission away from every real sink via central ingest's testMode\n // short-circuit; that branch also skips Turnstile enforcement, making the\n // token's value inconsequential — the CF public test sitekey documents the\n // zero-secret path past the client widget. String-form evaluate\n // (mirrors browser.ts) so the browser-context code isn't type-checked\n // against the Node lib (no DOM globals in this project's tsconfig). The\n // token value is a hardcoded constant (never user input), so inlining it\n // via JSON.stringify into the expression string is safe.\n await page.evaluate(`\n (function () {\n const f = document.querySelector(\"form\");\n if (!f) return;\n const add = (name, value) => {\n let el = f.querySelector('input[name=\"' + name + '\"]');\n if (!el) {\n el = document.createElement(\"input\");\n el.type = \"hidden\";\n el.name = name;\n f.appendChild(el);\n }\n el.value = value;\n };\n add(\"testMode\", \"true\");\n add(\"cf-turnstile-response\", ${JSON.stringify(`testmode-${testSitekey}`)});\n })();\n `);\n\n // Beat the bot-timing screen, then submit and wait for the success banner\n // (role=\"status\") the starter renders on a successful action.\n await page.waitForTimeout(FILL_SETTLE_MS);\n // Capture the action POST so a failure names the real server response\n // (espada 2026-07-10: three \"no success banner\" warns were undiagnosable\n // without it — the POST status/alert text is the evidence).\n // Stamped as a side-effect rather than awaited here on purpose: awaiting\n // the POST before the banner would serialize two 30s timeouts in the\n // no-POST case and double the worst-case run. `startedAt` is assigned at\n // the click below — but the predicate matches ANY POST, and a page is free\n // to fire one before the click (an analytics beacon, a consent ping). If\n // that happens `startedAt` is still 0 and `Date.now() - 0` is the epoch —\n // an absurd \"elapsed\" that would trip BUDGET_THIN, the exact false warn\n // this measurement exists to kill. Un-clicked → leave it undefined; the\n // audit treats absent timing as \"no claim to make\".\n let startedAt = 0;\n let postElapsedMs: number | undefined;\n const postResponse = page\n .waitForResponse((r) => r.request().method() === \"POST\", {\n timeout: PAGE_TIMEOUT_MS,\n })\n .then((r) => {\n if (startedAt > 0) postElapsedMs = Date.now() - startedAt;\n return r;\n })\n .catch(() => null);\n // Both standard submit controls: reddoor-website uses `<input type=\"submit\">`\n // (its first enrolled run timed out matching button-only and false-failed).\n // Timed from the click so `elapsedMs` measures what a visitor waits for —\n // the site action plus its central ingest call — and not the page load,\n // the fills, or the deliberate FILL_SETTLE_MS pause.\n startedAt = Date.now();\n await page\n .locator('button[type=\"submit\"], input[type=\"submit\"]')\n .first()\n .click({ timeout: PAGE_TIMEOUT_MS });\n const ok = await page\n .locator('[role=\"status\"]')\n .first()\n .waitFor({ state: \"visible\", timeout: PAGE_TIMEOUT_MS })\n .then(() => true)\n .catch(() => false);\n const elapsedMs = Date.now() - startedAt;\n if (ok)\n return {\n formPresent: true,\n success: true,\n elapsedMs,\n ...(postElapsedMs !== undefined ? { postElapsedMs } : {}),\n };\n const actionResp = await postResponse;\n const alertText = await page\n .locator('[role=\"alert\"]')\n .first()\n .textContent({ timeout: 1000 })\n .catch(() => null);\n const respBit = actionResp\n ? `POST ${actionResp.status()}${actionResp.status() >= 400 ? ` ${(await actionResp.text().catch(() => \"\")).slice(0, 80)}` : \"\"}`\n : \"no POST observed\";\n return {\n formPresent: true,\n success: false,\n detail: `no success banner after submit — ${respBit}${alertText ? `; alert: ${alertText.trim().slice(0, 80)}` : \"\"}`,\n };\n } catch (err) {\n return { formPresent: true, success: false, detail: String(err).slice(0, 120) };\n } finally {\n await browser.close().catch(() => {});\n }\n },\n };\n}\n","import type { AuditName, AuditResult, Site } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn } from \"./util/spawn.js\";\nimport { depsAudit } from \"./deps.js\";\nimport { lintAudit } from \"./lint.js\";\nimport { securityAudit } from \"./security.js\";\nimport { lighthouseAudit } from \"./lighthouse.js\";\nimport { a11yAudit } from \"./a11y.js\";\nimport { domainAudit } from \"./domain.js\";\nimport { browserAudit } from \"./browser.js\";\nimport { netlifyDeployAudit } from \"./netlify-deploy.js\";\nimport { functionHealthAudit } from \"./function-health.js\";\nimport { smokeAudit } from \"./smoke.js\";\nimport { formE2eAudit } from \"./form-e2e.js\";\n\nconst REGISTRY: Record<AuditName, (ctx: AuditContext) => Promise<AuditResult>> = {\n deps: depsAudit,\n lint: lintAudit,\n security: securityAudit,\n lighthouse: lighthouseAudit,\n a11y: a11yAudit,\n domain: domainAudit,\n browser: browserAudit,\n \"netlify-deploy\": netlifyDeployAudit,\n \"function-health\": functionHealthAudit,\n smoke: smokeAudit,\n \"form-e2e\": formE2eAudit,\n};\n\nexport const ALL_AUDIT_NAMES = Object.keys(REGISTRY) as AuditName[];\n\n/** Default per-audit spawn timeout when running via runAudits (30 s). */\nconst DEFAULT_AUDIT_TIMEOUT_MS = 30_000;\n\nfunction timedSpawn(timeoutMs: number): SpawnFn {\n return (cmd, args, opts = {}) =>\n defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });\n}\n\n/** Single-audit runner with the same error-to-result conversion that\n * `runAudits` applies. Exposed so the CLI can wrap each audit in its\n * own progress task (listr2) and surface per-audit completion timing,\n * while keeping audit implementations UI-free. */\nexport async function runOneAudit(site: Site, name: AuditName): Promise<AuditResult> {\n if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);\n const spawn = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);\n // `||` not `??`: an empty-string slug (Airtable Name with no slug-able chars)\n // must fall back to the path, not render a blank `AuditResult.site` that would\n // then collapse fleet write-back grouping under the \"\" key.\n const label = site.name || site.path;\n try {\n return await REGISTRY[name]({ site, spawn });\n } catch (err) {\n return {\n audit: name,\n site: label,\n status: \"fail\",\n summary: `${name}: unexpected error — ${String(err)}`,\n };\n }\n}\n\nexport async function runAudits(site: Site, which?: AuditName[]): Promise<AuditResult[]> {\n const names = which ?? ALL_AUDIT_NAMES;\n for (const n of names) {\n if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);\n }\n return Promise.all(names.map((n) => runOneAudit(site, n)));\n}\n\nexport async function runAuditsAcross(sites: Site[], which?: AuditName[]): Promise<AuditResult[]> {\n const all = await Promise.all(sites.map((s) => runAudits(s, which)));\n return all.flat();\n}\n\nexport { depsAudit, lintAudit, securityAudit, lighthouseAudit, a11yAudit };\n"],"mappings":"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","names":["join","join","readFile","join","join","readFile","readFile","join","readFile","join","PAGE_TIMEOUT_MS","HEALTH_TIMEOUT_MS"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/html.ts"],"sourcesContent":["/**\n * Shared HTML/XML escape. One implementation behind the dashboard renderers\n * (`src/dashboard/render.ts`, `fleet-render.ts`), the daily digest\n * (`src/reports/digest.ts`), and the MJML email templates\n * (`src/reports/*-email/template.ts`).\n *\n * The set is the strict-XML set (`& < > \" '`), which is exactly what MJML's\n * `validationLevel: \"strict\"` parser needs and a superset of what plain HTML text\n * interpolation needs — so the SAME function serves both sinks. Site names\n * (e.g. \"Brown & Co\"), URLs, and operator commentary must not break the markup or\n * inject. The MJML templates re-export this as `escapeXml` for their callers.\n */\nexport function escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Embed a value inside a `<script>` body as a JavaScript literal.\n *\n * **`escapeHtml` is the wrong tool here and fails in a way that looks like it\n * worked.** A `<script>` element's content is raw text: the HTML parser does not\n * decode entities inside it, so `"` reaching the browser stays the six\n * characters `"` — corrupting the JavaScript rather than escaping it. The\n * house style on these pages is \"escapeHtml everything\", so the first person to\n * interpolate into a script body will reach for it by reflex. This exists so\n * there is a correct thing to reach for instead.\n *\n * `JSON.stringify` produces a valid JS literal (quotes included — do NOT wrap the\n * result in quotes yourself). The extra replacements cover the two sequences JSON\n * does not escape but the HTML tokenizer still acts on inside raw text:\n * - `</` would end the script element early, so `</script>` in a string closes\n * the tag and spills the rest of the value into the document as markup;\n * - `<!--` opens a comment-like state that can swallow the code after it.\n *\n * U+2028/U+2029 are escaped too: they are literal line terminators in JS source\n * and JSON.stringify leaves them raw.\n */\nexport function scriptLiteral(value: unknown): string {\n return JSON.stringify(value ?? null)\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\n/** Allow only http(s) URLs in an href context; everything else collapses to \"#\".\n *\n * Returns the PARSED, percent-encoded `u.href` — never the caller's raw string.\n * `new URL()` only validates the scheme; it does not sanitize the rest of the\n * string, so a well-formed https URL carrying a quote-breakout payload\n * (`https://x.example/\"><script>...`) parses fine and, if handed back\n * verbatim, produces a live injection wherever a caller interpolates the\n * result into an href without a further escapeHtml pass. Returning `u.href`\n * closes that gap: the URL parser itself percent-encodes `\"`, `<`, `>`, etc.\n * in the path/query. This also normalizes a bare-origin URL to carry a\n * trailing slash (`https://x.example` → `https://x.example/`) — a caller\n * that additionally wraps the result in escapeHtml (the majority pattern in\n * this repo) sees no behavior change beyond that encoding. */\nexport function safeUrl(raw: string): string {\n try {\n const u = new URL(raw);\n if (u.protocol === \"http:\" || u.protocol === \"https:\") return u.href;\n } catch {\n // fall through\n }\n return \"#\";\n}\n"],"mappings":";AAYO,SAAS,WAAW,GAAmB;AAC5C,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AA4CO,SAAS,QAAQ,KAAqB;AAC3C,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,QAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAAU,QAAO,EAAE;AAAA,EAClE,QAAQ;AAAA,EAER;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/alerts/digest-state.ts
|
|
2
|
+
function diffAttention(items, prior, today) {
|
|
3
|
+
const tagged = [];
|
|
4
|
+
const next = {};
|
|
5
|
+
for (const it of items) {
|
|
6
|
+
const was = prior[it.key];
|
|
7
|
+
const exhaustedFlip = it.autoFixExhausted === true && was?.exhausted !== true;
|
|
8
|
+
let status;
|
|
9
|
+
let firstFlaggedAt;
|
|
10
|
+
if (!was) {
|
|
11
|
+
status = "new";
|
|
12
|
+
firstFlaggedAt = today;
|
|
13
|
+
} else if (it.metric > was.metric || exhaustedFlip) {
|
|
14
|
+
status = "worse";
|
|
15
|
+
firstFlaggedAt = was.firstFlaggedAt;
|
|
16
|
+
} else {
|
|
17
|
+
status = "standing";
|
|
18
|
+
firstFlaggedAt = was.firstFlaggedAt;
|
|
19
|
+
}
|
|
20
|
+
tagged.push({ ...it, status });
|
|
21
|
+
next[it.key] = {
|
|
22
|
+
metric: it.metric,
|
|
23
|
+
firstFlaggedAt,
|
|
24
|
+
...it.autoFixExhausted === true ? { exhausted: true } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { tagged, next };
|
|
28
|
+
}
|
|
29
|
+
var DIGEST_STATE_TABLE = "Digest State";
|
|
30
|
+
async function readDigestState(base) {
|
|
31
|
+
const rows = [];
|
|
32
|
+
await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).eachPage((records, fetchNextPage) => {
|
|
33
|
+
for (const rec of records) rows.push({ id: rec.id, fields: rec.fields });
|
|
34
|
+
fetchNextPage();
|
|
35
|
+
});
|
|
36
|
+
const first = rows[0];
|
|
37
|
+
if (!first) return {};
|
|
38
|
+
const raw = first.fields["Snapshot"];
|
|
39
|
+
if (typeof raw !== "string") return {};
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function writeDigestState(base, snap, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).eachPage((records, fetchNextPage) => {
|
|
49
|
+
for (const rec of records) rows.push({ id: rec.id });
|
|
50
|
+
fetchNextPage();
|
|
51
|
+
});
|
|
52
|
+
const fields = {
|
|
53
|
+
Snapshot: JSON.stringify(snap),
|
|
54
|
+
"Updated At": updatedAt
|
|
55
|
+
};
|
|
56
|
+
const existing = rows[0];
|
|
57
|
+
if (existing) {
|
|
58
|
+
await base(DIGEST_STATE_TABLE).update([{ id: existing.id, fields }]);
|
|
59
|
+
} else {
|
|
60
|
+
await base(DIGEST_STATE_TABLE).create([{ fields }]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
diffAttention,
|
|
66
|
+
DIGEST_STATE_TABLE,
|
|
67
|
+
readDigestState,
|
|
68
|
+
writeDigestState
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=chunk-BRYXFH77.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/alerts/digest-state.ts"],"sourcesContent":["// src/alerts/digest-state.ts\nimport type { FieldSet } from \"airtable\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport type { AttentionItem } from \"./attention.js\";\n\n/**\n * The persisted prior-run snapshot: stable item `key` → its last metric + the\n * date it was FIRST flagged. Lives as JSON in the single \"Digest State\" Airtable\n * row (the IO that loads/stores it — readDigestState/writeDigestState — is added\n * in component 2). `next` from diffAttention is what gets written back.\n * `exhausted` is carried only for vuln keys whose Renovate auto-fix has been\n * exhausted (absent = not exhausted; older snapshots without the field read the\n * same way).\n */\nexport type DigestSnapshot = Record<\n string,\n { metric: number; firstFlaggedAt: string; exhausted?: boolean }\n>;\n\n/**\n * PURE diff — the testable core of the hybrid \"snapshot now, mark what's new\".\n * For each current item vs the prior snapshot:\n * - key absent from prior → NEW (firstFlaggedAt = today)\n * - present and metric > prior.metric → WORSE (keep the original firstFlaggedAt)\n * - present and autoFixExhausted flipped on → WORSE (the count didn't move, but\n * \"Renovate gave up\" is an escalation — without this the digest's first mention\n * of a vuln, which is gated on exhaustion, would arrive unbadged)\n * - otherwise (equal or dropped) → STANDING (keep the original firstFlaggedAt)\n * `next` contains EXACTLY the current items' keys: resolved keys drop out, so a\n * fixed-then-recurring problem re-news correctly. Neither input is mutated.\n */\nexport function diffAttention(\n items: AttentionItem[],\n prior: DigestSnapshot,\n today: string,\n): { tagged: AttentionItem[]; next: DigestSnapshot } {\n const tagged: AttentionItem[] = [];\n const next: DigestSnapshot = {};\n for (const it of items) {\n const was = prior[it.key];\n const exhaustedFlip = it.autoFixExhausted === true && was?.exhausted !== true;\n let status: AttentionItem[\"status\"];\n let firstFlaggedAt: string;\n if (!was) {\n status = \"new\";\n firstFlaggedAt = today;\n } else if (it.metric > was.metric || exhaustedFlip) {\n status = \"worse\";\n firstFlaggedAt = was.firstFlaggedAt;\n } else {\n status = \"standing\";\n firstFlaggedAt = was.firstFlaggedAt;\n }\n tagged.push({ ...it, status });\n next[it.key] = {\n metric: it.metric,\n firstFlaggedAt,\n ...(it.autoFixExhausted === true ? { exhausted: true } : {}),\n };\n }\n return { tagged, next };\n}\n\n/** The single-row Airtable table that persists the prior digest snapshot. */\nexport const DIGEST_STATE_TABLE = \"Digest State\";\n\n/**\n * Read the persisted prior snapshot from the \"Digest State\" singleton.\n *\n * Reads the FIRST row of an unfiltered select (the table holds exactly one row;\n * the test fake does not evaluate filterByFormula, so we never rely on one). A\n * read miss (no row) OR a parse error (malformed Snapshot JSON) collapses to `{}`\n * — every key then reads as NEW once, which is safe degradation (never crashes\n * the digest).\n */\nexport async function readDigestState(base: AirtableBase): Promise<DigestSnapshot> {\n const rows: { id: string; fields: Record<string, unknown> }[] = [];\n await base(DIGEST_STATE_TABLE)\n .select({ maxRecords: 1, pageSize: 1 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push({ id: rec.id, fields: rec.fields });\n fetchNextPage();\n });\n const first = rows[0];\n if (!first) return {};\n const raw = first.fields[\"Snapshot\"];\n if (typeof raw !== \"string\") return {};\n try {\n return JSON.parse(raw) as DigestSnapshot;\n } catch {\n return {};\n }\n}\n\n/**\n * Persist the next snapshot to the \"Digest State\" singleton: get-or-create the\n * one row. If a row exists, UPDATE it (keyed by its record id); otherwise CREATE\n * one. `Snapshot` = JSON.stringify(snap); `Updated At` = the injected ISO\n * timestamp (or now). A caller that catches+logs a write failure keeps the\n * already-sent digest unaffected (next run re-news at worst).\n */\nexport async function writeDigestState(\n base: AirtableBase,\n snap: DigestSnapshot,\n updatedAt: string = new Date().toISOString(),\n): Promise<void> {\n const rows: { id: string }[] = [];\n await base(DIGEST_STATE_TABLE)\n .select({ maxRecords: 1, pageSize: 1 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push({ id: rec.id });\n fetchNextPage();\n });\n const fields: FieldSet = {\n Snapshot: JSON.stringify(snap),\n \"Updated At\": updatedAt,\n };\n const existing = rows[0];\n if (existing) {\n await base(DIGEST_STATE_TABLE).update([{ id: existing.id, fields }]);\n } else {\n await base(DIGEST_STATE_TABLE).create([{ fields }]);\n }\n}\n"],"mappings":";AA+BO,SAAS,cACd,OACA,OACA,OACmD;AACnD,QAAM,SAA0B,CAAC;AACjC,QAAM,OAAuB,CAAC;AAC9B,aAAW,MAAM,OAAO;AACtB,UAAM,MAAM,MAAM,GAAG,GAAG;AACxB,UAAM,gBAAgB,GAAG,qBAAqB,QAAQ,KAAK,cAAc;AACzE,QAAI;AACJ,QAAI;AACJ,QAAI,CAAC,KAAK;AACR,eAAS;AACT,uBAAiB;AAAA,IACnB,WAAW,GAAG,SAAS,IAAI,UAAU,eAAe;AAClD,eAAS;AACT,uBAAiB,IAAI;AAAA,IACvB,OAAO;AACL,eAAS;AACT,uBAAiB,IAAI;AAAA,IACvB;AACA,WAAO,KAAK,EAAE,GAAG,IAAI,OAAO,CAAC;AAC7B,SAAK,GAAG,GAAG,IAAI;AAAA,MACb,QAAQ,GAAG;AAAA,MACX;AAAA,MACA,GAAI,GAAG,qBAAqB,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;AAAA,IAC5D;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,KAAK;AACxB;AAGO,IAAM,qBAAqB;AAWlC,eAAsB,gBAAgB,MAA6C;AACjF,QAAM,OAA0D,CAAC;AACjE,QAAM,KAAK,kBAAkB,EAC1B,OAAO,EAAE,YAAY,GAAG,UAAU,EAAE,CAAC,EACrC,SAAS,CAAC,SAAS,kBAAkB;AACpC,eAAW,OAAO,QAAS,MAAK,KAAK,EAAE,IAAI,IAAI,IAAI,QAAQ,IAAI,OAAO,CAAC;AACvE,kBAAc;AAAA,EAChB,CAAC;AACH,QAAM,QAAQ,KAAK,CAAC;AACpB,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAM,MAAM,OAAO,UAAU;AACnC,MAAI,OAAO,QAAQ,SAAU,QAAO,CAAC;AACrC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AASA,eAAsB,iBACpB,MACA,MACA,aAAoB,oBAAI,KAAK,GAAE,YAAY,GAC5B;AACf,QAAM,OAAyB,CAAC;AAChC,QAAM,KAAK,kBAAkB,EAC1B,OAAO,EAAE,YAAY,GAAG,UAAU,EAAE,CAAC,EACrC,SAAS,CAAC,SAAS,kBAAkB;AACpC,eAAW,OAAO,QAAS,MAAK,KAAK,EAAE,IAAI,IAAI,GAAG,CAAC;AACnD,kBAAc;AAAA,EAChB,CAAC;AACH,QAAM,SAAmB;AAAA,IACvB,UAAU,KAAK,UAAU,IAAI;AAAA,IAC7B,cAAc;AAAA,EAChB;AACA,QAAM,WAAW,KAAK,CAAC;AACvB,MAAI,UAAU;AACZ,UAAM,KAAK,kBAAkB,EAAE,OAAO,CAAC,EAAE,IAAI,SAAS,IAAI,OAAO,CAAC,CAAC;AAAA,EACrE,OAAO;AACL,UAAM,KAAK,kBAAkB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,EACpD;AACF;","names":[]}
|