@reddoorla/maintenance 0.69.0 → 0.71.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 (130) hide show
  1. package/dist/airtable-2PJ4NSAV.js +9 -0
  2. package/dist/{announce-72R3LRNT.js → announce-3JE5FKW7.js} +6 -6
  3. package/dist/blux-FKZYMKLF.js +2205 -0
  4. package/dist/blux-FKZYMKLF.js.map +1 -0
  5. package/dist/{bump-deps-QNM3H4GI.js → bump-deps-K3JMLMBB.js} +3 -2
  6. package/dist/bump-deps-K3JMLMBB.js.map +1 -0
  7. package/dist/{chunk-RINXPU3H.js → chunk-2PWQ6MPG.js} +2 -2
  8. package/dist/{chunk-2DCA7FHZ.js → chunk-52TDNN7K.js} +34 -12
  9. package/dist/chunk-52TDNN7K.js.map +1 -0
  10. package/dist/{chunk-XWPZ46IN.js → chunk-5QC5KJHR.js} +2 -2
  11. package/dist/{chunk-O6G4HHMF.js → chunk-6J25IIAI.js} +162 -42
  12. package/dist/chunk-6J25IIAI.js.map +1 -0
  13. package/dist/chunk-7MJBTV2R.js +145 -0
  14. package/dist/chunk-7MJBTV2R.js.map +1 -0
  15. package/dist/{chunk-6JFOAPW4.js → chunk-AYM2EFSK.js} +38 -6
  16. package/dist/chunk-AYM2EFSK.js.map +1 -0
  17. package/dist/{chunk-GKSXZIMX.js → chunk-CMQMNFFJ.js} +77 -3
  18. package/dist/chunk-CMQMNFFJ.js.map +1 -0
  19. package/dist/{chunk-6MT5W6UF.js → chunk-F6OE6MGQ.js} +6 -3
  20. package/dist/chunk-F6OE6MGQ.js.map +1 -0
  21. package/dist/{chunk-OTBA7DON.js → chunk-J2SHWT2O.js} +1 -1
  22. package/dist/chunk-J2SHWT2O.js.map +1 -0
  23. package/dist/{chunk-AESJBEM7.js → chunk-KOAGMTRI.js} +3 -3
  24. package/dist/chunk-KRDLHAKK.js +17 -0
  25. package/dist/chunk-KRDLHAKK.js.map +1 -0
  26. package/dist/{chunk-LYCKN7ZR.js → chunk-LPMQXS2N.js} +8 -4
  27. package/dist/chunk-LPMQXS2N.js.map +1 -0
  28. package/dist/{chunk-6G7BC5SN.js → chunk-M7HOQ4W7.js} +385 -5
  29. package/dist/chunk-M7HOQ4W7.js.map +1 -0
  30. package/dist/{chunk-42KKVLYN.js → chunk-MPRLMGJU.js} +10 -2
  31. package/dist/chunk-MPRLMGJU.js.map +1 -0
  32. package/dist/chunk-PKVZMFEB.js +315 -0
  33. package/dist/chunk-PKVZMFEB.js.map +1 -0
  34. package/dist/{chunk-A5KYRGQA.js → chunk-PSJQE6T6.js} +4 -4
  35. package/dist/chunk-SL24DCGC.js +41 -0
  36. package/dist/chunk-SL24DCGC.js.map +1 -0
  37. package/dist/{chunk-E2GKKUEX.js → chunk-VR246V3G.js} +25 -5
  38. package/dist/chunk-VR246V3G.js.map +1 -0
  39. package/dist/{chunk-45AUGDFR.js → chunk-WN4NEHE7.js} +2 -7
  40. package/dist/chunk-WN4NEHE7.js.map +1 -0
  41. package/dist/{chunk-YQMYQAJX.js → chunk-Y7LZHPDF.js} +2 -2
  42. package/dist/{chunk-C3PDS623.js → chunk-YWRQEDB3.js} +53 -3
  43. package/dist/chunk-YWRQEDB3.js.map +1 -0
  44. package/dist/cli/bin.js +63 -19
  45. package/dist/cli/bin.js.map +1 -1
  46. package/dist/cli/commands/audit.d.ts +1 -1
  47. package/dist/cli/commands/audit.js +11 -9
  48. package/dist/cli/commands/audit.js.map +1 -1
  49. package/dist/configs/playwright-a11y.js +1 -1
  50. package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-6F4RAFEJ.js} +3 -2
  51. package/dist/convert-to-pnpm-6F4RAFEJ.js.map +1 -0
  52. package/dist/{digest-Q2TAQHXF.js → digest-GZCZYJKC.js} +8 -8
  53. package/dist/{ensure-site-2PF522VL.js → ensure-site-H5VMBPLF.js} +2 -2
  54. package/dist/forms/index.d.ts +2 -0
  55. package/dist/forms/index.js +7 -1
  56. package/dist/forms/index.js.map +1 -1
  57. package/dist/{github-signals-CLQC2IPY.js → github-signals-SQECQNST.js} +5 -5
  58. package/dist/health-endpoint-QXWM6GW2.js +54 -0
  59. package/dist/health-endpoint-QXWM6GW2.js.map +1 -0
  60. package/dist/index.d.ts +132 -17
  61. package/dist/index.js +207 -64
  62. package/dist/index.js.map +1 -1
  63. package/dist/{init-UA6D7GKW.js → init-KQOIPLVT.js} +9 -5
  64. package/dist/init-KQOIPLVT.js.map +1 -0
  65. package/dist/{launch-AENAS2O2.js → launch-RX4VI7F2.js} +9 -9
  66. package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-TTXULEGU.js} +3 -3
  67. package/dist/{onboard-X6KJ5Z2V.js → onboard-TFBL62J3.js} +2 -2
  68. package/dist/{orchestrate-GADNFGDH.js → orchestrate-DYA4XAQJ.js} +5 -5
  69. package/dist/{preflight-D5RQGX4V.js → preflight-S3GYME3Q.js} +7 -7
  70. package/dist/probe-DJAMLVTT.js +46 -0
  71. package/dist/probe-DJAMLVTT.js.map +1 -0
  72. package/dist/recipes/sync-configs.d.ts +1 -1
  73. package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-MZPAWVBL.js} +2 -2
  74. package/dist/{report-3FJ224H5.js → report-BLZ67QAQ.js} +12 -12
  75. package/dist/run-migration-LJNNZWIV.js +205 -0
  76. package/dist/run-migration-LJNNZWIV.js.map +1 -0
  77. package/dist/{self-updating-JTTFOJ7Q.js → self-updating-GSH427XP.js} +2 -2
  78. package/dist/{selftest-55OJIWEB.js → selftest-KPTL6KC4.js} +8 -8
  79. package/dist/smoke-suite-GP3W57JP.js +54 -0
  80. package/dist/smoke-suite-GP3W57JP.js.map +1 -0
  81. package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-GYZZX742.js} +3 -2
  82. package/dist/svelte-codemods-GYZZX742.js.map +1 -0
  83. package/dist/{sync-configs-TIBO6DUJ.js → sync-configs-CQNOTJ2B.js} +3 -2
  84. package/dist/sync-configs-CQNOTJ2B.js.map +1 -0
  85. package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
  86. package/dist/{upgrade-TPTE26FG.js → upgrade-RTAJZD56.js} +3 -2
  87. package/dist/upgrade-RTAJZD56.js.map +1 -0
  88. package/dist/{websites-6YTT5ZTO.js → websites-SVI4KYSH.js} +8 -2
  89. package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-3XFNWMUE.js} +4 -4
  90. package/package.json +4 -1
  91. package/dist/airtable-WGQQ3KAD.js +0 -9
  92. package/dist/bump-deps-QNM3H4GI.js.map +0 -1
  93. package/dist/chunk-2DCA7FHZ.js.map +0 -1
  94. package/dist/chunk-42KKVLYN.js.map +0 -1
  95. package/dist/chunk-45AUGDFR.js.map +0 -1
  96. package/dist/chunk-6G7BC5SN.js.map +0 -1
  97. package/dist/chunk-6JFOAPW4.js.map +0 -1
  98. package/dist/chunk-6MT5W6UF.js.map +0 -1
  99. package/dist/chunk-C3PDS623.js.map +0 -1
  100. package/dist/chunk-E2GKKUEX.js.map +0 -1
  101. package/dist/chunk-GKSXZIMX.js.map +0 -1
  102. package/dist/chunk-LYCKN7ZR.js.map +0 -1
  103. package/dist/chunk-O6G4HHMF.js.map +0 -1
  104. package/dist/chunk-OTBA7DON.js.map +0 -1
  105. package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
  106. package/dist/init-UA6D7GKW.js.map +0 -1
  107. package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
  108. package/dist/sync-configs-TIBO6DUJ.js.map +0 -1
  109. package/dist/upgrade-TPTE26FG.js.map +0 -1
  110. /package/dist/{airtable-WGQQ3KAD.js.map → airtable-2PJ4NSAV.js.map} +0 -0
  111. /package/dist/{announce-72R3LRNT.js.map → announce-3JE5FKW7.js.map} +0 -0
  112. /package/dist/{chunk-RINXPU3H.js.map → chunk-2PWQ6MPG.js.map} +0 -0
  113. /package/dist/{chunk-XWPZ46IN.js.map → chunk-5QC5KJHR.js.map} +0 -0
  114. /package/dist/{chunk-AESJBEM7.js.map → chunk-KOAGMTRI.js.map} +0 -0
  115. /package/dist/{chunk-A5KYRGQA.js.map → chunk-PSJQE6T6.js.map} +0 -0
  116. /package/dist/{chunk-YQMYQAJX.js.map → chunk-Y7LZHPDF.js.map} +0 -0
  117. /package/dist/{digest-Q2TAQHXF.js.map → digest-GZCZYJKC.js.map} +0 -0
  118. /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-H5VMBPLF.js.map} +0 -0
  119. /package/dist/{github-signals-CLQC2IPY.js.map → github-signals-SQECQNST.js.map} +0 -0
  120. /package/dist/{launch-AENAS2O2.js.map → launch-RX4VI7F2.js.map} +0 -0
  121. /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-TTXULEGU.js.map} +0 -0
  122. /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-TFBL62J3.js.map} +0 -0
  123. /package/dist/{orchestrate-GADNFGDH.js.map → orchestrate-DYA4XAQJ.js.map} +0 -0
  124. /package/dist/{preflight-D5RQGX4V.js.map → preflight-S3GYME3Q.js.map} +0 -0
  125. /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-MZPAWVBL.js.map} +0 -0
  126. /package/dist/{report-3FJ224H5.js.map → report-BLZ67QAQ.js.map} +0 -0
  127. /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-GSH427XP.js.map} +0 -0
  128. /package/dist/{selftest-55OJIWEB.js.map → selftest-KPTL6KC4.js.map} +0 -0
  129. /package/dist/{websites-6YTT5ZTO.js.map → websites-SVI4KYSH.js.map} +0 -0
  130. /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-3XFNWMUE.js.map} +0 -0
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  a11yRoutes,
21
21
  smokeRoutes
22
- } from "./chunk-LYCKN7ZR.js";
22
+ } from "./chunk-LPMQXS2N.js";
23
23
 
24
24
  // src/audits/deps.ts
25
25
  import { readFile } from "fs/promises";
@@ -1187,11 +1187,29 @@ function summarizeBrowser(routes, links, familyCounts) {
1187
1187
  const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
1188
1188
  const brokenLinks = links.filter((l) => isBroken(l.status)).length;
1189
1189
  const linksOk = links.length > 0 && brokenLinks === 0;
1190
+ const reachableOk = routes.length > 0 && routes.every((r) => r.status !== null && r.status >= 200 && r.status < 400);
1191
+ const trimmedTitles = routes.map((r) => (r.title ?? "").trim());
1192
+ const eachTitleMetaValid = routes.length > 0 && routes.every((r, i) => {
1193
+ const t = trimmedTitles[i];
1194
+ const desc = (r.metaDescription ?? "").trim();
1195
+ return t.length > 0 && t.length <= 70 && desc.length > 0;
1196
+ });
1197
+ const noDuplicateTitles = new Set(trimmedTitles).size === trimmedTitles.length;
1198
+ const titleMetaOk = eachTitleMetaValid && noDuplicateTitles;
1190
1199
  const engines = [...new Set(desktopChecks.map((d) => d.engine))];
1191
1200
  const devices = [...new Set(mobileChecks.map((m) => m.device))];
1192
1201
  const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
1193
1202
  const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
1194
- return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };
1203
+ return {
1204
+ desktopOk,
1205
+ mobileOk,
1206
+ linksOk,
1207
+ reachableOk,
1208
+ titleMetaOk,
1209
+ brokenLinks,
1210
+ routesChecked: routes.length,
1211
+ note
1212
+ };
1195
1213
  }
1196
1214
  async function browserAudit(ctx) {
1197
1215
  const { site } = ctx;
@@ -1263,6 +1281,9 @@ async function defaultBrowserRunner() {
1263
1281
  for (const url of urls) {
1264
1282
  const desktop = [];
1265
1283
  const linkSet = /* @__PURE__ */ new Set();
1284
+ let status = null;
1285
+ let title = null;
1286
+ let metaDescription = null;
1266
1287
  for (let i = 0; i < desktopEngines.length; i++) {
1267
1288
  const engine = desktopEngines[i].engine;
1268
1289
  const browser = browsers[i];
@@ -1279,6 +1300,11 @@ async function defaultBrowserRunner() {
1279
1300
  const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
1280
1301
  ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
1281
1302
  if (engine === "chromium") {
1303
+ status = resp ? resp.status() : null;
1304
+ title = (await page.title().catch(() => "")).trim() || null;
1305
+ metaDescription = (await page.evaluate(
1306
+ `document.querySelector('meta[name="description"]')?.getAttribute('content') || ''`
1307
+ ).catch(() => "") || "").trim() || null;
1282
1308
  const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
1283
1309
  const origin = new URL(url).origin;
1284
1310
  for (const h of hrefs) {
@@ -1320,7 +1346,15 @@ async function defaultBrowserRunner() {
1320
1346
  }
1321
1347
  mobile.push({ device, ok });
1322
1348
  }
1323
- results.push({ url, desktop, mobile, links: [...linkSet] });
1349
+ results.push({
1350
+ url,
1351
+ desktop,
1352
+ mobile,
1353
+ links: [...linkSet],
1354
+ status,
1355
+ title,
1356
+ metaDescription
1357
+ });
1324
1358
  }
1325
1359
  } finally {
1326
1360
  await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
@@ -1456,6 +1490,349 @@ async function netlifyDeployAudit(ctx) {
1456
1490
  };
1457
1491
  }
1458
1492
 
1493
+ // src/audits/function-health.ts
1494
+ var HEALTH_TIMEOUT_MS = 1e4;
1495
+ var ERRORING_BODY = { ok: false, prismic: null, forms: null };
1496
+ function parseHealthBody(raw) {
1497
+ if (!raw || typeof raw !== "object") return { present: false };
1498
+ const o = raw;
1499
+ if (typeof o["ok"] !== "boolean") return { present: false };
1500
+ const prismic = o["prismic"] === "ok" || o["prismic"] === "error" || o["prismic"] === "skipped" ? o["prismic"] : null;
1501
+ const f = o["forms"];
1502
+ const forms = f && typeof f === "object" ? {
1503
+ ingestUrl: f["ingestUrl"] === true,
1504
+ ingestToken: f["ingestToken"] === true,
1505
+ turnstile: f["turnstile"] === true
1506
+ } : null;
1507
+ return { present: true, body: { ok: o["ok"], prismic, forms } };
1508
+ }
1509
+ function defaultFunctionHealthDeps(now, fetchImpl = fetch) {
1510
+ return {
1511
+ now,
1512
+ fetchHealth: async (deployedUrl) => {
1513
+ let url;
1514
+ try {
1515
+ url = new URL("/health", deployedUrl).toString();
1516
+ } catch {
1517
+ return { present: false };
1518
+ }
1519
+ let res;
1520
+ try {
1521
+ res = await fetchImpl(url, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
1522
+ } catch {
1523
+ return { present: false };
1524
+ }
1525
+ if (res.status === 404) return { present: false };
1526
+ if (!res.ok) return { present: true, body: ERRORING_BODY };
1527
+ let body;
1528
+ try {
1529
+ body = await res.json();
1530
+ } catch {
1531
+ return { present: true, body: ERRORING_BODY };
1532
+ }
1533
+ return parseHealthBody(body);
1534
+ }
1535
+ };
1536
+ }
1537
+ async function functionHealthAudit(ctx) {
1538
+ const { site } = ctx;
1539
+ const label = siteLabel(site);
1540
+ if (!site.deployedUrl) {
1541
+ return { audit: "function-health", site: label, status: "skip", summary: "no deployed URL" };
1542
+ }
1543
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1544
+ const deps = ctx.functionHealthDeps ?? defaultFunctionHealthDeps(now);
1545
+ const fetched = await deps.fetchHealth(site.deployedUrl).catch(() => ({ present: false }));
1546
+ if (!fetched.present) {
1547
+ return {
1548
+ audit: "function-health",
1549
+ site: label,
1550
+ status: "skip",
1551
+ summary: "health endpoint unreachable / not adopted"
1552
+ };
1553
+ }
1554
+ const checkedAt = now.toISOString();
1555
+ const status = fetched.body.ok ? "pass" : "fail";
1556
+ const summary = `health ${fetched.body.ok ? "ok" : "not ok"} (prismic ${fetched.body.prismic ?? "?"})`;
1557
+ return {
1558
+ audit: "function-health",
1559
+ site: label,
1560
+ status,
1561
+ summary,
1562
+ details: {
1563
+ ok: fetched.body.ok,
1564
+ prismic: fetched.body.prismic,
1565
+ forms: fetched.body.forms,
1566
+ checkedAt
1567
+ }
1568
+ };
1569
+ }
1570
+
1571
+ // src/audits/smoke.ts
1572
+ import { access, readFile as readFile6 } from "fs/promises";
1573
+ import { join as join7 } from "path";
1574
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
1575
+ function summarizeSmokeFailure(stdout, stderr) {
1576
+ const lines = stdout.replace(ANSI, "").split("\n").map((l) => l.trim());
1577
+ const failing = [];
1578
+ for (let i = 0; i < lines.length; i++) {
1579
+ const line = lines[i];
1580
+ if (line === void 0) continue;
1581
+ if (/^\d+\)\s/.test(line)) {
1582
+ failing.push(line);
1583
+ let taken = 0;
1584
+ for (let j = i + 1; j < lines.length && taken < 3; j++) {
1585
+ const next = lines[j];
1586
+ if (!next) continue;
1587
+ if (/^\d+\)\s/.test(next)) break;
1588
+ failing.push(next);
1589
+ taken++;
1590
+ }
1591
+ break;
1592
+ }
1593
+ }
1594
+ const tally = lines.find((l) => l !== void 0 && /\b\d+\s+failed\b/.test(l));
1595
+ const distilled = [tally, ...failing].filter(Boolean).join(" | ");
1596
+ if (distilled) return distilled.slice(0, 300);
1597
+ const err = stderr.replace(ANSI, "").trim();
1598
+ return err ? err.slice(0, 200) : "no reporter output";
1599
+ }
1600
+ async function hasTestSmokeScript(sitePath) {
1601
+ try {
1602
+ const raw = await readFile6(join7(sitePath, "package.json"), "utf-8");
1603
+ const pkg = JSON.parse(raw);
1604
+ return typeof pkg.scripts?.["test:smoke"] === "string";
1605
+ } catch {
1606
+ return false;
1607
+ }
1608
+ }
1609
+ async function hasNodeModules(sitePath) {
1610
+ try {
1611
+ await access(join7(sitePath, "node_modules"));
1612
+ return true;
1613
+ } catch {
1614
+ return false;
1615
+ }
1616
+ }
1617
+ async function smokeAudit(ctx) {
1618
+ const spawn = ctx.spawn ?? defaultSpawn;
1619
+ const site = ctx.site;
1620
+ const label = siteLabel(site);
1621
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1622
+ const checkedAt = now.toISOString();
1623
+ if (!await hasTestSmokeScript(site.path)) {
1624
+ return {
1625
+ audit: "smoke",
1626
+ site: label,
1627
+ status: "skip",
1628
+ summary: "no test:smoke script"
1629
+ };
1630
+ }
1631
+ if (!await hasNodeModules(site.path)) {
1632
+ let install;
1633
+ try {
1634
+ install = await spawn("pnpm", ["install", "--frozen-lockfile"], {
1635
+ cwd: site.path,
1636
+ timeoutMs: 5 * 6e4
1637
+ });
1638
+ } catch (err) {
1639
+ const e = err;
1640
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1641
+ return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
1642
+ }
1643
+ throw err;
1644
+ }
1645
+ if (install.code !== 0) {
1646
+ const retry = await spawn("pnpm", ["install"], { cwd: site.path, timeoutMs: 5 * 6e4 });
1647
+ if (retry.code !== 0) {
1648
+ return {
1649
+ audit: "smoke",
1650
+ site: label,
1651
+ status: "skip",
1652
+ summary: `smoke: pnpm install failed (exit ${retry.code}) \u2014 deps unavailable`
1653
+ };
1654
+ }
1655
+ }
1656
+ }
1657
+ const port = await findFreePort();
1658
+ let raw;
1659
+ try {
1660
+ raw = await spawn("pnpm", ["test:smoke"], {
1661
+ cwd: site.path,
1662
+ env: { ...process.env, REDDOOR_SMOKE_PORT: String(port) },
1663
+ // Playwright on a cold tree installs chromium, boots the site's dev server,
1664
+ // and runs the smoke specs — the shared 30s default starves it (mirrors a11y).
1665
+ timeoutMs: 5 * 6e4
1666
+ });
1667
+ } catch (err) {
1668
+ const e = err;
1669
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1670
+ return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
1671
+ }
1672
+ throw err;
1673
+ }
1674
+ if (raw.code === 0) {
1675
+ return {
1676
+ audit: "smoke",
1677
+ site: label,
1678
+ status: "pass",
1679
+ summary: "smoke: suite green",
1680
+ details: { ok: "pass", checkedAt }
1681
+ };
1682
+ }
1683
+ return {
1684
+ audit: "smoke",
1685
+ site: label,
1686
+ status: "fail",
1687
+ summary: `smoke: suite failed (exit ${raw.code}) \u2014 ${summarizeSmokeFailure(raw.stdout, raw.stderr)}`,
1688
+ details: { ok: "fail", checkedAt }
1689
+ };
1690
+ }
1691
+
1692
+ // src/audits/form-e2e.ts
1693
+ var CF_TEST_SITEKEY = "1x00000000000000000000AA";
1694
+ var CONTACT_PATH = "/contact";
1695
+ var LIVE_ENV_VAR = "REDDOOR_FORM_E2E_LIVE";
1696
+ function liveRunnerEnabled(env = process.env) {
1697
+ return env[LIVE_ENV_VAR] === "1" || env[LIVE_ENV_VAR] === "true";
1698
+ }
1699
+ async function formE2eAudit(ctx) {
1700
+ const { site } = ctx;
1701
+ const label = siteLabel(site);
1702
+ if (!site.deployedUrl) {
1703
+ return { audit: "form-e2e", site: label, status: "skip", summary: "no deployed URL" };
1704
+ }
1705
+ if (!ctx.formRunner && !liveRunnerEnabled()) {
1706
+ return {
1707
+ audit: "form-e2e",
1708
+ site: label,
1709
+ status: "skip",
1710
+ summary: `live form-e2e disabled (central testMode ingest suppression not yet wired \u2014 set ${LIVE_ENV_VAR}=1 once it is)`
1711
+ };
1712
+ }
1713
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1714
+ const checkedAt = now.toISOString();
1715
+ const runner = ctx.formRunner ?? await defaultFormRunner();
1716
+ try {
1717
+ const outcome = await runner.submit({
1718
+ baseUrl: site.deployedUrl,
1719
+ testMode: true,
1720
+ testSitekey: CF_TEST_SITEKEY
1721
+ });
1722
+ if ("testModeUndeclared" in outcome) {
1723
+ return {
1724
+ audit: "form-e2e",
1725
+ site: label,
1726
+ status: "skip",
1727
+ summary: "site /health does not declare forms.testMode \u2014 probe refused (testMode forwarding not yet rolled out here)"
1728
+ };
1729
+ }
1730
+ if (!outcome.formPresent) {
1731
+ return {
1732
+ audit: "form-e2e",
1733
+ site: label,
1734
+ status: "skip",
1735
+ summary: "no contact form (n/a)",
1736
+ details: { ok: null, formPresent: false, checkedAt }
1737
+ };
1738
+ }
1739
+ const ok = outcome.success ? "pass" : "fail";
1740
+ return {
1741
+ audit: "form-e2e",
1742
+ site: label,
1743
+ status: outcome.success ? "pass" : "warn",
1744
+ summary: outcome.success ? "form-e2e: synthetic submission succeeded" : `form-e2e: synthetic submission failed${outcome.detail ? ` \u2014 ${outcome.detail}` : ""}`,
1745
+ details: { ok, formPresent: true, checkedAt }
1746
+ };
1747
+ } finally {
1748
+ await runner.close?.();
1749
+ }
1750
+ }
1751
+ var FILL_SETTLE_MS = 1200;
1752
+ var PAGE_TIMEOUT_MS2 = 3e4;
1753
+ var HEALTH_TIMEOUT_MS2 = 1e4;
1754
+ async function declaresTestModeForwarding(baseUrl) {
1755
+ try {
1756
+ const res = await fetch(new URL("/health", baseUrl), {
1757
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS2)
1758
+ });
1759
+ if (!res.ok) return false;
1760
+ const body = await res.json();
1761
+ if (typeof body !== "object" || body === null) return false;
1762
+ const forms = body.forms;
1763
+ if (typeof forms !== "object" || forms === null) return false;
1764
+ return forms.testMode === true;
1765
+ } catch {
1766
+ return false;
1767
+ }
1768
+ }
1769
+ async function defaultFormRunner() {
1770
+ const { chromium } = await import("@playwright/test");
1771
+ return {
1772
+ async submit({ baseUrl, testSitekey }) {
1773
+ if (!await declaresTestModeForwarding(baseUrl)) {
1774
+ return { testModeUndeclared: true };
1775
+ }
1776
+ const browser = await chromium.launch();
1777
+ try {
1778
+ const ctx = await browser.newContext();
1779
+ const page = await ctx.newPage();
1780
+ const url = new URL(CONTACT_PATH, baseUrl).toString();
1781
+ const resp = await page.goto(url, { waitUntil: "domcontentloaded", timeout: PAGE_TIMEOUT_MS2 }).catch(() => null);
1782
+ const form = page.locator("form").first();
1783
+ const hasForm = !!resp && resp.ok() && await form.count().catch(() => 0) > 0 && await page.locator('input[name="email"], input[type="email"]').count().catch(() => 0) > 0;
1784
+ if (!hasForm) return { formPresent: false };
1785
+ await page.fill('[name="name"]', "Reddoor Monitor").catch(() => {
1786
+ });
1787
+ await page.fill('[name="email"]', "monitor+e2e@reddoorla.com").catch(() => {
1788
+ });
1789
+ await page.fill('[name="phone"]', "5555550123").catch(() => {
1790
+ });
1791
+ await page.fill('[name="message"]', "Synthetic end-to-end health check \u2014 please ignore.").catch(() => {
1792
+ });
1793
+ await page.evaluate(`
1794
+ (function () {
1795
+ const f = document.querySelector("form");
1796
+ if (!f) return;
1797
+ const add = (name, value) => {
1798
+ let el = f.querySelector('input[name="' + name + '"]');
1799
+ if (!el) {
1800
+ el = document.createElement("input");
1801
+ el.type = "hidden";
1802
+ el.name = name;
1803
+ f.appendChild(el);
1804
+ }
1805
+ el.value = value;
1806
+ };
1807
+ add("testMode", "true");
1808
+ add("cf-turnstile-response", ${JSON.stringify(`testmode-${testSitekey}`)});
1809
+ })();
1810
+ `);
1811
+ await page.waitForTimeout(FILL_SETTLE_MS);
1812
+ const postResponse = page.waitForResponse((r) => r.request().method() === "POST", {
1813
+ timeout: PAGE_TIMEOUT_MS2
1814
+ }).catch(() => null);
1815
+ await page.locator('button[type="submit"], input[type="submit"]').first().click({ timeout: PAGE_TIMEOUT_MS2 });
1816
+ const ok = await page.locator('[role="status"]').first().waitFor({ state: "visible", timeout: PAGE_TIMEOUT_MS2 }).then(() => true).catch(() => false);
1817
+ if (ok) return { formPresent: true, success: true };
1818
+ const actionResp = await postResponse;
1819
+ const alertText = await page.locator('[role="alert"]').first().textContent({ timeout: 1e3 }).catch(() => null);
1820
+ const respBit = actionResp ? `POST ${actionResp.status()}${actionResp.status() >= 400 ? ` ${(await actionResp.text().catch(() => "")).slice(0, 80)}` : ""}` : "no POST observed";
1821
+ return {
1822
+ formPresent: true,
1823
+ success: false,
1824
+ detail: `no success banner after submit \u2014 ${respBit}${alertText ? `; alert: ${alertText.trim().slice(0, 80)}` : ""}`
1825
+ };
1826
+ } catch (err) {
1827
+ return { formPresent: true, success: false, detail: String(err).slice(0, 120) };
1828
+ } finally {
1829
+ await browser.close().catch(() => {
1830
+ });
1831
+ }
1832
+ }
1833
+ };
1834
+ }
1835
+
1459
1836
  // src/audits/index.ts
1460
1837
  var REGISTRY = {
1461
1838
  deps: depsAudit,
@@ -1465,7 +1842,10 @@ var REGISTRY = {
1465
1842
  a11y: a11yAudit,
1466
1843
  domain: domainAudit,
1467
1844
  browser: browserAudit,
1468
- "netlify-deploy": netlifyDeployAudit
1845
+ "netlify-deploy": netlifyDeployAudit,
1846
+ "function-health": functionHealthAudit,
1847
+ smoke: smokeAudit,
1848
+ "form-e2e": formE2eAudit
1469
1849
  };
1470
1850
  var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
1471
1851
  var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
@@ -1510,4 +1890,4 @@ export {
1510
1890
  runAudits,
1511
1891
  runAuditsAcross
1512
1892
  };
1513
- //# sourceMappingURL=chunk-6G7BC5SN.js.map
1893
+ //# sourceMappingURL=chunk-M7HOQ4W7.js.map