@reddoorla/maintenance 0.49.0 → 0.50.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.
@@ -1,4 +1,4 @@
1
- import { S as Site, a as AuditResult, A as AuditName } from '../../types-DeKpgkG-.js';
1
+ import { S as Site, a as AuditResult, A as AuditName } from '../../types-QG-QhCYh.js';
2
2
 
3
3
  type AuditCommandOptions = {
4
4
  only?: string;
@@ -32,10 +32,8 @@ declare function parseConcurrency(value: string | undefined): boolean | number;
32
32
  * null when there's nothing to warn about. Keeps the mixed-provenance result
33
33
  * table from silently confusing the operator. */
34
34
  declare function deployedUrlNotice(which: AuditName[], url: string | undefined, cwd: string): string | null;
35
- /** A fleet site needs a local checkout unless every requested audit can run
36
- * against its deployed URL. Today only lighthouse has a deployed mode, so a
37
- * site is checkout-free exactly when it has a `deployedUrl` and lighthouse is
38
- * the only requested audit. */
35
+ /** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site
36
+ * has a `deployedUrl` for them to run against. */
39
37
  declare function auditNeedsCheckout(site: Site, which: AuditName[]): boolean;
40
38
  /** Apply a single-site `--url` to the resolved sites. Returns the input
41
39
  * untouched when no url is given; otherwise requires exactly one site and
@@ -159,6 +159,14 @@ function mapRow(rec) {
159
159
  securityVulnsHigh: f["Security Vulns High"] ?? null,
160
160
  securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
161
161
  securityVulnsLow: f["Security Vulns Low"] ?? null,
162
+ lastSecurityAuditAt: f["Last security audit at"] ?? null,
163
+ certDaysRemaining: f["Cert days remaining"] ?? null,
164
+ domainCheckedAt: f["Domain checked at"] ?? null,
165
+ crossbrowserOk: typeof f["Crossbrowser OK"] === "boolean" ? f["Crossbrowser OK"] : null,
166
+ mobileOk: typeof f["Mobile OK"] === "boolean" ? f["Mobile OK"] : null,
167
+ linksOk: typeof f["Links OK"] === "boolean" ? f["Links OK"] : null,
168
+ brokenLinks: typeof f["Broken links"] === "number" ? f["Broken links"] : null,
169
+ browserCheckedAt: f["Browser checked at"] ?? null,
162
170
  copyIntro: trimToNull(f["Copy \u2014 Intro"]),
163
171
  copyContact: trimToNull(f["Copy \u2014 Contact"]),
164
172
  copyFooter: trimToNull(f["Copy \u2014 Footer"]),
@@ -220,7 +228,24 @@ function securityFields(counts) {
220
228
  "Security Vulns Critical": counts.critical,
221
229
  "Security Vulns High": counts.high,
222
230
  "Security Vulns Moderate": counts.moderate,
223
- "Security Vulns Low": counts.low
231
+ "Security Vulns Low": counts.low,
232
+ // Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent
233
+ // audit (a clean count from months ago must not silently keep ticking the box).
234
+ "Last security audit at": (/* @__PURE__ */ new Date()).toISOString()
235
+ };
236
+ }
237
+ function domainFields(result) {
238
+ const fields = { "Domain checked at": result.checkedAt };
239
+ if (result.certDaysRemaining !== null) fields["Cert days remaining"] = result.certDaysRemaining;
240
+ return fields;
241
+ }
242
+ function browserFields(r) {
243
+ return {
244
+ "Crossbrowser OK": r.desktopOk,
245
+ "Mobile OK": r.mobileOk,
246
+ "Links OK": r.linksOk,
247
+ "Broken links": r.brokenLinks,
248
+ "Browser checked at": r.checkedAt
224
249
  };
225
250
  }
226
251
  async function updateScores(base, recordId, scores) {
@@ -241,6 +266,8 @@ async function updateAuditFields(base, recordId, audits) {
241
266
  if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
242
267
  if (audits.deps) Object.assign(fields, depsFields(audits.deps));
243
268
  if (audits.security) Object.assign(fields, securityFields(audits.security));
269
+ if (audits.domain) Object.assign(fields, domainFields(audits.domain));
270
+ if (audits.browser) Object.assign(fields, browserFields(audits.browser));
244
271
  await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
245
272
  return fields;
246
273
  }
@@ -435,6 +462,53 @@ var init_security_airtable = __esm({
435
462
  }
436
463
  });
437
464
 
465
+ // src/audits/domain-airtable.ts
466
+ function hasDomainResult(result) {
467
+ if (result.audit !== "domain") return false;
468
+ const d = result.details;
469
+ return !!d && typeof d.checkedAt === "string";
470
+ }
471
+ function domainResultFromAudit(result) {
472
+ if (result.audit !== "domain") {
473
+ throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);
474
+ }
475
+ const d = result.details;
476
+ return {
477
+ certDaysRemaining: typeof d?.certDaysRemaining === "number" ? d.certDaysRemaining : null,
478
+ checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
479
+ };
480
+ }
481
+ var init_domain_airtable = __esm({
482
+ "src/audits/domain-airtable.ts"() {
483
+ "use strict";
484
+ }
485
+ });
486
+
487
+ // src/audits/browser-airtable.ts
488
+ function hasBrowserResult(result) {
489
+ if (result.audit !== "browser") return false;
490
+ const d = result.details;
491
+ return !!d && typeof d.checkedAt === "string";
492
+ }
493
+ function browserFieldsFromAudit(result) {
494
+ if (result.audit !== "browser") {
495
+ throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);
496
+ }
497
+ const d = result.details;
498
+ return {
499
+ desktopOk: d?.desktopOk === true,
500
+ mobileOk: d?.mobileOk === true,
501
+ linksOk: d?.linksOk === true,
502
+ brokenLinks: typeof d?.brokenLinks === "number" ? d.brokenLinks : 0,
503
+ checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
504
+ };
505
+ }
506
+ var init_browser_airtable = __esm({
507
+ "src/audits/browser-airtable.ts"() {
508
+ "use strict";
509
+ }
510
+ });
511
+
438
512
  // src/audits/write-audits-to-airtable.ts
439
513
  var write_audits_to_airtable_exports = {};
440
514
  __export(write_audits_to_airtable_exports, {
@@ -445,22 +519,14 @@ __export(write_audits_to_airtable_exports, {
445
519
  async function writeAuditsToAirtable(args) {
446
520
  const { base, websites, slug, results } = args;
447
521
  const lhResult = results.find((r) => r.audit === "lighthouse");
448
- if (!lhResult) {
449
- throw Object.assign(
450
- new Error(
451
- "--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
452
- ),
453
- { exitCode: 2 }
454
- );
455
- }
456
522
  const target = websites.find((w) => siteSlug(w.name) === slug);
457
523
  if (!target) {
458
524
  throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
459
525
  }
460
526
  const writes = [];
461
527
  const audits = {};
462
- const lhHasScores = hasRealScores(lhResult);
463
- if (lhHasScores) {
528
+ const lhHasScores = lhResult ? hasRealScores(lhResult) : false;
529
+ if (lhResult && lhHasScores) {
464
530
  const scores = lighthouseScoresFromResult(lhResult);
465
531
  audits.scores = scores;
466
532
  writes.push({ audit: "lighthouse", counts: scores });
@@ -483,10 +549,22 @@ async function writeAuditsToAirtable(args) {
483
549
  audits.security = counts;
484
550
  writes.push({ audit: "security", counts });
485
551
  }
552
+ const dom = results.find((r) => r.audit === "domain");
553
+ if (dom && hasDomainResult(dom)) {
554
+ const result = domainResultFromAudit(dom);
555
+ audits.domain = result;
556
+ writes.push({ audit: "domain", counts: result });
557
+ }
558
+ const browser = results.find((r) => r.audit === "browser");
559
+ if (browser && hasBrowserResult(browser)) {
560
+ const fields = browserFieldsFromAudit(browser);
561
+ audits.browser = fields;
562
+ writes.push({ audit: "browser", counts: fields });
563
+ }
486
564
  if (Object.keys(audits).length > 0) {
487
565
  await updateAuditFields(base, target.id, audits);
488
566
  }
489
- if (!lhHasScores) {
567
+ if (lhResult && !lhHasScores) {
490
568
  const persisted = writes.map((w) => w.audit);
491
569
  throw Object.assign(
492
570
  new Error(
@@ -537,6 +615,8 @@ var init_write_audits_to_airtable = __esm({
537
615
  init_a11y_airtable();
538
616
  init_deps_airtable();
539
617
  init_security_airtable();
618
+ init_domain_airtable();
619
+ init_browser_airtable();
540
620
  }
541
621
  });
542
622
 
@@ -1485,13 +1565,348 @@ async function a11yAudit(ctx) {
1485
1565
  }
1486
1566
  }
1487
1567
 
1568
+ // src/audits/domain.ts
1569
+ import { promises as dnsPromises } from "dns";
1570
+ import tls from "tls";
1571
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
1572
+ async function checkDomain(url, deps) {
1573
+ let host;
1574
+ try {
1575
+ host = new URL(url).hostname;
1576
+ } catch {
1577
+ return { resolved: false, certDaysRemaining: null };
1578
+ }
1579
+ try {
1580
+ await deps.lookup(host);
1581
+ } catch {
1582
+ return { resolved: false, certDaysRemaining: null };
1583
+ }
1584
+ let validTo;
1585
+ try {
1586
+ validTo = await deps.certValidTo(host);
1587
+ } catch {
1588
+ validTo = null;
1589
+ }
1590
+ if (!validTo || Number.isNaN(validTo.getTime()))
1591
+ return { resolved: true, certDaysRemaining: null };
1592
+ return {
1593
+ resolved: true,
1594
+ certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY)
1595
+ };
1596
+ }
1597
+ function defaultDomainDeps(now) {
1598
+ return {
1599
+ lookup: async (host) => {
1600
+ await dnsPromises.lookup(host);
1601
+ },
1602
+ certValidTo: (host) => new Promise((resolvePromise) => {
1603
+ const socket = tls.connect(
1604
+ { host, port: 443, servername: host, timeout: 1e4, rejectUnauthorized: true },
1605
+ () => {
1606
+ const cert = socket.authorized ? socket.getPeerCertificate() : null;
1607
+ socket.end();
1608
+ const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;
1609
+ resolvePromise(validTo);
1610
+ }
1611
+ );
1612
+ socket.on("error", () => resolvePromise(null));
1613
+ socket.on("timeout", () => {
1614
+ socket.destroy();
1615
+ resolvePromise(null);
1616
+ });
1617
+ }),
1618
+ now
1619
+ };
1620
+ }
1621
+ async function domainAudit(ctx) {
1622
+ const { site } = ctx;
1623
+ const label = siteLabel(site);
1624
+ if (!site.deployedUrl) {
1625
+ return { audit: "domain", site: label, status: "skip", summary: "no deployed URL" };
1626
+ }
1627
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1628
+ const deps = ctx.domainDeps ?? defaultDomainDeps(now);
1629
+ const check = await checkDomain(site.deployedUrl, deps);
1630
+ const checkedAt = now.toISOString();
1631
+ const status = check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14 ? "pass" : "warn";
1632
+ const summary = !check.resolved ? "did not resolve" : check.certDaysRemaining === null ? "resolved, no usable TLS cert" : `resolved, cert ${check.certDaysRemaining}d remaining`;
1633
+ return {
1634
+ audit: "domain",
1635
+ site: label,
1636
+ status,
1637
+ summary,
1638
+ details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt }
1639
+ };
1640
+ }
1641
+
1642
+ // src/audits/route-discovery.ts
1643
+ var DEFAULT_CAP = 15;
1644
+ function parseSitemapUrls(xml) {
1645
+ const out = [];
1646
+ const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
1647
+ let m;
1648
+ while ((m = re.exec(xml)) !== null) {
1649
+ const url = m[1];
1650
+ if (url) out.push(url.trim());
1651
+ }
1652
+ return out;
1653
+ }
1654
+ function parseHtmlLinks(html, baseUrl) {
1655
+ const out = /* @__PURE__ */ new Set();
1656
+ const re = /<a\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi;
1657
+ let m;
1658
+ while ((m = re.exec(html)) !== null) {
1659
+ const href = m[1];
1660
+ if (!href || href.startsWith("#") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;
1661
+ try {
1662
+ const u = new URL(href, baseUrl);
1663
+ if (u.origin !== new URL(baseUrl).origin) continue;
1664
+ out.add(u.pathname);
1665
+ } catch {
1666
+ }
1667
+ }
1668
+ return [...out];
1669
+ }
1670
+ function family(pathname) {
1671
+ return pathname.split("/").filter(Boolean)[0] ?? "";
1672
+ }
1673
+ function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
1674
+ const seen = /* @__PURE__ */ new Set(["/"]);
1675
+ const buckets = /* @__PURE__ */ new Map();
1676
+ for (const raw of urlsOrPaths) {
1677
+ let pathname;
1678
+ try {
1679
+ pathname = raw.startsWith("/") ? new URL(raw, "https://x.invalid").pathname : new URL(raw).pathname;
1680
+ } catch {
1681
+ continue;
1682
+ }
1683
+ if (pathname === "/") continue;
1684
+ if (seen.has(pathname)) continue;
1685
+ seen.add(pathname);
1686
+ const fam = family(pathname);
1687
+ const arr = buckets.get(fam) ?? [];
1688
+ arr.push(pathname);
1689
+ buckets.set(fam, arr);
1690
+ }
1691
+ const result = ["/"];
1692
+ const families = [...buckets.values()];
1693
+ let guard = 0;
1694
+ while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 1e4) {
1695
+ for (const fam of families) {
1696
+ if (result.length >= cap) break;
1697
+ const next = fam.shift();
1698
+ if (next) result.push(next);
1699
+ }
1700
+ }
1701
+ return result;
1702
+ }
1703
+ function familyCountsOf(paths) {
1704
+ const counts = {};
1705
+ for (const p of paths) {
1706
+ const key = p === "/" ? "/" : `/${family(p)}`;
1707
+ counts[key] = (counts[key] ?? 0) + 1;
1708
+ }
1709
+ return counts;
1710
+ }
1711
+ async function discoverRoutes(deployedUrl, deps, cap = DEFAULT_CAP) {
1712
+ const origin = new URL(deployedUrl).origin;
1713
+ const abs = (paths) => paths.map((p) => new URL(p, origin).href);
1714
+ const sitemapXml = await deps.fetchText(new URL("/sitemap.xml", origin).href);
1715
+ if (sitemapXml) {
1716
+ const urls = parseSitemapUrls(sitemapXml);
1717
+ if (urls.length > 0) {
1718
+ const paths = sampleRoutePaths(urls, cap);
1719
+ return { routes: abs(paths), source: "sitemap", familyCounts: familyCountsOf(paths) };
1720
+ }
1721
+ }
1722
+ const homeHtml = await deps.fetchText(origin);
1723
+ if (homeHtml) {
1724
+ const links = parseHtmlLinks(homeHtml, origin);
1725
+ if (links.length > 0) {
1726
+ const paths = sampleRoutePaths(links, cap);
1727
+ return { routes: abs(paths), source: "homepage-links", familyCounts: familyCountsOf(paths) };
1728
+ }
1729
+ }
1730
+ return { routes: [new URL("/", origin).href], source: "root-only", familyCounts: { "/": 1 } };
1731
+ }
1732
+
1733
+ // src/audits/browser.ts
1734
+ function isBroken(status) {
1735
+ return status === null || status >= 400;
1736
+ }
1737
+ function summarizeBrowser(routes, links, familyCounts) {
1738
+ const desktopChecks = routes.flatMap((r) => r.desktop);
1739
+ const mobileChecks = routes.flatMap((r) => r.mobile);
1740
+ const desktopOk = routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));
1741
+ const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
1742
+ const brokenLinks = links.filter((l) => isBroken(l.status)).length;
1743
+ const linksOk = links.length > 0 && brokenLinks === 0;
1744
+ const engines = [...new Set(desktopChecks.map((d) => d.engine))];
1745
+ const devices2 = [...new Set(mobileChecks.map((m) => m.device))];
1746
+ const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
1747
+ const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices2.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
1748
+ return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };
1749
+ }
1750
+ async function browserAudit(ctx) {
1751
+ const { site } = ctx;
1752
+ const label = siteLabel(site);
1753
+ if (!site.deployedUrl) {
1754
+ return { audit: "browser", site: label, status: "skip", summary: "no deployed URL" };
1755
+ }
1756
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
1757
+ const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();
1758
+ const runner = ctx.browserRunner ?? await defaultBrowserRunner();
1759
+ try {
1760
+ const discovered = await discoverRoutes(site.deployedUrl, discoverDeps);
1761
+ const routeResults = await runner.probe(discovered.routes);
1762
+ const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];
1763
+ const linkResults = await runner.checkLinks(internalLinks);
1764
+ const summary = summarizeBrowser(
1765
+ routeResults,
1766
+ linkResults,
1767
+ discovered.familyCounts ?? familyCountsOf(discovered.routes)
1768
+ );
1769
+ const status = summary.desktopOk && summary.mobileOk && summary.linksOk ? "pass" : "warn";
1770
+ return {
1771
+ audit: "browser",
1772
+ site: label,
1773
+ status,
1774
+ summary: summary.note,
1775
+ details: { ...summary, checkedAt: now.toISOString() }
1776
+ };
1777
+ } finally {
1778
+ await runner.close?.();
1779
+ }
1780
+ }
1781
+ function defaultDiscoverDeps() {
1782
+ return {
1783
+ fetchText: async (url) => {
1784
+ try {
1785
+ const res = await fetch(url, { redirect: "follow" });
1786
+ if (!res.ok) return null;
1787
+ return await res.text();
1788
+ } catch {
1789
+ return null;
1790
+ }
1791
+ }
1792
+ };
1793
+ }
1794
+ var DESKTOP_VIEWPORT = { width: 1366, height: 900 };
1795
+ var PAGE_TIMEOUT_MS = 3e4;
1796
+ async function defaultBrowserRunner() {
1797
+ const { chromium, firefox, webkit, devices: devices2 } = await import("@playwright/test");
1798
+ const desktopEngines = [
1799
+ { engine: "chromium", type: chromium },
1800
+ { engine: "firefox", type: firefox },
1801
+ { engine: "webkit", type: webkit }
1802
+ ];
1803
+ const mobileTargets = [
1804
+ { device: "Pixel 7", descriptor: devices2["Pixel 7"] },
1805
+ { device: "iPhone 14", descriptor: devices2["iPhone 14"] }
1806
+ ];
1807
+ return {
1808
+ async probe(urls) {
1809
+ const results = [];
1810
+ const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));
1811
+ const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));
1812
+ try {
1813
+ for (const url of urls) {
1814
+ const desktop = [];
1815
+ const linkSet = /* @__PURE__ */ new Set();
1816
+ for (let i = 0; i < desktopEngines.length; i++) {
1817
+ const engine = desktopEngines[i].engine;
1818
+ const browser = browsers[i];
1819
+ const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });
1820
+ const page = await ctx.newPage();
1821
+ const errors = [];
1822
+ page.on("pageerror", (e) => errors.push(String(e)));
1823
+ let ok = false;
1824
+ try {
1825
+ const resp = await page.goto(url, {
1826
+ waitUntil: "domcontentloaded",
1827
+ timeout: PAGE_TIMEOUT_MS
1828
+ });
1829
+ const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
1830
+ ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
1831
+ if (engine === "chromium") {
1832
+ const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
1833
+ const origin = new URL(url).origin;
1834
+ for (const h of hrefs) {
1835
+ try {
1836
+ if (new URL(h).origin === origin) linkSet.add(new URL(h).href);
1837
+ } catch {
1838
+ }
1839
+ }
1840
+ }
1841
+ } catch {
1842
+ ok = false;
1843
+ } finally {
1844
+ await ctx.close().catch(() => {
1845
+ });
1846
+ }
1847
+ desktop.push({ engine, ok });
1848
+ }
1849
+ const mobile = [];
1850
+ for (let i = 0; i < mobileTargets.length; i++) {
1851
+ const { device, descriptor } = mobileTargets[i];
1852
+ const browser = mobileBrowsers[i];
1853
+ const ctx = await browser.newContext({ ...descriptor });
1854
+ const page = await ctx.newPage();
1855
+ const errors = [];
1856
+ page.on("pageerror", (e) => errors.push(String(e)));
1857
+ let ok = false;
1858
+ try {
1859
+ const resp = await page.goto(url, {
1860
+ waitUntil: "domcontentloaded",
1861
+ timeout: PAGE_TIMEOUT_MS
1862
+ });
1863
+ const overflow = await page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2").catch(() => true);
1864
+ ok = !!resp && resp.ok() && errors.length === 0 && !overflow;
1865
+ } catch {
1866
+ ok = false;
1867
+ } finally {
1868
+ await ctx.close().catch(() => {
1869
+ });
1870
+ }
1871
+ mobile.push({ device, ok });
1872
+ }
1873
+ results.push({ url, desktop, mobile, links: [...linkSet] });
1874
+ }
1875
+ } finally {
1876
+ await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
1877
+ })));
1878
+ }
1879
+ return results;
1880
+ },
1881
+ async checkLinks(urls) {
1882
+ const out = [];
1883
+ for (const url of urls) {
1884
+ let status;
1885
+ try {
1886
+ let res = await fetch(url, { method: "HEAD", redirect: "follow" });
1887
+ if (res.status === 405 || res.status === 501) {
1888
+ res = await fetch(url, { method: "GET", redirect: "follow" });
1889
+ }
1890
+ status = res.status;
1891
+ } catch {
1892
+ status = null;
1893
+ }
1894
+ out.push({ url, status });
1895
+ }
1896
+ return out;
1897
+ }
1898
+ };
1899
+ }
1900
+
1488
1901
  // src/audits/index.ts
1489
1902
  var REGISTRY = {
1490
1903
  deps: depsAudit,
1491
1904
  lint: lintAudit,
1492
1905
  security: securityAudit,
1493
1906
  lighthouse: lighthouseAudit,
1494
- a11y: a11yAudit
1907
+ a11y: a11yAudit,
1908
+ domain: domainAudit,
1909
+ browser: browserAudit
1495
1910
  };
1496
1911
  var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
1497
1912
  var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
@@ -1902,8 +2317,13 @@ function deployedUrlNotice(which, url, cwd) {
1902
2317
  if (others.length === 0) return null;
1903
2318
  return `note: --url only affects lighthouse; ${others.join(", ")} ran against the local checkout at ${cwd}`;
1904
2319
  }
2320
+ var CHECKOUT_FREE_AUDITS = /* @__PURE__ */ new Set([
2321
+ "lighthouse",
2322
+ "domain",
2323
+ "browser"
2324
+ ]);
1905
2325
  function auditNeedsCheckout(site, which) {
1906
- const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => n === "lighthouse");
2326
+ const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => CHECKOUT_FREE_AUDITS.has(n));
1907
2327
  return !deployedCapable;
1908
2328
  }
1909
2329
  function applyDeployedUrl(sites, url) {