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 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/report/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { PageScanFailure, ScanFinding } from '../scan/index.js';
2
+ import type { Clauses } from './clauses.js';
3
+ import type { ImpactThreshold } from './verdict.js';
4
+ export interface ReportToolInfo {
5
+ readonly name: string;
6
+ readonly version: string;
7
+ }
8
+ /** The scan inputs the report echoes back, so a stored report is self-describing. */
9
+ export interface ReportTarget {
10
+ readonly baseUrl: string;
11
+ readonly wcagTags: readonly string[];
12
+ readonly failOn: ImpactThreshold;
13
+ }
14
+ /** Violation (rule) counts bucketed by axe-core impact; `unknown` is impact `null`. */
15
+ export interface ReportImpactCounts {
16
+ readonly critical: number;
17
+ readonly serious: number;
18
+ readonly moderate: number;
19
+ readonly minor: number;
20
+ readonly unknown: number;
21
+ }
22
+ export interface ReportSummary {
23
+ readonly pagesScanned: number;
24
+ readonly pagesFailed: number;
25
+ readonly pagesWithViolations: number;
26
+ /** Total violated rules across all pages (not node count). */
27
+ readonly totalViolations: number;
28
+ readonly violationsByImpact: ReportImpactCounts;
29
+ /**
30
+ * Distinct WCAG success criteria breached by at least one violation,
31
+ * dotted and numerically sorted (`["1.1.1", "1.4.3"]`). Incomplete
32
+ * results do not count — they are unconfirmed, not breaches.
33
+ */
34
+ readonly breachedSuccessCriteria: readonly string[];
35
+ /** Distinct EN 301 549 clauses breached by at least one violation, same shape. */
36
+ readonly breachedEn301549Clauses: readonly string[];
37
+ }
38
+ /** A scan finding with the standards clauses it breaches attached. */
39
+ export interface ReportFinding extends ScanFinding {
40
+ readonly clauses: Clauses;
41
+ }
42
+ /** A successfully scanned page, its findings enriched with clause mappings. */
43
+ export interface ReportPageSuccess {
44
+ readonly status: 'ok';
45
+ readonly url: string;
46
+ readonly violations: readonly ReportFinding[];
47
+ readonly incomplete: readonly ReportFinding[];
48
+ }
49
+ /** Per-page report entry: an enriched success, or a scan failure verbatim. */
50
+ export type ReportPage = ReportPageSuccess | PageScanFailure;
51
+ /**
52
+ * The pass/fail call against `target.failOn`.
53
+ *
54
+ * Scoped to what was actually scanned: a page that failed to load is
55
+ * reported in `summary.pagesFailed` and drives the CLI exit code, but it
56
+ * cannot make a verdict pass or fail, because its compliance is unknown.
57
+ */
58
+ export interface ReportVerdict {
59
+ /** Violated rules whose impact is at or above `target.failOn`. */
60
+ readonly violationsAtOrAboveThreshold: number;
61
+ /** Violated rules whose impact axe left unset, excluded from the count above. */
62
+ readonly unrankedViolations: number;
63
+ readonly passed: boolean;
64
+ }
65
+ export interface Report {
66
+ readonly schemaVersion: number;
67
+ readonly generatedAt: string;
68
+ readonly tool: ReportToolInfo;
69
+ readonly target: ReportTarget;
70
+ readonly summary: ReportSummary;
71
+ readonly verdict: ReportVerdict;
72
+ /**
73
+ * Per-page results. Scanned pages carry their findings enriched with the
74
+ * WCAG / EN 301 549 clauses each one breaches; pages that failed to load
75
+ * are passed through verbatim from the scan engine.
76
+ */
77
+ readonly pages: readonly ReportPage[];
78
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/report/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ import type { ImpactValue } from 'axe-core';
2
+ import type { ReportImpactCounts } from './types.js';
3
+ /**
4
+ * axe-core impacts from least to most severe. Index position is the rank;
5
+ * a finding meets a threshold when its rank is at or above the threshold's.
6
+ */
7
+ export declare const IMPACT_RANK: readonly ["minor", "moderate", "serious", "critical"];
8
+ export type ImpactThreshold = (typeof IMPACT_RANK)[number];
9
+ /**
10
+ * True when `impact` is at least as severe as `threshold`.
11
+ *
12
+ * A `null` impact (axe leaves it unset) is deliberately never a match: it
13
+ * cannot be ranked, so it can neither be trusted to clear a threshold nor
14
+ * be assumed to breach one. Such findings are counted separately in the
15
+ * report's `unknown` bucket and surfaced by the CLI, so they are never
16
+ * silently dropped.
17
+ */
18
+ export declare function meetsThreshold(impact: ImpactValue, threshold: ImpactThreshold): boolean;
19
+ /** Total violated rules whose impact is at or above `threshold`. */
20
+ export declare function countAtOrAbove(counts: ReportImpactCounts, threshold: ImpactThreshold): number;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * axe-core impacts from least to most severe. Index position is the rank;
3
+ * a finding meets a threshold when its rank is at or above the threshold's.
4
+ */
5
+ export const IMPACT_RANK = ['minor', 'moderate', 'serious', 'critical'];
6
+ /**
7
+ * True when `impact` is at least as severe as `threshold`.
8
+ *
9
+ * A `null` impact (axe leaves it unset) is deliberately never a match: it
10
+ * cannot be ranked, so it can neither be trusted to clear a threshold nor
11
+ * be assumed to breach one. Such findings are counted separately in the
12
+ * report's `unknown` bucket and surfaced by the CLI, so they are never
13
+ * silently dropped.
14
+ */
15
+ export function meetsThreshold(impact, threshold) {
16
+ if (impact === null) {
17
+ return false;
18
+ }
19
+ // -1 also covers an impact string a future axe version might add: an
20
+ // unrankable value is treated exactly like null rather than guessed at.
21
+ const impactRank = IMPACT_RANK.indexOf(impact);
22
+ return impactRank !== -1 && impactRank >= IMPACT_RANK.indexOf(threshold);
23
+ }
24
+ /** Total violated rules whose impact is at or above `threshold`. */
25
+ export function countAtOrAbove(counts, threshold) {
26
+ return IMPACT_RANK.filter((impact) => meetsThreshold(impact, threshold)).reduce((sum, impact) => sum + counts[impact], 0);
27
+ }
28
+ //# sourceMappingURL=verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verdict.js","sourceRoot":"","sources":["../../src/report/verdict.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAIjF;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAmB,EAAE,SAA0B;IAC5E,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,qEAAqE;IACrE,wEAAwE;IACxE,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,MAAyB,CAAC,CAAC;IAClE,OAAO,UAAU,KAAK,CAAC,CAAC,IAAI,UAAU,IAAI,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAC3E,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAAC,MAA0B,EAAE,SAA0B;IACnF,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,CAC7E,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,EACrC,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Report } from './types.js';
2
+ /** Canonical report filenames inside the configured output directory. */
3
+ export declare const REPORT_FILENAME = "report.json";
4
+ export declare const HTML_REPORT_FILENAME = "report.html";
5
+ export declare const PDF_REPORT_FILENAME = "report.pdf";
6
+ /**
7
+ * Writes the report as pretty-printed JSON to `<outputDir>/report.json`,
8
+ * creating the directory if needed and overwriting any previous run.
9
+ * Returns the path written.
10
+ */
11
+ export declare function writeReport(report: Report, outputDir: string): Promise<string>;
12
+ /**
13
+ * Writes a pre-rendered HTML report to `<outputDir>/report.html`, alongside
14
+ * the JSON. Same overwrite semantics. Returns the path written.
15
+ */
16
+ export declare function writeHtmlReport(html: string, outputDir: string): Promise<string>;
17
+ /**
18
+ * Writes pre-rendered PDF bytes to `<outputDir>/report.pdf`, alongside the
19
+ * JSON and HTML. Same overwrite semantics. Returns the path written.
20
+ */
21
+ export declare function writePdfReport(pdf: Uint8Array, outputDir: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /** Canonical report filenames inside the configured output directory. */
4
+ export const REPORT_FILENAME = 'report.json';
5
+ export const HTML_REPORT_FILENAME = 'report.html';
6
+ export const PDF_REPORT_FILENAME = 'report.pdf';
7
+ /**
8
+ * Writes the report as pretty-printed JSON to `<outputDir>/report.json`,
9
+ * creating the directory if needed and overwriting any previous run.
10
+ * Returns the path written.
11
+ */
12
+ export function writeReport(report, outputDir) {
13
+ return writeArtifact(outputDir, REPORT_FILENAME, `${JSON.stringify(report, null, 2)}\n`);
14
+ }
15
+ /**
16
+ * Writes a pre-rendered HTML report to `<outputDir>/report.html`, alongside
17
+ * the JSON. Same overwrite semantics. Returns the path written.
18
+ */
19
+ export function writeHtmlReport(html, outputDir) {
20
+ return writeArtifact(outputDir, HTML_REPORT_FILENAME, html.endsWith('\n') ? html : `${html}\n`);
21
+ }
22
+ /**
23
+ * Writes pre-rendered PDF bytes to `<outputDir>/report.pdf`, alongside the
24
+ * JSON and HTML. Same overwrite semantics. Returns the path written.
25
+ */
26
+ export function writePdfReport(pdf, outputDir) {
27
+ return writeArtifact(outputDir, PDF_REPORT_FILENAME, pdf);
28
+ }
29
+ async function writeArtifact(outputDir, filename, content) {
30
+ await mkdir(outputDir, { recursive: true });
31
+ const path = join(outputDir, filename);
32
+ await writeFile(path, content);
33
+ return path;
34
+ }
35
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/report/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,yEAAyE;AACzE,MAAM,CAAC,MAAM,eAAe,GAAG,aAAa,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,aAAa,CAAC;AAClD,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAEhD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,SAAiB;IAC3D,OAAO,aAAa,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,SAAiB;IAC7D,OAAO,aAAa,CAAC,SAAS,EAAE,oBAAoB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;AAClG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAe,EAAE,SAAiB;IAC/D,OAAO,aAAa,CAAC,SAAS,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,SAAiB,EACjB,QAAgB,EAChB,OAA4B;IAE5B,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACvC,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { scan } from './scan.js';
2
+ export type { PageScanFailure, PageScanResult, PageScanSuccess, ScanDeps, ScanFinding, ScanNode, ScanOptions, ScanResult, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { scan } from './scan.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scan/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ScanDeps, ScanOptions, ScanResult } from './types.js';
2
+ export declare function scan(urls: readonly string[], options: ScanOptions, deps?: ScanDeps): Promise<ScanResult>;
@@ -0,0 +1,82 @@
1
+ import { AxeBuilder } from '@axe-core/playwright';
2
+ import { chromium } from 'playwright';
3
+ import { runWithConcurrency } from '../shared/pool.js';
4
+ import { HostRateLimiter } from '../shared/rate-limiter.js';
5
+ import { USER_AGENT } from '../shared/user-agent.js';
6
+ const DEFAULT_TIMEOUT_MS = 15_000;
7
+ const DEFAULT_RETRIES = 1;
8
+ const DEFAULT_CONCURRENCY = 3;
9
+ const DEFAULT_HOST_RATE_LIMIT_MS = 1_000;
10
+ export async function scan(urls, options, deps = {}) {
11
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
12
+ const rateLimiter = new HostRateLimiter(options.hostRateLimitMs ?? DEFAULT_HOST_RATE_LIMIT_MS);
13
+ const browser = deps.browser ?? (await chromium.launch());
14
+ const launchedHere = deps.browser === undefined;
15
+ try {
16
+ const pages = await runWithConcurrency(urls, concurrency, (url) => scanPage(browser, url, options, rateLimiter));
17
+ return { pages };
18
+ }
19
+ finally {
20
+ if (launchedHere) {
21
+ await browser.close();
22
+ }
23
+ }
24
+ }
25
+ async function scanPage(browser, url, options, rateLimiter) {
26
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27
+ const retries = options.retries ?? DEFAULT_RETRIES;
28
+ const host = safeHost(url);
29
+ let lastError;
30
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
31
+ if (host !== null) {
32
+ await rateLimiter.wait(host);
33
+ }
34
+ const context = await browser.newContext({ userAgent: USER_AGENT });
35
+ try {
36
+ const page = await context.newPage();
37
+ await page.goto(url, { waitUntil: 'load', timeout: timeoutMs });
38
+ const results = await new AxeBuilder({ page }).withTags([...options.wcagTags]).analyze();
39
+ return {
40
+ status: 'ok',
41
+ url,
42
+ violations: results.violations.map(mapFinding),
43
+ incomplete: results.incomplete.map(mapFinding),
44
+ };
45
+ }
46
+ catch (error) {
47
+ lastError = error;
48
+ }
49
+ finally {
50
+ await context.close();
51
+ }
52
+ }
53
+ return {
54
+ status: 'error',
55
+ url,
56
+ error: lastError instanceof Error ? lastError.message : String(lastError),
57
+ };
58
+ }
59
+ function safeHost(url) {
60
+ try {
61
+ return new URL(url).host;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function mapFinding(result) {
68
+ return {
69
+ ruleId: result.id,
70
+ tags: result.tags,
71
+ impact: result.impact ?? null,
72
+ description: result.description,
73
+ help: result.help,
74
+ helpUrl: result.helpUrl,
75
+ nodes: result.nodes.map((node) => ({
76
+ target: node.target,
77
+ html: node.html,
78
+ failureSummary: node.failureSummary ?? null,
79
+ })),
80
+ };
81
+ }
82
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/scan/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { ImpactValue, UnlabelledFrameSelector } from 'axe-core';
2
+ import type { Browser } from 'playwright';
3
+ export interface ScanNode {
4
+ readonly target: UnlabelledFrameSelector;
5
+ readonly html: string;
6
+ readonly failureSummary: string | null;
7
+ }
8
+ export interface ScanFinding {
9
+ readonly ruleId: string;
10
+ /**
11
+ * The axe-core rule's tags, verbatim. Carries the standards mapping the
12
+ * report layer turns into clauses: `wcag<digits>` (a WCAG success
13
+ * criterion), `EN-9.x.x.x` (an EN 301 549 clause), plus category and
14
+ * ruleset markers. See `src/report/clauses.ts`.
15
+ */
16
+ readonly tags: readonly string[];
17
+ readonly impact: ImpactValue;
18
+ readonly description: string;
19
+ readonly help: string;
20
+ readonly helpUrl: string;
21
+ readonly nodes: readonly ScanNode[];
22
+ }
23
+ export interface PageScanSuccess {
24
+ readonly status: 'ok';
25
+ readonly url: string;
26
+ readonly violations: readonly ScanFinding[];
27
+ readonly incomplete: readonly ScanFinding[];
28
+ }
29
+ export interface PageScanFailure {
30
+ readonly status: 'error';
31
+ readonly url: string;
32
+ readonly error: string;
33
+ }
34
+ export type PageScanResult = PageScanSuccess | PageScanFailure;
35
+ export interface ScanResult {
36
+ readonly pages: readonly PageScanResult[];
37
+ }
38
+ export interface ScanOptions {
39
+ readonly wcagTags: readonly string[];
40
+ /** Navigation timeout per attempt, in milliseconds. Defaults to 15000. */
41
+ readonly timeoutMs?: number;
42
+ /** Extra attempts after the first one fails. Defaults to 1 (two attempts total). */
43
+ readonly retries?: number;
44
+ /** Maximum number of pages scanned in parallel. Defaults to 3. */
45
+ readonly concurrency?: number;
46
+ /** Minimum time between two requests to the same host, in milliseconds. Defaults to 1000. */
47
+ readonly hostRateLimitMs?: number;
48
+ }
49
+ export interface ScanDeps {
50
+ /**
51
+ * Reuse an already-launched browser instead of launching one per call.
52
+ * The caller owns its lifecycle: `scan` never closes a browser it did
53
+ * not launch. Each page still gets its own fresh, isolated context.
54
+ */
55
+ readonly browser?: Browser;
56
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/scan/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export { type ToolInfo, toolInfo } from './package-info.js';
2
+ export { runWithConcurrency } from './pool.js';
3
+ export { HostRateLimiter } from './rate-limiter.js';
4
+ export { fetchWithUserAgent, USER_AGENT } from './user-agent.js';
@@ -0,0 +1,5 @@
1
+ export { toolInfo } from './package-info.js';
2
+ export { runWithConcurrency } from './pool.js';
3
+ export { HostRateLimiter } from './rate-limiter.js';
4
+ export { fetchWithUserAgent, USER_AGENT } from './user-agent.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,10 @@
1
+ export interface ToolInfo {
2
+ readonly name: string;
3
+ readonly version: string;
4
+ }
5
+ /**
6
+ * The tool's own name and version, read once from the package manifest that
7
+ * ships next to the compiled code. Used to stamp reports and to answer
8
+ * `--version`.
9
+ */
10
+ export declare function toolInfo(): ToolInfo;
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /**
3
+ * The tool's own name and version, read once from the package manifest that
4
+ * ships next to the compiled code. Used to stamp reports and to answer
5
+ * `--version`.
6
+ */
7
+ export function toolInfo() {
8
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
9
+ return { name: pkg.name ?? 'bfsg-scanner', version: pkg.version ?? '0.0.0' };
10
+ }
11
+ //# sourceMappingURL=package-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-info.js","sourceRoot":"","sources":["../../src/shared/package-info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAOvC;;;;GAIG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAG1F,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs `worker` over `items` with at most `concurrency` calls in flight at
3
+ * once. Results are returned in input order regardless of completion order,
4
+ * so callers that care about stable output (e.g. per-page scan results)
5
+ * don't need to sort anything themselves.
6
+ */
7
+ export declare function runWithConcurrency<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Runs `worker` over `items` with at most `concurrency` calls in flight at
3
+ * once. Results are returned in input order regardless of completion order,
4
+ * so callers that care about stable output (e.g. per-page scan results)
5
+ * don't need to sort anything themselves.
6
+ */
7
+ export async function runWithConcurrency(items, concurrency, worker) {
8
+ const results = new Array(items.length);
9
+ let nextIndex = 0;
10
+ async function runWorker() {
11
+ for (;;) {
12
+ const index = nextIndex;
13
+ nextIndex += 1;
14
+ if (index >= items.length) {
15
+ return;
16
+ }
17
+ const item = items[index];
18
+ results[index] = await worker(item, index);
19
+ }
20
+ }
21
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
22
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
23
+ return results;
24
+ }
25
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/shared/pool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAmB,EACnB,WAAmB,EACnB,MAA8C;IAE9C,MAAM,OAAO,GAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,UAAU,SAAS;QACtB,SAAS,CAAC;YACR,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC1B,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAM,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACrE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAE1E,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Spaces out requests to the same host by at least `intervalMs`, so a
3
+ * concurrency pool of several workers never bursts a target server with
4
+ * simultaneous requests. Different hosts are independent of one another.
5
+ */
6
+ export declare class HostRateLimiter {
7
+ private readonly intervalMs;
8
+ private readonly nextAvailableAt;
9
+ constructor(intervalMs?: number);
10
+ wait(host: string): Promise<void>;
11
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Spaces out requests to the same host by at least `intervalMs`, so a
3
+ * concurrency pool of several workers never bursts a target server with
4
+ * simultaneous requests. Different hosts are independent of one another.
5
+ */
6
+ export class HostRateLimiter {
7
+ intervalMs;
8
+ nextAvailableAt = new Map();
9
+ constructor(intervalMs = 1000) {
10
+ this.intervalMs = intervalMs;
11
+ }
12
+ async wait(host) {
13
+ const now = Date.now();
14
+ const earliest = this.nextAvailableAt.get(host) ?? now;
15
+ const scheduledAt = Math.max(now, earliest);
16
+ this.nextAvailableAt.set(host, scheduledAt + this.intervalMs);
17
+ const delay = scheduledAt - now;
18
+ if (delay > 0) {
19
+ await sleep(delay);
20
+ }
21
+ }
22
+ }
23
+ function sleep(ms) {
24
+ return new Promise((resolve) => setTimeout(resolve, ms));
25
+ }
26
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/shared/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACT,UAAU,CAAS;IACnB,eAAe,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE7D,YAAY,UAAU,GAAG,IAAI;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;QACvD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;QAE9D,MAAM,KAAK,GAAG,WAAW,GAAG,GAAG,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;CACF;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Sent on every request the scanner makes — Playwright navigations and the
3
+ * plain fetch() calls used for sitemap/robots.txt/crawl discovery — so a
4
+ * site operator can identify, and if they choose, block traffic from this
5
+ * tool. Never omit it and never spoof a real browser's User-Agent.
6
+ */
7
+ export declare const USER_AGENT = "bfsg-scanner (+https://github.com/MihaelaAghirculesei/bfsg-scanner)";
8
+ /** Default fetch for discovery (sitemap/robots.txt/crawl): identical to the
9
+ * global fetch, but always carries USER_AGENT. Tests inject their own
10
+ * fetchFn and bypass this. */
11
+ export declare const fetchWithUserAgent: typeof fetch;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sent on every request the scanner makes — Playwright navigations and the
3
+ * plain fetch() calls used for sitemap/robots.txt/crawl discovery — so a
4
+ * site operator can identify, and if they choose, block traffic from this
5
+ * tool. Never omit it and never spoof a real browser's User-Agent.
6
+ */
7
+ export const USER_AGENT = 'bfsg-scanner (+https://github.com/MihaelaAghirculesei/bfsg-scanner)';
8
+ /** Default fetch for discovery (sitemap/robots.txt/crawl): identical to the
9
+ * global fetch, but always carries USER_AGENT. Tests inject their own
10
+ * fetchFn and bypass this. */
11
+ export const fetchWithUserAgent = (input, init) => fetch(input, { ...init, headers: { ...init?.headers, 'User-Agent': USER_AGENT } });
12
+ //# sourceMappingURL=user-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../../src/shared/user-agent.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,qEAAqE,CAAC;AAEhG;;8BAE8B;AAC9B,MAAM,CAAC,MAAM,kBAAkB,GAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAC9D,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC"}
@@ -0,0 +1,52 @@
1
+ # Responsible use
2
+
3
+ `bfsg-scanner` drives a real browser against whatever URL you give it and
4
+ follows links across the site. That is ordinary web traffic, but it is
5
+ traffic *you are causing to someone else's infrastructure*. Use it
6
+ accordingly.
7
+
8
+ ## Scan only what you are allowed to scan
9
+
10
+ Run it against:
11
+
12
+ - sites you own or operate, or
13
+ - sites you have explicit permission from the operator to test (a client
14
+ engagement, an employer's property, a written authorisation).
15
+
16
+ Accessibility testing is not a licence to crawl arbitrary third-party sites.
17
+ "It's just an audit" is not consent.
18
+
19
+ ## What the tool already does to be a good citizen
20
+
21
+ - **Identifiable User-Agent.** Every request — page navigations and the
22
+ `fetch` calls for `sitemap.xml` / `robots.txt` / crawling — carries
23
+ `bfsg-scanner (+https://github.com/MihaelaAghirculesei/bfsg-scanner)`. It
24
+ never spoofs a real browser's User-Agent, so an operator can see it in
25
+ their logs and block it if they want to.
26
+ - **Respects `robots.txt`.** The crawl obeys `Disallow` rules for its
27
+ User-Agent. If `robots.txt` disallows everything, the run discovers
28
+ nothing and exits `3`.
29
+ - **Rate-limited per host** (default: one request per second per host) and
30
+ **bounded** (`maxPages`, default 50). Raise these only against
31
+ infrastructure you know can take it.
32
+
33
+ Do not patch these safeguards out to scan a site that is trying to keep you
34
+ out.
35
+
36
+ ## Handle the output carefully
37
+
38
+ A report can contain substantial excerpts of the scanned site's markup —
39
+ copy, structure, sometimes data. Treat `report.*` files and any list of
40
+ client URLs as you would any other client material:
41
+
42
+ - The repo's `.gitignore` already excludes `/reports/`, `/private/`,
43
+ `sites.private.*`, and `*.bfsg-report.*`. Keep it that way.
44
+ - Don't paste report contents into public issues or chats without checking
45
+ what's in them.
46
+
47
+ ## Not legal advice
48
+
49
+ A passing scan is evidence, not a legal conclusion. WCAG and EN 301 549
50
+ cover only what automated tooling can check; conformance and the BFSG
51
+ obligations around it are broader. Treat the report as one input to a
52
+ compliance assessment, not the assessment itself.