@reddoorla/maintenance 0.93.1 → 0.94.1

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 (136) hide show
  1. package/README.md +14 -13
  2. package/dist/airtable-JGCPS7JH.js +9 -0
  3. package/dist/{announce-6OS4C57D.js → announce-PGT4SZZB.js} +6 -6
  4. package/dist/{bump-deps-GI7PHMOC.js → bump-deps-6OR7LL4E.js} +3 -3
  5. package/dist/{chunk-7NYN2P6I.js → chunk-3Z6X3BHU.js} +15 -8
  6. package/dist/chunk-3Z6X3BHU.js.map +1 -0
  7. package/dist/{chunk-MVDMKKBN.js → chunk-4AIYWWKV.js} +12 -5
  8. package/dist/chunk-4AIYWWKV.js.map +1 -0
  9. package/dist/{chunk-QLIUDTY6.js → chunk-5EEMTJMT.js} +10 -7
  10. package/dist/chunk-5EEMTJMT.js.map +1 -0
  11. package/dist/{chunk-4NUKMPOO.js → chunk-5FCBYK6L.js} +3 -3
  12. package/dist/{chunk-PR343K7F.js → chunk-5GUJ36TL.js} +2 -2
  13. package/dist/{chunk-P6AORG32.js → chunk-6R56OKJZ.js} +2 -2
  14. package/dist/{chunk-E22WAIF2.js → chunk-AEUABPAJ.js} +3 -3
  15. package/dist/{chunk-ZRI5VRPG.js → chunk-BYGPUUCT.js} +2 -2
  16. package/dist/{chunk-SKI23ZZH.js → chunk-HTABUCF4.js} +2 -2
  17. package/dist/{chunk-FCS36FDT.js → chunk-KQB2BX4V.js} +3 -3
  18. package/dist/{chunk-AX22DH6S.js → chunk-KZRR4NFG.js} +3 -3
  19. package/dist/{chunk-BJQCIJOY.js → chunk-REJCSA4H.js} +230 -805
  20. package/dist/chunk-REJCSA4H.js.map +1 -0
  21. package/dist/{chunk-CVJF2IC5.js → chunk-TGUESYGF.js} +17 -3
  22. package/dist/{chunk-CVJF2IC5.js.map → chunk-TGUESYGF.js.map} +1 -1
  23. package/dist/{chunk-JB4Q3C3M.js → chunk-TO7DJYLZ.js} +92 -15
  24. package/dist/{chunk-JB4Q3C3M.js.map → chunk-TO7DJYLZ.js.map} +1 -1
  25. package/dist/chunk-WJMPCVOR.js +4222 -0
  26. package/dist/chunk-WJMPCVOR.js.map +1 -0
  27. package/dist/{chunk-NDCOOI3V.js → chunk-WMBLR5VA.js} +3 -3
  28. package/dist/{chunk-QBAAZY7W.js → chunk-WVNDEYXL.js} +3 -3
  29. package/dist/{chunk-7BZUOZPU.js → chunk-XN37VTDB.js} +2 -2
  30. package/dist/{chunk-Q47DU255.js → chunk-Z2K4JPQ3.js} +2 -2
  31. package/dist/cli/bin.js +26 -26
  32. package/dist/cli/commands/audit.js +12 -12
  33. package/dist/{convert-to-pnpm-ONFP3JKF.js → convert-to-pnpm-3RVGUBBR.js} +3 -3
  34. package/dist/{db-HSZ6D5DU.js → db-FL7ZBF6H.js} +5 -5
  35. package/dist/{digest-EOA4JLTV.js → digest-TZ3QQOSF.js} +7 -7
  36. package/dist/{digest-collectors-ZA3BTS3R.js → digest-collectors-EHFY2OQC.js} +6 -6
  37. package/dist/{ensure-site-SF6KICTN.js → ensure-site-WIDO6LGU.js} +3 -3
  38. package/dist/{fleet-state-3LQUQHYD.js → fleet-state-UEVZ3X3J.js} +4 -4
  39. package/dist/{forms-notify-target-RMZPKT6S.js → forms-notify-target-2LWF5C5B.js} +3 -3
  40. package/dist/{github-signals-3WLIJ5RR.js → github-signals-GAHNOAHS.js} +5 -5
  41. package/dist/{header-image-DOCDFFRO.js → header-image-YN3AQRY3.js} +2 -2
  42. package/dist/{health-endpoint-DHKJUPRM.js → health-endpoint-XLNBWVHK.js} +3 -3
  43. package/dist/{health-mirror-55RRU6BI.js → health-mirror-EB76EERV.js} +4 -4
  44. package/dist/{import-airtable-OHVY344G.js → import-airtable-TAKSN4NV.js} +3 -3
  45. package/dist/index.d.ts +87 -6
  46. package/dist/index.js +15 -12
  47. package/dist/index.js.map +1 -1
  48. package/dist/{init-ZRKWYCKA.js → init-YCLSGIEP.js} +6 -6
  49. package/dist/{launch-6X5OXPF2.js → launch-FSHDTXSM.js} +9 -9
  50. package/dist/{lighthouse-airtable-3GUWHETW.js → lighthouse-airtable-YMH4UA7R.js} +3 -3
  51. package/dist/{onboard-6NHGCKGB.js → onboard-NKE6GZKL.js} +3 -3
  52. package/dist/{orchestrate-KMQIUP3H.js → orchestrate-MWXENQOS.js} +4 -4
  53. package/dist/{parity-623WNFZG.js → parity-CAJ477PS.js} +5 -5
  54. package/dist/{pipeline-AQBFG5WM.js → pipeline-MATFYQRC.js} +3 -2
  55. package/dist/{preflight-VPHQYWTY.js → preflight-4VQP7JJL.js} +5 -5
  56. package/dist/{prismic-ci-A2HI7NNJ.js → prismic-ci-DM352L2Q.js} +3 -3
  57. package/dist/{prismic-models-QTKIMFBC.js → prismic-models-Q32ARSYS.js} +7 -7
  58. package/dist/prospect/types.d.ts +584 -1
  59. package/dist/prospect/types.js +10 -1
  60. package/dist/{prospect-audit-RKUVSTLN.js → prospect-audit-MT6RGMOY.js} +3 -3
  61. package/dist/{render-7G43BSK5.js → render-JE5P26H5.js} +3 -2
  62. package/dist/{render-7G43BSK5.js.map → render-JE5P26H5.js.map} +1 -1
  63. package/dist/{render-from-row-P5CWM3MQ.js → render-from-row-IFSBHWDH.js} +3 -3
  64. package/dist/{renovate-dispatch-6GQWTRKX.js → renovate-dispatch-U5Q52SKT.js} +2 -2
  65. package/dist/{report-Y4LXDHRV.js → report-6FFHGUUS.js} +14 -14
  66. package/dist/{report-mirror-6ZDCAQZB.js → report-mirror-IP64R7B3.js} +4 -4
  67. package/dist/{self-updating-D5XOWS7V.js → self-updating-5DVDIEHB.js} +3 -3
  68. package/dist/{selftest-IFGSJP4X.js → selftest-LJYXBIO5.js} +6 -6
  69. package/dist/{site-mirror-3E7JYTJG.js → site-mirror-UKHREAD6.js} +4 -4
  70. package/dist/{smoke-suite-GXC5JZPH.js → smoke-suite-NUTC7IUK.js} +4 -4
  71. package/dist/{submissions-UMJS3YST.js → submissions-5GKJPCIH.js} +2 -2
  72. package/dist/{svelte-codemods-A3EARSRP.js → svelte-codemods-U4NIT6CV.js} +3 -3
  73. package/dist/{sync-ESP5ZQUU.js → sync-K3PKHVLH.js} +5 -5
  74. package/dist/{sync-configs-KSESOHFF.js → sync-configs-S2JYY6QU.js} +3 -3
  75. package/dist/{upgrade-ZLNVEIEA.js → upgrade-YFZCMUPE.js} +3 -3
  76. package/dist/{websites-D6WYFAES.js → websites-TB4TCAWM.js} +2 -2
  77. package/dist/{write-audits-to-airtable-K352LZQY.js → write-audits-to-airtable-53EVXKMU.js} +4 -4
  78. package/package.json +1 -1
  79. package/dist/airtable-QG5ICI3Y.js +0 -9
  80. package/dist/chunk-7NYN2P6I.js.map +0 -1
  81. package/dist/chunk-BJQCIJOY.js.map +0 -1
  82. package/dist/chunk-MVDMKKBN.js.map +0 -1
  83. package/dist/chunk-QLIUDTY6.js.map +0 -1
  84. /package/dist/{airtable-QG5ICI3Y.js.map → airtable-JGCPS7JH.js.map} +0 -0
  85. /package/dist/{announce-6OS4C57D.js.map → announce-PGT4SZZB.js.map} +0 -0
  86. /package/dist/{bump-deps-GI7PHMOC.js.map → bump-deps-6OR7LL4E.js.map} +0 -0
  87. /package/dist/{chunk-4NUKMPOO.js.map → chunk-5FCBYK6L.js.map} +0 -0
  88. /package/dist/{chunk-PR343K7F.js.map → chunk-5GUJ36TL.js.map} +0 -0
  89. /package/dist/{chunk-P6AORG32.js.map → chunk-6R56OKJZ.js.map} +0 -0
  90. /package/dist/{chunk-E22WAIF2.js.map → chunk-AEUABPAJ.js.map} +0 -0
  91. /package/dist/{chunk-ZRI5VRPG.js.map → chunk-BYGPUUCT.js.map} +0 -0
  92. /package/dist/{chunk-SKI23ZZH.js.map → chunk-HTABUCF4.js.map} +0 -0
  93. /package/dist/{chunk-FCS36FDT.js.map → chunk-KQB2BX4V.js.map} +0 -0
  94. /package/dist/{chunk-AX22DH6S.js.map → chunk-KZRR4NFG.js.map} +0 -0
  95. /package/dist/{chunk-NDCOOI3V.js.map → chunk-WMBLR5VA.js.map} +0 -0
  96. /package/dist/{chunk-QBAAZY7W.js.map → chunk-WVNDEYXL.js.map} +0 -0
  97. /package/dist/{chunk-7BZUOZPU.js.map → chunk-XN37VTDB.js.map} +0 -0
  98. /package/dist/{chunk-Q47DU255.js.map → chunk-Z2K4JPQ3.js.map} +0 -0
  99. /package/dist/{convert-to-pnpm-ONFP3JKF.js.map → convert-to-pnpm-3RVGUBBR.js.map} +0 -0
  100. /package/dist/{db-HSZ6D5DU.js.map → db-FL7ZBF6H.js.map} +0 -0
  101. /package/dist/{digest-EOA4JLTV.js.map → digest-TZ3QQOSF.js.map} +0 -0
  102. /package/dist/{digest-collectors-ZA3BTS3R.js.map → digest-collectors-EHFY2OQC.js.map} +0 -0
  103. /package/dist/{ensure-site-SF6KICTN.js.map → ensure-site-WIDO6LGU.js.map} +0 -0
  104. /package/dist/{fleet-state-3LQUQHYD.js.map → fleet-state-UEVZ3X3J.js.map} +0 -0
  105. /package/dist/{forms-notify-target-RMZPKT6S.js.map → forms-notify-target-2LWF5C5B.js.map} +0 -0
  106. /package/dist/{github-signals-3WLIJ5RR.js.map → github-signals-GAHNOAHS.js.map} +0 -0
  107. /package/dist/{header-image-DOCDFFRO.js.map → header-image-YN3AQRY3.js.map} +0 -0
  108. /package/dist/{health-endpoint-DHKJUPRM.js.map → health-endpoint-XLNBWVHK.js.map} +0 -0
  109. /package/dist/{health-mirror-55RRU6BI.js.map → health-mirror-EB76EERV.js.map} +0 -0
  110. /package/dist/{import-airtable-OHVY344G.js.map → import-airtable-TAKSN4NV.js.map} +0 -0
  111. /package/dist/{init-ZRKWYCKA.js.map → init-YCLSGIEP.js.map} +0 -0
  112. /package/dist/{launch-6X5OXPF2.js.map → launch-FSHDTXSM.js.map} +0 -0
  113. /package/dist/{lighthouse-airtable-3GUWHETW.js.map → lighthouse-airtable-YMH4UA7R.js.map} +0 -0
  114. /package/dist/{onboard-6NHGCKGB.js.map → onboard-NKE6GZKL.js.map} +0 -0
  115. /package/dist/{orchestrate-KMQIUP3H.js.map → orchestrate-MWXENQOS.js.map} +0 -0
  116. /package/dist/{parity-623WNFZG.js.map → parity-CAJ477PS.js.map} +0 -0
  117. /package/dist/{pipeline-AQBFG5WM.js.map → pipeline-MATFYQRC.js.map} +0 -0
  118. /package/dist/{preflight-VPHQYWTY.js.map → preflight-4VQP7JJL.js.map} +0 -0
  119. /package/dist/{prismic-ci-A2HI7NNJ.js.map → prismic-ci-DM352L2Q.js.map} +0 -0
  120. /package/dist/{prismic-models-QTKIMFBC.js.map → prismic-models-Q32ARSYS.js.map} +0 -0
  121. /package/dist/{prospect-audit-RKUVSTLN.js.map → prospect-audit-MT6RGMOY.js.map} +0 -0
  122. /package/dist/{render-from-row-P5CWM3MQ.js.map → render-from-row-IFSBHWDH.js.map} +0 -0
  123. /package/dist/{renovate-dispatch-6GQWTRKX.js.map → renovate-dispatch-U5Q52SKT.js.map} +0 -0
  124. /package/dist/{report-Y4LXDHRV.js.map → report-6FFHGUUS.js.map} +0 -0
  125. /package/dist/{report-mirror-6ZDCAQZB.js.map → report-mirror-IP64R7B3.js.map} +0 -0
  126. /package/dist/{self-updating-D5XOWS7V.js.map → self-updating-5DVDIEHB.js.map} +0 -0
  127. /package/dist/{selftest-IFGSJP4X.js.map → selftest-LJYXBIO5.js.map} +0 -0
  128. /package/dist/{site-mirror-3E7JYTJG.js.map → site-mirror-UKHREAD6.js.map} +0 -0
  129. /package/dist/{smoke-suite-GXC5JZPH.js.map → smoke-suite-NUTC7IUK.js.map} +0 -0
  130. /package/dist/{submissions-UMJS3YST.js.map → submissions-5GKJPCIH.js.map} +0 -0
  131. /package/dist/{svelte-codemods-A3EARSRP.js.map → svelte-codemods-U4NIT6CV.js.map} +0 -0
  132. /package/dist/{sync-ESP5ZQUU.js.map → sync-K3PKHVLH.js.map} +0 -0
  133. /package/dist/{sync-configs-KSESOHFF.js.map → sync-configs-S2JYY6QU.js.map} +0 -0
  134. /package/dist/{upgrade-ZLNVEIEA.js.map → upgrade-YFZCMUPE.js.map} +0 -0
  135. /package/dist/{websites-D6WYFAES.js.map → websites-TB4TCAWM.js.map} +0 -0
  136. /package/dist/{write-audits-to-airtable-K352LZQY.js.map → write-audits-to-airtable-53EVXKMU.js.map} +0 -0
@@ -1,3 +1,22 @@
1
+ import {
2
+ AI_AGENTS,
3
+ CLASSICAL_AGENTS,
4
+ USER_AGENT,
5
+ buildJourney,
6
+ canonicalizeUrl,
7
+ crawlSite,
8
+ defaultCrawlDeps,
9
+ defaultHttpProbeDeps,
10
+ pacedEach,
11
+ probeHttp,
12
+ readCapped,
13
+ readStack,
14
+ resolveNavigable,
15
+ runSiteChecks,
16
+ sleep,
17
+ summarizeAccessibility,
18
+ usablePages
19
+ } from "./chunk-WJMPCVOR.js";
1
20
  import {
2
21
  checkGoal
3
22
  } from "./chunk-T7ZUUOZ2.js";
@@ -5,699 +24,6 @@ import {
5
24
  isPrivateOrLoopbackHost
6
25
  } from "./chunk-RARREDQE.js";
7
26
 
8
- // src/prospect/crawl.ts
9
- import { parse as parse2, NodeType as NodeType2 } from "node-html-parser";
10
-
11
- // src/prospect/pages.ts
12
- function fetchedOk(page) {
13
- return page.status !== null && page.status < 400;
14
- }
15
- var INFRA_SEGMENTS = [/^cdn-cgi$/i, /^wp-admin$/i, /^wp-json$/i, /^xmlrpc\.php$/i];
16
- function isInfraPath(url) {
17
- let path;
18
- try {
19
- path = new URL(url).pathname;
20
- } catch {
21
- return false;
22
- }
23
- return path.split("/").filter(Boolean).some((segment) => INFRA_SEGMENTS.some((re) => re.test(segment)));
24
- }
25
- function usablePages(pages) {
26
- const candidates = pages.filter((p) => fetchedOk(p) && !isInfraPath(p.url));
27
- const withRendered = candidates.filter((p) => p.rendered !== null);
28
- const withRaw = candidates.filter((p) => p.raw !== null);
29
- const view = withRendered.length >= withRaw.length ? "rendered" : "raw";
30
- const kept = view === "rendered" ? withRendered : withRaw;
31
- const usable = [];
32
- for (const page of kept) {
33
- const extract = view === "rendered" ? page.rendered : page.raw;
34
- if (extract) usable.push({ page, extract });
35
- }
36
- return {
37
- pages: usable,
38
- view,
39
- excluded: candidates.length - usable.length,
40
- anchorsMeasured: usable.length > 0 && usable.every((p) => p.extract.anchors !== void 0)
41
- };
42
- }
43
-
44
- // src/prospect/extract.ts
45
- import { parse, NodeType } from "node-html-parser";
46
- var MAX_ANCHORS = 300;
47
- var NON_FIELD_INPUTS = /* @__PURE__ */ new Set(["hidden", "submit", "button", "image", "reset"]);
48
- var CONTACT_FIELD = /\b(e-?mail|phone|tel|mobile|contact)\b/i;
49
- var toWords = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ");
50
- var UNRENDERED_TAGS = /* @__PURE__ */ new Set(["STYLE", "NOSCRIPT", "TEMPLATE", "SVG"]);
51
- var BLOCK = /* @__PURE__ */ new Set([
52
- "ADDRESS",
53
- "ARTICLE",
54
- "ASIDE",
55
- "BLOCKQUOTE",
56
- "BR",
57
- "DD",
58
- "DIV",
59
- "DL",
60
- "DT",
61
- "FIELDSET",
62
- "FIGCAPTION",
63
- "FIGURE",
64
- "FOOTER",
65
- "FORM",
66
- "H1",
67
- "H2",
68
- "H3",
69
- "H4",
70
- "H5",
71
- "H6",
72
- "HEADER",
73
- "HR",
74
- "LI",
75
- "MAIN",
76
- "NAV",
77
- "OL",
78
- "P",
79
- "PRE",
80
- "SECTION",
81
- "TABLE",
82
- "TD",
83
- "TH",
84
- "TR",
85
- "UL"
86
- ]);
87
- var collapse = (s) => s.replace(/\s+/g, " ").trim();
88
- var MAX_WALK_DEPTH = 100;
89
- function textOf(el) {
90
- const parts = [];
91
- const walk = (node, depth) => {
92
- if (depth > MAX_WALK_DEPTH) return;
93
- for (const child of node.childNodes) {
94
- if (child.nodeType === NodeType.TEXT_NODE) {
95
- parts.push(child.text);
96
- continue;
97
- }
98
- if (child.nodeType !== NodeType.ELEMENT_NODE) continue;
99
- const e = child;
100
- const tag = e.tagName;
101
- if (UNRENDERED_TAGS.has(tag) || tag === "SCRIPT" || tag === "TITLE") continue;
102
- const block = BLOCK.has(tag);
103
- if (block) parts.push("\n");
104
- walk(e, depth + 1);
105
- if (block) parts.push("\n");
106
- }
107
- };
108
- walk(el, 0);
109
- return collapse(parts.join(""));
110
- }
111
- function collect(el, out, depth = 0) {
112
- if (depth > MAX_WALK_DEPTH) return;
113
- for (const child of el.childNodes) {
114
- if (child.nodeType !== NodeType.ELEMENT_NODE) continue;
115
- const e = child;
116
- const tag = e.tagName;
117
- if (UNRENDERED_TAGS.has(tag)) continue;
118
- switch (tag) {
119
- case "META":
120
- out.metas.push(e);
121
- break;
122
- case "LINK":
123
- out.links.push(e);
124
- break;
125
- case "IMG":
126
- out.images.push(e);
127
- break;
128
- case "A":
129
- if ((e.getAttribute("href") ?? "").trim()) out.anchors.push(e);
130
- break;
131
- case "FORM":
132
- out.forms.push(e);
133
- break;
134
- case "TITLE":
135
- if (out.title === null) out.title = collapse(e.text) || null;
136
- break;
137
- case "SCRIPT":
138
- if ((e.getAttribute("type") ?? "").toLowerCase().trim() === "application/ld+json") {
139
- out.jsonLd.push(e.text);
140
- }
141
- continue;
142
- case "H1":
143
- case "H2":
144
- case "H3":
145
- case "H4":
146
- case "H5":
147
- case "H6": {
148
- const text = textOf(e);
149
- if (text) out.headings.push({ level: Number(tag.slice(1)), text });
150
- break;
151
- }
152
- }
153
- collect(e, out, depth + 1);
154
- }
155
- }
156
- var NON_ENQUIRY_SEGMENTS = /* @__PURE__ */ new Set([
157
- // Account doors
158
- "login",
159
- "log-in",
160
- "log_in",
161
- "logon",
162
- "signin",
163
- "sign-in",
164
- "sign_in",
165
- "signup",
166
- "sign-up",
167
- "sign_up",
168
- "register",
169
- "registration",
170
- // Commerce
171
- "checkout",
172
- "cart",
173
- "basket",
174
- // Retrieval, not contact
175
- "search",
176
- // WordPress, whose comment endpoint is a fixed path on a great many sites
177
- "wp-login",
178
- "wp-signup",
179
- "wp-register",
180
- "wp-comments-post"
181
- ]);
182
- function isNonEnquiryAction(action) {
183
- if (!action) return false;
184
- let path;
185
- try {
186
- path = new URL(action, "https://form.invalid/").pathname;
187
- } catch {
188
- path = action;
189
- }
190
- return path.toLowerCase().split("/").some((segment) => NON_ENQUIRY_SEGMENTS.has(segment.replace(/\.[a-z0-9]+$/, "")));
191
- }
192
- function formShape(form) {
193
- const controls = form.querySelectorAll("input, textarea, select");
194
- let fieldCount = 0;
195
- let contactFieldCount = 0;
196
- let hasContactField = false;
197
- let hasPassword = false;
198
- let hasTextarea = false;
199
- let hasSubmit = form.querySelectorAll("button").length > 0;
200
- for (const control of controls) {
201
- const type = (control.getAttribute("type") ?? "").toLowerCase().trim();
202
- if (control.tagName === "INPUT" && NON_FIELD_INPUTS.has(type)) {
203
- if (type === "submit" || type === "image") hasSubmit = true;
204
- continue;
205
- }
206
- if (control.tagName === "TEXTAREA") hasTextarea = true;
207
- if (control.tagName === "INPUT" && type === "password") hasPassword = true;
208
- fieldCount += 1;
209
- const signature = toWords(
210
- [
211
- type,
212
- control.getAttribute("name") ?? "",
213
- control.getAttribute("id") ?? "",
214
- control.getAttribute("placeholder") ?? "",
215
- control.getAttribute("autocomplete") ?? "",
216
- control.getAttribute("aria-label") ?? ""
217
- ].join(" ")
218
- );
219
- if (type === "email" || type === "tel" || CONTACT_FIELD.test(signature)) {
220
- hasContactField = true;
221
- contactFieldCount += 1;
222
- }
223
- }
224
- const action = form.getAttribute("action")?.trim() || null;
225
- const isAccountForm = hasPassword || isNonEnquiryAction(action);
226
- const asksSomethingBack = hasTextarea || fieldCount - contactFieldCount >= 2;
227
- return {
228
- // A lone contact field is a newsletter box, not an enquiry form. See
229
- // `FormKind`: on one audited site a footer email box put every page at zero
230
- // clicks from "reaching them", when the only form that reaches a person
231
- // was the nine-field one on its contact page.
232
- kind: isAccountForm || !hasContactField ? "other" : fieldCount >= 2 && asksSomethingBack ? "enquiry" : "subscribe",
233
- action,
234
- method: (form.getAttribute("method") ?? "get").toLowerCase().trim() || "get",
235
- fieldCount,
236
- hasContactField,
237
- hasSubmit
238
- };
239
- }
240
- function extractPage(html) {
241
- const root = parse(html);
242
- const documentEl = root.querySelector("html") ?? root;
243
- const out = {
244
- metas: [],
245
- links: [],
246
- jsonLd: [],
247
- images: [],
248
- headings: [],
249
- title: null,
250
- anchors: [],
251
- forms: []
252
- };
253
- collect(documentEl, out);
254
- const social = {};
255
- let metaDescription = null;
256
- let hasViewportMeta = false;
257
- for (const m of out.metas) {
258
- const key = (m.getAttribute("property") ?? m.getAttribute("name") ?? "").toLowerCase().trim();
259
- if (!key) continue;
260
- const content = (m.getAttribute("content") ?? "").trim();
261
- if (key === "description") metaDescription = content || null;
262
- else if (key === "viewport") hasViewportMeta = content.length > 0;
263
- else if (key.startsWith("og:") || key.startsWith("twitter:")) social[key] = content;
264
- }
265
- const canonicalEl = out.links.find(
266
- (l) => (l.getAttribute("rel") ?? "").toLowerCase().trim() === "canonical"
267
- );
268
- return {
269
- title: out.title,
270
- metaDescription,
271
- canonical: canonicalEl?.getAttribute("href")?.trim() || null,
272
- social,
273
- headings: out.headings,
274
- jsonLd: out.jsonLd,
275
- images: {
276
- total: out.images.length,
277
- withAlt: out.images.filter((i) => (i.getAttribute("alt") ?? "").trim().length > 0).length
278
- },
279
- hasViewportMeta,
280
- // Body-scoped: <head> has no visible text, and scoping here rather than
281
- // filtering keeps the rule obvious.
282
- text: textOf(root.querySelector("body") ?? documentEl),
283
- anchors: out.anchors.slice(0, MAX_ANCHORS).map((a) => ({
284
- href: (a.getAttribute("href") ?? "").trim(),
285
- // The visible label, not the raw text: an anchor wrapping an icon and a
286
- // span should read as its span. `textOf` already drops the unrendered
287
- // subtrees an icon sprite lives in.
288
- text: textOf(a).slice(0, 120),
289
- rel: (a.getAttribute("rel") ?? "").toLowerCase().trim()
290
- })),
291
- // The TRUE total, so a capped list is never mistaken for a complete one.
292
- anchorCount: out.anchors.length,
293
- imageSrcs: out.images.map((i) => (i.getAttribute("src") ?? "").trim()).filter((src) => src.length > 0),
294
- forms: out.forms.map(formShape)
295
- };
296
- }
297
-
298
- // src/prospect/crawl.ts
299
- var AI_AGENTS = [
300
- "GPTBot",
301
- "OAI-SearchBot",
302
- "ClaudeBot",
303
- "PerplexityBot",
304
- "Google-Extended",
305
- "CCBot"
306
- ];
307
- var CLASSICAL_AGENTS = ["Googlebot", "Bingbot"];
308
- var ALL_AGENTS = [...AI_AGENTS, ...CLASSICAL_AGENTS];
309
- function parseRobots(txt) {
310
- const groups = [];
311
- let current = null;
312
- let lastWasAgent = false;
313
- for (const rawLine of txt.split(/\r?\n/)) {
314
- const line = (rawLine.split("#")[0] ?? "").trim();
315
- if (!line) continue;
316
- const idx = line.indexOf(":");
317
- if (idx < 0) continue;
318
- const field = line.slice(0, idx).trim().toLowerCase();
319
- const value = line.slice(idx + 1).trim();
320
- if (field === "user-agent") {
321
- if (!current || !lastWasAgent) {
322
- current = { agents: [], rules: [] };
323
- groups.push(current);
324
- }
325
- current.agents.push(value.toLowerCase());
326
- lastWasAgent = true;
327
- } else if (field === "allow" || field === "disallow") {
328
- if (!current) continue;
329
- current.rules.push({ type: field === "allow" ? "allow" : "disallow", path: value, line });
330
- lastWasAgent = false;
331
- }
332
- }
333
- return groups;
334
- }
335
- function pathCoversRoot(pattern) {
336
- if (!pattern) return false;
337
- const anchored = pattern.endsWith("$");
338
- const body = anchored ? pattern.slice(0, -1) : pattern;
339
- const source = body.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
340
- return new RegExp(`^${source}${anchored ? "$" : ""}`).test("/");
341
- }
342
- function evaluateAgentAccess(robotsTxt) {
343
- if (robotsTxt === null) {
344
- return ALL_AGENTS.map((agent) => ({ agent, allowed: true, matchedRule: null }));
345
- }
346
- const groups = parseRobots(robotsTxt);
347
- return ALL_AGENTS.map((agent) => {
348
- const lower = agent.toLowerCase();
349
- const named = groups.filter((g) => g.agents.includes(lower));
350
- const matched = named.length > 0 ? named : groups.filter((g) => g.agents.includes("*"));
351
- if (matched.length === 0) return { agent, allowed: true, matchedRule: null };
352
- const header = `User-agent: ${named.length > 0 ? agent : "*"}`;
353
- const rootRules = matched.flatMap((g) => g.rules).filter((r) => pathCoversRoot(r.path));
354
- const block = rootRules.find((r) => r.type === "disallow");
355
- const allow = rootRules.find((r) => r.type === "allow");
356
- if (block && !allow) return { agent, allowed: false, matchedRule: `${header} \u2192 ${block.line}` };
357
- return { agent, allowed: true, matchedRule: allow ? `${header} \u2192 ${allow.line}` : null };
358
- });
359
- }
360
- var MAX_WALK_DEPTH2 = 100;
361
- function sameOriginLinks(html, baseUrl) {
362
- const site = new URL(baseUrl);
363
- const doc = parse2(html);
364
- const baseHref = doc.querySelector("base")?.getAttribute("href");
365
- let resolveBase = site;
366
- if (baseHref) {
367
- try {
368
- resolveBase = new URL(baseHref, site);
369
- } catch {
370
- }
371
- }
372
- const out = [];
373
- const seen = /* @__PURE__ */ new Set();
374
- const walk = (el, depth) => {
375
- if (depth > MAX_WALK_DEPTH2) return;
376
- for (const child of el.childNodes) {
377
- if (child.nodeType !== NodeType2.ELEMENT_NODE) continue;
378
- const e = child;
379
- if (UNRENDERED_TAGS.has(e.tagName)) continue;
380
- if (e.tagName === "A") {
381
- const href = e.getAttribute("href");
382
- if (href) {
383
- let u;
384
- try {
385
- u = new URL(href, resolveBase);
386
- } catch {
387
- u = null;
388
- }
389
- if (u && u.origin === site.origin && (u.protocol === "http:" || u.protocol === "https:")) {
390
- u.hash = "";
391
- const norm = u.toString();
392
- if (!seen.has(norm)) {
393
- seen.add(norm);
394
- out.push(norm);
395
- }
396
- }
397
- }
398
- }
399
- walk(e, depth + 1);
400
- }
401
- };
402
- walk(doc, 0);
403
- return out;
404
- }
405
- function decodeXmlText(s) {
406
- return s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1").replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
407
- }
408
- function parseSitemapLocs(xml) {
409
- return [...xml.matchAll(/<(?:[\w-]+:)?loc\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?loc>/gi)].map((m) => decodeXmlText(m[1] ?? "").trim()).filter(Boolean);
410
- }
411
- function isSafeNestedSitemap(child, origin) {
412
- let url;
413
- try {
414
- url = new URL(child, origin);
415
- } catch {
416
- return false;
417
- }
418
- if (url.protocol !== "https:" && url.protocol !== "http:") return false;
419
- if (url.origin !== origin) return false;
420
- return !isPrivateOrLoopbackHost(url.hostname);
421
- }
422
- function isSitemapIndex(xml) {
423
- return /<(?:[\w-]+:)?sitemapindex[\s>]/i.test(xml);
424
- }
425
- var USER_AGENT = "ReddoorAudit/1.0 (+https://reddoorla.com/; operator-run site audit)";
426
- var ASSET_EXT = /\.(pdf|jpe?g|png|gif|webp|avif|svg|zip|mp4|mov|css|js|xml|json)$/i;
427
- var MAX_RESPONSE_BYTES = 5e6;
428
- var RENDER_SETTLE_MS = 1500;
429
- var ResponseTooLargeError = class extends Error {
430
- constructor(url) {
431
- super(`response exceeds the ${MAX_RESPONSE_BYTES}-byte limit: ${url}`);
432
- this.name = "ResponseTooLargeError";
433
- }
434
- };
435
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
436
- async function pacedEach(items, delayMs, fn, sleepFn = sleep) {
437
- for (let i = 0; i < items.length; i++) {
438
- if (i > 0 && delayMs > 0) await sleepFn(delayMs);
439
- await fn(items[i]);
440
- }
441
- }
442
- async function optional(deps, url) {
443
- try {
444
- const res = await deps.fetchUrl(url);
445
- return res.status >= 400 ? { res: null, error: null } : { res, error: null };
446
- } catch (err) {
447
- if (err instanceof ResponseTooLargeError) return { res: null, error: null };
448
- return { res: null, error: err instanceof Error ? err.message : String(err) };
449
- }
450
- }
451
- async function optionalRobots(deps, url) {
452
- const first = await optional(deps, url);
453
- return first.error === null ? first : optional(deps, url);
454
- }
455
- function textSidecar(res) {
456
- if (!res) return null;
457
- const body = res.body.trim();
458
- if (!body || body.startsWith("<")) return null;
459
- return res.body;
460
- }
461
- function headerValue(headers, name) {
462
- for (const [k, v] of Object.entries(headers)) {
463
- if (k.toLowerCase() === name) return v;
464
- }
465
- return null;
466
- }
467
- function samePageKey(u) {
468
- const path = u.pathname.replace(/\/+$/, "") || "/";
469
- return `${u.origin.toLowerCase()}${path}${u.search}`;
470
- }
471
- function normalizeCandidates(urls, origin, max) {
472
- const out = [];
473
- const seen = /* @__PURE__ */ new Set();
474
- for (const raw of urls) {
475
- let u;
476
- try {
477
- u = new URL(raw);
478
- } catch {
479
- continue;
480
- }
481
- if (u.origin !== origin) continue;
482
- if (ASSET_EXT.test(u.pathname)) continue;
483
- if (isInfraPath(u.toString())) continue;
484
- u.hash = "";
485
- const key = samePageKey(u);
486
- if (seen.has(key)) continue;
487
- seen.add(key);
488
- out.push(u.toString());
489
- if (out.length >= max) break;
490
- }
491
- return out;
492
- }
493
- async function fetchSidecars(origin, deps) {
494
- const robots = await optionalRobots(deps, `${origin}/robots.txt`);
495
- const robotsTxt = textSidecar(robots.res);
496
- const agentAccess = evaluateAgentAccess(
497
- robotsTxt && /user-agent/i.test(robotsTxt) ? robotsTxt : null
498
- );
499
- const llms = await optional(deps, `${origin}/llms.txt`);
500
- const llmsRaw = textSidecar(llms.res);
501
- const llmsTxt = llmsRaw ? {
502
- present: true,
503
- firstLine: llmsRaw.split(/\r?\n/).find((l) => l.trim())?.trim() ?? null
504
- } : { present: false, firstLine: null };
505
- const sitemap = await optional(deps, `${origin}/sitemap.xml`);
506
- let sitemapUrls = [];
507
- let sitemapPresent = false;
508
- if (sitemap.res && /<(urlset|sitemapindex)[\s>]/i.test(sitemap.res.body)) {
509
- sitemapPresent = true;
510
- if (isSitemapIndex(sitemap.res.body)) {
511
- const children = parseSitemapLocs(sitemap.res.body).filter((child) => isSafeNestedSitemap(child, origin)).slice(0, 3);
512
- for (const child of children) {
513
- const nested = await optional(deps, child);
514
- if (nested.res) sitemapUrls.push(...parseSitemapLocs(nested.res.body));
515
- }
516
- } else {
517
- sitemapUrls = parseSitemapLocs(sitemap.res.body);
518
- }
519
- }
520
- return {
521
- robotsTxt,
522
- agentAccess,
523
- llmsTxt,
524
- sitemapUrls,
525
- sitemapPresent,
526
- sidecarErrors: { robots: robots.error, llms: llms.error, sitemap: sitemap.error }
527
- };
528
- }
529
- async function crawlSite(rawUrl, deps) {
530
- const start = new URL(rawUrl);
531
- start.hash = "";
532
- start.username = "";
533
- start.password = "";
534
- if (isPrivateOrLoopbackHost(start.hostname)) {
535
- throw Object.assign(
536
- new Error(
537
- `${start.toString()} is a private address (${start.hostname}) \u2014 refusing to crawl it.`
538
- ),
539
- { exitCode: 1 }
540
- );
541
- }
542
- let home;
543
- try {
544
- home = await deps.fetchUrl(start.toString());
545
- } catch (err) {
546
- throw Object.assign(
547
- new Error(
548
- `Could not reach ${start.toString()}: ${err instanceof Error ? err.message : String(err)}`
549
- ),
550
- { exitCode: 1 }
551
- );
552
- }
553
- if (home.status >= 400) {
554
- throw Object.assign(
555
- new Error(`${start.toString()} returned HTTP ${home.status} \u2014 nothing to audit.`),
556
- { exitCode: 1 }
557
- );
558
- }
559
- let resolved;
560
- try {
561
- resolved = new URL(home.url ?? start.toString());
562
- } catch {
563
- resolved = start;
564
- }
565
- if (isPrivateOrLoopbackHost(resolved.hostname)) {
566
- throw Object.assign(
567
- new Error(
568
- `${start.toString()} redirected to a private address (${resolved.hostname}) \u2014 refusing to crawl it.`
569
- ),
570
- { exitCode: 1 }
571
- );
572
- }
573
- const resolvedUrl = resolved.toString();
574
- const origin = resolved.origin;
575
- const sidecars = await fetchSidecars(origin, deps);
576
- const pageUrls = normalizeCandidates(
577
- [resolvedUrl, ...sidecars.sitemapUrls, ...sameOriginLinks(home.body, resolvedUrl)],
578
- origin,
579
- deps.maxPages
580
- );
581
- const rendered = await deps.renderPages(pageUrls).catch(() => /* @__PURE__ */ new Map());
582
- const pages = [];
583
- for (const url of pageUrls) {
584
- let res = null;
585
- let error = null;
586
- if (url === resolvedUrl) {
587
- res = home;
588
- } else {
589
- if (deps.delayMs > 0) await sleep(deps.delayMs);
590
- try {
591
- res = await deps.fetchUrl(url);
592
- } catch (err) {
593
- error = err instanceof Error ? err.message : String(err);
594
- }
595
- }
596
- const renderedHtml = rendered.get(url) ?? null;
597
- const contentType = res ? headerValue(res.headers, "content-type") : null;
598
- const notHtmlReason = contentType !== null && !contentType.toLowerCase().includes("html") ? `not HTML (${contentType})` : null;
599
- const usable = res !== null && res.status < 400 && notHtmlReason === null;
600
- pages.push({
601
- url,
602
- status: res?.status ?? null,
603
- raw: usable ? extractPage(res.body) : null,
604
- // Gated on `usable` for the same reason `raw` is. A browser paints
605
- // something for a 404 — Cloudflare's email-protection interstitial is the
606
- // case that bit us — and a captured paint is not evidence that the URL is
607
- // a page of this website. Leaving it non-null let every cross-page check
608
- // that asked "is there an extract?" admit a page the server refused, and
609
- // that one URL became the only dead end and the only off-template page
610
- // found in the entire stored corpus.
611
- rendered: usable && renderedHtml ? extractPage(renderedHtml) : null,
612
- error: error ?? (res && res.status >= 400 ? `HTTP ${res.status}` : notHtmlReason)
613
- });
614
- }
615
- const homeHeaders = {};
616
- for (const [k, v] of Object.entries(home.headers)) homeHeaders[k.toLowerCase()] = v;
617
- return {
618
- origin,
619
- robotsTxt: sidecars.robotsTxt,
620
- agentAccess: sidecars.agentAccess,
621
- sitemap: { present: sidecars.sitemapPresent, urlCount: sidecars.sitemapUrls.length },
622
- llmsTxt: sidecars.llmsTxt,
623
- sidecarErrors: sidecars.sidecarErrors,
624
- homeHeaders,
625
- pages
626
- };
627
- }
628
- async function readCapped(res, url) {
629
- const declared = res.headers.get("content-length");
630
- if (declared !== null && Number(declared) > MAX_RESPONSE_BYTES) {
631
- throw new ResponseTooLargeError(url);
632
- }
633
- if (!res.body) return "";
634
- const reader = res.body.getReader();
635
- const chunks = [];
636
- let total = 0;
637
- for (; ; ) {
638
- const { done, value } = await reader.read();
639
- if (done) break;
640
- if (!value) continue;
641
- total += value.byteLength;
642
- if (total > MAX_RESPONSE_BYTES) {
643
- await reader.cancel();
644
- throw new ResponseTooLargeError(url);
645
- }
646
- chunks.push(value);
647
- }
648
- const merged = new Uint8Array(total);
649
- let offset = 0;
650
- for (const chunk of chunks) {
651
- merged.set(chunk, offset);
652
- offset += chunk.byteLength;
653
- }
654
- return new TextDecoder("utf-8").decode(merged);
655
- }
656
- function defaultCrawlDeps(over = {}) {
657
- const maxPages = over.maxPages ?? 20;
658
- const delayMs = over.delayMs ?? 500;
659
- return {
660
- async fetchUrl(url) {
661
- const res = await fetch(url, {
662
- headers: {
663
- "user-agent": USER_AGENT,
664
- accept: "text/html,application/xhtml+xml,text/plain,*/*"
665
- },
666
- redirect: "follow",
667
- signal: AbortSignal.timeout(2e4)
668
- });
669
- const headers = {};
670
- res.headers.forEach((v, k) => {
671
- headers[k] = v;
672
- });
673
- return { status: res.status, body: await readCapped(res, url), headers, url: res.url };
674
- },
675
- async renderPages(urls) {
676
- const { chromium } = await import("@playwright/test");
677
- const out = /* @__PURE__ */ new Map();
678
- const browser = await chromium.launch();
679
- try {
680
- const ctx = await browser.newContext({ userAgent: USER_AGENT });
681
- const page = await ctx.newPage();
682
- await pacedEach(urls, delayMs, async (url) => {
683
- try {
684
- await page.goto(url, { waitUntil: "load", timeout: 2e4 });
685
- await page.waitForTimeout(RENDER_SETTLE_MS);
686
- out.set(url, await page.content());
687
- } catch {
688
- }
689
- });
690
- } finally {
691
- await browser.close();
692
- }
693
- return out;
694
- },
695
- maxPages,
696
- delayMs,
697
- ...over
698
- };
699
- }
700
-
701
27
  // src/prospect/consistency.ts
702
28
  var PHONE_IN_TEXT = /(?<!\d)(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}(?!\d)/g;
703
29
  var COPYRIGHT_YEAR = /(?:©|&copy;|copyright)([^\d<>]{0,30}?)(?:(\d{4})\s*[-–—]\s*)?(\d{4})/gi;
@@ -776,114 +102,6 @@ function checkConsistency(pages) {
776
102
  };
777
103
  }
778
104
 
779
- // src/prospect/journey.ts
780
- function canonicalizeUrl(raw) {
781
- try {
782
- const u = new URL(raw);
783
- if (u.protocol !== "http:" && u.protocol !== "https:") return null;
784
- const path = u.pathname.replace(/\/+$/, "") || "/";
785
- return `${u.hostname.replace(/^www\./i, "").toLowerCase()}${path}`;
786
- } catch {
787
- return null;
788
- }
789
- }
790
- function resolveNavigable(href, pageUrl) {
791
- const trimmed = href.trim();
792
- if (!trimmed || trimmed.startsWith("#")) return null;
793
- if (/^(tel:|mailto:|javascript:|sms:|data:)/i.test(trimmed)) return null;
794
- try {
795
- return new URL(trimmed, pageUrl).toString();
796
- } catch {
797
- return null;
798
- }
799
- }
800
- var TEL_HREF = /^tel:(.+)$/i;
801
- var MAILTO_HREF = /^mailto:([^?]+)/i;
802
- function affordancesOn(page, view) {
803
- const extract = view ?? page.rendered ?? page.raw;
804
- if (!extract) return [];
805
- const found = [];
806
- for (const anchor of extract.anchors ?? []) {
807
- const tel = TEL_HREF.exec(anchor.href);
808
- if (tel?.[1]) {
809
- found.push({ kind: "tel", page: page.url, detail: tel[1].trim() });
810
- continue;
811
- }
812
- const mail = MAILTO_HREF.exec(anchor.href);
813
- if (mail?.[1]) {
814
- found.push({ kind: "mailto", page: page.url, detail: mail[1].trim() });
815
- }
816
- }
817
- for (const form of extract.forms ?? []) {
818
- if (form.kind !== "enquiry") continue;
819
- found.push({ kind: "form", page: page.url, detail: form.action ?? page.url });
820
- }
821
- return found;
822
- }
823
- function buildJourney(pages) {
824
- const usable = usablePages(pages);
825
- const nodes = /* @__PURE__ */ new Map();
826
- for (const entry of usable.pages) {
827
- const key = canonicalizeUrl(entry.page.url);
828
- if (key) nodes.set(key, entry);
829
- }
830
- const affordances = [];
831
- const hasContact = /* @__PURE__ */ new Set();
832
- for (const [key, entry] of nodes) {
833
- const found = affordancesOn(entry.page, entry.extract);
834
- affordances.push(...found);
835
- if (found.length > 0) hasContact.add(key);
836
- }
837
- const outgoing = /* @__PURE__ */ new Map();
838
- const incoming = /* @__PURE__ */ new Map();
839
- for (const [key, entry] of nodes) {
840
- const out = /* @__PURE__ */ new Set();
841
- for (const anchor of entry.extract.anchors ?? []) {
842
- const abs = resolveNavigable(anchor.href, entry.page.url);
843
- if (!abs) continue;
844
- const target = canonicalizeUrl(abs);
845
- if (!target || target === key || !nodes.has(target)) continue;
846
- out.add(target);
847
- if (!incoming.has(target)) incoming.set(target, /* @__PURE__ */ new Set());
848
- incoming.get(target)?.add(key);
849
- }
850
- outgoing.set(key, out);
851
- }
852
- const distance = /* @__PURE__ */ new Map();
853
- let frontier = [...hasContact];
854
- for (const key of frontier) distance.set(key, 0);
855
- let depth = 0;
856
- while (frontier.length > 0) {
857
- depth += 1;
858
- const next = [];
859
- for (const key of frontier) {
860
- for (const source of incoming.get(key) ?? []) {
861
- if (distance.has(source)) continue;
862
- distance.set(source, depth);
863
- next.push(source);
864
- }
865
- }
866
- frontier = next;
867
- }
868
- const journeys = [...nodes.entries()].map(([key, entry]) => ({
869
- url: entry.page.url,
870
- clicksToContact: usable.anchorsMeasured ? distance.get(key) ?? null : null,
871
- internalLinks: outgoing.get(key)?.size ?? 0
872
- }));
873
- const reachable = journeys.map((j) => j.clicksToContact).filter((d) => d !== null);
874
- return {
875
- affordances,
876
- pages: journeys,
877
- // Without recorded anchors there is no evidence either way, so there is no
878
- // finding — not "every page is a dead end", which is what reading the
879
- // absent array as an empty one used to produce.
880
- deadEnds: usable.anchorsMeasured ? journeys.filter((j) => j.clicksToContact === null).map((j) => j.url) : [],
881
- worstClicksToContact: reachable.length > 0 ? Math.max(...reachable) : null,
882
- pagesExamined: journeys.length,
883
- anchorsMeasured: usable.anchorsMeasured
884
- };
885
- }
886
-
887
105
  // src/prospect/checks.ts
888
106
  var SECURITY_HEADERS = [
889
107
  "strict-transport-security",
@@ -2786,7 +2004,7 @@ function summarizeUnverified(probed) {
2786
2004
  function extractOf(page) {
2787
2005
  return page.rendered ?? page.raw;
2788
2006
  }
2789
- function collect2(pages, hrefsOf, sameSiteOnly, origin) {
2007
+ function collect(pages, hrefsOf, sameSiteOnly, origin) {
2790
2008
  const found = /* @__PURE__ */ new Map();
2791
2009
  const originKey = canonicalizeUrl(origin)?.split("/")[0] ?? "";
2792
2010
  for (const page of pages) {
@@ -2851,8 +2069,8 @@ async function probeAll(jobs, deps) {
2851
2069
  return out;
2852
2070
  }
2853
2071
  async function checkAssets(pages, origin, deps) {
2854
- const links = collect2(pages, (e) => (e.anchors ?? []).map((a) => a.href), true, origin);
2855
- const images = collect2(pages, (e) => e.imageSrcs ?? [], false, origin);
2072
+ const links = collect(pages, (e) => (e.anchors ?? []).map((a) => a.href), true, origin);
2073
+ const images = collect(pages, (e) => e.imageSrcs ?? [], false, origin);
2856
2074
  const linkEntries = [...links.entries()].slice(0, deps.maxLinks);
2857
2075
  const imageEntries = [...images.entries()].slice(0, deps.maxImages);
2858
2076
  const jobs = [
@@ -3197,6 +2415,173 @@ async function checkBasics(crawl, deps) {
3197
2415
  };
3198
2416
  }
3199
2417
 
2418
+ // src/prospect/dns.ts
2419
+ var bootstrapPromise = null;
2420
+ async function rdapBootstrap() {
2421
+ bootstrapPromise ??= (async () => {
2422
+ try {
2423
+ const res = await fetch("https://data.iana.org/rdap/dns.json", {
2424
+ signal: AbortSignal.timeout(1e4)
2425
+ });
2426
+ if (!res.ok) return null;
2427
+ const body = await res.json();
2428
+ const map = /* @__PURE__ */ new Map();
2429
+ for (const [tlds, urls] of body.services ?? []) {
2430
+ const base = urls.find((u) => u.startsWith("https://"));
2431
+ if (!base) continue;
2432
+ for (const tld of tlds) map.set(tld.toLowerCase(), base.endsWith("/") ? base : `${base}/`);
2433
+ }
2434
+ return map.size > 0 ? map : null;
2435
+ } catch {
2436
+ return null;
2437
+ }
2438
+ })();
2439
+ return bootstrapPromise;
2440
+ }
2441
+ async function rdapBaseFor(domain) {
2442
+ const map = await rdapBootstrap();
2443
+ if (!map) return null;
2444
+ const labels = domain.split(".");
2445
+ for (let i = 1; i < labels.length; i++) {
2446
+ const suffix = labels.slice(i).join(".");
2447
+ const base = map.get(suffix);
2448
+ if (base) return base;
2449
+ }
2450
+ return null;
2451
+ }
2452
+ function defaultDnsDeps() {
2453
+ return {
2454
+ async resolveTxt(name) {
2455
+ const { resolveTxt } = await import("dns/promises");
2456
+ return resolveTxt(name);
2457
+ },
2458
+ async resolveMx(name) {
2459
+ const { resolveMx } = await import("dns/promises");
2460
+ return resolveMx(name);
2461
+ },
2462
+ async rdap(domain) {
2463
+ const base = await rdapBaseFor(domain);
2464
+ if (!base) return null;
2465
+ try {
2466
+ const res = await fetch(`${base}domain/${encodeURIComponent(domain)}`, {
2467
+ headers: { accept: "application/rdap+json" },
2468
+ redirect: "follow",
2469
+ signal: AbortSignal.timeout(1e4)
2470
+ });
2471
+ if (!res.ok) return null;
2472
+ return await res.json();
2473
+ } catch {
2474
+ return null;
2475
+ }
2476
+ }
2477
+ };
2478
+ }
2479
+ var TWO_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
2480
+ "co.uk",
2481
+ "org.uk",
2482
+ "me.uk",
2483
+ "ac.uk",
2484
+ "gov.uk",
2485
+ "com.au",
2486
+ "net.au",
2487
+ "org.au",
2488
+ "co.nz",
2489
+ "co.za",
2490
+ "com.br",
2491
+ "co.jp",
2492
+ "co.in",
2493
+ "com.mx",
2494
+ "com.sg"
2495
+ ]);
2496
+ function registrableDomain(hostname) {
2497
+ const host = hostname.toLowerCase().replace(/\.$/, "");
2498
+ if (!host || !host.includes(".")) return null;
2499
+ const parts = host.split(".");
2500
+ if (parts.length < 2) return null;
2501
+ const lastTwo = parts.slice(-2).join(".");
2502
+ if (TWO_LABEL_SUFFIXES.has(lastTwo) && parts.length >= 3) return parts.slice(-3).join(".");
2503
+ return lastTwo;
2504
+ }
2505
+ var joinTxt = (chunks) => chunks.map((c) => c.join(""));
2506
+ async function txtStartingWith(deps, name, prefix) {
2507
+ try {
2508
+ const records = joinTxt(await deps.resolveTxt(name));
2509
+ return records.find((r) => prefix.test(r.trim())) ?? null;
2510
+ } catch (err) {
2511
+ const code = err.code;
2512
+ if (code === "ENOTFOUND" || code === "ENODATA") return null;
2513
+ return void 0;
2514
+ }
2515
+ }
2516
+ function expiryFrom(rdap) {
2517
+ if (!rdap || typeof rdap !== "object") return void 0;
2518
+ const events = rdap.events;
2519
+ if (!Array.isArray(events)) return void 0;
2520
+ for (const e of events) {
2521
+ if (!e || typeof e !== "object") continue;
2522
+ const action = String(e.eventAction ?? "").toLowerCase();
2523
+ if (!/expir/.test(action)) continue;
2524
+ const date = e.eventDate;
2525
+ if (typeof date === "string" && !Number.isNaN(Date.parse(date))) return date;
2526
+ }
2527
+ return void 0;
2528
+ }
2529
+ async function lookupDns(origin, contactEmails, deps) {
2530
+ let domain = null;
2531
+ try {
2532
+ domain = registrableDomain(new URL(origin).hostname);
2533
+ } catch {
2534
+ domain = null;
2535
+ }
2536
+ if (!domain) {
2537
+ return {
2538
+ measured: false,
2539
+ domain: null,
2540
+ spf: void 0,
2541
+ dmarc: void 0,
2542
+ mx: void 0,
2543
+ contactMx: void 0,
2544
+ expiresAt: void 0
2545
+ };
2546
+ }
2547
+ const [spf, dmarc] = await Promise.all([
2548
+ txtStartingWith(deps, domain, /^v=spf1\b/i),
2549
+ txtStartingWith(deps, `_dmarc.${domain}`, /^v=DMARC1\b/i)
2550
+ ]);
2551
+ let mx;
2552
+ try {
2553
+ mx = (await deps.resolveMx(domain)).sort((a, b) => a.priority - b.priority).map((r) => r.exchange.toLowerCase());
2554
+ } catch (err) {
2555
+ const code = err.code;
2556
+ mx = code === "ENOTFOUND" || code === "ENODATA" ? [] : void 0;
2557
+ }
2558
+ const otherDomains = [
2559
+ ...new Set(
2560
+ contactEmails.map((e) => registrableDomain(e.split("@")[1] ?? "")).filter((d) => d !== null && d !== domain)
2561
+ )
2562
+ ];
2563
+ let contactMx = null;
2564
+ if (otherDomains.length > 0) {
2565
+ const target = otherDomains[0];
2566
+ try {
2567
+ const records = await deps.resolveMx(target);
2568
+ contactMx = { domain: target, hasMx: records.length > 0 };
2569
+ } catch (err) {
2570
+ const code = err.code;
2571
+ contactMx = code === "ENOTFOUND" || code === "ENODATA" ? { domain: target, hasMx: false } : void 0;
2572
+ }
2573
+ }
2574
+ return {
2575
+ measured: true,
2576
+ domain,
2577
+ spf,
2578
+ dmarc,
2579
+ mx,
2580
+ contactMx,
2581
+ expiresAt: expiryFrom(await deps.rdap(domain))
2582
+ };
2583
+ }
2584
+
3200
2585
  // src/prospect/measured-fixes.ts
3201
2586
  var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
3202
2587
  function measuredFixes(input) {
@@ -3397,6 +2782,16 @@ async function runProspectAudit(url, opts, deps = {}) {
3397
2782
  probeAs: deps.basics?.probeAs ?? (deps.basics?.probe ? (url2) => deps.basics.probe(url2) : defaultBasicsProbe)
3398
2783
  })
3399
2784
  );
2785
+ const stack = await stage(
2786
+ "stack",
2787
+ deps,
2788
+ async () => readStack(crawlData)
2789
+ );
2790
+ const accessibility = await stage(
2791
+ "accessibility",
2792
+ deps,
2793
+ async () => summarizeAccessibility(crawlData.pages)
2794
+ );
3400
2795
  const lighthouse = await stage(
3401
2796
  "lighthouse",
3402
2797
  deps,
@@ -3422,6 +2817,31 @@ async function runProspectAudit(url, opts, deps = {}) {
3422
2817
  )
3423
2818
  ) : { ok: false, error: ANALYZE_SKIPPED };
3424
2819
  const businessName = opts.business?.trim() || (analyze.ok ? analyze.data.businessName : "") || null;
2820
+ const dns = await stage(
2821
+ "dns",
2822
+ deps,
2823
+ async () => lookupDns(
2824
+ crawlData.origin,
2825
+ checks.ok ? (checks.data.consistency?.emails ?? []).map((e) => e.normalized) : [],
2826
+ deps.dns ?? defaultDnsDeps()
2827
+ )
2828
+ );
2829
+ const http = await stage(
2830
+ "http",
2831
+ deps,
2832
+ async () => probeHttp(crawlData, { ...defaultHttpProbeDeps(USER_AGENT), ...deps.http })
2833
+ );
2834
+ const siteChecks = await stage(
2835
+ "siteChecks",
2836
+ deps,
2837
+ async () => runSiteChecks(
2838
+ crawlData,
2839
+ checks.ok ? checks.data : null,
2840
+ businessName,
2841
+ dns.ok ? dns.data : null,
2842
+ http.ok ? http.data : null
2843
+ )
2844
+ );
3425
2845
  const probeOpts = {
3426
2846
  ...deps.probeDelayMs !== void 0 ? { delayMs: deps.probeDelayMs } : {},
3427
2847
  ...deps.probeSleep !== void 0 ? { sleep: deps.probeSleep } : {}
@@ -3511,6 +2931,11 @@ async function runProspectAudit(url, opts, deps = {}) {
3511
2931
  probes,
3512
2932
  assets,
3513
2933
  basics,
2934
+ stack,
2935
+ siteChecks,
2936
+ accessibility,
2937
+ dns,
2938
+ http,
3514
2939
  goalFit,
3515
2940
  accuracy
3516
2941
  };
@@ -3531,4 +2956,4 @@ export {
3531
2956
  mergeFixes,
3532
2957
  runProspectAudit
3533
2958
  };
3534
- //# sourceMappingURL=chunk-BJQCIJOY.js.map
2959
+ //# sourceMappingURL=chunk-REJCSA4H.js.map