@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.
- package/README.md +14 -13
- package/dist/airtable-JGCPS7JH.js +9 -0
- package/dist/{announce-6OS4C57D.js → announce-PGT4SZZB.js} +6 -6
- package/dist/{bump-deps-GI7PHMOC.js → bump-deps-6OR7LL4E.js} +3 -3
- package/dist/{chunk-7NYN2P6I.js → chunk-3Z6X3BHU.js} +15 -8
- package/dist/chunk-3Z6X3BHU.js.map +1 -0
- package/dist/{chunk-MVDMKKBN.js → chunk-4AIYWWKV.js} +12 -5
- package/dist/chunk-4AIYWWKV.js.map +1 -0
- package/dist/{chunk-QLIUDTY6.js → chunk-5EEMTJMT.js} +10 -7
- package/dist/chunk-5EEMTJMT.js.map +1 -0
- package/dist/{chunk-4NUKMPOO.js → chunk-5FCBYK6L.js} +3 -3
- package/dist/{chunk-PR343K7F.js → chunk-5GUJ36TL.js} +2 -2
- package/dist/{chunk-P6AORG32.js → chunk-6R56OKJZ.js} +2 -2
- package/dist/{chunk-E22WAIF2.js → chunk-AEUABPAJ.js} +3 -3
- package/dist/{chunk-ZRI5VRPG.js → chunk-BYGPUUCT.js} +2 -2
- package/dist/{chunk-SKI23ZZH.js → chunk-HTABUCF4.js} +2 -2
- package/dist/{chunk-FCS36FDT.js → chunk-KQB2BX4V.js} +3 -3
- package/dist/{chunk-AX22DH6S.js → chunk-KZRR4NFG.js} +3 -3
- package/dist/{chunk-BJQCIJOY.js → chunk-REJCSA4H.js} +230 -805
- package/dist/chunk-REJCSA4H.js.map +1 -0
- package/dist/{chunk-CVJF2IC5.js → chunk-TGUESYGF.js} +17 -3
- package/dist/{chunk-CVJF2IC5.js.map → chunk-TGUESYGF.js.map} +1 -1
- package/dist/{chunk-JB4Q3C3M.js → chunk-TO7DJYLZ.js} +92 -15
- package/dist/{chunk-JB4Q3C3M.js.map → chunk-TO7DJYLZ.js.map} +1 -1
- package/dist/chunk-WJMPCVOR.js +4222 -0
- package/dist/chunk-WJMPCVOR.js.map +1 -0
- package/dist/{chunk-NDCOOI3V.js → chunk-WMBLR5VA.js} +3 -3
- package/dist/{chunk-QBAAZY7W.js → chunk-WVNDEYXL.js} +3 -3
- package/dist/{chunk-7BZUOZPU.js → chunk-XN37VTDB.js} +2 -2
- package/dist/{chunk-Q47DU255.js → chunk-Z2K4JPQ3.js} +2 -2
- package/dist/cli/bin.js +26 -26
- package/dist/cli/commands/audit.js +12 -12
- package/dist/{convert-to-pnpm-ONFP3JKF.js → convert-to-pnpm-3RVGUBBR.js} +3 -3
- package/dist/{db-HSZ6D5DU.js → db-FL7ZBF6H.js} +5 -5
- package/dist/{digest-EOA4JLTV.js → digest-TZ3QQOSF.js} +7 -7
- package/dist/{digest-collectors-ZA3BTS3R.js → digest-collectors-EHFY2OQC.js} +6 -6
- package/dist/{ensure-site-SF6KICTN.js → ensure-site-WIDO6LGU.js} +3 -3
- package/dist/{fleet-state-3LQUQHYD.js → fleet-state-UEVZ3X3J.js} +4 -4
- package/dist/{forms-notify-target-RMZPKT6S.js → forms-notify-target-2LWF5C5B.js} +3 -3
- package/dist/{github-signals-3WLIJ5RR.js → github-signals-GAHNOAHS.js} +5 -5
- package/dist/{header-image-DOCDFFRO.js → header-image-YN3AQRY3.js} +2 -2
- package/dist/{health-endpoint-DHKJUPRM.js → health-endpoint-XLNBWVHK.js} +3 -3
- package/dist/{health-mirror-55RRU6BI.js → health-mirror-EB76EERV.js} +4 -4
- package/dist/{import-airtable-OHVY344G.js → import-airtable-TAKSN4NV.js} +3 -3
- package/dist/index.d.ts +87 -6
- package/dist/index.js +15 -12
- package/dist/index.js.map +1 -1
- package/dist/{init-ZRKWYCKA.js → init-YCLSGIEP.js} +6 -6
- package/dist/{launch-6X5OXPF2.js → launch-FSHDTXSM.js} +9 -9
- package/dist/{lighthouse-airtable-3GUWHETW.js → lighthouse-airtable-YMH4UA7R.js} +3 -3
- package/dist/{onboard-6NHGCKGB.js → onboard-NKE6GZKL.js} +3 -3
- package/dist/{orchestrate-KMQIUP3H.js → orchestrate-MWXENQOS.js} +4 -4
- package/dist/{parity-623WNFZG.js → parity-CAJ477PS.js} +5 -5
- package/dist/{pipeline-AQBFG5WM.js → pipeline-MATFYQRC.js} +3 -2
- package/dist/{preflight-VPHQYWTY.js → preflight-4VQP7JJL.js} +5 -5
- package/dist/{prismic-ci-A2HI7NNJ.js → prismic-ci-DM352L2Q.js} +3 -3
- package/dist/{prismic-models-QTKIMFBC.js → prismic-models-Q32ARSYS.js} +7 -7
- package/dist/prospect/types.d.ts +584 -1
- package/dist/prospect/types.js +10 -1
- package/dist/{prospect-audit-RKUVSTLN.js → prospect-audit-MT6RGMOY.js} +3 -3
- package/dist/{render-7G43BSK5.js → render-JE5P26H5.js} +3 -2
- package/dist/{render-7G43BSK5.js.map → render-JE5P26H5.js.map} +1 -1
- package/dist/{render-from-row-P5CWM3MQ.js → render-from-row-IFSBHWDH.js} +3 -3
- package/dist/{renovate-dispatch-6GQWTRKX.js → renovate-dispatch-U5Q52SKT.js} +2 -2
- package/dist/{report-Y4LXDHRV.js → report-6FFHGUUS.js} +14 -14
- package/dist/{report-mirror-6ZDCAQZB.js → report-mirror-IP64R7B3.js} +4 -4
- package/dist/{self-updating-D5XOWS7V.js → self-updating-5DVDIEHB.js} +3 -3
- package/dist/{selftest-IFGSJP4X.js → selftest-LJYXBIO5.js} +6 -6
- package/dist/{site-mirror-3E7JYTJG.js → site-mirror-UKHREAD6.js} +4 -4
- package/dist/{smoke-suite-GXC5JZPH.js → smoke-suite-NUTC7IUK.js} +4 -4
- package/dist/{submissions-UMJS3YST.js → submissions-5GKJPCIH.js} +2 -2
- package/dist/{svelte-codemods-A3EARSRP.js → svelte-codemods-U4NIT6CV.js} +3 -3
- package/dist/{sync-ESP5ZQUU.js → sync-K3PKHVLH.js} +5 -5
- package/dist/{sync-configs-KSESOHFF.js → sync-configs-S2JYY6QU.js} +3 -3
- package/dist/{upgrade-ZLNVEIEA.js → upgrade-YFZCMUPE.js} +3 -3
- package/dist/{websites-D6WYFAES.js → websites-TB4TCAWM.js} +2 -2
- package/dist/{write-audits-to-airtable-K352LZQY.js → write-audits-to-airtable-53EVXKMU.js} +4 -4
- package/package.json +1 -1
- package/dist/airtable-QG5ICI3Y.js +0 -9
- package/dist/chunk-7NYN2P6I.js.map +0 -1
- package/dist/chunk-BJQCIJOY.js.map +0 -1
- package/dist/chunk-MVDMKKBN.js.map +0 -1
- package/dist/chunk-QLIUDTY6.js.map +0 -1
- /package/dist/{airtable-QG5ICI3Y.js.map → airtable-JGCPS7JH.js.map} +0 -0
- /package/dist/{announce-6OS4C57D.js.map → announce-PGT4SZZB.js.map} +0 -0
- /package/dist/{bump-deps-GI7PHMOC.js.map → bump-deps-6OR7LL4E.js.map} +0 -0
- /package/dist/{chunk-4NUKMPOO.js.map → chunk-5FCBYK6L.js.map} +0 -0
- /package/dist/{chunk-PR343K7F.js.map → chunk-5GUJ36TL.js.map} +0 -0
- /package/dist/{chunk-P6AORG32.js.map → chunk-6R56OKJZ.js.map} +0 -0
- /package/dist/{chunk-E22WAIF2.js.map → chunk-AEUABPAJ.js.map} +0 -0
- /package/dist/{chunk-ZRI5VRPG.js.map → chunk-BYGPUUCT.js.map} +0 -0
- /package/dist/{chunk-SKI23ZZH.js.map → chunk-HTABUCF4.js.map} +0 -0
- /package/dist/{chunk-FCS36FDT.js.map → chunk-KQB2BX4V.js.map} +0 -0
- /package/dist/{chunk-AX22DH6S.js.map → chunk-KZRR4NFG.js.map} +0 -0
- /package/dist/{chunk-NDCOOI3V.js.map → chunk-WMBLR5VA.js.map} +0 -0
- /package/dist/{chunk-QBAAZY7W.js.map → chunk-WVNDEYXL.js.map} +0 -0
- /package/dist/{chunk-7BZUOZPU.js.map → chunk-XN37VTDB.js.map} +0 -0
- /package/dist/{chunk-Q47DU255.js.map → chunk-Z2K4JPQ3.js.map} +0 -0
- /package/dist/{convert-to-pnpm-ONFP3JKF.js.map → convert-to-pnpm-3RVGUBBR.js.map} +0 -0
- /package/dist/{db-HSZ6D5DU.js.map → db-FL7ZBF6H.js.map} +0 -0
- /package/dist/{digest-EOA4JLTV.js.map → digest-TZ3QQOSF.js.map} +0 -0
- /package/dist/{digest-collectors-ZA3BTS3R.js.map → digest-collectors-EHFY2OQC.js.map} +0 -0
- /package/dist/{ensure-site-SF6KICTN.js.map → ensure-site-WIDO6LGU.js.map} +0 -0
- /package/dist/{fleet-state-3LQUQHYD.js.map → fleet-state-UEVZ3X3J.js.map} +0 -0
- /package/dist/{forms-notify-target-RMZPKT6S.js.map → forms-notify-target-2LWF5C5B.js.map} +0 -0
- /package/dist/{github-signals-3WLIJ5RR.js.map → github-signals-GAHNOAHS.js.map} +0 -0
- /package/dist/{header-image-DOCDFFRO.js.map → header-image-YN3AQRY3.js.map} +0 -0
- /package/dist/{health-endpoint-DHKJUPRM.js.map → health-endpoint-XLNBWVHK.js.map} +0 -0
- /package/dist/{health-mirror-55RRU6BI.js.map → health-mirror-EB76EERV.js.map} +0 -0
- /package/dist/{import-airtable-OHVY344G.js.map → import-airtable-TAKSN4NV.js.map} +0 -0
- /package/dist/{init-ZRKWYCKA.js.map → init-YCLSGIEP.js.map} +0 -0
- /package/dist/{launch-6X5OXPF2.js.map → launch-FSHDTXSM.js.map} +0 -0
- /package/dist/{lighthouse-airtable-3GUWHETW.js.map → lighthouse-airtable-YMH4UA7R.js.map} +0 -0
- /package/dist/{onboard-6NHGCKGB.js.map → onboard-NKE6GZKL.js.map} +0 -0
- /package/dist/{orchestrate-KMQIUP3H.js.map → orchestrate-MWXENQOS.js.map} +0 -0
- /package/dist/{parity-623WNFZG.js.map → parity-CAJ477PS.js.map} +0 -0
- /package/dist/{pipeline-AQBFG5WM.js.map → pipeline-MATFYQRC.js.map} +0 -0
- /package/dist/{preflight-VPHQYWTY.js.map → preflight-4VQP7JJL.js.map} +0 -0
- /package/dist/{prismic-ci-A2HI7NNJ.js.map → prismic-ci-DM352L2Q.js.map} +0 -0
- /package/dist/{prismic-models-QTKIMFBC.js.map → prismic-models-Q32ARSYS.js.map} +0 -0
- /package/dist/{prospect-audit-RKUVSTLN.js.map → prospect-audit-MT6RGMOY.js.map} +0 -0
- /package/dist/{render-from-row-P5CWM3MQ.js.map → render-from-row-IFSBHWDH.js.map} +0 -0
- /package/dist/{renovate-dispatch-6GQWTRKX.js.map → renovate-dispatch-U5Q52SKT.js.map} +0 -0
- /package/dist/{report-Y4LXDHRV.js.map → report-6FFHGUUS.js.map} +0 -0
- /package/dist/{report-mirror-6ZDCAQZB.js.map → report-mirror-IP64R7B3.js.map} +0 -0
- /package/dist/{self-updating-D5XOWS7V.js.map → self-updating-5DVDIEHB.js.map} +0 -0
- /package/dist/{selftest-IFGSJP4X.js.map → selftest-LJYXBIO5.js.map} +0 -0
- /package/dist/{site-mirror-3E7JYTJG.js.map → site-mirror-UKHREAD6.js.map} +0 -0
- /package/dist/{smoke-suite-GXC5JZPH.js.map → smoke-suite-NUTC7IUK.js.map} +0 -0
- /package/dist/{submissions-UMJS3YST.js.map → submissions-5GKJPCIH.js.map} +0 -0
- /package/dist/{svelte-codemods-A3EARSRP.js.map → svelte-codemods-U4NIT6CV.js.map} +0 -0
- /package/dist/{sync-ESP5ZQUU.js.map → sync-K3PKHVLH.js.map} +0 -0
- /package/dist/{sync-configs-KSESOHFF.js.map → sync-configs-S2JYY6QU.js.map} +0 -0
- /package/dist/{upgrade-ZLNVEIEA.js.map → upgrade-YFZCMUPE.js.map} +0 -0
- /package/dist/{websites-D6WYFAES.js.map → websites-TB4TCAWM.js.map} +0 -0
- /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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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 = /(?:©|©|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
|
|
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 =
|
|
2855
|
-
const images =
|
|
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-
|
|
2959
|
+
//# sourceMappingURL=chunk-REJCSA4H.js.map
|