@eightmedia/flightcheck 0.0.0-stage → 0.1.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 +15 -0
  2. package/LICENSE +691 -0
  3. package/README.md +106 -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,11 @@
1
+ export declare const DEFAULT_MAX_PAGES = 1000;
2
+ export declare const DEFAULT_DELAY_MS = 500;
3
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
4
+ export declare const DEFAULT_CONCURRENCY = 1;
5
+ export declare const DEFAULT_BURST_DELAY_MS = 0;
6
+ export declare const DEFAULT_OUTPUT_DIR = "./flightcheck";
7
+ export declare const DEFAULT_SITEMAP_URL_SUFFIX = "/sitemap.xml";
8
+ export declare const DEFAULT_MAX_SITEMAPS = 20;
9
+ export declare const DEFAULT_MAX_SITEMAP_URLS = 10000;
10
+ export declare const DEFAULT_USER_AGENT = "h1-crawler/2.0 (+https://local-script)";
11
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/crawler/defaults.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AACxC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAClD,eAAO,MAAM,0BAA0B,iBAAiB,CAAC;AACzD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAC9C,eAAO,MAAM,kBAAkB,2CAA2C,CAAC"}
@@ -0,0 +1,10 @@
1
+ export const DEFAULT_MAX_PAGES = 1000;
2
+ export const DEFAULT_DELAY_MS = 500;
3
+ export const DEFAULT_TIMEOUT_MS = 10000;
4
+ export const DEFAULT_CONCURRENCY = 1;
5
+ export const DEFAULT_BURST_DELAY_MS = 0;
6
+ export const DEFAULT_OUTPUT_DIR = "./flightcheck";
7
+ export const DEFAULT_SITEMAP_URL_SUFFIX = "/sitemap.xml";
8
+ export const DEFAULT_MAX_SITEMAPS = 20;
9
+ export const DEFAULT_MAX_SITEMAP_URLS = 10000;
10
+ export const DEFAULT_USER_AGENT = "h1-crawler/2.0 (+https://local-script)";
@@ -0,0 +1,5 @@
1
+ import { type VerificationRecord } from "./schema.js";
2
+ import { type CheckGroup } from "./checkGroups.js";
3
+ export declare function extractVerificationRecord(html: string, url: string, status: number | null, contentType: string, activeGroups?: Set<CheckGroup>): VerificationRecord;
4
+ export declare function extractInternalLinks(html: string, currentUrl: string, origin: string): Set<string>;
5
+ //# sourceMappingURL=extractors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractors.d.ts","sourceRoot":"","sources":["../../src/crawler/extractors.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGrF,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAwBjE,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,GAAG,CAAC,UAAU,CAAyB,GACpD,kBAAkB,CAgFpB;AAmDD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA8BlG"}
@@ -0,0 +1,169 @@
1
+ import * as cheerio from "cheerio";
2
+ import { createEmptyVerificationRecord } from "./schema.js";
3
+ import { extractTracking } from "./tracking.js";
4
+ import { resolveUrlMaybe } from "./urlUtils.js";
5
+ import { CHECK_GROUPS } from "./checkGroups.js";
6
+ import { extractA11yStaticIssues, extractImageIssues } from "./tier1Extractors.js";
7
+ function textOrEmpty(value) {
8
+ return (value ?? "").trim();
9
+ }
10
+ function attrOrEmpty($, selector, attrName, options) {
11
+ if (options?.containsRel) {
12
+ const containsRel = options.containsRel.toLowerCase();
13
+ const node = $(selector)
14
+ .toArray()
15
+ .map((el) => $(el))
16
+ .find((nodeEl) => textOrEmpty(nodeEl.attr("rel")).toLowerCase().includes(containsRel));
17
+ return textOrEmpty(node?.attr(attrName));
18
+ }
19
+ return textOrEmpty($(selector).first().attr(attrName));
20
+ }
21
+ export function extractVerificationRecord(html, url, status, contentType, activeGroups = new Set(CHECK_GROUPS)) {
22
+ const empty = createEmptyVerificationRecord(url, status);
23
+ if (!contentType.includes("text/html")) {
24
+ return {
25
+ ...empty,
26
+ note: `Skipped non-HTML content-type: ${contentType || "unknown"}`,
27
+ };
28
+ }
29
+ const $ = cheerio.load(html);
30
+ const result = { ...empty };
31
+ if (activeGroups.has("meta")) {
32
+ const title = textOrEmpty($("title").first().text());
33
+ result.title = title;
34
+ result.h1 = textOrEmpty($("h1").first().text());
35
+ result.metaDescription = attrOrEmpty($, 'meta[name="description"]', "content");
36
+ result.robots = attrOrEmpty($, 'meta[name="robots"]', "content");
37
+ result.template = attrOrEmpty($, 'meta[name="template"]', "content");
38
+ result.colorScheme = attrOrEmpty($, 'meta[name="color-scheme"]', "content");
39
+ result.themeColor = attrOrEmpty($, 'meta[name="theme-color"]', "content");
40
+ result.viewport = attrOrEmpty($, 'meta[name="viewport"]', "content");
41
+ result.mainCount = $("main").length;
42
+ result.language = textOrEmpty($("html").first().attr("lang"));
43
+ result.charset = textOrEmpty($("meta[charset]").first().attr("charset"));
44
+ const doctypeMatch = html.match(/<!doctype\s+([^>]+)>/i);
45
+ result.doctype = textOrEmpty(doctypeMatch?.[1]);
46
+ result.canonical = resolveUrlMaybe(attrOrEmpty($, 'link[rel="canonical"]', "href"), url);
47
+ }
48
+ if (activeGroups.has("social")) {
49
+ result.og = {
50
+ title: attrOrEmpty($, 'meta[property="og:title"]', "content"),
51
+ description: attrOrEmpty($, 'meta[property="og:description"]', "content"),
52
+ image: resolveUrlMaybe(attrOrEmpty($, 'meta[property="og:image"]', "content"), url),
53
+ url: resolveUrlMaybe(attrOrEmpty($, 'meta[property="og:url"]', "content"), url),
54
+ type: attrOrEmpty($, 'meta[property="og:type"]', "content"),
55
+ siteName: attrOrEmpty($, 'meta[property="og:site_name"]', "content"),
56
+ };
57
+ result.twitter = {
58
+ card: attrOrEmpty($, 'meta[name="twitter:card"]', "content"),
59
+ title: attrOrEmpty($, 'meta[name="twitter:title"]', "content"),
60
+ description: attrOrEmpty($, 'meta[name="twitter:description"]', "content"),
61
+ image: resolveUrlMaybe(attrOrEmpty($, 'meta[name="twitter:image"]', "content"), url),
62
+ };
63
+ }
64
+ if (activeGroups.has("meta")) {
65
+ result.metaTitle = textOrEmpty(result.og.title || result.twitter.title || result.title);
66
+ }
67
+ if (activeGroups.has("icons")) {
68
+ result.favicon = resolveUrlMaybe(attrOrEmpty($, "link[rel]", "href", { containsRel: "icon" }), url);
69
+ result.icons = $("link[rel]")
70
+ .toArray()
71
+ .map((el) => $(el))
72
+ .filter((el) => textOrEmpty(el.attr("rel")).toLowerCase().includes("icon"))
73
+ .map((el) => ({
74
+ rel: textOrEmpty(el.attr("rel")).toLowerCase(),
75
+ href: resolveUrlMaybe(textOrEmpty(el.attr("href")), url),
76
+ type: textOrEmpty(el.attr("type")),
77
+ sizes: textOrEmpty(el.attr("sizes")),
78
+ media: textOrEmpty(el.attr("media")),
79
+ }));
80
+ }
81
+ if (activeGroups.has("tracking")) {
82
+ result.tracking = extractTracking(html);
83
+ }
84
+ if (activeGroups.has("images")) {
85
+ result.images = { issues: extractImageIssues($, url) };
86
+ }
87
+ if (activeGroups.has("a11y-static")) {
88
+ result.a11yStatic = { issues: extractA11yStaticIssues($) };
89
+ }
90
+ return result;
91
+ }
92
+ const NON_HTML_EXTENSIONS = new Set([
93
+ "pdf",
94
+ "zip",
95
+ "rar",
96
+ "gz",
97
+ "tar",
98
+ "7z",
99
+ "doc",
100
+ "docx",
101
+ "xls",
102
+ "xlsx",
103
+ "ppt",
104
+ "pptx",
105
+ "csv",
106
+ "rtf",
107
+ "png",
108
+ "jpg",
109
+ "jpeg",
110
+ "gif",
111
+ "webp",
112
+ "svg",
113
+ "ico",
114
+ "bmp",
115
+ "tiff",
116
+ "mp3",
117
+ "wav",
118
+ "ogg",
119
+ "mp4",
120
+ "webm",
121
+ "mov",
122
+ "avi",
123
+ "mkv",
124
+ "woff",
125
+ "woff2",
126
+ "ttf",
127
+ "eot",
128
+ "dmg",
129
+ "exe",
130
+ "apk",
131
+ ]);
132
+ function hasNonHtmlExtension(pathname) {
133
+ const lastSegment = pathname.split("/").pop() ?? "";
134
+ const dotIndex = lastSegment.lastIndexOf(".");
135
+ if (dotIndex === -1)
136
+ return false;
137
+ const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
138
+ return NON_HTML_EXTENSIONS.has(extension);
139
+ }
140
+ export function extractInternalLinks(html, currentUrl, origin) {
141
+ const $ = cheerio.load(html);
142
+ const links = new Set();
143
+ $("a[href]").each((_, element) => {
144
+ const rawHref = textOrEmpty($(element).attr("href"));
145
+ if (!rawHref ||
146
+ rawHref.startsWith("#") ||
147
+ rawHref.startsWith("mailto:") ||
148
+ rawHref.startsWith("tel:") ||
149
+ rawHref.startsWith("javascript:")) {
150
+ return;
151
+ }
152
+ try {
153
+ const absolute = new URL(rawHref, currentUrl);
154
+ if (absolute.origin !== origin)
155
+ return;
156
+ if (absolute.protocol !== "http:" && absolute.protocol !== "https:")
157
+ return;
158
+ if (hasNonHtmlExtension(absolute.pathname))
159
+ return;
160
+ absolute.hash = "";
161
+ absolute.search = "";
162
+ links.add(absolute.toString());
163
+ }
164
+ catch {
165
+ // Ignore malformed links.
166
+ }
167
+ });
168
+ return links;
169
+ }
@@ -0,0 +1,2 @@
1
+ export declare function extractSameOriginScripts(html: string, baseUrl: string, origin: string): string[];
2
+ //# sourceMappingURL=jsAssets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsAssets.d.ts","sourceRoot":"","sources":["../../src/crawler/jsAssets.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBhG"}
@@ -0,0 +1,22 @@
1
+ import * as cheerio from "cheerio";
2
+ export function extractSameOriginScripts(html, baseUrl, origin) {
3
+ const $ = cheerio.load(html);
4
+ const urls = new Set();
5
+ $("script[src]").each((_, element) => {
6
+ const src = ($(element).attr("src") ?? "").trim();
7
+ if (!src)
8
+ return;
9
+ try {
10
+ const absolute = new URL(src, baseUrl);
11
+ if (absolute.origin !== origin)
12
+ return;
13
+ if (absolute.protocol !== "http:" && absolute.protocol !== "https:")
14
+ return;
15
+ urls.add(absolute.toString());
16
+ }
17
+ catch {
18
+ // Ignore malformed src values.
19
+ }
20
+ });
21
+ return [...urls];
22
+ }
@@ -0,0 +1,14 @@
1
+ export declare function formatRunTimestamp(date: Date): string;
2
+ export declare function hostnameFromUrl(url: string): string;
3
+ export declare function buildDefaultOutputPaths(startUrl: string, now?: Date): {
4
+ rawJson: string;
5
+ reportMd: string;
6
+ reportJson: string;
7
+ };
8
+ export declare function defaultReportPathForInput(inputPath: string): string;
9
+ export declare function defaultReportJsonPathForInput(inputPath: string): string;
10
+ /** @deprecated Fix-manifest output is removed; kept for callers that still resolve paths. */
11
+ export declare function defaultManifestPathForInput(inputPath: string): string;
12
+ /** @deprecated Agent-jobs output is removed; kept for callers that still resolve paths. */
13
+ export declare function defaultAgentJobsPathForInput(inputPath: string): string;
14
+ //# sourceMappingURL=outputPaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outputPaths.d.ts","sourceRoot":"","sources":["../../src/crawler/outputPaths.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAGrD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,GAAG,OAAa,GACf;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAS3D;AAqBD,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,6FAA6F;AAC7F,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,2FAA2F;AAC3F,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtE"}
@@ -0,0 +1,51 @@
1
+ import { DEFAULT_OUTPUT_DIR } from "./defaults.js";
2
+ export function formatRunTimestamp(date) {
3
+ const pad = (value) => String(value).padStart(2, "0");
4
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.${pad(date.getHours())}${pad(date.getMinutes())}`;
5
+ }
6
+ export function hostnameFromUrl(url) {
7
+ const hostname = new URL(url).hostname;
8
+ return hostname.startsWith("www.") ? hostname.slice(4) : hostname;
9
+ }
10
+ export function buildDefaultOutputPaths(startUrl, now = new Date()) {
11
+ const stamp = formatRunTimestamp(now);
12
+ const host = hostnameFromUrl(startUrl);
13
+ const base = `${stamp}-${host}`;
14
+ return {
15
+ rawJson: `${DEFAULT_OUTPUT_DIR}/${base}-raw.json`,
16
+ reportMd: `${DEFAULT_OUTPUT_DIR}/${base}-report.md`,
17
+ reportJson: `${DEFAULT_OUTPUT_DIR}/${base}-report.json`,
18
+ };
19
+ }
20
+ function reportBasename(inputPath) {
21
+ if (inputPath.endsWith("-raw.json")) {
22
+ return inputPath.slice(0, -"-raw.json".length);
23
+ }
24
+ if (inputPath.endsWith("-report.md")) {
25
+ return inputPath.slice(0, -"-report.md".length);
26
+ }
27
+ if (inputPath.endsWith("-report.json")) {
28
+ return inputPath.slice(0, -"-report.json".length);
29
+ }
30
+ if (inputPath.endsWith("-fix-manifest.json")) {
31
+ return inputPath.slice(0, -"-fix-manifest.json".length);
32
+ }
33
+ if (inputPath.endsWith(".json")) {
34
+ return inputPath.slice(0, -".json".length);
35
+ }
36
+ return inputPath;
37
+ }
38
+ export function defaultReportPathForInput(inputPath) {
39
+ return `${reportBasename(inputPath)}-report.md`;
40
+ }
41
+ export function defaultReportJsonPathForInput(inputPath) {
42
+ return `${reportBasename(inputPath)}-report.json`;
43
+ }
44
+ /** @deprecated Fix-manifest output is removed; kept for callers that still resolve paths. */
45
+ export function defaultManifestPathForInput(inputPath) {
46
+ return `${reportBasename(inputPath)}-fix-manifest.json`;
47
+ }
48
+ /** @deprecated Agent-jobs output is removed; kept for callers that still resolve paths. */
49
+ export function defaultAgentJobsPathForInput(inputPath) {
50
+ return `${reportBasename(inputPath)}-agent-jobs`;
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { VerificationRecord } from "./schema.js";
2
+ export declare function writeReportToFile(records: VerificationRecord[], outputPath: string): void;
3
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/crawler/reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMtD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAMzF"}
@@ -0,0 +1,12 @@
1
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ function ensureParentDir(filePath) {
4
+ mkdirSync(dirname(filePath), { recursive: true });
5
+ }
6
+ export function writeReportToFile(records, outputPath) {
7
+ ensureParentDir(outputPath);
8
+ const tempPath = `${outputPath}.tmp`;
9
+ const json = JSON.stringify(records, null, 2) + "\n";
10
+ writeFileSync(tempPath, json, "utf8");
11
+ renameSync(tempPath, outputPath);
12
+ }
@@ -0,0 +1,5 @@
1
+ export type RobotsPolicy = {
2
+ isAllowed: (url: string) => boolean;
3
+ };
4
+ export declare function loadRobotsPolicy(origin: string, timeoutMs: number): Promise<RobotsPolicy | null>;
5
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/crawler/robots.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA8BtG"}
@@ -0,0 +1,29 @@
1
+ import robotsParser from "robots-parser";
2
+ import { DEFAULT_USER_AGENT } from "./defaults.js";
3
+ export async function loadRobotsPolicy(origin, timeoutMs) {
4
+ const createRobotsParser = robotsParser;
5
+ const robotsUrl = `${origin}/robots.txt`;
6
+ try {
7
+ const controller = new AbortController();
8
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
9
+ const response = await fetch(robotsUrl, {
10
+ redirect: "follow",
11
+ signal: controller.signal,
12
+ headers: {
13
+ "user-agent": DEFAULT_USER_AGENT,
14
+ },
15
+ });
16
+ clearTimeout(timeout);
17
+ if (!response.ok) {
18
+ return null;
19
+ }
20
+ const body = await response.text();
21
+ const parsed = createRobotsParser(robotsUrl, body);
22
+ return {
23
+ isAllowed: (url) => parsed.isAllowed(url, DEFAULT_USER_AGENT) !== false,
24
+ };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
@@ -0,0 +1,22 @@
1
+ import { type CheckGroup } from "./checkGroups.js";
2
+ import type { VerificationRecord } from "./schema.js";
3
+ export type RunCrawlerOptions = {
4
+ startUrl: string;
5
+ outputPath: string;
6
+ maxPages?: number;
7
+ delayMs?: number;
8
+ timeoutMs?: number;
9
+ concurrency?: number;
10
+ burstDelayMs?: number;
11
+ seedUrls?: string[];
12
+ excludePatterns?: string[];
13
+ respectRobotsTxt?: boolean;
14
+ activeGroups?: Set<CheckGroup>;
15
+ revealSecrets?: boolean;
16
+ quiet?: boolean;
17
+ };
18
+ export declare function runCrawler(options: RunCrawlerOptions): Promise<{
19
+ records: VerificationRecord[];
20
+ interrupted: boolean;
21
+ }>;
22
+ //# sourceMappingURL=runCrawler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runCrawler.d.ts","sourceRoot":"","sources":["../../src/crawler/runCrawler.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOjE,OAAO,KAAK,EAAiB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAqK7H"}
@@ -0,0 +1,172 @@
1
+ import { CheerioCrawler, RequestQueue, log, LogLevel } from "crawlee";
2
+ import pino from "pino";
3
+ import { DEFAULT_BURST_DELAY_MS, DEFAULT_CONCURRENCY, DEFAULT_DELAY_MS, DEFAULT_MAX_PAGES, DEFAULT_TIMEOUT_MS, DEFAULT_USER_AGENT, } from "./defaults.js";
4
+ import { CHECK_GROUPS } from "./checkGroups.js";
5
+ import { extractInternalLinks, extractVerificationRecord } from "./extractors.js";
6
+ import { detectStackFromHtml } from "./stack.js";
7
+ import { extractSameOriginScripts } from "./jsAssets.js";
8
+ import { loadRobotsPolicy } from "./robots.js";
9
+ import { writeReportToFile } from "./reporter.js";
10
+ import { scanText } from "./secrets.js";
11
+ import { matchesExcludePattern, normalizeUrl, sleep } from "./urlUtils.js";
12
+ const MAX_JS_ASSET_BYTES = 5_000_000; // 5 MB — skip larger bundles to bound memory
13
+ export async function runCrawler(options) {
14
+ const logger = pino({ level: "info" });
15
+ const startUrl = normalizeUrl(options.startUrl);
16
+ const origin = new URL(startUrl).origin;
17
+ const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
18
+ const delayMs = options.delayMs ?? DEFAULT_DELAY_MS;
19
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
20
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
21
+ const burstDelayMs = options.burstDelayMs ?? DEFAULT_BURST_DELAY_MS;
22
+ const excludePatterns = options.excludePatterns ?? [];
23
+ const seedUrls = (options.seedUrls ?? [startUrl]).map((url) => normalizeUrl(url));
24
+ const respectRobotsTxt = options.respectRobotsTxt ?? false;
25
+ const quiet = options.quiet ?? false;
26
+ if (quiet) {
27
+ log.setLevel(LogLevel.WARNING);
28
+ }
29
+ const activeGroups = options.activeGroups ?? new Set(CHECK_GROUPS);
30
+ const revealSecrets = options.revealSecrets ?? false;
31
+ const jsScanCache = new Map();
32
+ const failedAssets = new Set();
33
+ const fetchText = async (assetUrl) => {
34
+ const controller = new AbortController();
35
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const res = await fetch(assetUrl, {
38
+ signal: controller.signal,
39
+ headers: { "user-agent": DEFAULT_USER_AGENT },
40
+ });
41
+ if (!res.ok)
42
+ return null;
43
+ // Fast reject when the server declares an oversize body up front.
44
+ const contentLength = Number(res.headers.get("content-length") ?? "0");
45
+ if (contentLength > MAX_JS_ASSET_BYTES)
46
+ return null;
47
+ // Backstop for chunked/gzip responses that omit content-length: skip
48
+ // scanning bodies that turn out larger than the cap once read.
49
+ const text = await res.text();
50
+ if (text.length > MAX_JS_ASSET_BYTES)
51
+ return null;
52
+ return text;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ finally {
58
+ clearTimeout(timer);
59
+ }
60
+ };
61
+ const scanSecurity = async (html, pageUrl) => {
62
+ const secrets = [...scanText(html, "html", revealSecrets)];
63
+ const scripts = extractSameOriginScripts(html, pageUrl, origin);
64
+ let scannedAssets = 0;
65
+ for (const scriptUrl of scripts) {
66
+ if (failedAssets.has(scriptUrl))
67
+ continue;
68
+ let assetFindings = jsScanCache.get(scriptUrl);
69
+ if (!assetFindings) {
70
+ const js = await fetchText(scriptUrl);
71
+ if (js === null) {
72
+ failedAssets.add(scriptUrl);
73
+ continue;
74
+ }
75
+ assetFindings = scanText(js, scriptUrl, revealSecrets);
76
+ jsScanCache.set(scriptUrl, assetFindings);
77
+ }
78
+ scannedAssets += 1;
79
+ secrets.push(...assetFindings);
80
+ }
81
+ return { secrets, scannedAssets };
82
+ };
83
+ const robotsPolicy = respectRobotsTxt ? await loadRobotsPolicy(origin, timeoutMs) : null;
84
+ const requestQueue = await RequestQueue.open();
85
+ const records = [];
86
+ const seen = new Set();
87
+ for (const seed of seedUrls) {
88
+ if (!matchesExcludePattern(seed, excludePatterns)) {
89
+ await requestQueue.addRequest({ url: seed, uniqueKey: normalizeUrl(seed) });
90
+ seen.add(normalizeUrl(seed));
91
+ }
92
+ }
93
+ let processedSinceBurst = 0;
94
+ let interrupted = false;
95
+ const persist = () => {
96
+ writeReportToFile(records, options.outputPath);
97
+ return records;
98
+ };
99
+ process.on("SIGINT", async () => {
100
+ interrupted = true;
101
+ logger.info("[crawl] SIGINT ontvangen, graceful stop gestart");
102
+ });
103
+ logger.info(`[crawl] start url=${startUrl} output=${options.outputPath} maxPages=${maxPages} concurrency=${concurrency} delayMs=${delayMs} burstDelayMs=${burstDelayMs} timeoutMs=${timeoutMs} seeds=${seedUrls.length} respectRobotsTxt=${respectRobotsTxt}`);
104
+ const crawler = new CheerioCrawler({
105
+ requestQueue,
106
+ maxRequestsPerCrawl: maxPages,
107
+ maxConcurrency: concurrency,
108
+ // +60 s headroom when security scanning is active: the handler does sequential JS fetches
109
+ // per page; the failedAssets/jsScanCache ensures dead/cached assets aren't re-fetched.
110
+ requestHandlerTimeoutSecs: Math.ceil(timeoutMs / 1000) + (activeGroups.has("security") ? 60 : 0),
111
+ requestHandler: async ({ request, body, contentType, response }) => {
112
+ if (interrupted)
113
+ return;
114
+ if (delayMs > 0) {
115
+ await sleep(delayMs);
116
+ }
117
+ if (burstDelayMs > 0 && processedSinceBurst > 0 && processedSinceBurst % concurrency === 0) {
118
+ await sleep(burstDelayMs);
119
+ }
120
+ const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
121
+ if (robotsPolicy && !robotsPolicy.isAllowed(currentUrl)) {
122
+ const skipped = extractVerificationRecord("", currentUrl, null, "");
123
+ skipped.note = "Skipped by robots.txt policy";
124
+ records.push(skipped);
125
+ const currentRecords = persist();
126
+ processedSinceBurst += 1;
127
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=robots-blocked`);
128
+ return;
129
+ }
130
+ const html = typeof body === "string" ? body : "";
131
+ const status = response?.statusCode ?? null;
132
+ const effectiveContentType = contentType?.type ?? "";
133
+ const record = extractVerificationRecord(html, currentUrl, status, effectiveContentType, activeGroups);
134
+ if (activeGroups.has("stack") && effectiveContentType.includes("text/html") && html) {
135
+ record.stack = { detections: detectStackFromHtml(html, currentUrl) };
136
+ }
137
+ if (activeGroups.has("security") && effectiveContentType.includes("text/html") && html) {
138
+ record.security = await scanSecurity(html, currentUrl);
139
+ }
140
+ records.push(record);
141
+ const currentRecords = persist();
142
+ processedSinceBurst += 1;
143
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=${status ?? "error"}`);
144
+ if (effectiveContentType.includes("text/html")) {
145
+ const links = extractInternalLinks(html, currentUrl, origin);
146
+ for (const link of links) {
147
+ const normalized = normalizeUrl(link);
148
+ if (seen.has(normalized))
149
+ continue;
150
+ if (matchesExcludePattern(normalized, excludePatterns))
151
+ continue;
152
+ if (robotsPolicy && !robotsPolicy.isAllowed(normalized))
153
+ continue;
154
+ seen.add(normalized);
155
+ await requestQueue.addRequest({ url: normalized, uniqueKey: normalized });
156
+ }
157
+ }
158
+ },
159
+ failedRequestHandler: async ({ request }, error) => {
160
+ const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
161
+ const record = extractVerificationRecord("", currentUrl, null, "");
162
+ record.error = error instanceof Error ? error.message : "Unknown error";
163
+ records.push(record);
164
+ const currentRecords = persist();
165
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=error`);
166
+ },
167
+ });
168
+ await crawler.run();
169
+ const finalRecords = persist();
170
+ logger.info(`[crawl] klaar pages=${finalRecords.length} interrupted=${interrupted}`);
171
+ return { records: finalRecords, interrupted };
172
+ }
@@ -0,0 +1,87 @@
1
+ export type SecretSeverity = "critical" | "review";
2
+ export type SecretFinding = {
3
+ type: string;
4
+ provider: string;
5
+ severity: SecretSeverity;
6
+ masked: string;
7
+ source: string;
8
+ };
9
+ export type TrackingItem = {
10
+ provider: string;
11
+ id: string;
12
+ };
13
+ export type StackDetection = {
14
+ name: string;
15
+ categories: string[];
16
+ confidence: "high" | "medium" | "low";
17
+ evidence: string[];
18
+ website?: string;
19
+ version?: string;
20
+ };
21
+ export type CrawlIssueSeverity = "critical" | "error" | "warning" | "review";
22
+ export type CrawlIssue = {
23
+ id: string;
24
+ severity: CrawlIssueSeverity;
25
+ message: string;
26
+ };
27
+ export type VerificationRecord = {
28
+ url: string;
29
+ status: number | null;
30
+ h1: string;
31
+ title: string;
32
+ metaTitle: string;
33
+ metaDescription: string;
34
+ robots: string;
35
+ template: string;
36
+ mainCount: number | null;
37
+ charset: string;
38
+ language: string;
39
+ colorScheme: string;
40
+ themeColor: string;
41
+ doctype: string;
42
+ viewport: string;
43
+ canonical: string;
44
+ favicon: string;
45
+ icons: Array<{
46
+ rel: string;
47
+ href: string;
48
+ type: string;
49
+ sizes: string;
50
+ media: string;
51
+ }>;
52
+ og: {
53
+ title: string;
54
+ description: string;
55
+ image: string;
56
+ url: string;
57
+ type: string;
58
+ siteName: string;
59
+ };
60
+ twitter: {
61
+ card: string;
62
+ title: string;
63
+ description: string;
64
+ image: string;
65
+ };
66
+ tracking: {
67
+ tagManager: TrackingItem[];
68
+ analytics: TrackingItem[];
69
+ };
70
+ stack?: {
71
+ detections: StackDetection[];
72
+ };
73
+ security: {
74
+ secrets: SecretFinding[];
75
+ scannedAssets: number;
76
+ };
77
+ images?: {
78
+ issues: CrawlIssue[];
79
+ };
80
+ a11yStatic?: {
81
+ issues: CrawlIssue[];
82
+ };
83
+ note?: string;
84
+ error?: string;
85
+ };
86
+ export declare function createEmptyVerificationRecord(url: string, status: number | null): VerificationRecord;
87
+ //# sourceMappingURL=schema.d.ts.map