@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,604 @@
1
+ import * as cheerio from "cheerio";
2
+ function collectHtmlSignals(html, pageUrl) {
3
+ const $ = cheerio.load(html);
4
+ const scriptSrc = [];
5
+ const inlineScripts = [];
6
+ const linkHrefs = [];
7
+ const meta = {};
8
+ $("script[src]").each((_, element) => {
9
+ const src = ($(element).attr("src") ?? "").trim();
10
+ if (src)
11
+ scriptSrc.push(src);
12
+ });
13
+ $("script:not([src])").each((_, element) => {
14
+ const content = $(element).html();
15
+ if (content?.trim())
16
+ inlineScripts.push(content);
17
+ });
18
+ $("link[href]").each((_, element) => {
19
+ const href = ($(element).attr("href") ?? "").trim();
20
+ if (href)
21
+ linkHrefs.push(href);
22
+ });
23
+ $("meta").each((_, element) => {
24
+ const name = ($(element).attr("name") || $(element).attr("property") || "").trim().toLowerCase();
25
+ const content = ($(element).attr("content") ?? "").trim();
26
+ if (name && content)
27
+ meta[name] = content;
28
+ });
29
+ return { html, pageUrl, scriptSrc, inlineScripts, meta, linkHrefs };
30
+ }
31
+ function includesAny(haystack, needles) {
32
+ const lower = haystack.toLowerCase();
33
+ for (const needle of needles) {
34
+ if (lower.includes(needle.toLowerCase()))
35
+ return needle;
36
+ }
37
+ return null;
38
+ }
39
+ function scriptSrcMatch(signals, patterns) {
40
+ for (const src of signals.scriptSrc) {
41
+ const hit = includesAny(src, patterns);
42
+ if (hit)
43
+ return src;
44
+ }
45
+ return null;
46
+ }
47
+ function htmlMatch(signals, patterns) {
48
+ for (const pattern of patterns) {
49
+ const match = pattern.exec(signals.html);
50
+ if (match)
51
+ return match;
52
+ }
53
+ return null;
54
+ }
55
+ const HTML_RULES = [
56
+ // Frameworks
57
+ {
58
+ name: "Next.js",
59
+ category: "Framework",
60
+ website: "https://nextjs.org",
61
+ confidence: "high",
62
+ detect: (signals) => {
63
+ if (signals.html.includes("__NEXT_DATA__"))
64
+ return { evidence: "html: `__NEXT_DATA__`" };
65
+ if (signals.html.includes("data-next-head"))
66
+ return { evidence: "html: `data-next-head`" };
67
+ if (signals.html.includes('id="__next"'))
68
+ return { evidence: "html: `#__next` root" };
69
+ const src = scriptSrcMatch(signals, ["/_next/static/"]);
70
+ if (src)
71
+ return { evidence: `script: \`${src}\`` };
72
+ return null;
73
+ },
74
+ },
75
+ {
76
+ name: "Nuxt",
77
+ category: "Framework",
78
+ website: "https://nuxt.com",
79
+ confidence: "high",
80
+ detect: (signals) => {
81
+ if (signals.html.includes("__NUXT__"))
82
+ return { evidence: "html: `__NUXT__`" };
83
+ const src = scriptSrcMatch(signals, ["/_nuxt/"]);
84
+ if (src)
85
+ return { evidence: `script: \`${src}\`` };
86
+ return null;
87
+ },
88
+ },
89
+ {
90
+ name: "Gatsby",
91
+ category: "Framework",
92
+ website: "https://www.gatsbyjs.com",
93
+ confidence: "high",
94
+ detect: (signals) => {
95
+ if (signals.html.includes("___gatsby"))
96
+ return { evidence: "html: `___gatsby`" };
97
+ const src = scriptSrcMatch(signals, ["gatsby"]);
98
+ if (src)
99
+ return { evidence: `script: \`${src}\`` };
100
+ return null;
101
+ },
102
+ },
103
+ {
104
+ name: "Remix",
105
+ category: "Framework",
106
+ website: "https://remix.run",
107
+ confidence: "high",
108
+ detect: (signals) => {
109
+ if (signals.html.includes("__remixContext"))
110
+ return { evidence: "html: `__remixContext`" };
111
+ return null;
112
+ },
113
+ },
114
+ {
115
+ name: "Astro",
116
+ category: "Framework",
117
+ website: "https://astro.build",
118
+ confidence: "high",
119
+ detect: (signals) => {
120
+ if (signals.html.includes("astro-island"))
121
+ return { evidence: "html: `astro-island`" };
122
+ const src = scriptSrcMatch(signals, ["/_astro/"]);
123
+ if (src)
124
+ return { evidence: `script: \`${src}\`` };
125
+ return null;
126
+ },
127
+ },
128
+ {
129
+ name: "Angular",
130
+ category: "Framework",
131
+ website: "https://angular.io",
132
+ confidence: "high",
133
+ detect: (signals) => {
134
+ if (/ng-version\s*=\s*["'][\d.]+["']/i.test(signals.html)) {
135
+ const match = signals.html.match(/ng-version\s*=\s*["']([\d.]+)["']/i);
136
+ return { evidence: "html: `ng-version`", version: match?.[1] };
137
+ }
138
+ if (signals.html.includes("ng-app"))
139
+ return { evidence: "html: `ng-app`" };
140
+ const src = scriptSrcMatch(signals, ["angular.min.js", "angular.js", "@angular/"]);
141
+ if (src)
142
+ return { evidence: `script: \`${src}\`` };
143
+ return null;
144
+ },
145
+ },
146
+ {
147
+ name: "Vue",
148
+ category: "Framework",
149
+ website: "https://vuejs.org",
150
+ confidence: "high",
151
+ detect: (signals) => {
152
+ if (signals.html.includes("data-v-"))
153
+ return { evidence: "html: scoped `data-v-*` attributes" };
154
+ const src = scriptSrcMatch(signals, ["vue.global", "vue.runtime", "/vue@", "vue.min.js"]);
155
+ if (src)
156
+ return { evidence: `script: \`${src}\`` };
157
+ return null;
158
+ },
159
+ },
160
+ {
161
+ name: "SvelteKit",
162
+ category: "Framework",
163
+ website: "https://kit.svelte.dev",
164
+ confidence: "high",
165
+ detect: (signals) => {
166
+ if (signals.html.includes("__sveltekit"))
167
+ return { evidence: "html: `__sveltekit`" };
168
+ return null;
169
+ },
170
+ },
171
+ // CMS
172
+ {
173
+ name: "WordPress",
174
+ category: "CMS",
175
+ website: "https://wordpress.org",
176
+ confidence: "high",
177
+ detect: (signals) => {
178
+ const generator = signals.meta.generator ?? "";
179
+ if (/wordpress/i.test(generator)) {
180
+ const version = generator.match(/WordPress\s+([\d.]+)/i)?.[1];
181
+ return { evidence: `meta generator: \`${generator}\``, version };
182
+ }
183
+ if (includesAny(signals.html, ["/wp-content/", "/wp-includes/"])) {
184
+ return { evidence: "html: WordPress asset paths (`/wp-content/` or `/wp-includes/`)" };
185
+ }
186
+ return null;
187
+ },
188
+ },
189
+ {
190
+ name: "Sanity",
191
+ category: "CMS",
192
+ website: "https://www.sanity.io",
193
+ confidence: "high",
194
+ detect: (signals) => {
195
+ const hit = includesAny(signals.html, ["cdn.sanity.io", "sanity.io", "sanity-cdn"]) ??
196
+ scriptSrcMatch(signals, ["sanity.io", "@sanity/"]);
197
+ if (hit)
198
+ return { evidence: `Sanity CDN or script: \`${hit}\`` };
199
+ return null;
200
+ },
201
+ },
202
+ {
203
+ name: "Contentful",
204
+ category: "CMS",
205
+ website: "https://www.contentful.com",
206
+ confidence: "high",
207
+ detect: (signals) => {
208
+ const hit = includesAny(signals.html, ["ctfassets.net", "contentful.com"]);
209
+ if (hit)
210
+ return { evidence: `Contentful asset host: \`${hit}\`` };
211
+ return null;
212
+ },
213
+ },
214
+ {
215
+ name: "Storyblok",
216
+ category: "CMS",
217
+ website: "https://www.storyblok.com",
218
+ confidence: "high",
219
+ detect: (signals) => {
220
+ const hit = includesAny(signals.html, ["a.storyblok.com", "storyblok.com"]);
221
+ if (hit)
222
+ return { evidence: `Storyblok CDN: \`${hit}\`` };
223
+ return null;
224
+ },
225
+ },
226
+ {
227
+ name: "Shopify",
228
+ category: "CMS",
229
+ website: "https://www.shopify.com",
230
+ confidence: "high",
231
+ detect: (signals) => {
232
+ if (signals.html.includes("Shopify.theme"))
233
+ return { evidence: "script: `Shopify.theme`" };
234
+ const hit = includesAny(signals.html, ["cdn.shopify.com", "myshopify.com"]);
235
+ if (hit)
236
+ return { evidence: `Shopify CDN: \`${hit}\`` };
237
+ return null;
238
+ },
239
+ },
240
+ {
241
+ name: "Drupal",
242
+ category: "CMS",
243
+ website: "https://www.drupal.org",
244
+ confidence: "high",
245
+ detect: (signals) => {
246
+ const generator = signals.meta.generator ?? "";
247
+ if (/drupal/i.test(generator))
248
+ return { evidence: `meta generator: \`${generator}\`` };
249
+ const src = scriptSrcMatch(signals, ["drupal.js"]);
250
+ if (src)
251
+ return { evidence: `script: \`${src}\`` };
252
+ return null;
253
+ },
254
+ },
255
+ {
256
+ name: "Webflow",
257
+ category: "CMS",
258
+ website: "https://webflow.com",
259
+ confidence: "high",
260
+ detect: (signals) => {
261
+ if (signals.html.includes("data-wf-site"))
262
+ return { evidence: "html: `data-wf-site`" };
263
+ const src = scriptSrcMatch(signals, ["webflow.js", "assets.website-files.com"]);
264
+ if (src)
265
+ return { evidence: `script: \`${src}\`` };
266
+ return null;
267
+ },
268
+ },
269
+ {
270
+ name: "Prismic",
271
+ category: "CMS",
272
+ website: "https://prismic.io",
273
+ confidence: "high",
274
+ detect: (signals) => {
275
+ const hit = includesAny(signals.html, ["prismic.io", "prismiccdn.net"]);
276
+ if (hit)
277
+ return { evidence: `Prismic CDN: \`${hit}\`` };
278
+ return null;
279
+ },
280
+ },
281
+ // Search
282
+ {
283
+ name: "Algolia",
284
+ category: "Search",
285
+ website: "https://www.algolia.com",
286
+ confidence: "high",
287
+ detect: (signals) => {
288
+ const hit = includesAny(signals.html, ["algolia.net", "algolianet.com", "algolia.com"]) ??
289
+ scriptSrcMatch(signals, ["algoliasearch", "instantsearch"]);
290
+ if (hit)
291
+ return { evidence: `Algolia: \`${hit}\`` };
292
+ return null;
293
+ },
294
+ },
295
+ {
296
+ name: "Meilisearch",
297
+ category: "Search",
298
+ website: "https://www.meilisearch.com",
299
+ confidence: "high",
300
+ detect: (signals) => {
301
+ const hit = scriptSrcMatch(signals, ["meilisearch", "@meilisearch/"]);
302
+ if (hit)
303
+ return { evidence: `script: \`${hit}\`` };
304
+ return null;
305
+ },
306
+ },
307
+ {
308
+ name: "Typesense",
309
+ category: "Search",
310
+ website: "https://typesense.org",
311
+ confidence: "high",
312
+ detect: (signals) => {
313
+ const hit = includesAny(signals.html, ["typesense.org", "typesense.net"]);
314
+ if (hit)
315
+ return { evidence: `Typesense: \`${hit}\`` };
316
+ return null;
317
+ },
318
+ },
319
+ ];
320
+ const IMPLIED = {
321
+ "Next.js": [{ name: "Node.js", category: "Platform", confidence: "medium", evidence: "implied by Next.js" }],
322
+ Nuxt: [{ name: "Node.js", category: "Platform", confidence: "medium", evidence: "implied by Nuxt" }],
323
+ Gatsby: [{ name: "Node.js", category: "Platform", confidence: "medium", evidence: "implied by Gatsby" }],
324
+ Remix: [{ name: "Node.js", category: "Platform", confidence: "medium", evidence: "implied by Remix" }],
325
+ Astro: [{ name: "Node.js", category: "Platform", confidence: "medium", evidence: "implied by Astro" }],
326
+ WordPress: [{ name: "PHP", category: "Platform", confidence: "high", evidence: "implied by WordPress" }],
327
+ Drupal: [{ name: "PHP", category: "Platform", confidence: "high", evidence: "implied by Drupal" }],
328
+ };
329
+ const EXCLUDES = {
330
+ "Next.js": ["React"],
331
+ Nuxt: ["Vue"],
332
+ Gatsby: ["React"],
333
+ Remix: ["React"],
334
+ };
335
+ const HEADER_RULES = [
336
+ {
337
+ name: "Vercel",
338
+ category: "Hosting",
339
+ website: "https://vercel.com",
340
+ confidence: "high",
341
+ detect: (headers) => (headers["x-vercel-id"] ? { evidence: `header: x-vercel-id` } : null),
342
+ },
343
+ {
344
+ name: "Netlify",
345
+ category: "Hosting",
346
+ website: "https://www.netlify.com",
347
+ confidence: "high",
348
+ detect: (headers) => {
349
+ const server = headers.server ?? "";
350
+ if (/netlify/i.test(server))
351
+ return { evidence: `header: server \`${server}\`` };
352
+ if (headers["x-nf-request-id"])
353
+ return { evidence: "header: x-nf-request-id" };
354
+ return null;
355
+ },
356
+ },
357
+ {
358
+ name: "Cloudflare",
359
+ category: "CDN",
360
+ website: "https://www.cloudflare.com",
361
+ confidence: "high",
362
+ detect: (headers) => {
363
+ if (headers["cf-ray"])
364
+ return { evidence: `header: cf-ray \`${headers["cf-ray"]}\`` };
365
+ const server = headers.server ?? "";
366
+ if (/cloudflare/i.test(server))
367
+ return { evidence: `header: server \`${server}\`` };
368
+ return null;
369
+ },
370
+ },
371
+ {
372
+ name: "AWS CloudFront",
373
+ category: "CDN",
374
+ website: "https://aws.amazon.com/cloudfront/",
375
+ confidence: "high",
376
+ detect: (headers) => (headers["x-amz-cf-id"] ? { evidence: "header: x-amz-cf-id" } : null),
377
+ },
378
+ {
379
+ name: "PHP",
380
+ category: "Platform",
381
+ website: "https://php.net",
382
+ confidence: "high",
383
+ detect: (headers) => {
384
+ const powered = headers["x-powered-by"] ?? "";
385
+ if (/php/i.test(powered)) {
386
+ const version = powered.match(/PHP\/([\d.]+)/i)?.[1];
387
+ return { evidence: `header: x-powered-by \`${powered}\``, version };
388
+ }
389
+ return null;
390
+ },
391
+ },
392
+ {
393
+ name: "Node.js",
394
+ category: "Platform",
395
+ website: "https://nodejs.org",
396
+ confidence: "medium",
397
+ detect: (headers) => {
398
+ const powered = headers["x-powered-by"] ?? "";
399
+ if (/express/i.test(powered))
400
+ return { evidence: `header: x-powered-by \`${powered}\`` };
401
+ return null;
402
+ },
403
+ },
404
+ {
405
+ name: "Ruby on Rails",
406
+ category: "Platform",
407
+ website: "https://rubyonrails.org",
408
+ confidence: "high",
409
+ detect: (headers) => {
410
+ const powered = headers["x-powered-by"] ?? "";
411
+ if (/phusion passenger|rails/i.test(powered)) {
412
+ return { evidence: `header: x-powered-by \`${powered}\`` };
413
+ }
414
+ return null;
415
+ },
416
+ },
417
+ {
418
+ name: "ASP.NET",
419
+ category: "Platform",
420
+ website: "https://dotnet.microsoft.com/apps/aspnet",
421
+ confidence: "high",
422
+ detect: (headers) => {
423
+ const powered = headers["x-powered-by"] ?? "";
424
+ if (/asp\.net/i.test(powered))
425
+ return { evidence: `header: x-powered-by \`${powered}\`` };
426
+ if (headers["x-aspnet-version"])
427
+ return { evidence: `header: x-aspnet-version` };
428
+ return null;
429
+ },
430
+ },
431
+ ];
432
+ const DNS_RULES = [
433
+ {
434
+ name: "Vercel",
435
+ category: "Hosting",
436
+ website: "https://vercel.com",
437
+ confidence: "high",
438
+ detect: ({ cname }) => {
439
+ const hit = cname.find((value) => /vercel\.app/i.test(value));
440
+ return hit ? { evidence: `dns cname: \`${hit}\`` } : null;
441
+ },
442
+ },
443
+ {
444
+ name: "Netlify",
445
+ category: "Hosting",
446
+ website: "https://www.netlify.com",
447
+ confidence: "high",
448
+ detect: ({ cname }) => {
449
+ const hit = cname.find((value) => /\.netlify\.(app|com)/i.test(value));
450
+ return hit ? { evidence: `dns cname: \`${hit}\`` } : null;
451
+ },
452
+ },
453
+ {
454
+ name: "Cloudflare",
455
+ category: "CDN",
456
+ website: "https://www.cloudflare.com",
457
+ confidence: "medium",
458
+ detect: ({ ns }) => {
459
+ const hit = ns.find((value) => /cloudflare\.com/i.test(value));
460
+ return hit ? { evidence: `dns ns: \`${hit}\`` } : null;
461
+ },
462
+ },
463
+ ];
464
+ function normalizeHeaders(headers) {
465
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
466
+ }
467
+ function applyImpliesAndExcludes(detections) {
468
+ const byName = new Map(detections.map((detection) => [detection.name, detection]));
469
+ for (const detection of [...detections]) {
470
+ for (const implied of IMPLIED[detection.name] ?? []) {
471
+ if (byName.has(implied.name))
472
+ continue;
473
+ const entry = {
474
+ name: implied.name,
475
+ categories: [implied.category],
476
+ confidence: implied.confidence,
477
+ evidence: [implied.evidence],
478
+ detector: "flightcheck-v1",
479
+ };
480
+ byName.set(implied.name, entry);
481
+ detections.push(entry);
482
+ }
483
+ }
484
+ const excluded = new Set();
485
+ for (const detection of detections) {
486
+ for (const name of EXCLUDES[detection.name] ?? []) {
487
+ excluded.add(name);
488
+ }
489
+ }
490
+ return detections.filter((detection) => !excluded.has(detection.name));
491
+ }
492
+ function toDetection(name, category, confidence, match, website) {
493
+ return {
494
+ name,
495
+ categories: [category],
496
+ confidence,
497
+ evidence: [match.evidence],
498
+ website,
499
+ version: match.version,
500
+ detector: "flightcheck-v1",
501
+ };
502
+ }
503
+ export function detectStackFromHtml(html, pageUrl) {
504
+ if (!html.trim())
505
+ return [];
506
+ const signals = collectHtmlSignals(html, pageUrl);
507
+ const detections = [];
508
+ for (const rule of HTML_RULES) {
509
+ const match = rule.detect(signals);
510
+ if (!match)
511
+ continue;
512
+ detections.push(toDetection(rule.name, rule.category, rule.confidence, match, rule.website));
513
+ }
514
+ // Platform signals from HTML (only when not already implied)
515
+ const railsMeta = htmlMatch(signals, [/name=["']csrf-token["']/i, /data-turbo/i]);
516
+ if (railsMeta) {
517
+ detections.push({
518
+ name: "Ruby on Rails",
519
+ categories: ["Platform"],
520
+ confidence: "high",
521
+ evidence: [`html: \`${railsMeta[0]}\``],
522
+ website: "https://rubyonrails.org",
523
+ detector: "flightcheck-v1",
524
+ });
525
+ }
526
+ const djangoToken = htmlMatch(signals, [/name=["']csrfmiddlewaretoken["']/i]);
527
+ if (djangoToken) {
528
+ detections.push({
529
+ name: "Django",
530
+ categories: ["Platform"],
531
+ confidence: "high",
532
+ evidence: [`html: \`${djangoToken[0]}\``],
533
+ website: "https://www.djangoproject.com",
534
+ detector: "flightcheck-v1",
535
+ });
536
+ }
537
+ const aspViewState = signals.html.includes("__VIEWSTATE");
538
+ if (aspViewState) {
539
+ detections.push({
540
+ name: "ASP.NET",
541
+ categories: ["Platform"],
542
+ confidence: "high",
543
+ evidence: ["html: `__VIEWSTATE`"],
544
+ website: "https://dotnet.microsoft.com/apps/aspnet",
545
+ detector: "flightcheck-v1",
546
+ });
547
+ }
548
+ const phpPath = scriptSrcMatch(signals, [".php"]) ??
549
+ (signals.linkHrefs.find((href) => /\.php(\?|$)/i.test(href)) ?? null);
550
+ if (phpPath && !detections.some((item) => item.name === "PHP")) {
551
+ detections.push({
552
+ name: "PHP",
553
+ categories: ["Platform"],
554
+ confidence: "medium",
555
+ evidence: [`asset: \`${phpPath}\``],
556
+ website: "https://php.net",
557
+ detector: "flightcheck-v1",
558
+ });
559
+ }
560
+ return applyImpliesAndExcludes(detections);
561
+ }
562
+ export function detectStackFromHeaders(headers) {
563
+ const normalized = normalizeHeaders(headers);
564
+ const detections = [];
565
+ for (const rule of HEADER_RULES) {
566
+ const match = rule.detect(normalized);
567
+ if (!match)
568
+ continue;
569
+ detections.push(toDetection(rule.name, rule.category, rule.confidence, match, rule.website));
570
+ }
571
+ return applyImpliesAndExcludes(detections);
572
+ }
573
+ export function detectStackFromDns(records) {
574
+ const normalized = {
575
+ cname: records.cname,
576
+ ns: records.ns,
577
+ mx: records.mx.map((entry) => entry.exchange),
578
+ txt: records.txt,
579
+ };
580
+ const detections = [];
581
+ for (const rule of DNS_RULES) {
582
+ const match = rule.detect(normalized);
583
+ if (!match)
584
+ continue;
585
+ detections.push(toDetection(rule.name, rule.category, rule.confidence, match, rule.website));
586
+ }
587
+ return applyImpliesAndExcludes(detections);
588
+ }
589
+ export function detectStackFromRobots(_body) {
590
+ return [];
591
+ }
592
+ export const CURATED_STACK_NAMES = new Set([
593
+ ...HTML_RULES.map((rule) => rule.name),
594
+ ...HEADER_RULES.map((rule) => rule.name),
595
+ ...DNS_RULES.map((rule) => rule.name),
596
+ "Node.js",
597
+ "PHP",
598
+ "Ruby on Rails",
599
+ "Django",
600
+ "ASP.NET",
601
+ ]);
602
+ export function isCuratedStackName(name) {
603
+ return CURATED_STACK_NAMES.has(name);
604
+ }
@@ -0,0 +1,5 @@
1
+ import type { CheerioAPI } from "cheerio";
2
+ import type { CrawlIssue } from "./schema.js";
3
+ export declare function extractImageIssues($: CheerioAPI, pageUrl: string): CrawlIssue[];
4
+ export declare function extractA11yStaticIssues($: CheerioAPI): CrawlIssue[];
5
+ //# sourceMappingURL=tier1Extractors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tier1Extractors.d.ts","sourceRoot":"","sources":["../../src/crawler/tier1Extractors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAkM9C,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAkC/E;AAiBD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CA6CnE"}