bfsg-scanner 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 (104) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +147 -0
  4. package/dist/cli/index.d.ts +2 -0
  5. package/dist/cli/index.js +4 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/run.d.ts +29 -0
  8. package/dist/cli/run.js +202 -0
  9. package/dist/cli/run.js.map +1 -0
  10. package/dist/config/index.d.ts +2 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/index.js.map +1 -0
  13. package/dist/config/load.d.ts +12 -0
  14. package/dist/config/load.js +47 -0
  15. package/dist/config/load.js.map +1 -0
  16. package/dist/config/schema.d.ts +51 -0
  17. package/dist/config/schema.js +16 -0
  18. package/dist/config/schema.js.map +1 -0
  19. package/dist/discovery/crawl.d.ts +21 -0
  20. package/dist/discovery/crawl.js +79 -0
  21. package/dist/discovery/crawl.js.map +1 -0
  22. package/dist/discovery/discover-site.d.ts +26 -0
  23. package/dist/discovery/discover-site.js +50 -0
  24. package/dist/discovery/discover-site.js.map +1 -0
  25. package/dist/discovery/discover.d.ts +25 -0
  26. package/dist/discovery/discover.js +81 -0
  27. package/dist/discovery/discover.js.map +1 -0
  28. package/dist/discovery/index.d.ts +11 -0
  29. package/dist/discovery/index.js +7 -0
  30. package/dist/discovery/index.js.map +1 -0
  31. package/dist/discovery/links.d.ts +11 -0
  32. package/dist/discovery/links.js +69 -0
  33. package/dist/discovery/links.js.map +1 -0
  34. package/dist/discovery/robots.d.ts +18 -0
  35. package/dist/discovery/robots.js +91 -0
  36. package/dist/discovery/robots.js.map +1 -0
  37. package/dist/discovery/sitemap.d.ts +18 -0
  38. package/dist/discovery/sitemap.js +36 -0
  39. package/dist/discovery/sitemap.js.map +1 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/report/build.d.ts +14 -0
  44. package/dist/report/build.js +100 -0
  45. package/dist/report/build.js.map +1 -0
  46. package/dist/report/clauses.d.ts +32 -0
  47. package/dist/report/clauses.js +57 -0
  48. package/dist/report/clauses.js.map +1 -0
  49. package/dist/report/i18n.d.ts +54 -0
  50. package/dist/report/i18n.js +95 -0
  51. package/dist/report/i18n.js.map +1 -0
  52. package/dist/report/index.d.ts +12 -0
  53. package/dist/report/index.js +8 -0
  54. package/dist/report/index.js.map +1 -0
  55. package/dist/report/pdf.d.ts +12 -0
  56. package/dist/report/pdf.js +25 -0
  57. package/dist/report/pdf.js.map +1 -0
  58. package/dist/report/render.d.ts +8 -0
  59. package/dist/report/render.js +204 -0
  60. package/dist/report/render.js.map +1 -0
  61. package/dist/report/types.d.ts +78 -0
  62. package/dist/report/types.js +2 -0
  63. package/dist/report/types.js.map +1 -0
  64. package/dist/report/verdict.d.ts +20 -0
  65. package/dist/report/verdict.js +28 -0
  66. package/dist/report/verdict.js.map +1 -0
  67. package/dist/report/write.d.ts +21 -0
  68. package/dist/report/write.js +35 -0
  69. package/dist/report/write.js.map +1 -0
  70. package/dist/scan/index.d.ts +2 -0
  71. package/dist/scan/index.js +2 -0
  72. package/dist/scan/index.js.map +1 -0
  73. package/dist/scan/scan.d.ts +2 -0
  74. package/dist/scan/scan.js +82 -0
  75. package/dist/scan/scan.js.map +1 -0
  76. package/dist/scan/types.d.ts +56 -0
  77. package/dist/scan/types.js +2 -0
  78. package/dist/scan/types.js.map +1 -0
  79. package/dist/shared/index.d.ts +4 -0
  80. package/dist/shared/index.js +5 -0
  81. package/dist/shared/index.js.map +1 -0
  82. package/dist/shared/package-info.d.ts +10 -0
  83. package/dist/shared/package-info.js +11 -0
  84. package/dist/shared/package-info.js.map +1 -0
  85. package/dist/shared/pool.d.ts +7 -0
  86. package/dist/shared/pool.js +25 -0
  87. package/dist/shared/pool.js.map +1 -0
  88. package/dist/shared/rate-limiter.d.ts +11 -0
  89. package/dist/shared/rate-limiter.js +26 -0
  90. package/dist/shared/rate-limiter.js.map +1 -0
  91. package/dist/shared/user-agent.d.ts +11 -0
  92. package/dist/shared/user-agent.js +12 -0
  93. package/dist/shared/user-agent.js.map +1 -0
  94. package/docs/SCANNING-ETHICS.md +52 -0
  95. package/docs/adr/0001-page-discovery-strategy.md +74 -0
  96. package/docs/adr/0002-json-report-output.md +54 -0
  97. package/docs/adr/0003-failon-threshold-and-exit-codes.md +72 -0
  98. package/docs/adr/0004-clause-mapping-from-axe-tags.md +81 -0
  99. package/docs/adr/0005-html-report-rendering.md +71 -0
  100. package/docs/adr/0006-pdf-report-rendering.md +58 -0
  101. package/docs/adr/0007-cli-argument-surface.md +73 -0
  102. package/docs/adr/0008-report-json-schema.md +59 -0
  103. package/package.json +73 -0
  104. package/schema/report.v1.json +156 -0
@@ -0,0 +1,79 @@
1
+ import { fetchWithUserAgent } from '../shared/user-agent.js';
2
+ import { extractLinks, isCrawlableUrl, matchesExcludePattern } from './links.js';
3
+ import { fetchRobotsRules, isAllowedByRobots } from './robots.js';
4
+ /**
5
+ * Breadth-first fallback crawl, used when a site has no sitemap. Starts at
6
+ * baseUrl, follows same-origin links in document order, and stops once
7
+ * maxPages pages have been collected. robots.txt, the configured
8
+ * excludePaths, and non-page assets (mailto:, PDFs, images, ...) are all
9
+ * applied identically to the seed URL and to every discovered link, so a
10
+ * disallowed or excluded home page yields zero results rather than being
11
+ * scanned anyway.
12
+ *
13
+ * Deterministic: links are visited in a plain FIFO queue in the order they
14
+ * appear in each page's HTML, so the same input always produces the same
15
+ * page list in the same order.
16
+ */
17
+ export async function crawlSite(options) {
18
+ const { baseUrl, maxPages, excludePaths = [], fetchFn = fetchWithUserAgent } = options;
19
+ const origin = new URL(baseUrl).origin;
20
+ const robotsRules = await fetchRobotsRules(origin, fetchFn);
21
+ const isPermitted = (url) => url.origin === origin &&
22
+ isCrawlableUrl(url) &&
23
+ !matchesExcludePattern(url.pathname, excludePaths) &&
24
+ isAllowedByRobots(url.pathname + url.search, robotsRules);
25
+ const startUrl = new URL(baseUrl);
26
+ startUrl.hash = '';
27
+ const queue = [];
28
+ const seen = new Set();
29
+ const collected = [];
30
+ if (isPermitted(startUrl)) {
31
+ const startHref = startUrl.toString();
32
+ queue.push(startHref);
33
+ seen.add(startHref);
34
+ }
35
+ while (queue.length > 0 && collected.length < maxPages) {
36
+ const currentUrl = queue.shift();
37
+ if (currentUrl === undefined) {
38
+ break;
39
+ }
40
+ const html = await fetchPage(fetchFn, currentUrl);
41
+ if (html === null) {
42
+ continue;
43
+ }
44
+ collected.push(currentUrl);
45
+ if (collected.length >= maxPages) {
46
+ break;
47
+ }
48
+ enqueueLinks(extractLinks(html, currentUrl), isPermitted, seen, queue);
49
+ }
50
+ return collected;
51
+ }
52
+ async function fetchPage(fetchFn, url) {
53
+ try {
54
+ const response = await fetchFn(url);
55
+ return response.ok ? await response.text() : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function enqueueLinks(hrefs, isPermitted, seen, queue) {
62
+ for (const href of hrefs) {
63
+ let url;
64
+ try {
65
+ url = new URL(href);
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ url.hash = '';
71
+ const normalized = url.toString();
72
+ if (seen.has(normalized) || !isPermitted(url)) {
73
+ continue;
74
+ }
75
+ seen.add(normalized);
76
+ queue.push(normalized);
77
+ }
78
+ }
79
+ //# sourceMappingURL=crawl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawl.js","sourceRoot":"","sources":["../../src/discovery/crawl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface DiscoverSiteOptions {
2
+ readonly baseUrl: string;
3
+ readonly maxPages: number;
4
+ /** Glob patterns matched against a URL pathname; matches are skipped. Defaults to none. */
5
+ readonly excludePaths?: readonly string[];
6
+ /** Injectable for tests; defaults to the global fetch (with User-Agent). */
7
+ readonly fetchFn?: typeof fetch;
8
+ }
9
+ /**
10
+ * Resolves the list of page URLs to scan for a site.
11
+ *
12
+ * Strategy: prefer the site's sitemap.xml — it is the owner's own
13
+ * declaration of which pages exist. If the root sitemap is missing or
14
+ * unparseable, fall back to a breadth-first crawl from baseUrl.
15
+ *
16
+ * Both strategies apply excludePaths and the non-page-asset filter
17
+ * identically. They differ on robots.txt: the crawl path obeys it (a
18
+ * disallowed page is never fetched, and a disallowed home page yields an
19
+ * empty list), while the sitemap path does not — a URL the owner put in
20
+ * their own sitemap is taken as intended for scanning.
21
+ *
22
+ * On the sitemap path, baseUrl is prepended when the sitemap did not list
23
+ * it, so the home page is always covered; the crawl path already starts
24
+ * from baseUrl. The result never exceeds maxPages.
25
+ */
26
+ export declare function discoverSite(options: DiscoverSiteOptions): Promise<string[]>;
@@ -0,0 +1,50 @@
1
+ import { crawlSite } from './crawl.js';
2
+ import { discoverFromSitemap } from './discover.js';
3
+ import { SitemapError } from './sitemap.js';
4
+ /**
5
+ * Resolves the list of page URLs to scan for a site.
6
+ *
7
+ * Strategy: prefer the site's sitemap.xml — it is the owner's own
8
+ * declaration of which pages exist. If the root sitemap is missing or
9
+ * unparseable, fall back to a breadth-first crawl from baseUrl.
10
+ *
11
+ * Both strategies apply excludePaths and the non-page-asset filter
12
+ * identically. They differ on robots.txt: the crawl path obeys it (a
13
+ * disallowed page is never fetched, and a disallowed home page yields an
14
+ * empty list), while the sitemap path does not — a URL the owner put in
15
+ * their own sitemap is taken as intended for scanning.
16
+ *
17
+ * On the sitemap path, baseUrl is prepended when the sitemap did not list
18
+ * it, so the home page is always covered; the crawl path already starts
19
+ * from baseUrl. The result never exceeds maxPages.
20
+ */
21
+ export async function discoverSite(options) {
22
+ const { baseUrl, maxPages, excludePaths = [], fetchFn } = options;
23
+ const shared = { baseUrl, maxPages, excludePaths, ...(fetchFn ? { fetchFn } : {}) };
24
+ try {
25
+ const pages = await discoverFromSitemap(shared);
26
+ return withBaseUrl(pages, baseUrl, maxPages);
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof SitemapError)) {
30
+ throw error;
31
+ }
32
+ }
33
+ return crawlSite(shared);
34
+ }
35
+ function withBaseUrl(pages, baseUrl, maxPages) {
36
+ const normalized = new URL(baseUrl).toString();
37
+ const alreadyListed = pages.some((page) => {
38
+ try {
39
+ return new URL(page).toString() === normalized;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ });
45
+ if (alreadyListed) {
46
+ return [...pages];
47
+ }
48
+ return [normalized, ...pages].slice(0, maxPages);
49
+ }
50
+ //# sourceMappingURL=discover-site.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover-site.js","sourceRoot":"","sources":["../../src/discovery/discover-site.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAW5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAA4B;IAC7D,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAClE,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAEpF,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC,MAAM,CAAC,CAAC;QAChD,OAAO,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,YAAY,CAAC,EAAE,CAAC;YACrC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,WAAW,CAAC,KAAwB,EAAE,OAAe,EAAE,QAAgB;IAC9E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC/C,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACxC,IAAI,CAAC;YACH,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,KAAK,UAAU,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,CAAC,UAAU,EAAE,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,25 @@
1
+ export interface DiscoverFromSitemapOptions {
2
+ readonly baseUrl: string;
3
+ readonly maxPages: number;
4
+ /** Path to the root sitemap, relative to baseUrl. Defaults to "/sitemap.xml". */
5
+ readonly sitemapPath?: string;
6
+ /** Glob patterns matched against a URL pathname; matches are skipped. Defaults to none. */
7
+ readonly excludePaths?: readonly string[];
8
+ /** Injectable for tests; defaults to the global fetch. */
9
+ readonly fetchFn?: typeof fetch;
10
+ /** Guards against pathological or cyclic sitemap indexes. Defaults to 3. */
11
+ readonly maxSitemapDepth?: number;
12
+ }
13
+ /**
14
+ * Discovers page URLs from a site's sitemap, following a sitemap index to
15
+ * its sub-sitemaps as needed. Off-origin entries are dropped (a sitemap
16
+ * should only ever list its own site, but nothing enforces that), non-page
17
+ * assets and excludePaths matches are filtered out (identically to
18
+ * crawlSite), duplicate URLs are collapsed, and discovery stops as soon as
19
+ * maxPages is reached.
20
+ *
21
+ * A failure to load the root sitemap is fatal (the caller should fall back
22
+ * to crawling). A failure on a sub-sitemap referenced by an index is not:
23
+ * that one sub-sitemap is skipped and discovery continues with the rest.
24
+ */
25
+ export declare function discoverFromSitemap(options: DiscoverFromSitemapOptions): Promise<string[]>;
@@ -0,0 +1,81 @@
1
+ import { fetchWithUserAgent } from '../shared/user-agent.js';
2
+ import { isCrawlableUrl, matchesExcludePattern } from './links.js';
3
+ import { parseSitemapXml, SitemapError } from './sitemap.js';
4
+ /**
5
+ * Discovers page URLs from a site's sitemap, following a sitemap index to
6
+ * its sub-sitemaps as needed. Off-origin entries are dropped (a sitemap
7
+ * should only ever list its own site, but nothing enforces that), non-page
8
+ * assets and excludePaths matches are filtered out (identically to
9
+ * crawlSite), duplicate URLs are collapsed, and discovery stops as soon as
10
+ * maxPages is reached.
11
+ *
12
+ * A failure to load the root sitemap is fatal (the caller should fall back
13
+ * to crawling). A failure on a sub-sitemap referenced by an index is not:
14
+ * that one sub-sitemap is skipped and discovery continues with the rest.
15
+ */
16
+ export async function discoverFromSitemap(options) {
17
+ const { baseUrl, maxPages, sitemapPath = '/sitemap.xml', excludePaths = [], fetchFn = fetchWithUserAgent, maxSitemapDepth = 3, } = options;
18
+ const origin = new URL(baseUrl).origin;
19
+ const rootSitemapUrl = new URL(sitemapPath, baseUrl).toString();
20
+ const collected = [];
21
+ const seenPages = new Set();
22
+ const visitedSitemaps = new Set();
23
+ await visitSitemap(rootSitemapUrl, 0);
24
+ return collected.slice(0, maxPages);
25
+ async function visitSitemap(sitemapUrl, depth) {
26
+ if (collected.length >= maxPages) {
27
+ return;
28
+ }
29
+ if (visitedSitemaps.has(sitemapUrl) || depth > maxSitemapDepth) {
30
+ return;
31
+ }
32
+ visitedSitemaps.add(sitemapUrl);
33
+ let xml;
34
+ try {
35
+ const response = await fetchFn(sitemapUrl);
36
+ if (!response.ok) {
37
+ throw new Error(`HTTP ${response.status}`);
38
+ }
39
+ xml = await response.text();
40
+ }
41
+ catch (cause) {
42
+ if (depth === 0) {
43
+ throw new SitemapError(`Cannot load sitemap at "${sitemapUrl}": ${cause instanceof Error ? cause.message : String(cause)}`);
44
+ }
45
+ return;
46
+ }
47
+ const doc = parseSitemapXml(xml);
48
+ if (doc.kind === 'sitemapindex') {
49
+ for (const childUrl of doc.sitemaps) {
50
+ if (collected.length >= maxPages) {
51
+ return;
52
+ }
53
+ await visitSitemap(childUrl, depth + 1);
54
+ }
55
+ return;
56
+ }
57
+ for (const pageUrl of doc.urls) {
58
+ if (collected.length >= maxPages) {
59
+ return;
60
+ }
61
+ if (seenPages.has(pageUrl)) {
62
+ continue;
63
+ }
64
+ seenPages.add(pageUrl);
65
+ let parsed;
66
+ try {
67
+ parsed = new URL(pageUrl);
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ if (parsed.origin !== origin ||
73
+ !isCrawlableUrl(parsed) ||
74
+ matchesExcludePattern(parsed.pathname, excludePaths)) {
75
+ continue;
76
+ }
77
+ collected.push(pageUrl);
78
+ }
79
+ }
80
+ }
81
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/discovery/discover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export type { CrawlOptions } from './crawl.js';
2
+ export { crawlSite } from './crawl.js';
3
+ export type { DiscoverFromSitemapOptions } from './discover.js';
4
+ export { discoverFromSitemap } from './discover.js';
5
+ export type { DiscoverSiteOptions } from './discover-site.js';
6
+ export { discoverSite } from './discover-site.js';
7
+ export { extractLinks, isCrawlableUrl, matchesExcludePattern } from './links.js';
8
+ export type { RobotsRule } from './robots.js';
9
+ export { fetchRobotsRules, isAllowedByRobots, parseRobotsTxt } from './robots.js';
10
+ export type { SitemapDocument } from './sitemap.js';
11
+ export { parseSitemapXml, SitemapError } from './sitemap.js';
@@ -0,0 +1,7 @@
1
+ export { crawlSite } from './crawl.js';
2
+ export { discoverFromSitemap } from './discover.js';
3
+ export { discoverSite } from './discover-site.js';
4
+ export { extractLinks, isCrawlableUrl, matchesExcludePattern } from './links.js';
5
+ export { fetchRobotsRules, isAllowedByRobots, parseRobotsTxt } from './robots.js';
6
+ export { parseSitemapXml, SitemapError } from './sitemap.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/discovery/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Extracts every <a href> from raw HTML, resolved against pageUrl. Href
2
+ * values that fail to resolve (empty, malformed) are silently skipped —
3
+ * this is a best-effort fallback crawler, not an HTML validator. */
4
+ export declare function extractLinks(html: string, pageUrl: string): string[];
5
+ /** True for http(s) links that don't point at a known non-page asset.
6
+ * Rejects mailto:/tel:/javascript: via the protocol check. */
7
+ export declare function isCrawlableUrl(url: URL): boolean;
8
+ /** Simple glob matching against a URL pathname: `*` matches any sequence of
9
+ * characters, and a pattern with no trailing wildcard also matches
10
+ * everything under it (e.g. "/impressum" matches "/impressum/legal"). */
11
+ export declare function matchesExcludePattern(pathname: string, patterns: readonly string[]): boolean;
@@ -0,0 +1,69 @@
1
+ const NON_PAGE_EXTENSIONS = new Set([
2
+ '.pdf',
3
+ '.jpg',
4
+ '.jpeg',
5
+ '.png',
6
+ '.gif',
7
+ '.svg',
8
+ '.webp',
9
+ '.ico',
10
+ '.css',
11
+ '.js',
12
+ '.mjs',
13
+ '.json',
14
+ '.xml',
15
+ '.zip',
16
+ '.mp4',
17
+ '.mp3',
18
+ '.woff',
19
+ '.woff2',
20
+ '.ttf',
21
+ '.eot',
22
+ '.doc',
23
+ '.docx',
24
+ '.xls',
25
+ '.xlsx',
26
+ ]);
27
+ const HREF_PATTERN = /<a\s[^>]*?href\s*=\s*("([^"]*)"|'([^']*)')/gi;
28
+ /** Extracts every <a href> from raw HTML, resolved against pageUrl. Href
29
+ * values that fail to resolve (empty, malformed) are silently skipped —
30
+ * this is a best-effort fallback crawler, not an HTML validator. */
31
+ export function extractLinks(html, pageUrl) {
32
+ const links = [];
33
+ for (const match of html.matchAll(HREF_PATTERN)) {
34
+ const raw = match[2] ?? match[3] ?? '';
35
+ if (raw.length === 0) {
36
+ continue;
37
+ }
38
+ try {
39
+ links.push(new URL(raw, pageUrl).toString());
40
+ }
41
+ catch {
42
+ // ignore unparsable hrefs
43
+ }
44
+ }
45
+ return links;
46
+ }
47
+ /** True for http(s) links that don't point at a known non-page asset.
48
+ * Rejects mailto:/tel:/javascript: via the protocol check. */
49
+ export function isCrawlableUrl(url) {
50
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
51
+ return false;
52
+ }
53
+ const lastSegment = url.pathname.slice(url.pathname.lastIndexOf('/') + 1);
54
+ const lastDot = lastSegment.lastIndexOf('.');
55
+ if (lastDot === -1) {
56
+ return true;
57
+ }
58
+ return !NON_PAGE_EXTENSIONS.has(lastSegment.slice(lastDot).toLowerCase());
59
+ }
60
+ /** Simple glob matching against a URL pathname: `*` matches any sequence of
61
+ * characters, and a pattern with no trailing wildcard also matches
62
+ * everything under it (e.g. "/impressum" matches "/impressum/legal"). */
63
+ export function matchesExcludePattern(pathname, patterns) {
64
+ return patterns.some((pattern) => {
65
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
66
+ return new RegExp(`^${escaped}(?:/.*)?$`).test(pathname);
67
+ });
68
+ }
69
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/discovery/links.ts"],"names":[],"mappings":"AAAA,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC;IACvD,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;IACR,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,8CAA8C,CAAC;AAEpE;;oEAEoE;AACpE,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe;IACxD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;8DAC8D;AAC9D,MAAM,UAAU,cAAc,CAAC,GAAQ;IACrC,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,WAAW,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED;;yEAEyE;AACzE,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,QAA2B;IACjF,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACnF,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface RobotsRule {
2
+ readonly pattern: string;
3
+ readonly allow: boolean;
4
+ }
5
+ /**
6
+ * Parses the rule group matching `userAgent` (default "*") out of a
7
+ * robots.txt file. Only Allow/Disallow/User-agent are recognised — Sitemap,
8
+ * Crawl-delay, and anything else are ignored, since this parser exists only
9
+ * to decide whether a URL may be crawled.
10
+ */
11
+ export declare function parseRobotsTxt(text: string, userAgent?: string): readonly RobotsRule[];
12
+ /**
13
+ * Standard robots.txt precedence: the most specific (longest) matching
14
+ * pattern wins; an Allow breaks a tie with a Disallow of equal length.
15
+ * With no matching rule, the path is allowed by default.
16
+ */
17
+ export declare function isAllowedByRobots(pathAndQuery: string, rules: readonly RobotsRule[]): boolean;
18
+ export declare function fetchRobotsRules(origin: string, fetchFn?: typeof fetch): Promise<readonly RobotsRule[]>;
@@ -0,0 +1,91 @@
1
+ import { fetchWithUserAgent } from '../shared/user-agent.js';
2
+ /**
3
+ * Parses the rule group matching `userAgent` (default "*") out of a
4
+ * robots.txt file. Only Allow/Disallow/User-agent are recognised — Sitemap,
5
+ * Crawl-delay, and anything else are ignored, since this parser exists only
6
+ * to decide whether a URL may be crawled.
7
+ */
8
+ export function parseRobotsTxt(text, userAgent = '*') {
9
+ const lines = stripBom(text).split(/\r?\n/);
10
+ const groups = [];
11
+ let current = null;
12
+ for (const rawLine of lines) {
13
+ const line = stripComment(rawLine).trim();
14
+ if (line.length === 0) {
15
+ continue;
16
+ }
17
+ const separatorIndex = line.indexOf(':');
18
+ if (separatorIndex === -1) {
19
+ continue;
20
+ }
21
+ const directive = line.slice(0, separatorIndex).trim().toLowerCase();
22
+ const value = line.slice(separatorIndex + 1).trim();
23
+ if (directive === 'user-agent') {
24
+ // A fresh User-agent line after rules have already been recorded
25
+ // starts a new group; consecutive User-agent lines with no rules yet
26
+ // belong to the same group (robots.txt allows grouping agents).
27
+ if (current === null || current.rules.length > 0) {
28
+ current = { agents: [], rules: [] };
29
+ groups.push(current);
30
+ }
31
+ current.agents.push(value.toLowerCase());
32
+ }
33
+ else if (directive === 'allow' || directive === 'disallow') {
34
+ if (current === null || (directive === 'disallow' && value.length === 0)) {
35
+ continue;
36
+ }
37
+ current.rules.push({ pattern: value, allow: directive === 'allow' });
38
+ }
39
+ }
40
+ const group = groups.find((g) => g.agents.includes(userAgent.toLowerCase()));
41
+ return group?.rules ?? [];
42
+ }
43
+ /**
44
+ * Standard robots.txt precedence: the most specific (longest) matching
45
+ * pattern wins; an Allow breaks a tie with a Disallow of equal length.
46
+ * With no matching rule, the path is allowed by default.
47
+ */
48
+ export function isAllowedByRobots(pathAndQuery, rules) {
49
+ let best = null;
50
+ for (const rule of rules) {
51
+ if (!matchesRobotsPattern(pathAndQuery, rule.pattern)) {
52
+ continue;
53
+ }
54
+ if (best === null ||
55
+ rule.pattern.length > best.pattern.length ||
56
+ (rule.pattern.length === best.pattern.length && rule.allow && !best.allow)) {
57
+ best = rule;
58
+ }
59
+ }
60
+ return best === null || best.allow;
61
+ }
62
+ export async function fetchRobotsRules(origin, fetchFn = fetchWithUserAgent) {
63
+ try {
64
+ const response = await fetchFn(new URL('/robots.txt', origin).toString());
65
+ if (!response.ok) {
66
+ return [];
67
+ }
68
+ return parseRobotsTxt(await response.text());
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ }
74
+ function matchesRobotsPattern(pathAndQuery, pattern) {
75
+ if (pattern.length === 0) {
76
+ return false;
77
+ }
78
+ const endsWithDollar = pattern.endsWith('$');
79
+ const body = endsWithDollar ? pattern.slice(0, -1) : pattern;
80
+ const escaped = body.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
81
+ const regex = new RegExp(`^${escaped}${endsWithDollar ? '$' : ''}`);
82
+ return regex.test(pathAndQuery);
83
+ }
84
+ function stripBom(text) {
85
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
86
+ }
87
+ function stripComment(line) {
88
+ const hashIndex = line.indexOf('#');
89
+ return hashIndex === -1 ? line : line.slice(0, hashIndex);
90
+ }
91
+ //# sourceMappingURL=robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/discovery/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAO7D;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,SAAS,GAAG,GAAG;IAC1D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAgD,EAAE,CAAC;IAC/D,IAAI,OAAO,GAAqD,IAAI,CAAC;IAErE,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,cAAc,KAAK,CAAC,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEpD,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;YAC/B,iEAAiE;YACjE,qEAAqE;YACrE,gEAAgE;YAChE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACjD,OAAO,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAC3C,CAAC;aAAM,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7D,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,SAAS,KAAK,UAAU,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;gBACzE,SAAS;YACX,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,KAAK,OAAO,EAAE,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC7E,OAAO,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB,EAAE,KAA4B;IAClF,IAAI,IAAI,GAAsB,IAAI,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,oBAAoB,CAAC,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,SAAS;QACX,CAAC;QACD,IACE,IAAI,KAAK,IAAI;YACb,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM;YACzC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAC1E,CAAC;YACD,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,UAAwB,kBAAkB;IAE1C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,cAAc,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,YAAoB,EAAE,OAAe;IACjE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare class SitemapError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export type SitemapDocument = {
5
+ readonly kind: 'urlset';
6
+ readonly urls: readonly string[];
7
+ } | {
8
+ readonly kind: 'sitemapindex';
9
+ readonly sitemaps: readonly string[];
10
+ };
11
+ /**
12
+ * Parses a sitemaps.org XML document (urlset or sitemapindex), extracting
13
+ * only each entry's own <loc>. Extensions nested under a <url> — most
14
+ * commonly <image:image><image:loc>...</image:loc></image:image> in
15
+ * WordPress-generated sitemaps — are deliberately ignored: they share the
16
+ * tag name "loc" but are not page URLs.
17
+ */
18
+ export declare function parseSitemapXml(xml: string): SitemapDocument;
@@ -0,0 +1,36 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+ export class SitemapError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'SitemapError';
6
+ }
7
+ }
8
+ const parser = new XMLParser({ ignoreAttributes: true });
9
+ /**
10
+ * Parses a sitemaps.org XML document (urlset or sitemapindex), extracting
11
+ * only each entry's own <loc>. Extensions nested under a <url> — most
12
+ * commonly <image:image><image:loc>...</image:loc></image:image> in
13
+ * WordPress-generated sitemaps — are deliberately ignored: they share the
14
+ * tag name "loc" but are not page URLs.
15
+ */
16
+ export function parseSitemapXml(xml) {
17
+ const doc = parser.parse(xml);
18
+ if (isRecord(doc.urlset)) {
19
+ return { kind: 'urlset', urls: extractLocs(doc.urlset.url) };
20
+ }
21
+ if (isRecord(doc.sitemapindex)) {
22
+ return { kind: 'sitemapindex', sitemaps: extractLocs(doc.sitemapindex.sitemap) };
23
+ }
24
+ throw new SitemapError('Expected a <urlset> or <sitemapindex> root element');
25
+ }
26
+ function extractLocs(entries) {
27
+ const list = Array.isArray(entries) ? entries : entries === undefined ? [] : [entries];
28
+ return list
29
+ .filter(isRecord)
30
+ .map((entry) => entry.loc)
31
+ .filter((loc) => typeof loc === 'string');
32
+ }
33
+ function isRecord(value) {
34
+ return typeof value === 'object' && value !== null;
35
+ }
36
+ //# sourceMappingURL=sitemap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/discovery/sitemap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAMD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;IAEzD,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,IAAI,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;IACnF,CAAC;IAED,MAAM,IAAI,YAAY,CAAC,oDAAoD,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,WAAW,CAAC,OAAgB;IACnC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACvF,OAAO,IAAI;SACR,MAAM,CAAC,QAAQ,CAAC;SAChB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;SACzB,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const SCHEMA_VERSION = 1;
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export const SCHEMA_VERSION = 1;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ScanResult } from '../scan/index.js';
2
+ import type { Report, ReportTarget, ReportToolInfo } from './types.js';
3
+ export interface BuildReportOptions {
4
+ /** Overrides the generated-at timestamp; defaults to now. Injectable for tests. */
5
+ readonly generatedAt?: Date;
6
+ /** Overrides tool name/version; defaults to package.json. Injectable for tests. */
7
+ readonly tool?: ReportToolInfo;
8
+ }
9
+ /**
10
+ * Assembles a versioned, self-describing report from a scan result. Pure
11
+ * apart from the default clock and the one-time package.json read, both of
12
+ * which can be overridden via options.
13
+ */
14
+ export declare function buildReport(scanResult: ScanResult, target: ReportTarget, options?: BuildReportOptions): Report;