@reddoorla/maintenance 0.69.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +15 -14
  2. package/dist/airtable-U2GTDTK5.js +9 -0
  3. package/dist/{announce-72R3LRNT.js → announce-HJCGEVJA.js} +6 -6
  4. package/dist/blux-FKZYMKLF.js +2205 -0
  5. package/dist/blux-FKZYMKLF.js.map +1 -0
  6. package/dist/{bump-deps-QNM3H4GI.js → bump-deps-GZEJAUKI.js} +3 -2
  7. package/dist/bump-deps-GZEJAUKI.js.map +1 -0
  8. package/dist/chunk-7MJBTV2R.js +145 -0
  9. package/dist/chunk-7MJBTV2R.js.map +1 -0
  10. package/dist/{chunk-2DCA7FHZ.js → chunk-7P7YETHG.js} +34 -12
  11. package/dist/chunk-7P7YETHG.js.map +1 -0
  12. package/dist/{chunk-42KKVLYN.js → chunk-APTAITXZ.js} +10 -2
  13. package/dist/chunk-APTAITXZ.js.map +1 -0
  14. package/dist/{chunk-6G7BC5SN.js → chunk-BHE5CUWI.js} +392 -7
  15. package/dist/chunk-BHE5CUWI.js.map +1 -0
  16. package/dist/{chunk-XWPZ46IN.js → chunk-BJCA7O4A.js} +2 -2
  17. package/dist/{chunk-C3PDS623.js → chunk-BWKK2ZXS.js} +67 -8
  18. package/dist/chunk-BWKK2ZXS.js.map +1 -0
  19. package/dist/{chunk-45AUGDFR.js → chunk-D4FVAIEJ.js} +6 -20
  20. package/dist/chunk-D4FVAIEJ.js.map +1 -0
  21. package/dist/{chunk-YQMYQAJX.js → chunk-GI4M2AMO.js} +2 -2
  22. package/dist/{chunk-OTBA7DON.js → chunk-GYNRZCM6.js} +7 -2
  23. package/dist/chunk-GYNRZCM6.js.map +1 -0
  24. package/dist/{chunk-6MT5W6UF.js → chunk-HR5SONJC.js} +6 -3
  25. package/dist/chunk-HR5SONJC.js.map +1 -0
  26. package/dist/{chunk-6JFOAPW4.js → chunk-JCITWZQR.js} +51 -19
  27. package/dist/chunk-JCITWZQR.js.map +1 -0
  28. package/dist/{chunk-GKSXZIMX.js → chunk-JGLAWXUC.js} +77 -3
  29. package/dist/chunk-JGLAWXUC.js.map +1 -0
  30. package/dist/{chunk-E2GKKUEX.js → chunk-KLB2OLPW.js} +26 -6
  31. package/dist/chunk-KLB2OLPW.js.map +1 -0
  32. package/dist/{chunk-RINXPU3H.js → chunk-KLVMJYZZ.js} +2 -2
  33. package/dist/chunk-KRDLHAKK.js +17 -0
  34. package/dist/chunk-KRDLHAKK.js.map +1 -0
  35. package/dist/{chunk-AESJBEM7.js → chunk-KRHPS3R4.js} +3 -3
  36. package/dist/{chunk-LYCKN7ZR.js → chunk-LJPCMXR7.js} +8 -5
  37. package/dist/chunk-LJPCMXR7.js.map +1 -0
  38. package/dist/{chunk-O6G4HHMF.js → chunk-NRISG4XD.js} +308 -104
  39. package/dist/chunk-NRISG4XD.js.map +1 -0
  40. package/dist/chunk-PKVZMFEB.js +315 -0
  41. package/dist/chunk-PKVZMFEB.js.map +1 -0
  42. package/dist/chunk-SL24DCGC.js +41 -0
  43. package/dist/chunk-SL24DCGC.js.map +1 -0
  44. package/dist/{chunk-A5KYRGQA.js → chunk-SQJ53Z7R.js} +4 -4
  45. package/dist/cli/bin.js +63 -19
  46. package/dist/cli/bin.js.map +1 -1
  47. package/dist/cli/commands/audit.d.ts +1 -1
  48. package/dist/cli/commands/audit.js +11 -9
  49. package/dist/cli/commands/audit.js.map +1 -1
  50. package/dist/configs/playwright-a11y.d.ts +1 -1
  51. package/dist/configs/playwright-a11y.js +1 -3
  52. package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-SVFDLXZS.js} +3 -2
  53. package/dist/convert-to-pnpm-SVFDLXZS.js.map +1 -0
  54. package/dist/{digest-Q2TAQHXF.js → digest-VX7Q4MNF.js} +8 -8
  55. package/dist/{ensure-site-2PF522VL.js → ensure-site-O7G5ZJS7.js} +2 -2
  56. package/dist/forms/index.d.ts +5 -2
  57. package/dist/forms/index.js +22 -9
  58. package/dist/forms/index.js.map +1 -1
  59. package/dist/{github-signals-CLQC2IPY.js → github-signals-QVW2LB7N.js} +5 -5
  60. package/dist/health-endpoint-IYR5VBUT.js +54 -0
  61. package/dist/health-endpoint-IYR5VBUT.js.map +1 -0
  62. package/dist/index.d.ts +136 -20
  63. package/dist/index.js +207 -64
  64. package/dist/index.js.map +1 -1
  65. package/dist/{init-UA6D7GKW.js → init-CU6PVZRG.js} +9 -5
  66. package/dist/init-CU6PVZRG.js.map +1 -0
  67. package/dist/{launch-AENAS2O2.js → launch-JWXCTJRG.js} +9 -9
  68. package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-J5TLASJC.js} +3 -3
  69. package/dist/{onboard-X6KJ5Z2V.js → onboard-6UQE5QIO.js} +2 -2
  70. package/dist/{orchestrate-GADNFGDH.js → orchestrate-ZDH24Z6Y.js} +5 -5
  71. package/dist/{preflight-D5RQGX4V.js → preflight-BNTMWTPG.js} +7 -7
  72. package/dist/probe-DJAMLVTT.js +46 -0
  73. package/dist/probe-DJAMLVTT.js.map +1 -0
  74. package/dist/recipes/sync-configs.d.ts +1 -1
  75. package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-C3ALLACX.js} +2 -2
  76. package/dist/{report-3FJ224H5.js → report-YLFSGNWR.js} +12 -12
  77. package/dist/run-migration-LJNNZWIV.js +205 -0
  78. package/dist/run-migration-LJNNZWIV.js.map +1 -0
  79. package/dist/{self-updating-JTTFOJ7Q.js → self-updating-64FNRQB5.js} +2 -2
  80. package/dist/{selftest-55OJIWEB.js → selftest-7DPNGL6R.js} +12 -9
  81. package/dist/selftest-7DPNGL6R.js.map +1 -0
  82. package/dist/smoke-suite-TWILWDSF.js +54 -0
  83. package/dist/smoke-suite-TWILWDSF.js.map +1 -0
  84. package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-RVDTTXFD.js} +3 -2
  85. package/dist/svelte-codemods-RVDTTXFD.js.map +1 -0
  86. package/dist/{sync-configs-TIBO6DUJ.js → sync-configs-BUUD6L5Q.js} +3 -2
  87. package/dist/sync-configs-BUUD6L5Q.js.map +1 -0
  88. package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
  89. package/dist/{upgrade-TPTE26FG.js → upgrade-HHKWYJRN.js} +3 -2
  90. package/dist/upgrade-HHKWYJRN.js.map +1 -0
  91. package/dist/{websites-6YTT5ZTO.js → websites-EH2ULELZ.js} +8 -2
  92. package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-LZDYTNX5.js} +4 -4
  93. package/package.json +4 -1
  94. package/dist/airtable-WGQQ3KAD.js +0 -9
  95. package/dist/bump-deps-QNM3H4GI.js.map +0 -1
  96. package/dist/chunk-2DCA7FHZ.js.map +0 -1
  97. package/dist/chunk-42KKVLYN.js.map +0 -1
  98. package/dist/chunk-45AUGDFR.js.map +0 -1
  99. package/dist/chunk-6G7BC5SN.js.map +0 -1
  100. package/dist/chunk-6JFOAPW4.js.map +0 -1
  101. package/dist/chunk-6MT5W6UF.js.map +0 -1
  102. package/dist/chunk-C3PDS623.js.map +0 -1
  103. package/dist/chunk-E2GKKUEX.js.map +0 -1
  104. package/dist/chunk-GKSXZIMX.js.map +0 -1
  105. package/dist/chunk-LYCKN7ZR.js.map +0 -1
  106. package/dist/chunk-O6G4HHMF.js.map +0 -1
  107. package/dist/chunk-OTBA7DON.js.map +0 -1
  108. package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
  109. package/dist/init-UA6D7GKW.js.map +0 -1
  110. package/dist/selftest-55OJIWEB.js.map +0 -1
  111. package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
  112. package/dist/sync-configs-TIBO6DUJ.js.map +0 -1
  113. package/dist/upgrade-TPTE26FG.js.map +0 -1
  114. /package/dist/{airtable-WGQQ3KAD.js.map → airtable-U2GTDTK5.js.map} +0 -0
  115. /package/dist/{announce-72R3LRNT.js.map → announce-HJCGEVJA.js.map} +0 -0
  116. /package/dist/{chunk-XWPZ46IN.js.map → chunk-BJCA7O4A.js.map} +0 -0
  117. /package/dist/{chunk-YQMYQAJX.js.map → chunk-GI4M2AMO.js.map} +0 -0
  118. /package/dist/{chunk-RINXPU3H.js.map → chunk-KLVMJYZZ.js.map} +0 -0
  119. /package/dist/{chunk-AESJBEM7.js.map → chunk-KRHPS3R4.js.map} +0 -0
  120. /package/dist/{chunk-A5KYRGQA.js.map → chunk-SQJ53Z7R.js.map} +0 -0
  121. /package/dist/{digest-Q2TAQHXF.js.map → digest-VX7Q4MNF.js.map} +0 -0
  122. /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-O7G5ZJS7.js.map} +0 -0
  123. /package/dist/{github-signals-CLQC2IPY.js.map → github-signals-QVW2LB7N.js.map} +0 -0
  124. /package/dist/{launch-AENAS2O2.js.map → launch-JWXCTJRG.js.map} +0 -0
  125. /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-J5TLASJC.js.map} +0 -0
  126. /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-6UQE5QIO.js.map} +0 -0
  127. /package/dist/{orchestrate-GADNFGDH.js.map → orchestrate-ZDH24Z6Y.js.map} +0 -0
  128. /package/dist/{preflight-D5RQGX4V.js.map → preflight-BNTMWTPG.js.map} +0 -0
  129. /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-C3ALLACX.js.map} +0 -0
  130. /package/dist/{report-3FJ224H5.js.map → report-YLFSGNWR.js.map} +0 -0
  131. /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-64FNRQB5.js.map} +0 -0
  132. /package/dist/{websites-6YTT5ZTO.js.map → websites-EH2ULELZ.js.map} +0 -0
  133. /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-LZDYTNX5.js.map} +0 -0
@@ -2,7 +2,7 @@ import {
2
2
  isPendingApproval,
3
3
  listReportsForSite,
4
4
  setDraftReady
5
- } from "./chunk-6JFOAPW4.js";
5
+ } from "./chunk-JCITWZQR.js";
6
6
 
7
7
  // src/reports/queue.ts
8
8
  var REPORT_TIER = {
@@ -35,4 +35,4 @@ export {
35
35
  reportTier,
36
36
  queueDraft
37
37
  };
38
- //# sourceMappingURL=chunk-RINXPU3H.js.map
38
+ //# sourceMappingURL=chunk-KLVMJYZZ.js.map
@@ -0,0 +1,17 @@
1
+ // src/recipes/_prettier.ts
2
+ var PRETTIER_FLAG_NOTE = "could not prettier-format written files (prettier unavailable?) \u2014 verify CI formatting";
3
+ async function formatWithPrettier(spawn, cwd, relPaths) {
4
+ if (relPaths.length === 0) return true;
5
+ try {
6
+ const res = await spawn("pnpm", ["exec", "prettier", "--write", ...relPaths], { cwd });
7
+ return res.code === 0;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export {
14
+ PRETTIER_FLAG_NOTE,
15
+ formatWithPrettier
16
+ };
17
+ //# sourceMappingURL=chunk-KRDLHAKK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/_prettier.ts"],"sourcesContent":["import type { SpawnFn } from \"../audits/util/spawn.js\";\n\n/** Flag note recipes surface when best-effort formatting couldn't run, so the\n * operator knows to eyeball CI's format check for that site. */\nexport const PRETTIER_FLAG_NOTE =\n \"could not prettier-format written files (prettier unavailable?) — verify CI formatting\";\n\n/**\n * Format recipe-written files with the SITE's own prettier\n * (`pnpm exec prettier --write`), so they match that site's config — quote\n * style, tabs vs spaces, and printWidth all vary across the fleet, and a single\n * hardcoded template style reds the CI format check on the sites that differ.\n *\n * Best-effort by contract: returns `false` (never throws) when prettier isn't\n * installed or exits non-zero, so a recipe can commit the file unformatted and\n * flag it rather than fail the whole rollout. Files in the site's\n * `.prettierignore` are skipped by prettier itself. Returns `true` immediately\n * (no spawn) when `relPaths` is empty, so a recipe that wrote nothing this run\n * doesn't shell out.\n *\n * Only pass paths the recipe actually wrote/changed — never operator files it\n * left untouched, which must not be reformatted.\n */\nexport async function formatWithPrettier(\n spawn: SpawnFn,\n cwd: string,\n relPaths: readonly string[],\n): Promise<boolean> {\n if (relPaths.length === 0) return true;\n try {\n const res = await spawn(\"pnpm\", [\"exec\", \"prettier\", \"--write\", ...relPaths], { cwd });\n return res.code === 0;\n } catch {\n return false;\n }\n}\n"],"mappings":";AAIO,IAAM,qBACX;AAkBF,eAAsB,mBACpB,OACA,KACA,UACkB;AAClB,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,QAAQ,CAAC,QAAQ,YAAY,WAAW,GAAG,QAAQ,GAAG,EAAE,IAAI,CAAC;AACrF,WAAO,IAAI,SAAS;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  approveBlockers
3
- } from "./chunk-E2GKKUEX.js";
3
+ } from "./chunk-KLB2OLPW.js";
4
4
  import {
5
5
  siteSlug
6
- } from "./chunk-C3PDS623.js";
6
+ } from "./chunk-BWKK2ZXS.js";
7
7
 
8
8
  // src/alerts/digest-collectors.ts
9
9
  function dashboardUrl(baseUrl, siteName) {
@@ -248,4 +248,4 @@ export {
248
248
  readDigestState,
249
249
  writeDigestState
250
250
  };
251
- //# sourceMappingURL=chunk-AESJBEM7.js.map
251
+ //# sourceMappingURL=chunk-KRHPS3R4.js.map
@@ -5,6 +5,8 @@ var a11yRoutes = [
5
5
  { path: "/dev/animate-in", name: "animate-in demo" }
6
6
  ];
7
7
  var smokeRoutes = [{ path: "/", name: "home" }];
8
+ var smokePort = process.env.REDDOOR_SMOKE_PORT;
9
+ var port = smokePort || "5173";
8
10
  var playwrightA11yConfig = defineConfig({
9
11
  testDir: "tests",
10
12
  testMatch: /.*\.spec\.ts$/,
@@ -13,7 +15,7 @@ var playwrightA11yConfig = defineConfig({
13
15
  retries: process.env.CI ? 2 : 0,
14
16
  reporter: process.env.CI ? "github" : "list",
15
17
  use: {
16
- baseURL: "http://localhost:5173",
18
+ baseURL: `http://localhost:${port}`,
17
19
  trace: "on-first-retry"
18
20
  },
19
21
  projects: [
@@ -24,8 +26,10 @@ var playwrightA11yConfig = defineConfig({
24
26
  ],
25
27
  webServer: {
26
28
  // Portable across pnpm and npm sites — pnpm respects `npm run` too.
27
- command: "npm run vite:dev",
28
- url: "http://localhost:5173/dev/a11y-fixtures",
29
+ // `--strictPort` only when a port was explicitly allocated: fail loudly
30
+ // rather than let vite drift to a free port the baseURL doesn't point at.
31
+ command: smokePort ? `npm run vite:dev -- --port ${smokePort} --strictPort` : "npm run vite:dev",
32
+ url: `http://localhost:${port}/dev/a11y-fixtures`,
29
33
  reuseExistingServer: !process.env.CI,
30
34
  timeout: 12e4
31
35
  }
@@ -35,7 +39,6 @@ var playwright_a11y_default = playwrightA11yConfig;
35
39
  export {
36
40
  a11yRoutes,
37
41
  smokeRoutes,
38
- playwrightA11yConfig,
39
42
  playwright_a11y_default
40
43
  };
41
- //# sourceMappingURL=chunk-LYCKN7ZR.js.map
44
+ //# sourceMappingURL=chunk-LJPCMXR7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\nconst port = smokePort || \"5173\";\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\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 projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n // `--strictPort` only when a port was explicitly allocated: fail loudly\n // rather than let vite drift to a free port the baseURL doesn't point at.\n command: smokePort\n ? `npm run vite:dev -- --port ${smokePort} --strictPort`\n : \"npm run vite:dev\",\n url: `http://localhost:${port}/dev/a11y-fixtures`,\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAC9B,IAAM,OAAO,aAAa;AAK1B,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;AAAA;AAAA;AAAA;AAAA,IAIT,SAAS,YACL,8BAA8B,SAAS,kBACvC;AAAA,IACJ,KAAK,oBAAoB,IAAI;AAAA,IAC7B,qBAAqB,CAAC,QAAQ,IAAI;AAAA,IAClC,SAAS;AAAA,EACX;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}
@@ -1,21 +1,22 @@
1
1
  import {
2
2
  queueDraft
3
- } from "./chunk-RINXPU3H.js";
3
+ } from "./chunk-KLVMJYZZ.js";
4
4
  import {
5
5
  checklistFor,
6
6
  createDraft,
7
+ gatingFields,
7
8
  listReportsForSite,
8
9
  renderReportHtml,
9
10
  resolveCopy,
10
11
  uploadAttachment
11
- } from "./chunk-6JFOAPW4.js";
12
+ } from "./chunk-JCITWZQR.js";
12
13
  import {
13
14
  isNetlifyAppUrl
14
15
  } from "./chunk-U5SZ4FOP.js";
15
16
  import {
16
17
  siteSlug,
17
18
  updateAnalyticsHealth
18
- } from "./chunk-C3PDS623.js";
19
+ } from "./chunk-BWKK2ZXS.js";
19
20
  import {
20
21
  defaultCredentialsPath
21
22
  } from "./chunk-6VAL7XJT.js";
@@ -23,10 +24,10 @@ import {
23
24
  // src/reports/ga/config.ts
24
25
  import { dirname, join } from "path";
25
26
  function readGaConfig() {
26
- const subject = process.env.GA_SUBJECT?.trim();
27
- if (!subject) return null;
27
+ const subjects = (process.env.GA_SUBJECT ?? "").split(",").map((s) => s.trim()).filter(Boolean);
28
+ if (subjects.length === 0) return null;
28
29
  const keyPath = process.env.GA_SA_KEY_PATH?.trim() || join(dirname(defaultCredentialsPath()), "ga-service-account.json");
29
- return { subject, keyPath };
30
+ return { subjects, keyPath };
30
31
  }
31
32
 
32
33
  // src/reports/draft.ts
@@ -45,44 +46,65 @@ function isFresh(checkedAt, now) {
45
46
  var CERT_MIN_DAYS = 14;
46
47
  function autoTickChecklist(site, reportType, now, signals) {
47
48
  const out = /* @__PURE__ */ new Map();
48
- const fields = new Set(checklistFor(reportType).map((i) => i.field));
49
- if (fields.has("Maint: Google Indexed")) {
50
- const g = googleEvidence(now, signals.search);
51
- if (g) out.set("Maint: Google Indexed", g);
52
- }
53
- if (fields.has("Maint: Security Updates")) {
54
- const s = securityEvidence(site, now);
55
- if (s) out.set("Maint: Security Updates", s);
56
- }
57
- if (fields.has("Maint: Domain, DNS & SSL")) {
58
- const d = domainEvidence(site, now);
59
- if (d) out.set("Maint: Domain, DNS & SSL", d);
60
- }
61
- if (fields.has("Test: Desktop Browsers")) {
62
- const e = browserEvidence(
63
- site.crossbrowserOk,
64
- site,
65
- now,
66
- "Desktop renders cleanly",
67
- "render errors"
68
- );
69
- if (e) out.set("Test: Desktop Browsers", e);
70
- }
71
- if (fields.has("Test: Mobile Browsers")) {
72
- const e = browserEvidence(
73
- site.mobileOk,
74
- site,
75
- now,
76
- "Mobile renders cleanly",
77
- "overflow/errors"
78
- );
79
- if (e) out.set("Test: Mobile Browsers", e);
80
- }
81
- if (fields.has("Test: Links & Navigation")) {
82
- const broken = site.brokenLinks;
83
- const failNote = broken && broken > 0 ? `${broken} broken link(s)` : "broken links / nav";
84
- const e = browserEvidence(site.linksOk, site, now, "All internal links resolve", failNote);
85
- if (e) out.set("Test: Links & Navigation", e);
49
+ const gating = new Set(gatingFields(reportType));
50
+ for (const item of checklistFor(reportType)) {
51
+ let ev;
52
+ switch (item.field) {
53
+ case "Maint: Google Indexed":
54
+ ev = googleEvidence(now, signals.search);
55
+ break;
56
+ case "Maint: Deploy & Function Health":
57
+ ev = deployEvidence(site, now);
58
+ break;
59
+ case "Maint: CMS Checked":
60
+ ev = cmsEvidence(site, now);
61
+ break;
62
+ case "Maint: Domain, DNS & SSL":
63
+ ev = domainEvidence(site, now);
64
+ break;
65
+ case "Maint: Security Updates":
66
+ ev = securityEvidence(site, now);
67
+ break;
68
+ case "Maint: Uptime Checked":
69
+ ev = uptimeEvidence(site, now);
70
+ break;
71
+ case "Test: Desktop Browsers":
72
+ ev = browserEvidence(
73
+ site.crossbrowserOk,
74
+ site,
75
+ now,
76
+ "Desktop renders cleanly",
77
+ "render errors"
78
+ );
79
+ break;
80
+ case "Test: Mobile Browsers":
81
+ ev = browserEvidence(site.mobileOk, site, now, "Mobile renders cleanly", "overflow/errors");
82
+ break;
83
+ case "Test: Page Titles & Meta":
84
+ ev = titlesEvidence(site, now);
85
+ break;
86
+ case "Test: Links & Navigation": {
87
+ const broken = site.brokenLinks;
88
+ const failNote = broken && broken > 0 ? `${broken} broken link(s)` : "broken links / nav";
89
+ ev = browserEvidence(site.linksOk, site, now, "All internal links resolve", failNote);
90
+ break;
91
+ }
92
+ case "Test: Form Functionality":
93
+ ev = formsEvidence(site, now);
94
+ break;
95
+ case "Test: Interactions & Animations":
96
+ ev = interactionsEvidence(site, now);
97
+ break;
98
+ case "Test: Verified After Updates":
99
+ ev = updatesEvidence(site, now);
100
+ break;
101
+ default:
102
+ ev = null;
103
+ }
104
+ if (ev === null && gating.has(item.field)) {
105
+ ev = { result: "unknown", checkedAt: null, note: "Not yet measured" };
106
+ }
107
+ if (ev !== null) out.set(item.field, ev);
86
108
  }
87
109
  return out;
88
110
  }
@@ -144,11 +166,175 @@ function domainEvidence(site, now) {
144
166
  }
145
167
  return { result: "pass", checkedAt: at, note: `Custom domain, valid cert (${days}d left)` };
146
168
  }
169
+ function deployEvidence(site, now) {
170
+ if (!site.deployCheckedAt && !site.functionHealthCheckedAt) return null;
171
+ if (!isFresh(site.deployCheckedAt, now) || !isFresh(site.functionHealthCheckedAt, now)) {
172
+ return {
173
+ result: "unknown",
174
+ checkedAt: site.functionHealthCheckedAt ?? site.deployCheckedAt,
175
+ note: "Deploy/function-health check is stale (>3d)"
176
+ };
177
+ }
178
+ const ready = site.deployStatus === "ready";
179
+ const fnOk = site.functionHealth === "pass";
180
+ if (ready && fnOk) {
181
+ return {
182
+ result: "pass",
183
+ checkedAt: site.functionHealthCheckedAt,
184
+ note: "Netlify build ready + functions respond"
185
+ };
186
+ }
187
+ const why = !ready && !fnOk ? "build not ready + functions unhealthy" : !ready ? "build not ready" : "functions unhealthy";
188
+ return {
189
+ result: "fail",
190
+ checkedAt: site.functionHealthCheckedAt,
191
+ note: `Deploy/function-health failing \u2014 ${why}`
192
+ };
193
+ }
194
+ function cmsEvidence(site, now) {
195
+ if (!site.functionHealthCheckedAt) return null;
196
+ const at = site.functionHealthCheckedAt;
197
+ if (!isFresh(at, now)) {
198
+ return { result: "unknown", checkedAt: at, note: "CMS check is stale (>3d)" };
199
+ }
200
+ if (site.cmsReachable === "pass") {
201
+ return { result: "pass", checkedAt: at, note: "Prismic reachable (server-side)" };
202
+ }
203
+ if (site.cmsReachable === "fail") {
204
+ return { result: "fail", checkedAt: at, note: "Prismic unreachable (server-side)" };
205
+ }
206
+ return { result: "unknown", checkedAt: at, note: "CMS reachability not reported" };
207
+ }
208
+ function uptimeEvidence(site, now) {
209
+ if (site.reachableOk === null || !site.browserCheckedAt) return null;
210
+ const at = site.browserCheckedAt;
211
+ if (!isFresh(at, now)) {
212
+ return { result: "unknown", checkedAt: at, note: "Uptime check is stale (>3d)" };
213
+ }
214
+ return site.reachableOk === "pass" ? { result: "pass", checkedAt: at, note: "All sampled routes reachable (point-in-time)" } : { result: "fail", checkedAt: at, note: "One or more sampled routes did not respond 2xx/3xx" };
215
+ }
216
+ function titlesEvidence(site, now) {
217
+ if (site.titleMetaOk === null || !site.browserCheckedAt) return null;
218
+ const at = site.browserCheckedAt;
219
+ if (!isFresh(at, now)) {
220
+ return { result: "unknown", checkedAt: at, note: "Titles/meta check is stale (>3d)" };
221
+ }
222
+ return site.titleMetaOk === "pass" ? { result: "pass", checkedAt: at, note: "Titles + meta present" } : {
223
+ result: "fail",
224
+ checkedAt: at,
225
+ note: "Missing/duplicate title or missing meta description"
226
+ };
227
+ }
228
+ function formsEvidence(site, now) {
229
+ if (!site.formE2eCheckedAt) return null;
230
+ const at = site.formE2eCheckedAt;
231
+ if (!isFresh(at, now)) {
232
+ return { result: "unknown", checkedAt: at, note: "Form E2E check is stale (>3d)" };
233
+ }
234
+ if (site.formE2eOk === "pass") {
235
+ return { result: "pass", checkedAt: at, note: "Synthetic submission succeeded" };
236
+ }
237
+ if (site.formE2eOk === "fail") {
238
+ return { result: "fail", checkedAt: at, note: "Synthetic submission failed" };
239
+ }
240
+ return { result: "n/a", checkedAt: at, note: "No contact form on this site" };
241
+ }
242
+ function interactionsEvidence(site, now) {
243
+ if (site.smokeOk === null || !site.lastSmokeAt) return null;
244
+ const at = site.lastSmokeAt;
245
+ if (!isFresh(at, now)) {
246
+ return { result: "unknown", checkedAt: at, note: "Smoke suite is stale (>3d)" };
247
+ }
248
+ return site.smokeOk === "pass" ? { result: "pass", checkedAt: at, note: "Smoke suite green" } : { result: "fail", checkedAt: at, note: "Smoke suite red" };
249
+ }
250
+ function updatesEvidence(site, now) {
251
+ if (site.defaultBranchCi === null || !site.githubSignalsAt) return null;
252
+ const at = site.githubSignalsAt;
253
+ if (site.defaultBranchCi === "none") {
254
+ return { result: "n/a", checkedAt: at, note: "Repository has no CI" };
255
+ }
256
+ if (!isFresh(at, now)) {
257
+ return { result: "unknown", checkedAt: at, note: "CI signal is stale (>3d)" };
258
+ }
259
+ if (site.defaultBranchCi === "passing") {
260
+ return { result: "pass", checkedAt: at, note: "Default-branch CI green on latest commit" };
261
+ }
262
+ if (site.defaultBranchCi === "failing") {
263
+ return { result: "fail", checkedAt: at, note: "Default-branch CI is failing" };
264
+ }
265
+ return { result: "unknown", checkedAt: at, note: "Default-branch CI is pending" };
266
+ }
147
267
 
148
268
  // src/reports/ga/client.ts
149
269
  import { readFileSync } from "fs";
150
270
  import { JWT } from "google-auth-library";
151
271
  import { BetaAnalyticsDataClient } from "@google-analytics/data";
272
+
273
+ // src/reports/ga/failover.ts
274
+ var AUTH_HTTP_STATUS = /* @__PURE__ */ new Set([401, 403]);
275
+ var AUTH_GRPC_CODES = /* @__PURE__ */ new Set([7, 16]);
276
+ var AUTH_MESSAGE = /permission[_ ]denied|unauthenticated|unauthorized|invalid_grant|access[_ ]denied|forbidden|insufficient permission|status code 40[13]/i;
277
+ var QUOTA_REASONS = /* @__PURE__ */ new Set([
278
+ "userratelimitexceeded",
279
+ "ratelimitexceeded",
280
+ "quotaexceeded",
281
+ "dailylimitexceeded"
282
+ ]);
283
+ var QUOTA_MESSAGE = /quota exceeded|rate limit exceeded|user rate limit/i;
284
+ function isAuthShapedError(e) {
285
+ const err = e;
286
+ if (err?.failoverToNextSubject === true) return true;
287
+ if ([err?.status, err?.response?.status].some((s) => AUTH_HTTP_STATUS.has(Number(s))))
288
+ return true;
289
+ if (AUTH_HTTP_STATUS.has(Number(err?.code)) || AUTH_GRPC_CODES.has(Number(err?.code)))
290
+ return true;
291
+ const msg = e instanceof Error ? e.message : String(e);
292
+ return AUTH_MESSAGE.test(msg);
293
+ }
294
+ function isFailoverSentinel(e) {
295
+ return e?.failoverToNextSubject === true;
296
+ }
297
+ function isQuotaShapedError(e) {
298
+ const err = e;
299
+ if (err?.failoverToNextSubject === true) return false;
300
+ const reasons = err?.response?.data?.error?.errors;
301
+ if (Array.isArray(reasons) && reasons.length > 0) {
302
+ return reasons.some((r) => r?.reason != null && QUOTA_REASONS.has(r.reason.toLowerCase()));
303
+ }
304
+ const msg = e instanceof Error ? e.message : String(e);
305
+ return QUOTA_MESSAGE.test(msg);
306
+ }
307
+ async function withSubjectFailover(subjects, label, attempt) {
308
+ if (subjects.length === 0) throw new Error(`${label}: no impersonation subjects configured`);
309
+ const failed = [];
310
+ let lastRealError;
311
+ let lastSentinel;
312
+ let allQuota = true;
313
+ for (const subject of subjects) {
314
+ try {
315
+ const result = await attempt(subject);
316
+ if (failed.length > 0) {
317
+ console.warn(
318
+ allQuota ? `\u26A0 ${label} subject failover: ${failed.join("; ")} \u2014 succeeded via ${subject} (transient quota/rate-limit, not an access loss).` : `\u26A0 ${label} subject failover: ${failed.join("; ")} \u2014 succeeded via ${subject}. Restore the failing subject's access or drop it from GA_SUBJECT (docs/runbooks/ga-search-role-account-cutover.md).`
319
+ );
320
+ }
321
+ return result;
322
+ } catch (e) {
323
+ if (!isAuthShapedError(e)) throw e;
324
+ failed.push(`${subject} (${e instanceof Error ? e.message : String(e)})`);
325
+ if (isFailoverSentinel(e)) {
326
+ lastSentinel = e;
327
+ allQuota = false;
328
+ } else {
329
+ lastRealError = e;
330
+ if (!isQuotaShapedError(e)) allQuota = false;
331
+ }
332
+ }
333
+ }
334
+ throw lastRealError ?? lastSentinel;
335
+ }
336
+
337
+ // src/reports/ga/client.ts
152
338
  var ANALYTICS_READONLY = "https://www.googleapis.com/auth/analytics.readonly";
153
339
  var MS_PER_DAY2 = 864e5;
154
340
  function ymd(d) {
@@ -156,30 +342,32 @@ function ymd(d) {
156
342
  }
157
343
  async function fetchPeriodUsers(query, periodStart, periodEnd) {
158
344
  const key = JSON.parse(readFileSync(query.keyPath, "utf8"));
159
- const authClient = new JWT({
160
- email: key.client_email,
161
- key: key.private_key,
162
- scopes: [ANALYTICS_READONLY],
163
- subject: query.subject
164
- });
165
- const client = new BetaAnalyticsDataClient({ authClient });
166
345
  const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY2);
167
346
  const prevEnd = new Date(periodStart.getTime() - MS_PER_DAY2);
168
347
  const prevStart = new Date(prevEnd.getTime() - lengthDays * MS_PER_DAY2);
169
348
  const property = `properties/${query.propertyId}`;
170
- const run = async (start, end) => {
171
- const [resp] = await client.runReport({
172
- property,
173
- dateRanges: [{ startDate: ymd(start), endDate: ymd(end) }],
174
- metrics: [{ name: "activeUsers" }]
349
+ return withSubjectFailover(query.subjects, "GA", async (subject) => {
350
+ const authClient = new JWT({
351
+ email: key.client_email,
352
+ key: key.private_key,
353
+ scopes: [ANALYTICS_READONLY],
354
+ subject
175
355
  });
176
- const raw = resp.rows?.[0]?.metricValues?.[0]?.value ?? "0";
177
- const n = Number.parseInt(raw, 10);
178
- return Number.isFinite(n) ? n : 0;
179
- };
180
- const current = await run(periodStart, periodEnd);
181
- const previous = await run(prevStart, prevEnd);
182
- return { current, previous };
356
+ const client = new BetaAnalyticsDataClient({ authClient });
357
+ const run = async (start, end) => {
358
+ const [resp] = await client.runReport({
359
+ property,
360
+ dateRanges: [{ startDate: ymd(start), endDate: ymd(end) }],
361
+ metrics: [{ name: "activeUsers" }]
362
+ });
363
+ const raw = resp.rows?.[0]?.metricValues?.[0]?.value ?? "0";
364
+ const n = Number.parseInt(raw, 10);
365
+ return Number.isFinite(n) ? n : 0;
366
+ };
367
+ const current = await run(periodStart, periodEnd);
368
+ const previous = await run(prevStart, prevEnd);
369
+ return { current, previous };
370
+ });
183
371
  }
184
372
 
185
373
  // src/reports/search/client.ts
@@ -221,50 +409,66 @@ function selectBrandPosition(rows, exactQuery) {
221
409
  if (exact) return exact.position;
222
410
  return pickBrandQuery(rows);
223
411
  }
412
+ var NoPropertyForSubjectError = class extends Error {
413
+ failoverToNextSubject = true;
414
+ constructor(subject, host) {
415
+ super(`no Search Console property matching ${host} is visible to ${subject}`);
416
+ }
417
+ };
224
418
  async function fetchSearchPresence(q, periodStart, periodEnd) {
225
419
  const key = JSON.parse(readFileSync2(q.keyPath, "utf8"));
226
- const jwt = new JWT2({
227
- email: key.client_email,
228
- key: key.private_key,
229
- scopes: [WEBMASTERS_READONLY],
230
- subject: q.subject
231
- });
232
- const explicit = q.property?.trim();
233
- let candidates;
234
- if (explicit) {
235
- candidates = [explicit];
236
- } else {
237
- const list = await jwt.request({
238
- url: `${SC_BASE}/sites`,
239
- method: "GET"
240
- });
241
- candidates = resolvePropertyCandidates(list.data.siteEntry ?? [], q.host);
242
- if (candidates.length === 0) return { foundOnPage1: false, position: null };
243
- }
244
- for (const property of candidates) {
245
- const res = await jwt.request({
246
- url: `${SC_BASE}/sites/${encodeURIComponent(property)}/searchAnalytics/query`,
247
- method: "POST",
248
- data: {
249
- startDate: ymd2(periodStart),
250
- endDate: ymd2(periodEnd),
251
- dimensions: ["query"],
252
- dimensionFilterGroups: [
253
- {
254
- filters: [
255
- { dimension: "query", operator: "contains", expression: q.query.toLowerCase() }
256
- ]
420
+ try {
421
+ return await withSubjectFailover(q.subjects, "Search Console", async (subject) => {
422
+ const jwt = new JWT2({
423
+ email: key.client_email,
424
+ key: key.private_key,
425
+ scopes: [WEBMASTERS_READONLY],
426
+ subject
427
+ });
428
+ const explicit = q.property?.trim();
429
+ let candidates;
430
+ if (explicit) {
431
+ candidates = [explicit];
432
+ } else {
433
+ const list = await jwt.request({
434
+ url: `${SC_BASE}/sites`,
435
+ method: "GET"
436
+ });
437
+ candidates = resolvePropertyCandidates(list.data.siteEntry ?? [], q.host);
438
+ if (candidates.length === 0) throw new NoPropertyForSubjectError(subject, q.host);
439
+ }
440
+ for (const property of candidates) {
441
+ const res = await jwt.request({
442
+ url: `${SC_BASE}/sites/${encodeURIComponent(property)}/searchAnalytics/query`,
443
+ method: "POST",
444
+ data: {
445
+ startDate: ymd2(periodStart),
446
+ endDate: ymd2(periodEnd),
447
+ dimensions: ["query"],
448
+ dimensionFilterGroups: [
449
+ {
450
+ filters: [
451
+ { dimension: "query", operator: "contains", expression: q.query.toLowerCase() }
452
+ ]
453
+ }
454
+ ],
455
+ rowLimit: BRAND_QUERY_ROW_LIMIT
257
456
  }
258
- ],
259
- rowLimit: BRAND_QUERY_ROW_LIMIT
457
+ });
458
+ const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());
459
+ if (typeof pos === "number") {
460
+ return {
461
+ foundOnPage1: pos <= PAGE_1_MAX_POSITION,
462
+ position: Math.max(1, Math.round(pos))
463
+ };
464
+ }
260
465
  }
466
+ return { foundOnPage1: false, position: null };
261
467
  });
262
- const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());
263
- if (typeof pos === "number") {
264
- return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };
265
- }
468
+ } catch (e) {
469
+ if (e instanceof NoPropertyForSubjectError) return { foundOnPage1: false, position: null };
470
+ throw e;
266
471
  }
267
- return { foundOnPage1: false, position: null };
268
472
  }
269
473
 
270
474
  // src/reports/draft.ts
@@ -392,7 +596,7 @@ async function fetchGaUsers(siteRow, periodStart, periodEnd) {
392
596
  if (!cfg || !siteRow.ga4PropertyId) return NO_ENRICHMENT;
393
597
  try {
394
598
  const value = await fetchPeriodUsers(
395
- { propertyId: siteRow.ga4PropertyId, subject: cfg.subject, keyPath: cfg.keyPath },
599
+ { propertyId: siteRow.ga4PropertyId, subjects: cfg.subjects, keyPath: cfg.keyPath },
396
600
  periodStart,
397
601
  periodEnd
398
602
  );
@@ -409,7 +613,7 @@ async function fetchSearch(siteRow, periodStart, periodEnd) {
409
613
  const value = await fetchSearchPresence(
410
614
  {
411
615
  keyPath: cfg.keyPath,
412
- subject: cfg.subject,
616
+ subjects: cfg.subjects,
413
617
  property: siteRow.searchConsoleProperty ?? void 0,
414
618
  host: siteRow.url,
415
619
  query: siteRow.searchQuery
@@ -439,4 +643,4 @@ export {
439
643
  fetchGaUsers,
440
644
  fetchSearch
441
645
  };
442
- //# sourceMappingURL=chunk-O6G4HHMF.js.map
646
+ //# sourceMappingURL=chunk-NRISG4XD.js.map