@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
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  a11yRoutes,
21
21
  smokeRoutes
22
- } from "./chunk-LYCKN7ZR.js";
22
+ } from "./chunk-LJPCMXR7.js";
23
23
 
24
24
  // src/audits/deps.ts
25
25
  import { readFile } from "fs/promises";
@@ -496,7 +496,10 @@ async function dependabotAudit(deps, repo, label) {
496
496
  let alerts;
497
497
  try {
498
498
  alerts = await deps.listAlerts(repo);
499
- } catch {
499
+ } catch (e) {
500
+ console.error(
501
+ `[security] ${label}: dependabot API failed \u2014 degraded to pnpm audit: ${e instanceof Error ? e.message : String(e)}`
502
+ );
500
503
  return null;
501
504
  }
502
505
  const counts = { low: 0, moderate: 0, high: 0, critical: 0 };
@@ -1187,11 +1190,29 @@ function summarizeBrowser(routes, links, familyCounts) {
1187
1190
  const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
1188
1191
  const brokenLinks = links.filter((l) => isBroken(l.status)).length;
1189
1192
  const linksOk = links.length > 0 && brokenLinks === 0;
1193
+ const reachableOk = routes.length > 0 && routes.every((r) => r.status !== null && r.status >= 200 && r.status < 400);
1194
+ const trimmedTitles = routes.map((r) => (r.title ?? "").trim());
1195
+ const eachTitleMetaValid = routes.length > 0 && routes.every((r, i) => {
1196
+ const t = trimmedTitles[i];
1197
+ const desc = (r.metaDescription ?? "").trim();
1198
+ return t.length > 0 && t.length <= 70 && desc.length > 0;
1199
+ });
1200
+ const noDuplicateTitles = new Set(trimmedTitles).size === trimmedTitles.length;
1201
+ const titleMetaOk = eachTitleMetaValid && noDuplicateTitles;
1190
1202
  const engines = [...new Set(desktopChecks.map((d) => d.engine))];
1191
1203
  const devices = [...new Set(mobileChecks.map((m) => m.device))];
1192
1204
  const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
1193
1205
  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 };
1206
+ return {
1207
+ desktopOk,
1208
+ mobileOk,
1209
+ linksOk,
1210
+ reachableOk,
1211
+ titleMetaOk,
1212
+ brokenLinks,
1213
+ routesChecked: routes.length,
1214
+ note
1215
+ };
1195
1216
  }
1196
1217
  async function browserAudit(ctx) {
1197
1218
  const { site } = ctx;
@@ -1263,6 +1284,9 @@ async function defaultBrowserRunner() {
1263
1284
  for (const url of urls) {
1264
1285
  const desktop = [];
1265
1286
  const linkSet = /* @__PURE__ */ new Set();
1287
+ let status = null;
1288
+ let title = null;
1289
+ let metaDescription = null;
1266
1290
  for (let i = 0; i < desktopEngines.length; i++) {
1267
1291
  const engine = desktopEngines[i].engine;
1268
1292
  const browser = browsers[i];
@@ -1279,6 +1303,11 @@ async function defaultBrowserRunner() {
1279
1303
  const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
1280
1304
  ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
1281
1305
  if (engine === "chromium") {
1306
+ status = resp ? resp.status() : null;
1307
+ title = (await page.title().catch(() => "")).trim() || null;
1308
+ metaDescription = (await page.evaluate(
1309
+ `document.querySelector('meta[name="description"]')?.getAttribute('content') || ''`
1310
+ ).catch(() => "") || "").trim() || null;
1282
1311
  const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
1283
1312
  const origin = new URL(url).origin;
1284
1313
  for (const h of hrefs) {
@@ -1320,7 +1349,15 @@ async function defaultBrowserRunner() {
1320
1349
  }
1321
1350
  mobile.push({ device, ok });
1322
1351
  }
1323
- results.push({ url, desktop, mobile, links: [...linkSet] });
1352
+ results.push({
1353
+ url,
1354
+ desktop,
1355
+ mobile,
1356
+ links: [...linkSet],
1357
+ status,
1358
+ title,
1359
+ metaDescription
1360
+ });
1324
1361
  }
1325
1362
  } finally {
1326
1363
  await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
@@ -1357,6 +1394,7 @@ async function defaultBrowserRunner() {
1357
1394
  }
1358
1395
 
1359
1396
  // src/audits/netlify-deploy.ts
1397
+ var NETLIFY_FETCH_TIMEOUT_MS = 15e3;
1360
1398
  var READY_STATES = /* @__PURE__ */ new Set(["ready"]);
1361
1399
  var FAILED_STATES = /* @__PURE__ */ new Set(["error", "failed", "rejected"]);
1362
1400
  function isReadyState(state) {
@@ -1381,7 +1419,8 @@ function defaultNetlifyDeployDeps(token, now, fetchImpl = fetch) {
1381
1419
  let res;
1382
1420
  try {
1383
1421
  res = await fetchImpl(url, {
1384
- headers: { Authorization: `Bearer ${token}` }
1422
+ headers: { Authorization: `Bearer ${token}` },
1423
+ signal: AbortSignal.timeout(NETLIFY_FETCH_TIMEOUT_MS)
1385
1424
  });
1386
1425
  } catch {
1387
1426
  return { ok: false };
@@ -1456,6 +1495,349 @@ async function netlifyDeployAudit(ctx) {
1456
1495
  };
1457
1496
  }
1458
1497
 
1498
+ // src/audits/function-health.ts
1499
+ var HEALTH_TIMEOUT_MS = 1e4;
1500
+ var ERRORING_BODY = { ok: false, prismic: null, forms: null };
1501
+ function parseHealthBody(raw) {
1502
+ if (!raw || typeof raw !== "object") return { present: false };
1503
+ const o = raw;
1504
+ if (typeof o["ok"] !== "boolean") return { present: false };
1505
+ const prismic = o["prismic"] === "ok" || o["prismic"] === "error" || o["prismic"] === "skipped" ? o["prismic"] : null;
1506
+ const f = o["forms"];
1507
+ const forms = f && typeof f === "object" ? {
1508
+ ingestUrl: f["ingestUrl"] === true,
1509
+ ingestToken: f["ingestToken"] === true,
1510
+ turnstile: f["turnstile"] === true
1511
+ } : null;
1512
+ return { present: true, body: { ok: o["ok"], prismic, forms } };
1513
+ }
1514
+ function defaultFunctionHealthDeps(now, fetchImpl = fetch) {
1515
+ return {
1516
+ now,
1517
+ fetchHealth: async (deployedUrl) => {
1518
+ let url;
1519
+ try {
1520
+ url = new URL("/health", deployedUrl).toString();
1521
+ } catch {
1522
+ return { present: false };
1523
+ }
1524
+ let res;
1525
+ try {
1526
+ res = await fetchImpl(url, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
1527
+ } catch {
1528
+ return { present: false };
1529
+ }
1530
+ if (res.status === 404) return { present: false };
1531
+ if (!res.ok) return { present: true, body: ERRORING_BODY };
1532
+ let body;
1533
+ try {
1534
+ body = await res.json();
1535
+ } catch {
1536
+ return { present: true, body: ERRORING_BODY };
1537
+ }
1538
+ return parseHealthBody(body);
1539
+ }
1540
+ };
1541
+ }
1542
+ async function functionHealthAudit(ctx) {
1543
+ const { site } = ctx;
1544
+ const label = siteLabel(site);
1545
+ if (!site.deployedUrl) {
1546
+ return { audit: "function-health", site: label, status: "skip", summary: "no deployed URL" };
1547
+ }
1548
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1549
+ const deps = ctx.functionHealthDeps ?? defaultFunctionHealthDeps(now);
1550
+ const fetched = await deps.fetchHealth(site.deployedUrl).catch(() => ({ present: false }));
1551
+ if (!fetched.present) {
1552
+ return {
1553
+ audit: "function-health",
1554
+ site: label,
1555
+ status: "skip",
1556
+ summary: "health endpoint unreachable / not adopted"
1557
+ };
1558
+ }
1559
+ const checkedAt = now.toISOString();
1560
+ const status = fetched.body.ok ? "pass" : "fail";
1561
+ const summary = `health ${fetched.body.ok ? "ok" : "not ok"} (prismic ${fetched.body.prismic ?? "?"})`;
1562
+ return {
1563
+ audit: "function-health",
1564
+ site: label,
1565
+ status,
1566
+ summary,
1567
+ details: {
1568
+ ok: fetched.body.ok,
1569
+ prismic: fetched.body.prismic,
1570
+ forms: fetched.body.forms,
1571
+ checkedAt
1572
+ }
1573
+ };
1574
+ }
1575
+
1576
+ // src/audits/smoke.ts
1577
+ import { access, readFile as readFile6 } from "fs/promises";
1578
+ import { join as join7 } from "path";
1579
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
1580
+ function summarizeSmokeFailure(stdout, stderr) {
1581
+ const lines = stdout.replace(ANSI, "").split("\n").map((l) => l.trim());
1582
+ const failing = [];
1583
+ for (let i = 0; i < lines.length; i++) {
1584
+ const line = lines[i];
1585
+ if (line === void 0) continue;
1586
+ if (/^\d+\)\s/.test(line)) {
1587
+ failing.push(line);
1588
+ let taken = 0;
1589
+ for (let j = i + 1; j < lines.length && taken < 3; j++) {
1590
+ const next = lines[j];
1591
+ if (!next) continue;
1592
+ if (/^\d+\)\s/.test(next)) break;
1593
+ failing.push(next);
1594
+ taken++;
1595
+ }
1596
+ break;
1597
+ }
1598
+ }
1599
+ const tally = lines.find((l) => l !== void 0 && /\b\d+\s+failed\b/.test(l));
1600
+ const distilled = [tally, ...failing].filter(Boolean).join(" | ");
1601
+ if (distilled) return distilled.slice(0, 300);
1602
+ const err = stderr.replace(ANSI, "").trim();
1603
+ return err ? err.slice(0, 200) : "no reporter output";
1604
+ }
1605
+ async function hasTestSmokeScript(sitePath) {
1606
+ try {
1607
+ const raw = await readFile6(join7(sitePath, "package.json"), "utf-8");
1608
+ const pkg = JSON.parse(raw);
1609
+ return typeof pkg.scripts?.["test:smoke"] === "string";
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ }
1614
+ async function hasNodeModules(sitePath) {
1615
+ try {
1616
+ await access(join7(sitePath, "node_modules"));
1617
+ return true;
1618
+ } catch {
1619
+ return false;
1620
+ }
1621
+ }
1622
+ async function smokeAudit(ctx) {
1623
+ const spawn = ctx.spawn ?? defaultSpawn;
1624
+ const site = ctx.site;
1625
+ const label = siteLabel(site);
1626
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1627
+ const checkedAt = now.toISOString();
1628
+ if (!await hasTestSmokeScript(site.path)) {
1629
+ return {
1630
+ audit: "smoke",
1631
+ site: label,
1632
+ status: "skip",
1633
+ summary: "no test:smoke script"
1634
+ };
1635
+ }
1636
+ if (!await hasNodeModules(site.path)) {
1637
+ let install;
1638
+ try {
1639
+ install = await spawn("pnpm", ["install", "--frozen-lockfile"], {
1640
+ cwd: site.path,
1641
+ timeoutMs: 5 * 6e4
1642
+ });
1643
+ } catch (err) {
1644
+ const e = err;
1645
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1646
+ return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
1647
+ }
1648
+ throw err;
1649
+ }
1650
+ if (install.code !== 0) {
1651
+ const retry = await spawn("pnpm", ["install"], { cwd: site.path, timeoutMs: 5 * 6e4 });
1652
+ if (retry.code !== 0) {
1653
+ return {
1654
+ audit: "smoke",
1655
+ site: label,
1656
+ status: "skip",
1657
+ summary: `smoke: pnpm install failed (exit ${retry.code}) \u2014 deps unavailable`
1658
+ };
1659
+ }
1660
+ }
1661
+ }
1662
+ const port = await findFreePort();
1663
+ let raw;
1664
+ try {
1665
+ raw = await spawn("pnpm", ["test:smoke"], {
1666
+ cwd: site.path,
1667
+ env: { ...process.env, REDDOOR_SMOKE_PORT: String(port) },
1668
+ // Playwright on a cold tree installs chromium, boots the site's dev server,
1669
+ // and runs the smoke specs — the shared 30s default starves it (mirrors a11y).
1670
+ timeoutMs: 5 * 6e4
1671
+ });
1672
+ } catch (err) {
1673
+ const e = err;
1674
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1675
+ return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
1676
+ }
1677
+ throw err;
1678
+ }
1679
+ if (raw.code === 0) {
1680
+ return {
1681
+ audit: "smoke",
1682
+ site: label,
1683
+ status: "pass",
1684
+ summary: "smoke: suite green",
1685
+ details: { ok: "pass", checkedAt }
1686
+ };
1687
+ }
1688
+ return {
1689
+ audit: "smoke",
1690
+ site: label,
1691
+ status: "fail",
1692
+ summary: `smoke: suite failed (exit ${raw.code}) \u2014 ${summarizeSmokeFailure(raw.stdout, raw.stderr)}`,
1693
+ details: { ok: "fail", checkedAt }
1694
+ };
1695
+ }
1696
+
1697
+ // src/audits/form-e2e.ts
1698
+ var CF_TEST_SITEKEY = "1x00000000000000000000AA";
1699
+ var CONTACT_PATH = "/contact";
1700
+ var LIVE_ENV_VAR = "REDDOOR_FORM_E2E_LIVE";
1701
+ function liveRunnerEnabled(env = process.env) {
1702
+ return env[LIVE_ENV_VAR] === "1" || env[LIVE_ENV_VAR] === "true";
1703
+ }
1704
+ async function formE2eAudit(ctx) {
1705
+ const { site } = ctx;
1706
+ const label = siteLabel(site);
1707
+ if (!site.deployedUrl) {
1708
+ return { audit: "form-e2e", site: label, status: "skip", summary: "no deployed URL" };
1709
+ }
1710
+ if (!ctx.formRunner && !liveRunnerEnabled()) {
1711
+ return {
1712
+ audit: "form-e2e",
1713
+ site: label,
1714
+ status: "skip",
1715
+ summary: `live form-e2e disabled (central testMode ingest suppression not yet wired \u2014 set ${LIVE_ENV_VAR}=1 once it is)`
1716
+ };
1717
+ }
1718
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1719
+ const checkedAt = now.toISOString();
1720
+ const runner = ctx.formRunner ?? await defaultFormRunner();
1721
+ try {
1722
+ const outcome = await runner.submit({
1723
+ baseUrl: site.deployedUrl,
1724
+ testMode: true,
1725
+ testSitekey: CF_TEST_SITEKEY
1726
+ });
1727
+ if ("testModeUndeclared" in outcome) {
1728
+ return {
1729
+ audit: "form-e2e",
1730
+ site: label,
1731
+ status: "skip",
1732
+ summary: "site /health does not declare forms.testMode \u2014 probe refused (testMode forwarding not yet rolled out here)"
1733
+ };
1734
+ }
1735
+ if (!outcome.formPresent) {
1736
+ return {
1737
+ audit: "form-e2e",
1738
+ site: label,
1739
+ status: "skip",
1740
+ summary: "no contact form (n/a)",
1741
+ details: { ok: null, formPresent: false, checkedAt }
1742
+ };
1743
+ }
1744
+ const ok = outcome.success ? "pass" : "fail";
1745
+ return {
1746
+ audit: "form-e2e",
1747
+ site: label,
1748
+ status: outcome.success ? "pass" : "warn",
1749
+ summary: outcome.success ? "form-e2e: synthetic submission succeeded" : `form-e2e: synthetic submission failed${outcome.detail ? ` \u2014 ${outcome.detail}` : ""}`,
1750
+ details: { ok, formPresent: true, checkedAt }
1751
+ };
1752
+ } finally {
1753
+ await runner.close?.();
1754
+ }
1755
+ }
1756
+ var FILL_SETTLE_MS = 1200;
1757
+ var PAGE_TIMEOUT_MS2 = 3e4;
1758
+ var HEALTH_TIMEOUT_MS2 = 1e4;
1759
+ async function declaresTestModeForwarding(baseUrl) {
1760
+ try {
1761
+ const res = await fetch(new URL("/health", baseUrl), {
1762
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS2)
1763
+ });
1764
+ if (!res.ok) return false;
1765
+ const body = await res.json();
1766
+ if (typeof body !== "object" || body === null) return false;
1767
+ const forms = body.forms;
1768
+ if (typeof forms !== "object" || forms === null) return false;
1769
+ return forms.testMode === true;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ }
1774
+ async function defaultFormRunner() {
1775
+ const { chromium } = await import("@playwright/test");
1776
+ return {
1777
+ async submit({ baseUrl, testSitekey }) {
1778
+ if (!await declaresTestModeForwarding(baseUrl)) {
1779
+ return { testModeUndeclared: true };
1780
+ }
1781
+ const browser = await chromium.launch();
1782
+ try {
1783
+ const ctx = await browser.newContext();
1784
+ const page = await ctx.newPage();
1785
+ const url = new URL(CONTACT_PATH, baseUrl).toString();
1786
+ const resp = await page.goto(url, { waitUntil: "domcontentloaded", timeout: PAGE_TIMEOUT_MS2 }).catch(() => null);
1787
+ const form = page.locator("form").first();
1788
+ const hasForm = !!resp && resp.ok() && await form.count().catch(() => 0) > 0 && await page.locator('input[name="email"], input[type="email"]').count().catch(() => 0) > 0;
1789
+ if (!hasForm) return { formPresent: false };
1790
+ await page.fill('[name="name"]', "Reddoor Monitor").catch(() => {
1791
+ });
1792
+ await page.fill('[name="email"]', "monitor+e2e@reddoorla.com").catch(() => {
1793
+ });
1794
+ await page.fill('[name="phone"]', "5555550123").catch(() => {
1795
+ });
1796
+ await page.fill('[name="message"]', "Synthetic end-to-end health check \u2014 please ignore.").catch(() => {
1797
+ });
1798
+ await page.evaluate(`
1799
+ (function () {
1800
+ const f = document.querySelector("form");
1801
+ if (!f) return;
1802
+ const add = (name, value) => {
1803
+ let el = f.querySelector('input[name="' + name + '"]');
1804
+ if (!el) {
1805
+ el = document.createElement("input");
1806
+ el.type = "hidden";
1807
+ el.name = name;
1808
+ f.appendChild(el);
1809
+ }
1810
+ el.value = value;
1811
+ };
1812
+ add("testMode", "true");
1813
+ add("cf-turnstile-response", ${JSON.stringify(`testmode-${testSitekey}`)});
1814
+ })();
1815
+ `);
1816
+ await page.waitForTimeout(FILL_SETTLE_MS);
1817
+ const postResponse = page.waitForResponse((r) => r.request().method() === "POST", {
1818
+ timeout: PAGE_TIMEOUT_MS2
1819
+ }).catch(() => null);
1820
+ await page.locator('button[type="submit"], input[type="submit"]').first().click({ timeout: PAGE_TIMEOUT_MS2 });
1821
+ const ok = await page.locator('[role="status"]').first().waitFor({ state: "visible", timeout: PAGE_TIMEOUT_MS2 }).then(() => true).catch(() => false);
1822
+ if (ok) return { formPresent: true, success: true };
1823
+ const actionResp = await postResponse;
1824
+ const alertText = await page.locator('[role="alert"]').first().textContent({ timeout: 1e3 }).catch(() => null);
1825
+ const respBit = actionResp ? `POST ${actionResp.status()}${actionResp.status() >= 400 ? ` ${(await actionResp.text().catch(() => "")).slice(0, 80)}` : ""}` : "no POST observed";
1826
+ return {
1827
+ formPresent: true,
1828
+ success: false,
1829
+ detail: `no success banner after submit \u2014 ${respBit}${alertText ? `; alert: ${alertText.trim().slice(0, 80)}` : ""}`
1830
+ };
1831
+ } catch (err) {
1832
+ return { formPresent: true, success: false, detail: String(err).slice(0, 120) };
1833
+ } finally {
1834
+ await browser.close().catch(() => {
1835
+ });
1836
+ }
1837
+ }
1838
+ };
1839
+ }
1840
+
1459
1841
  // src/audits/index.ts
1460
1842
  var REGISTRY = {
1461
1843
  deps: depsAudit,
@@ -1465,7 +1847,10 @@ var REGISTRY = {
1465
1847
  a11y: a11yAudit,
1466
1848
  domain: domainAudit,
1467
1849
  browser: browserAudit,
1468
- "netlify-deploy": netlifyDeployAudit
1850
+ "netlify-deploy": netlifyDeployAudit,
1851
+ "function-health": functionHealthAudit,
1852
+ smoke: smokeAudit,
1853
+ "form-e2e": formE2eAudit
1469
1854
  };
1470
1855
  var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
1471
1856
  var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
@@ -1510,4 +1895,4 @@ export {
1510
1895
  runAudits,
1511
1896
  runAuditsAcross
1512
1897
  };
1513
- //# sourceMappingURL=chunk-6G7BC5SN.js.map
1898
+ //# sourceMappingURL=chunk-BHE5CUWI.js.map