@eightmedia/flightcheck 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +691 -0
  3. package/README.md +152 -2
  4. package/dist/analyze/fixManifest.d.ts +65 -0
  5. package/dist/analyze/fixManifest.d.ts.map +1 -0
  6. package/dist/analyze/fixManifest.js +588 -0
  7. package/dist/analyze/report.d.ts +283 -0
  8. package/dist/analyze/report.d.ts.map +1 -0
  9. package/dist/analyze/report.js +726 -0
  10. package/dist/analyze/reportTier1.d.ts +46 -0
  11. package/dist/analyze/reportTier1.d.ts.map +1 -0
  12. package/dist/analyze/reportTier1.js +251 -0
  13. package/dist/analyze/stack.d.ts +33 -0
  14. package/dist/analyze/stack.d.ts.map +1 -0
  15. package/dist/analyze/stack.js +188 -0
  16. package/dist/analyze/structuredReport.d.ts +62 -0
  17. package/dist/analyze/structuredReport.d.ts.map +1 -0
  18. package/dist/analyze/structuredReport.js +524 -0
  19. package/dist/analyze.d.ts +3 -0
  20. package/dist/analyze.d.ts.map +1 -0
  21. package/dist/analyze.js +40 -0
  22. package/dist/checks/agent-ready.d.ts +63 -0
  23. package/dist/checks/agent-ready.d.ts.map +1 -0
  24. package/dist/checks/agent-ready.js +219 -0
  25. package/dist/checks/analyze.d.ts +15 -0
  26. package/dist/checks/analyze.d.ts.map +1 -0
  27. package/dist/checks/analyze.js +63 -0
  28. package/dist/checks/crawl.d.ts +28 -0
  29. package/dist/checks/crawl.d.ts.map +1 -0
  30. package/dist/checks/crawl.js +86 -0
  31. package/dist/checks/dns.d.ts +25 -0
  32. package/dist/checks/dns.d.ts.map +1 -0
  33. package/dist/checks/dns.js +80 -0
  34. package/dist/checks/index.d.ts +24 -0
  35. package/dist/checks/index.d.ts.map +1 -0
  36. package/dist/checks/index.js +12 -0
  37. package/dist/checks/issues.d.ts +8 -0
  38. package/dist/checks/issues.d.ts.map +1 -0
  39. package/dist/checks/issues.js +3 -0
  40. package/dist/checks/redirect.d.ts +38 -0
  41. package/dist/checks/redirect.d.ts.map +1 -0
  42. package/dist/checks/redirect.js +209 -0
  43. package/dist/checks/registry.d.ts +8 -0
  44. package/dist/checks/registry.d.ts.map +1 -0
  45. package/dist/checks/registry.js +275 -0
  46. package/dist/checks/robots-txt.d.ts +16 -0
  47. package/dist/checks/robots-txt.d.ts.map +1 -0
  48. package/dist/checks/robots-txt.js +99 -0
  49. package/dist/checks/runner.d.ts +9 -0
  50. package/dist/checks/runner.d.ts.map +1 -0
  51. package/dist/checks/runner.js +110 -0
  52. package/dist/checks/security-headers.d.ts +12 -0
  53. package/dist/checks/security-headers.d.ts.map +1 -0
  54. package/dist/checks/security-headers.js +53 -0
  55. package/dist/checks/sitemap.d.ts +36 -0
  56. package/dist/checks/sitemap.d.ts.map +1 -0
  57. package/dist/checks/sitemap.js +140 -0
  58. package/dist/checks/types.d.ts +72 -0
  59. package/dist/checks/types.d.ts.map +1 -0
  60. package/dist/checks/types.js +20 -0
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.d.ts.map +1 -0
  63. package/dist/cli.js +125 -0
  64. package/dist/crawler/checkGroups.d.ts +7 -0
  65. package/dist/crawler/checkGroups.d.ts.map +1 -0
  66. package/dist/crawler/checkGroups.js +29 -0
  67. package/dist/crawler/crawlState.d.ts +7 -0
  68. package/dist/crawler/crawlState.d.ts.map +1 -0
  69. package/dist/crawler/crawlState.js +6 -0
  70. package/dist/crawler/defaults.d.ts +11 -0
  71. package/dist/crawler/defaults.d.ts.map +1 -0
  72. package/dist/crawler/defaults.js +10 -0
  73. package/dist/crawler/extractors.d.ts +5 -0
  74. package/dist/crawler/extractors.d.ts.map +1 -0
  75. package/dist/crawler/extractors.js +169 -0
  76. package/dist/crawler/jsAssets.d.ts +2 -0
  77. package/dist/crawler/jsAssets.d.ts.map +1 -0
  78. package/dist/crawler/jsAssets.js +22 -0
  79. package/dist/crawler/outputPaths.d.ts +14 -0
  80. package/dist/crawler/outputPaths.d.ts.map +1 -0
  81. package/dist/crawler/outputPaths.js +51 -0
  82. package/dist/crawler/reporter.d.ts +3 -0
  83. package/dist/crawler/reporter.d.ts.map +1 -0
  84. package/dist/crawler/reporter.js +12 -0
  85. package/dist/crawler/robots.d.ts +5 -0
  86. package/dist/crawler/robots.d.ts.map +1 -0
  87. package/dist/crawler/robots.js +29 -0
  88. package/dist/crawler/runCrawler.d.ts +22 -0
  89. package/dist/crawler/runCrawler.d.ts.map +1 -0
  90. package/dist/crawler/runCrawler.js +172 -0
  91. package/dist/crawler/schema.d.ts +87 -0
  92. package/dist/crawler/schema.d.ts.map +1 -0
  93. package/dist/crawler/schema.js +44 -0
  94. package/dist/crawler/secrets.d.ts +4 -0
  95. package/dist/crawler/secrets.d.ts.map +1 -0
  96. package/dist/crawler/secrets.js +70 -0
  97. package/dist/crawler/sitemap.d.ts +8 -0
  98. package/dist/crawler/sitemap.d.ts.map +1 -0
  99. package/dist/crawler/sitemap.js +105 -0
  100. package/dist/crawler/stack.d.ts +3 -0
  101. package/dist/crawler/stack.d.ts.map +1 -0
  102. package/dist/crawler/stack.js +1 -0
  103. package/dist/crawler/stackDetect.d.ts +24 -0
  104. package/dist/crawler/stackDetect.d.ts.map +1 -0
  105. package/dist/crawler/stackDetect.js +604 -0
  106. package/dist/crawler/tier1Extractors.d.ts +5 -0
  107. package/dist/crawler/tier1Extractors.d.ts.map +1 -0
  108. package/dist/crawler/tier1Extractors.js +255 -0
  109. package/dist/crawler/tracking.d.ts +8 -0
  110. package/dist/crawler/tracking.d.ts.map +1 -0
  111. package/dist/crawler/tracking.js +50 -0
  112. package/dist/crawler/urlUtils.d.ts +12 -0
  113. package/dist/crawler/urlUtils.d.ts.map +1 -0
  114. package/dist/crawler/urlUtils.js +53 -0
  115. package/dist/index.d.ts +14 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +7 -0
  118. package/docs/agent-fixes.md +17 -0
  119. package/docs/agent-ready.md +44 -0
  120. package/docs/analyze.md +103 -0
  121. package/docs/check-api.md +142 -0
  122. package/docs/checks.md +116 -0
  123. package/docs/crawler.md +158 -0
  124. package/docs/index.md +24 -0
  125. package/docs/qc-checks-plan.md +796 -0
  126. package/docs/stack.md +99 -0
  127. package/package.json +86 -4
@@ -0,0 +1,209 @@
1
+ import { DEFAULT_TIMEOUT_MS, DEFAULT_USER_AGENT } from "../crawler/defaults.js";
2
+ import { isPlatformPreviewHostname, normalizeUrl } from "../crawler/urlUtils.js";
3
+ import { issue } from "./issues.js";
4
+ import { DEFAULT_CHECK_IDS } from "./types.js";
5
+ export const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
6
+ export const PERMANENT_REDIRECT_STATUS_CODES = new Set([301, 308]);
7
+ export function toggleWwwHostname(hostname) {
8
+ const lower = hostname.toLowerCase();
9
+ return lower.startsWith("www.") ? lower.slice(4) : `www.${lower}`;
10
+ }
11
+ export function buildAlternateHostUrl(url) {
12
+ const parsed = new URL(normalizeUrl(url));
13
+ parsed.hostname = toggleWwwHostname(parsed.hostname);
14
+ return parsed.toString();
15
+ }
16
+ export function resolveCanonicalUrl(startUrl, probe) {
17
+ return probe.resolvedTarget ?? normalizeUrl(startUrl);
18
+ }
19
+ function hostnameFromUrl(url) {
20
+ return new URL(url).hostname.toLowerCase();
21
+ }
22
+ export function shouldCheckWwwAlias(hostname) {
23
+ if (hostname === "localhost")
24
+ return false;
25
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname))
26
+ return false;
27
+ if (hostname.includes(":"))
28
+ return false;
29
+ if (isPlatformPreviewHostname(hostname))
30
+ return false;
31
+ return true;
32
+ }
33
+ export const REDIRECT_ONLY_CHECK_IDS = ["dns", "analyze"];
34
+ function headersToRecord(headers) {
35
+ const record = {};
36
+ headers.forEach((value, key) => {
37
+ record[key.toLowerCase()] = value;
38
+ });
39
+ return record;
40
+ }
41
+ function resolveLocation(url, location) {
42
+ if (!location?.trim())
43
+ return null;
44
+ try {
45
+ return normalizeUrl(new URL(location.trim(), url).toString());
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export function analyzeRedirect(input) {
52
+ const issues = [];
53
+ const isRedirect = input.status !== null && REDIRECT_STATUS_CODES.has(input.status);
54
+ const redirectOnly = isRedirect;
55
+ const isHttps = input.url.startsWith("https://");
56
+ if (!isRedirect) {
57
+ return {
58
+ ...input,
59
+ isRedirect: false,
60
+ redirectOnly: false,
61
+ issues,
62
+ };
63
+ }
64
+ if (!input.location) {
65
+ issues.push(issue("redirect-missing-location", "error", "Redirect response is missing a Location header."));
66
+ }
67
+ else if (!input.resolvedTarget) {
68
+ issues.push(issue("redirect-invalid-location", "error", "Redirect Location header is not a valid URL."));
69
+ }
70
+ if (input.status !== null && !PERMANENT_REDIRECT_STATUS_CODES.has(input.status)) {
71
+ issues.push(issue("redirect-temporary-status", "warning", `Redirect uses status ${input.status}; use 301 or 308 for permanent domain redirects.`));
72
+ }
73
+ if (isHttps && !input.headers["strict-transport-security"]) {
74
+ issues.push(issue("redirect-missing-hsts", "warning", "Strict-Transport-Security header is missing on the redirect response."));
75
+ }
76
+ if (input.resolvedTarget && input.targetStatus !== null && input.targetStatus >= 400) {
77
+ issues.push(issue("redirect-target-unreachable", "error", `Redirect target returned status ${input.targetStatus}: ${input.resolvedTarget}`));
78
+ }
79
+ return {
80
+ ...input,
81
+ isRedirect,
82
+ redirectOnly,
83
+ issues,
84
+ };
85
+ }
86
+ export function analyzeWwwAliasRedirect(canonicalUrl, probe) {
87
+ const issues = [];
88
+ const canonicalHost = hostnameFromUrl(canonicalUrl);
89
+ if (probe.status === null) {
90
+ issues.push(issue("redirect-www-alias-unreachable", "error", `Alternate host is unreachable and should redirect to ${canonicalHost}: ${probe.url}`));
91
+ return issues;
92
+ }
93
+ if (!REDIRECT_STATUS_CODES.has(probe.status)) {
94
+ issues.push(issue("redirect-www-alias-no-redirect", "error", `Alternate host should redirect to ${canonicalHost} but returned status ${probe.status}: ${probe.url}`));
95
+ return issues;
96
+ }
97
+ if (!probe.location) {
98
+ issues.push(issue("redirect-www-alias-missing-location", "error", `Alternate host redirect is missing a Location header: ${probe.url}`));
99
+ }
100
+ else if (!probe.resolvedTarget) {
101
+ issues.push(issue("redirect-www-alias-invalid-location", "error", `Alternate host redirect Location header is not a valid URL: ${probe.url}`));
102
+ }
103
+ else if (hostnameFromUrl(probe.resolvedTarget) !== canonicalHost) {
104
+ issues.push(issue("redirect-www-alias-wrong-target", "error", `Alternate host redirects to ${probe.resolvedTarget} instead of ${canonicalHost}: ${probe.url}`));
105
+ }
106
+ if (probe.status !== null && !PERMANENT_REDIRECT_STATUS_CODES.has(probe.status)) {
107
+ issues.push(issue("redirect-www-alias-temporary-status", "warning", `Alternate host redirect uses status ${probe.status}; use 301 or 308 for permanent domain redirects: ${probe.url}`));
108
+ }
109
+ if (probe.resolvedTarget && probe.targetStatus !== null && probe.targetStatus >= 400) {
110
+ issues.push(issue("redirect-www-alias-target-unreachable", "error", `Alternate host redirect target returned status ${probe.targetStatus}: ${probe.resolvedTarget}`));
111
+ }
112
+ return issues;
113
+ }
114
+ export function isDefaultCheckSet(requestedChecks) {
115
+ return (requestedChecks.length === DEFAULT_CHECK_IDS.length &&
116
+ DEFAULT_CHECK_IDS.every((id) => requestedChecks.includes(id)));
117
+ }
118
+ export function resolveChecksForRedirect(requestedChecks, redirect) {
119
+ if (!redirect.redirectOnly) {
120
+ return requestedChecks;
121
+ }
122
+ const explicit = new Set(requestedChecks);
123
+ if (explicit.has("crawl") && !isDefaultCheckSet(requestedChecks)) {
124
+ return requestedChecks;
125
+ }
126
+ return REDIRECT_ONLY_CHECK_IDS.filter((id) => explicit.has(id));
127
+ }
128
+ export function checksSkippedByRedirect(requestedChecks, effectiveChecks) {
129
+ const effective = new Set(effectiveChecks);
130
+ return requestedChecks.filter((id) => !effective.has(id));
131
+ }
132
+ async function probeTargetStatus(targetUrl, timeoutMs) {
133
+ try {
134
+ const controller = new AbortController();
135
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
136
+ const response = await fetch(targetUrl, {
137
+ method: "HEAD",
138
+ redirect: "manual",
139
+ signal: controller.signal,
140
+ headers: { "user-agent": DEFAULT_USER_AGENT },
141
+ });
142
+ clearTimeout(timeout);
143
+ return response.status;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ async function probeRedirect(url, timeoutMs) {
150
+ try {
151
+ const controller = new AbortController();
152
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
153
+ const response = await fetch(url, {
154
+ redirect: "manual",
155
+ signal: controller.signal,
156
+ headers: { "user-agent": DEFAULT_USER_AGENT },
157
+ });
158
+ clearTimeout(timeout);
159
+ const location = response.headers.get("location");
160
+ const resolvedTarget = resolveLocation(url, location);
161
+ const headers = headersToRecord(response.headers);
162
+ const targetStatus = resolvedTarget && response.status !== null && REDIRECT_STATUS_CODES.has(response.status)
163
+ ? await probeTargetStatus(resolvedTarget, timeoutMs)
164
+ : null;
165
+ return {
166
+ url,
167
+ status: response.status,
168
+ location,
169
+ resolvedTarget,
170
+ headers,
171
+ targetStatus,
172
+ };
173
+ }
174
+ catch {
175
+ return {
176
+ url,
177
+ status: null,
178
+ location: null,
179
+ resolvedTarget: null,
180
+ headers: {},
181
+ targetStatus: null,
182
+ };
183
+ }
184
+ }
185
+ export async function runRedirectCheck(startUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
186
+ const url = normalizeUrl(startUrl);
187
+ const startProbe = await probeRedirect(url, timeoutMs);
188
+ const result = analyzeRedirect(startProbe);
189
+ const canonicalUrl = resolveCanonicalUrl(url, startProbe);
190
+ const canonicalHost = hostnameFromUrl(canonicalUrl);
191
+ if (!shouldCheckWwwAlias(canonicalHost)) {
192
+ return result;
193
+ }
194
+ const alternateUrl = buildAlternateHostUrl(canonicalUrl);
195
+ const alternateProbe = hostnameFromUrl(url) === hostnameFromUrl(alternateUrl)
196
+ ? startProbe
197
+ : await probeRedirect(alternateUrl, timeoutMs);
198
+ const wwwAliasIssues = analyzeWwwAliasRedirect(canonicalUrl, alternateProbe);
199
+ result.wwwAlias = {
200
+ canonicalUrl,
201
+ alternateUrl,
202
+ status: alternateProbe.status,
203
+ location: alternateProbe.location,
204
+ resolvedTarget: alternateProbe.resolvedTarget,
205
+ targetStatus: alternateProbe.targetStatus,
206
+ };
207
+ result.issues.push(...wwwAliasIssues);
208
+ return result;
209
+ }
@@ -0,0 +1,8 @@
1
+ import { CHECK_IDS, type CheckDefinition, type CheckExecutionResult, type CheckId, type CheckRunContext } from "./types.js";
2
+ export { CHECK_IDS };
3
+ export declare const CHECK_DEFINITIONS: Record<CheckId, CheckDefinition>;
4
+ export declare function getCheckDefinition(id: CheckId): CheckDefinition;
5
+ export declare function listChecks(): CheckDefinition[];
6
+ export declare function listImplementedChecks(): CheckDefinition[];
7
+ export declare function runRegisteredCheck(context: CheckRunContext, id: CheckId): Promise<CheckExecutionResult>;
8
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/checks/registry.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,oBAAoB,EAAE,KAAK,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5H,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,OAAO,EAAE,eAAe,CAyP9D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,eAAe,CAE/D;AAED,wBAAgB,UAAU,IAAI,eAAe,EAAE,CAE9C;AAED,wBAAgB,qBAAqB,IAAI,eAAe,EAAE,CAEzD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAE7G"}
@@ -0,0 +1,275 @@
1
+ import { runAgentReadyCheck } from "./agent-ready.js";
2
+ import { runAnalyzeCheck } from "./analyze.js";
3
+ import { runCrawlCheck } from "./crawl.js";
4
+ import { runDnsCheck } from "./dns.js";
5
+ import { runRobotsTxtCheck } from "./robots-txt.js";
6
+ import { runSecurityHeadersCheck } from "./security-headers.js";
7
+ import { runSitemapCheck } from "./sitemap.js";
8
+ import { normalizeUrl } from "../crawler/urlUtils.js";
9
+ import { CHECK_IDS } from "./types.js";
10
+ export { CHECK_IDS };
11
+ export const CHECK_DEFINITIONS = {
12
+ crawl: {
13
+ id: "crawl",
14
+ name: "Crawl",
15
+ description: "Crawl pages and collect SEO, metadata, tracking, and security verification data.",
16
+ dependencies: [],
17
+ implemented: true,
18
+ run: async (context) => {
19
+ const startedAt = Date.now();
20
+ try {
21
+ const data = await runCrawlCheck({
22
+ startUrl: context.startUrl,
23
+ ...context.options.crawl,
24
+ });
25
+ context.outputs.crawl = data;
26
+ return {
27
+ id: "crawl",
28
+ status: "success",
29
+ data,
30
+ durationMs: Date.now() - startedAt,
31
+ };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ id: "crawl",
36
+ status: "failure",
37
+ error: error instanceof Error ? error.message : String(error),
38
+ durationMs: Date.now() - startedAt,
39
+ };
40
+ }
41
+ },
42
+ },
43
+ analyze: {
44
+ id: "analyze",
45
+ name: "Analyze",
46
+ description: "Turn crawl JSON into structured report JSON + Markdown.",
47
+ dependencies: ["crawl"],
48
+ implemented: true,
49
+ run: async (context) => {
50
+ const startedAt = Date.now();
51
+ try {
52
+ const crawlOutput = context.outputs.crawl;
53
+ const redirectOnly = context.outputs.redirect?.redirectOnly ?? false;
54
+ const data = await runAnalyzeCheck({
55
+ ...context.options.analyze,
56
+ records: context.options.analyze?.records ??
57
+ crawlOutput?.records ??
58
+ (redirectOnly ? [] : undefined),
59
+ inputPath: context.options.analyze?.inputPath ??
60
+ crawlOutput?.outputPath ??
61
+ (redirectOnly ? "redirect-only.json" : undefined),
62
+ reportPath: context.options.analyze?.reportPath,
63
+ reportJsonPath: context.options.analyze?.reportJsonPath,
64
+ tier1: {
65
+ redirect: context.outputs.redirect,
66
+ securityHeaders: context.outputs.securityHeaders,
67
+ dns: context.outputs.dns,
68
+ robotsTxt: context.outputs.robotsTxt,
69
+ sitemap: context.outputs.sitemap,
70
+ agentReady: context.outputs.agentReady,
71
+ ...context.options.analyze?.tier1,
72
+ },
73
+ redirectOnly,
74
+ });
75
+ context.outputs.analyze = data;
76
+ return {
77
+ id: "analyze",
78
+ status: "success",
79
+ data,
80
+ durationMs: Date.now() - startedAt,
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ id: "analyze",
86
+ status: "failure",
87
+ error: error instanceof Error ? error.message : String(error),
88
+ durationMs: Date.now() - startedAt,
89
+ };
90
+ }
91
+ },
92
+ },
93
+ lighthouse: {
94
+ id: "lighthouse",
95
+ name: "Lighthouse",
96
+ description: "Run Lighthouse performance, accessibility, and best-practices audits.",
97
+ dependencies: [],
98
+ implemented: false,
99
+ run: async () => ({
100
+ id: "lighthouse",
101
+ status: "unsupported",
102
+ error: "Lighthouse check is not implemented yet.",
103
+ durationMs: 0,
104
+ }),
105
+ },
106
+ "security-headers": {
107
+ id: "security-headers",
108
+ name: "Security headers",
109
+ description: "Verify HTTP security headers such as CSP, HSTS, and X-Frame-Options.",
110
+ dependencies: [],
111
+ implemented: true,
112
+ run: async (context) => {
113
+ const startedAt = Date.now();
114
+ try {
115
+ const data = await runSecurityHeadersCheck(context.startUrl);
116
+ context.outputs.securityHeaders = data;
117
+ return {
118
+ id: "security-headers",
119
+ status: "success",
120
+ data,
121
+ durationMs: Date.now() - startedAt,
122
+ };
123
+ }
124
+ catch (error) {
125
+ return {
126
+ id: "security-headers",
127
+ status: "failure",
128
+ error: error instanceof Error ? error.message : String(error),
129
+ durationMs: Date.now() - startedAt,
130
+ };
131
+ }
132
+ },
133
+ },
134
+ dns: {
135
+ id: "dns",
136
+ name: "DNS ownership",
137
+ description: "Verify DNS records and domain ownership signals.",
138
+ dependencies: [],
139
+ implemented: true,
140
+ run: async (context) => {
141
+ const startedAt = Date.now();
142
+ try {
143
+ const data = await runDnsCheck(context.startUrl);
144
+ context.outputs.dns = data;
145
+ return {
146
+ id: "dns",
147
+ status: "success",
148
+ data,
149
+ durationMs: Date.now() - startedAt,
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ id: "dns",
155
+ status: "failure",
156
+ error: error instanceof Error ? error.message : String(error),
157
+ durationMs: Date.now() - startedAt,
158
+ };
159
+ }
160
+ },
161
+ },
162
+ "robots-txt": {
163
+ id: "robots-txt",
164
+ name: "Robots.txt",
165
+ description: "Report robots.txt presence, sitemap directives, and crawl blocks.",
166
+ dependencies: [],
167
+ implemented: true,
168
+ run: async (context) => {
169
+ const startedAt = Date.now();
170
+ try {
171
+ const data = await runRobotsTxtCheck(context.startUrl);
172
+ context.outputs.robotsTxt = data;
173
+ return {
174
+ id: "robots-txt",
175
+ status: "success",
176
+ data,
177
+ durationMs: Date.now() - startedAt,
178
+ };
179
+ }
180
+ catch (error) {
181
+ return {
182
+ id: "robots-txt",
183
+ status: "failure",
184
+ error: error instanceof Error ? error.message : String(error),
185
+ durationMs: Date.now() - startedAt,
186
+ };
187
+ }
188
+ },
189
+ },
190
+ sitemap: {
191
+ id: "sitemap",
192
+ name: "Sitemap",
193
+ description: "Verify sitemap exists and matches crawled URLs.",
194
+ dependencies: ["crawl"],
195
+ implemented: true,
196
+ run: async (context) => {
197
+ const startedAt = Date.now();
198
+ const crawlOutput = context.outputs.crawl;
199
+ if (!crawlOutput) {
200
+ return {
201
+ id: "sitemap",
202
+ status: "skipped",
203
+ error: "Sitemap check requires crawl output.",
204
+ durationMs: Date.now() - startedAt,
205
+ };
206
+ }
207
+ try {
208
+ const crawledUrls = crawlOutput.records.map((record) => normalizeUrl(record.url));
209
+ const data = await runSitemapCheck({
210
+ startUrl: context.startUrl,
211
+ crawledUrls,
212
+ robotsSitemapUrls: context.outputs.robotsTxt?.sitemapUrls,
213
+ excludePatterns: context.options.sitemap?.excludePatterns ?? context.options.crawl?.excludePatterns,
214
+ mismatchThresholdPercent: context.options.sitemap?.mismatchThresholdPercent,
215
+ timeoutMs: context.options.sitemap?.timeoutMs,
216
+ });
217
+ context.outputs.sitemap = data;
218
+ return {
219
+ id: "sitemap",
220
+ status: "success",
221
+ data,
222
+ durationMs: Date.now() - startedAt,
223
+ };
224
+ }
225
+ catch (error) {
226
+ return {
227
+ id: "sitemap",
228
+ status: "failure",
229
+ error: error instanceof Error ? error.message : String(error),
230
+ durationMs: Date.now() - startedAt,
231
+ };
232
+ }
233
+ },
234
+ },
235
+ "agent-ready": {
236
+ id: "agent-ready",
237
+ name: "Agent readiness",
238
+ description: "Check AI agent discoverability: Content Signals, AI bot rules, llms.txt, Markdown negotiation, Link headers, and well-known protocol cards.",
239
+ dependencies: [],
240
+ implemented: true,
241
+ run: async (context) => {
242
+ const startedAt = Date.now();
243
+ try {
244
+ const data = await runAgentReadyCheck(context.startUrl);
245
+ context.outputs.agentReady = data;
246
+ return {
247
+ id: "agent-ready",
248
+ status: "success",
249
+ data,
250
+ durationMs: Date.now() - startedAt,
251
+ };
252
+ }
253
+ catch (error) {
254
+ return {
255
+ id: "agent-ready",
256
+ status: "failure",
257
+ error: error instanceof Error ? error.message : String(error),
258
+ durationMs: Date.now() - startedAt,
259
+ };
260
+ }
261
+ },
262
+ },
263
+ };
264
+ export function getCheckDefinition(id) {
265
+ return CHECK_DEFINITIONS[id];
266
+ }
267
+ export function listChecks() {
268
+ return CHECK_IDS.map((id) => CHECK_DEFINITIONS[id]);
269
+ }
270
+ export function listImplementedChecks() {
271
+ return listChecks().filter((check) => check.implemented);
272
+ }
273
+ export async function runRegisteredCheck(context, id) {
274
+ return CHECK_DEFINITIONS[id].run(context);
275
+ }
@@ -0,0 +1,16 @@
1
+ import { type CheckIssue } from "./issues.js";
2
+ export type AnalyzeRobotsTxtInput = {
3
+ url: string;
4
+ status: number | null;
5
+ body?: string;
6
+ };
7
+ export type RobotsTxtCheckResult = {
8
+ url: string;
9
+ status: number | null;
10
+ body?: string;
11
+ sitemapUrls: string[];
12
+ issues: CheckIssue[];
13
+ };
14
+ export declare function analyzeRobotsTxt(input: AnalyzeRobotsTxtInput): RobotsTxtCheckResult;
15
+ export declare function runRobotsTxtCheck(startUrl: string, timeoutMs?: number): Promise<RobotsTxtCheckResult>;
16
+ //# sourceMappingURL=robots-txt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-txt.d.ts","sourceRoot":"","sources":["../../src/checks/robots-txt.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB,CAAC;AAgDF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,oBAAoB,CA2CnF;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAmB3G"}
@@ -0,0 +1,99 @@
1
+ import robotsParser from "robots-parser";
2
+ import { issue } from "./issues.js";
3
+ import { DEFAULT_USER_AGENT } from "../crawler/defaults.js";
4
+ function extractSitemapUrls(body) {
5
+ const urls = [];
6
+ for (const line of body.split("\n")) {
7
+ const match = line.match(/^\s*sitemap:\s*(.+)\s*$/i);
8
+ if (match) {
9
+ urls.push(match[1].trim());
10
+ }
11
+ }
12
+ return urls;
13
+ }
14
+ function hasMalformedRobotsLines(body) {
15
+ for (const line of body.split("\n")) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith("#"))
18
+ continue;
19
+ if (/^sitemap:/i.test(trimmed))
20
+ continue;
21
+ const colonIndex = trimmed.indexOf(":");
22
+ if (colonIndex === -1) {
23
+ if (/^(user-agent|disallow|allow|crawl-delay)\b/i.test(trimmed)) {
24
+ return true;
25
+ }
26
+ continue;
27
+ }
28
+ const directive = trimmed.slice(0, colonIndex).trim();
29
+ const value = trimmed.slice(colonIndex + 1).trim();
30
+ if (/^(disallow|allow|crawl-delay)$/i.test(directive) && value.length === 0) {
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+ function blocksAllUserAgents(body, robotsUrl) {
37
+ try {
38
+ const createRobotsParser = robotsParser;
39
+ const parsed = createRobotsParser(robotsUrl, body);
40
+ return parsed.isAllowed(`${new URL(robotsUrl).origin}/`, DEFAULT_USER_AGENT) === false;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ export function analyzeRobotsTxt(input) {
47
+ const issues = [];
48
+ const { url, status, body } = input;
49
+ if (status === null || status === 404) {
50
+ issues.push(issue("robots-missing", "warning", "robots.txt is missing."));
51
+ return { url, status, body, sitemapUrls: [], issues };
52
+ }
53
+ if (!body) {
54
+ issues.push(issue("robots-unparseable", "error", "robots.txt response body is empty."));
55
+ return { url, status, body, sitemapUrls: [], issues };
56
+ }
57
+ if (hasMalformedRobotsLines(body)) {
58
+ issues.push(issue("robots-unparseable", "error", "robots.txt contains malformed directives."));
59
+ }
60
+ let sitemapUrls = [];
61
+ try {
62
+ const createRobotsParser = robotsParser;
63
+ const parsed = createRobotsParser(url, body);
64
+ sitemapUrls = parsed.getSitemaps();
65
+ }
66
+ catch {
67
+ issues.push(issue("robots-unparseable", "error", "robots.txt could not be parsed."));
68
+ return { url, status, body, sitemapUrls: [], issues };
69
+ }
70
+ if (sitemapUrls.length === 0) {
71
+ sitemapUrls = extractSitemapUrls(body);
72
+ }
73
+ if (blocksAllUserAgents(body, url)) {
74
+ issues.push(issue("robots-blocks-all", "critical", "robots.txt disallows all crawling for User-agent: *."));
75
+ }
76
+ if (sitemapUrls.length === 0) {
77
+ issues.push(issue("robots-missing-sitemap", "warning", "robots.txt has no Sitemap directive."));
78
+ }
79
+ return { url, status, body, sitemapUrls, issues };
80
+ }
81
+ export async function runRobotsTxtCheck(startUrl, timeoutMs = 10_000) {
82
+ const origin = new URL(startUrl).origin;
83
+ const robotsUrl = `${origin}/robots.txt`;
84
+ try {
85
+ const controller = new AbortController();
86
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
87
+ const response = await fetch(robotsUrl, {
88
+ redirect: "follow",
89
+ signal: controller.signal,
90
+ headers: { "user-agent": DEFAULT_USER_AGENT },
91
+ });
92
+ clearTimeout(timeout);
93
+ const body = response.ok ? await response.text() : undefined;
94
+ return analyzeRobotsTxt({ url: robotsUrl, status: response.status, body });
95
+ }
96
+ catch {
97
+ return analyzeRobotsTxt({ url: robotsUrl, status: null });
98
+ }
99
+ }
@@ -0,0 +1,9 @@
1
+ import { CHECK_DEFINITIONS, getCheckDefinition, listChecks, listImplementedChecks } from "./registry.js";
2
+ import { analyzeRedirect, resolveChecksForRedirect, type RedirectCheckResult } from "./redirect.js";
3
+ import { type CheckExecutionResult, type CheckId, type RunChecksOptions, type RunChecksResult } from "./types.js";
4
+ export declare function planCheckWaves(requestedChecks: CheckId[]): CheckId[][];
5
+ export declare function runCheck(id: CheckId, options: RunChecksOptions): Promise<CheckExecutionResult>;
6
+ export declare function probeStartUrlRedirect(startUrl: string): Promise<RedirectCheckResult>;
7
+ export declare function runChecks(options: RunChecksOptions): Promise<RunChecksResult>;
8
+ export { CHECK_DEFINITIONS, getCheckDefinition, listChecks, listImplementedChecks, analyzeRedirect, resolveChecksForRedirect };
9
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/checks/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,UAAU,EAAE,qBAAqB,EAAsB,MAAM,eAAe,CAAC;AAC7H,OAAO,EACL,eAAe,EAEf,wBAAwB,EAExB,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAqB,KAAK,oBAAoB,EAAE,KAAK,OAAO,EAAwB,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE3J,wBAAgB,cAAc,CAAC,eAAe,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE,CAqBtE;AAqDD,wBAAsB,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAOpG;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAE1F;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA+CnF;AAED,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,UAAU,EAAE,qBAAqB,EAAE,eAAe,EAAE,wBAAwB,EAAE,CAAC"}