privalint 0.1.1 → 0.3.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.
- package/README.de.md +117 -0
- package/README.md +55 -1
- package/dist/assessment.d.ts +6 -0
- package/dist/assessment.js +79 -0
- package/dist/assessment.js.map +1 -0
- package/dist/capture.d.ts +5 -0
- package/dist/capture.js +155 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/consent.d.ts +3 -0
- package/dist/consent.js +13 -6
- package/dist/consent.js.map +1 -1
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.js +187 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.js +3 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/legal.d.ts +10 -0
- package/dist/legal.js +99 -0
- package/dist/legal.js.map +1 -0
- package/dist/network.d.ts +67 -0
- package/dist/network.js +235 -0
- package/dist/network.js.map +1 -0
- package/dist/observations.d.ts +3 -0
- package/dist/observations.js +4 -0
- package/dist/observations.js.map +1 -0
- package/dist/reporters/capture.d.ts +5 -0
- package/dist/reporters/capture.js +35 -0
- package/dist/reporters/capture.js.map +1 -0
- package/dist/reporters/markdown.js +21 -12
- package/dist/reporters/markdown.js.map +1 -1
- package/dist/reporters/messages.d.ts +21 -0
- package/dist/reporters/messages.js +68 -0
- package/dist/reporters/messages.js.map +1 -0
- package/dist/reporters/sarif.js +31 -1
- package/dist/reporters/sarif.js.map +1 -1
- package/dist/reporters/shared.d.ts +1 -3
- package/dist/reporters/shared.js +0 -17
- package/dist/reporters/shared.js.map +1 -1
- package/dist/reporters/text.js +23 -15
- package/dist/reporters/text.js.map +1 -1
- package/dist/rules/google-fonts.js +28 -10
- package/dist/rules/google-fonts.js.map +1 -1
- package/dist/rules/impressum-link.js +21 -8
- package/dist/rules/impressum-link.js.map +1 -1
- package/dist/rules/links.d.ts +2 -0
- package/dist/rules/links.js +2 -0
- package/dist/rules/links.js.map +1 -1
- package/dist/rules/pre-consent-trackers.js +20 -7
- package/dist/rules/pre-consent-trackers.js.map +1 -1
- package/dist/rules/privacy-policy-link.js +12 -5
- package/dist/rules/privacy-policy-link.js.map +1 -1
- package/dist/rules/reject-option.js +28 -8
- package/dist/rules/reject-option.js.map +1 -1
- package/dist/rules/third-party-cdns.js +10 -3
- package/dist/rules/third-party-cdns.js.map +1 -1
- package/dist/rules/third-party-embeds.js +10 -3
- package/dist/rules/third-party-embeds.js.map +1 -1
- package/dist/rules/third-party-hits.js +2 -1
- package/dist/rules/third-party-hits.js.map +1 -1
- package/dist/rules/tracker-hits.d.ts +4 -2
- package/dist/rules/tracker-hits.js +16 -4
- package/dist/rules/tracker-hits.js.map +1 -1
- package/dist/rules/trackers-after-reject.js +24 -12
- package/dist/rules/trackers-after-reject.js.map +1 -1
- package/dist/scan-options.d.ts +39 -0
- package/dist/scan-options.js +2 -0
- package/dist/scan-options.js.map +1 -0
- package/dist/scanner.d.ts +4 -32
- package/dist/scanner.js +87 -104
- package/dist/scanner.js.map +1 -1
- package/dist/types.d.ts +70 -0
- package/package.json +11 -6
package/README.de.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# privalint
|
|
2
|
+
|
|
3
|
+
**English:** [README.md](README.md)
|
|
4
|
+
|
|
5
|
+
privalint prüft Websites auf die Datenschutzfehler, für die es Abmahnungen gibt: Tracker, die vor
|
|
6
|
+
der Einwilligung laden oder nach „Ablehnen“ weiterlaufen, Google Fonts vom Google-Server,
|
|
7
|
+
eingebundene Inhalte wie YouTube oder Google Maps und ein fehlendes Impressum.
|
|
8
|
+
|
|
9
|
+
privalint öffnet die Seite in einem echten Chromium-Browser ohne Cookies, wie beim ersten Besuch.
|
|
10
|
+
Es zeichnet auf, was geladen wird, bevor jemand das Cookie-Banner bedient, klickt dann auf
|
|
11
|
+
„Ablehnen“ und prüft, was trotzdem noch lädt. Es läuft im Terminal oder als GitHub Action bei
|
|
12
|
+
jedem Pull Request.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx privalint https://ihre-seite.de --lang de
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> privalint führt automatisierte Prüfungen durch. Ein sauberes Ergebnis bedeutet, dass keines der
|
|
20
|
+
> geprüften Probleme gefunden wurde. Es macht eine Website nicht DSGVO-konform und ist keine
|
|
21
|
+
> Rechtsberatung.
|
|
22
|
+
|
|
23
|
+
## Deutschland, Österreich, Schweiz
|
|
24
|
+
|
|
25
|
+
Mit `--region` wählen Sie das Referenz- und Einstufungsprofil des Tools. Das Profil bestimmt
|
|
26
|
+
nicht, welches Recht für eine Website anwendbar ist. Befunde sind keine rechtliche Beurteilung.
|
|
27
|
+
|
|
28
|
+
| Region | Cookies und Tracking | Impressum | Besonderheit |
|
|
29
|
+
| --------------- | ----------------------------------------- | ----------------------------------------------- | ----------------------------------------------------- |
|
|
30
|
+
| `de` (Standard) | Opt-in, § 25 TDDDG | § 5 DDG | – |
|
|
31
|
+
| `at` | Opt-in, § 165 Abs. 3 TKG 2021 | § 5 ECG, § 25 MedienG | – |
|
|
32
|
+
| `ch` | Opt-out, Art. 45c FMG | Art. 3 Abs. 1 lit. s UWG (bei Online-Angeboten) | Tracker, Einbindungen und Impressum eine Stufe milder |
|
|
33
|
+
| `eu` | Opt-in, Art. 5 Abs. 3 ePrivacy-Richtlinie | Art. 5 E-Commerce-Richtlinie | – |
|
|
34
|
+
|
|
35
|
+
Tracker, die nach einem ausdrücklichen „Ablehnen“ weiterlaufen, bleiben überall ein Fehler, auch in
|
|
36
|
+
der Schweiz (Art. 30 Abs. 2 lit. b DSG). Richtet sich eine Schweizer Website auch an Besucher aus
|
|
37
|
+
der EU, prüfen Sie sie zusätzlich mit `--region eu`.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx privalint https://ihre-seite.at --region at --lang de --max-pages 20
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Was geprüft wird
|
|
44
|
+
|
|
45
|
+
| Regel | Was sie findet |
|
|
46
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `pre-consent-trackers` | Analyse-, Werbe- und Session-Replay-Tools, die vor der Einwilligung Anfragen senden oder Cookies setzen |
|
|
48
|
+
| `trackers-after-reject` | Tracker, die nach Klick auf „Ablehnen“ und Neuladen weiterlaufen |
|
|
49
|
+
| `reject-option` | Cookie-Banner mit „Zustimmen“, aber ohne „Ablehnen“ auf der ersten Ebene; Pur-Abo-Modelle als Hinweis |
|
|
50
|
+
| `google-fonts` | Schriften von `fonts.googleapis.com` / `fonts.gstatic.com` |
|
|
51
|
+
| `third-party-embeds` | YouTube, Vimeo, Google Maps, reCAPTCHA, Gravatar und Social-Media-Einbindungen vor der Einwilligung |
|
|
52
|
+
| `third-party-cdns` | Skripte und Styles von jsDelivr, unpkg, cdnjs und anderen öffentlichen CDNs (Hinweis) |
|
|
53
|
+
| `impressum-link` | Seiten ohne Link zum Impressum |
|
|
54
|
+
| `privacy-policy-link` | Seiten ohne Link zur Datenschutzerklärung |
|
|
55
|
+
|
|
56
|
+
Details, Hintergründe und Lösungen stehen auf Englisch in [`docs/rules`](docs/rules).
|
|
57
|
+
|
|
58
|
+
## GitHub Action
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
- uses: jexcellence/privalint@v0
|
|
62
|
+
with:
|
|
63
|
+
url: https://ihre-seite.de
|
|
64
|
+
max-pages: 20
|
|
65
|
+
lang: de
|
|
66
|
+
region: de
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Der Bericht erscheint auf Deutsch in der Zusammenfassung des Workflow-Laufs. Alle Eingaben und
|
|
70
|
+
die Anbindung an GitHub Code Scanning beschreibt die [englische README](README.md#github-action).
|
|
71
|
+
|
|
72
|
+
## Öffentliche Webscanner und kontrollierte Erfassung
|
|
73
|
+
|
|
74
|
+
`scan()` unterstützt einen `networkAdapter` und ein `AbortSignal`. Jeder HTTP-Hop einschließlich
|
|
75
|
+
Redirects durchläuft den Adapter. Er gibt eine Antwort zurück oder lehnt mit einem typisierten
|
|
76
|
+
`NetworkError` ab. Eine Warteschlange puffert Lastspitzen; Parallelität, Anfragemenge,
|
|
77
|
+
Antwortgrößen und Zeitlimits sind getrennt. Im Adapter-Modus gibt es keinen Browser-HTTP-Fallback.
|
|
78
|
+
|
|
79
|
+
PrivaLint erstellt und schließt Browser sowie frische Contexts selbst, auch bei Abbruch. Der
|
|
80
|
+
Aufrufer verwaltet gemeinsam genutzte Transport-Clients. Die bestehende Egress-Policy muss bei
|
|
81
|
+
jedem Hop DNS-Auflösung und tatsächliche Socket-Ziele kontrollieren und das Request-Signal beachten.
|
|
82
|
+
Die [Anleitung](docs/controlled-scanner.de.md) beschreibt den Vertrag und die Zähler. Ein
|
|
83
|
+
[typgeprüftes Beispiel](examples/controlled-scanner.ts) nutzt ausschließlich vom Betreiber
|
|
84
|
+
freigegebene Origins und festgelegte IP-Adressen.
|
|
85
|
+
|
|
86
|
+
`findings` enthält ausschließlich Regelbefunde. Technische Einschränkungen stehen getrennt unter
|
|
87
|
+
`pages[].capture.diagnostics`. Bei 48 Anfragen mit 21 blockierten oder fehlgeschlagenen Ressourcen
|
|
88
|
+
bleibt die Erfassung unvollständig, auch wenn keine Findings vorliegen. `ruleEvaluations` zeigt
|
|
89
|
+
pro Seite und Regel `executed`, `skipped` oder `not-evaluable`; `partial: true` kennzeichnet die
|
|
90
|
+
Auswertung verfügbarer positiver Evidenz bei eingeschränkter Beobachtung.
|
|
91
|
+
|
|
92
|
+
Consent-Zustände unterscheiden „kein Banner erkannt“, „keine unterstützte Ablehnen-Möglichkeit“,
|
|
93
|
+
„Interaktion fehlgeschlagen“ und „Ablehnen getestet“. Fehlende Erkennung allein ist kein
|
|
94
|
+
Datenschutzverstoß. Ein getesteter Klick sagt noch nichts über vollständige Ressourcen-Erfassung aus.
|
|
95
|
+
|
|
96
|
+
Bestehende CLI-, API- und Action-Aufrufe bleiben gültig. Startseiten-Ladefehler werfen weiterhin
|
|
97
|
+
einen Fehler; `failureMode: 'report'` liefert stattdessen einen strukturierten Bericht und ist im
|
|
98
|
+
Adapter-Modus die Vorgabe. Es werden keine Zusagen rechtlicher Konformität gemacht.
|
|
99
|
+
|
|
100
|
+
## Grenzen
|
|
101
|
+
|
|
102
|
+
- Manche Seiten zeigen ihr Cookie-Banner Headless-Browsern nicht. Dann meldet privalint „kein
|
|
103
|
+
Banner gefunden“. Lokal hilft `--headed --channel msedge` (oder `chrome`).
|
|
104
|
+
- Viele Consent-Tools entscheiden anhand der IP-Adresse, ob sie ein Banner zeigen. GitHub-Runner
|
|
105
|
+
haben US-Adressen und sehen deshalb unter Umständen etwas anderes als Besucher aus der EU.
|
|
106
|
+
- Serverseitiges Tracking ist im Browser nicht sichtbar.
|
|
107
|
+
|
|
108
|
+
## Mitmachen
|
|
109
|
+
|
|
110
|
+
Fehlalarme, fehlende Tracker, Consent-Tools und neue Regeln sind sehr willkommen. Gerade
|
|
111
|
+
Anbieter aus dem deutschsprachigen Raum fehlen noch. Der Einstieg steht in
|
|
112
|
+
[CONTRIBUTING.md](CONTRIBUTING.md), passende Aufgaben tragen das Label
|
|
113
|
+
[`good first issue`](https://github.com/jexcellence/privalint/labels/good%20first%20issue).
|
|
114
|
+
|
|
115
|
+
## Lizenz
|
|
116
|
+
|
|
117
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/privalint)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
+
**Deutsch:** [README.de.md](README.de.md)
|
|
8
|
+
|
|
7
9
|
Lint your website for the GDPR mistakes that get people warning letters: trackers that fire before
|
|
8
10
|
consent or keep running after "reject", Google Fonts loaded from Google, embeds and a missing
|
|
9
11
|
Impressum.
|
|
@@ -84,6 +86,8 @@ preview URL from your hosting provider's deployment event or action output as `u
|
|
|
84
86
|
| `max-pages` | `1` | How many pages of the same site to crawl. |
|
|
85
87
|
| `fail-on` | `error` | `error`, `warning` or `never`. |
|
|
86
88
|
| `disable` | | Comma-separated rule ids to skip. |
|
|
89
|
+
| `lang` | `en` | Language of the report: `en` or `de`. |
|
|
90
|
+
| `region` | `de` | Whose law to cite: `de`, `at`, `ch`, `eu`. |
|
|
87
91
|
| `sarif-file` | `privalint.sarif` | Where to write SARIF. Empty to skip. |
|
|
88
92
|
| `job-summary` | `true` | Add the Markdown report to the job summary. |
|
|
89
93
|
|
|
@@ -113,6 +117,8 @@ privalint <url> [options]
|
|
|
113
117
|
--settle <ms> Wait this long after load for late tags (default 2000)
|
|
114
118
|
--timeout <ms> Navigation timeout per page (default 30000)
|
|
115
119
|
--locale <locale> Browser locale (default de-DE)
|
|
120
|
+
--lang <lang> Language of the report: en | de (default en)
|
|
121
|
+
--region <region> Whose law to cite: de | at | ch | eu (default de)
|
|
116
122
|
--skip-consent Don't click "reject" on the consent banner
|
|
117
123
|
--headed Open a visible browser window instead of running headless
|
|
118
124
|
--channel <name> Use an installed browser: chrome or msedge
|
|
@@ -127,17 +133,65 @@ The first run needs a browser. If Playwright hasn't installed one yet:
|
|
|
127
133
|
npx playwright install chromium
|
|
128
134
|
```
|
|
129
135
|
|
|
136
|
+
## Countries and languages
|
|
137
|
+
|
|
138
|
+
`--region` selects the references and severity profile used by the tool. It does not determine
|
|
139
|
+
which law applies to a website, and findings are not legal determinations:
|
|
140
|
+
|
|
141
|
+
| Region | Cookies and tracking | Imprint | Changes |
|
|
142
|
+
| -------------- | ------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
143
|
+
| `de` (default) | Opt-in, § 25 TDDDG | § 5 DDG | none |
|
|
144
|
+
| `at` | Opt-in, § 165 Abs. 3 TKG 2021 | § 5 ECG, § 25 MedienG | none |
|
|
145
|
+
| `ch` | Opt-out, Art. 45c FMG | Art. 3 Abs. 1 lit. s UWG (e-commerce) | `pre-consent-trackers`, `third-party-embeds` and `impressum-link` drop one level |
|
|
146
|
+
| `eu` | Opt-in, Art. 5(3) ePrivacy Directive | Art. 5 Directive 2000/31/EC | none |
|
|
147
|
+
|
|
148
|
+
Every finding lists its legal basis for the chosen country. Trackers that run after an explicit
|
|
149
|
+
"reject" stay an error everywhere, Switzerland included (Art. 30 Abs. 2 lit. b DSG). If a Swiss
|
|
150
|
+
site also targets visitors in the EU, scan it with `--region eu` as well.
|
|
151
|
+
|
|
152
|
+
`--lang de` writes findings, fixes and the report in German:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx privalint https://ihre-seite.ch --region ch --lang de
|
|
156
|
+
```
|
|
157
|
+
|
|
130
158
|
## Use it from code
|
|
131
159
|
|
|
132
160
|
```ts
|
|
133
161
|
import { scan } from 'privalint';
|
|
134
162
|
|
|
135
|
-
const result = await scan('https://your-site.de', { maxPages: 10 });
|
|
163
|
+
const result = await scan('https://your-site.de', { maxPages: 10, region: 'at', lang: 'de' });
|
|
136
164
|
for (const finding of result.findings) {
|
|
137
165
|
console.log(finding.severity, finding.ruleId, finding.pageUrl);
|
|
138
166
|
}
|
|
139
167
|
```
|
|
140
168
|
|
|
169
|
+
### Public web scanners and controlled capture
|
|
170
|
+
|
|
171
|
+
`scan()` accepts a `networkAdapter` and `AbortSignal`. The adapter receives each HTTP hop,
|
|
172
|
+
including redirects, and returns a response or throws a typed `NetworkError`. Requests wait
|
|
173
|
+
in a FIFO queue; concurrency, request count, response bytes and timeouts are independent limits.
|
|
174
|
+
There is no browser HTTP fallback in adapter mode.
|
|
175
|
+
|
|
176
|
+
The scanner owns its browser and fresh contexts and closes them on completion or cancellation.
|
|
177
|
+
The caller owns the adapter's shared clients. Keep your existing egress policy: validate and pin
|
|
178
|
+
DNS/socket destinations for **every** hop, and honour the provided per-request abort signal.
|
|
179
|
+
|
|
180
|
+
See the [controlled scanner guide](docs/controlled-scanner.md) and the type-checked
|
|
181
|
+
[pinned-transport example](examples/controlled-scanner.ts). The example denies origins absent
|
|
182
|
+
from a trusted deployment allowlist and pins socket addresses; the allowlist must not come from
|
|
183
|
+
the person submitting the scan.
|
|
184
|
+
|
|
185
|
+
Reports separate `findings` from `pages[].capture.diagnostics`. A page with 48 requests and 21
|
|
186
|
+
blocked/failed requests is marked incomplete even if no findings were produced. Per-page
|
|
187
|
+
`ruleEvaluations` distinguish `executed`, `skipped` and `not-evaluable`; positive evidence may
|
|
188
|
+
still be evaluated with `partial: true`. No banner detected is not itself a privacy finding,
|
|
189
|
+
and does not mean that the reject path was tested.
|
|
190
|
+
|
|
191
|
+
Existing CLI, API and GitHub Action calls still work. Start-page navigation failures retain the
|
|
192
|
+
legacy throw behaviour unless `failureMode: 'report'` is selected (the default with an adapter).
|
|
193
|
+
No report claims legal compliance.
|
|
194
|
+
|
|
141
195
|
## How it works
|
|
142
196
|
|
|
143
197
|
1. Every page is opened in a **fresh browser context**: no cookies and no storage, the same as a
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Lang } from './i18n.js';
|
|
2
|
+
import type { Finding, PageSnapshot, Rule, RuleEvaluation } from './types.js';
|
|
3
|
+
export declare function assessPage(page: PageSnapshot, rules: readonly Rule[], siteUrl: URL, lang: Lang, isStart: boolean, consentEnabled: boolean): {
|
|
4
|
+
findings: Finding[];
|
|
5
|
+
evaluations: RuleEvaluation[];
|
|
6
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { observedRequests } from './observations.js';
|
|
2
|
+
import { defaultRules } from './rules/index.js';
|
|
3
|
+
import { isFirstParty } from './url.js';
|
|
4
|
+
const REQUIREMENTS = {
|
|
5
|
+
'pre-consent-trackers': ['network'],
|
|
6
|
+
'google-fonts': ['network'],
|
|
7
|
+
'third-party-embeds': ['network'],
|
|
8
|
+
'third-party-cdns': ['network'],
|
|
9
|
+
'impressum-link': ['document'],
|
|
10
|
+
'privacy-policy-link': ['document'],
|
|
11
|
+
'reject-option': ['consent'],
|
|
12
|
+
'trackers-after-reject': ['after-reject'],
|
|
13
|
+
};
|
|
14
|
+
export function assessPage(page, rules, siteUrl, lang, isStart, consentEnabled) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
const allRules = new Map([...defaultRules, ...rules].map((rule) => [rule.id, rule]));
|
|
17
|
+
const selected = new Set(rules.map((rule) => rule.id));
|
|
18
|
+
const complete = page.capture?.beforeConsentComplete ?? page.capture?.complete ?? page.error === undefined;
|
|
19
|
+
const validPage = (page.error === undefined || page.capture?.document === true) &&
|
|
20
|
+
page.status !== null &&
|
|
21
|
+
page.status < 400 &&
|
|
22
|
+
isFirstParty(page.finalUrl, siteUrl);
|
|
23
|
+
const available = {
|
|
24
|
+
document: validPage && complete && (page.capture?.document ?? true),
|
|
25
|
+
network: validPage,
|
|
26
|
+
consent: validPage && page.consent?.bannerFound === true,
|
|
27
|
+
'after-reject': validPage && page.consent?.rejected === true,
|
|
28
|
+
};
|
|
29
|
+
const input = {
|
|
30
|
+
...page,
|
|
31
|
+
requests: observedRequests(page.requests),
|
|
32
|
+
// A preconnect hint is not evidence of an actual connection in an offline adapter context.
|
|
33
|
+
resourceHints: complete ? page.resourceHints : [],
|
|
34
|
+
...(page.consent?.afterReject === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: {
|
|
37
|
+
consent: {
|
|
38
|
+
...page.consent,
|
|
39
|
+
afterReject: {
|
|
40
|
+
...page.consent.afterReject,
|
|
41
|
+
requests: observedRequests(page.consent.afterReject.requests),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
const evaluations = [...allRules.values()].map((rule) => {
|
|
47
|
+
const requirements = rule.requires ?? REQUIREMENTS[rule.id] ?? ['document'];
|
|
48
|
+
const consentRule = requirements.some((kind) => kind === 'consent' || kind === 'after-reject');
|
|
49
|
+
if (!selected.has(rule.id)) {
|
|
50
|
+
return { ruleId: rule.id, status: 'skipped', reason: 'disabled', partial: false };
|
|
51
|
+
}
|
|
52
|
+
if (consentRule && (!isStart || !consentEnabled)) {
|
|
53
|
+
return {
|
|
54
|
+
ruleId: rule.id,
|
|
55
|
+
status: 'skipped',
|
|
56
|
+
reason: !isStart ? 'not-start-page' : 'consent-disabled',
|
|
57
|
+
partial: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!requirements.every((kind) => available[kind])) {
|
|
61
|
+
return {
|
|
62
|
+
ruleId: rule.id,
|
|
63
|
+
status: 'not-evaluable',
|
|
64
|
+
reason: 'missing-observation',
|
|
65
|
+
partial: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
findings.push(...rule.check({ page: input, siteUrl, lang }));
|
|
69
|
+
return {
|
|
70
|
+
ruleId: rule.id,
|
|
71
|
+
status: 'executed',
|
|
72
|
+
partial: requirements.includes('after-reject')
|
|
73
|
+
? page.consent?.afterRejectComplete === false
|
|
74
|
+
: !complete,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
return { findings, evaluations };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=assessment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assessment.js","sourceRoot":"","sources":["../src/assessment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
2
|
+
import { type ScanOptions } from './scan-options.js';
|
|
3
|
+
import type { PageSnapshot } from './types.js';
|
|
4
|
+
/** Always owns and closes its context. Each crawl page starts without cookies/storage. */
|
|
5
|
+
export declare function capturePage(browser: Browser, url: string, siteUrl: URL, options: ScanOptions, checkConsent: boolean): Promise<PageSnapshot>;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { observeConsent } from './consent.js';
|
|
2
|
+
import { classifyLoadError, Diagnostics } from './diagnostics.js';
|
|
3
|
+
import { ControlledNetwork, networkLimits } from './network.js';
|
|
4
|
+
import { DEFAULTS } from './scan-options.js';
|
|
5
|
+
import { isFirstParty } from './url.js';
|
|
6
|
+
/** Always owns and closes its context. Each crawl page starts without cookies/storage. */
|
|
7
|
+
export async function capturePage(browser, url, siteUrl, options, checkConsent) {
|
|
8
|
+
const controlled = options.networkAdapter !== undefined;
|
|
9
|
+
const context = await browser.newContext({
|
|
10
|
+
locale: options.locale ?? DEFAULTS.locale,
|
|
11
|
+
serviceWorkers: controlled ? 'block' : 'allow',
|
|
12
|
+
// fulfill() works offline. Unhandled browser HTTP traffic has no network fallback.
|
|
13
|
+
offline: controlled,
|
|
14
|
+
});
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const captureMs = options.captureTimeoutMs ?? (controlled ? 60_000 : undefined);
|
|
17
|
+
const deadline = captureMs === undefined ? undefined : AbortSignal.timeout(captureMs);
|
|
18
|
+
const signal = AbortSignal.any([
|
|
19
|
+
controller.signal,
|
|
20
|
+
...(options.signal ? [options.signal] : []),
|
|
21
|
+
...(deadline ? [deadline] : []),
|
|
22
|
+
]);
|
|
23
|
+
const diagnostics = new Diagnostics(options.maxDiagnostics ?? 100, controlled ? networkLimits(options.networkLimits).maxRequests : 5_000);
|
|
24
|
+
const requests = [];
|
|
25
|
+
diagnostics.attach(context, requests);
|
|
26
|
+
const network = options.networkAdapter
|
|
27
|
+
? new ControlledNetwork(options.networkAdapter, networkLimits(options.networkLimits), diagnostics, signal)
|
|
28
|
+
: undefined;
|
|
29
|
+
let closing;
|
|
30
|
+
const closeOnAbort = () => {
|
|
31
|
+
closing ??= context.close().catch(() => { });
|
|
32
|
+
};
|
|
33
|
+
signal.addEventListener('abort', closeOnAbort, { once: true });
|
|
34
|
+
const snapshot = {
|
|
35
|
+
url,
|
|
36
|
+
finalUrl: url,
|
|
37
|
+
status: null,
|
|
38
|
+
requests: [],
|
|
39
|
+
cookies: [],
|
|
40
|
+
links: [],
|
|
41
|
+
resourceHints: [],
|
|
42
|
+
};
|
|
43
|
+
let document = false;
|
|
44
|
+
let beforeComplete = false;
|
|
45
|
+
try {
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
closeOnAbort();
|
|
48
|
+
await network?.attach(context);
|
|
49
|
+
if (options.offline) {
|
|
50
|
+
await context.route('**/*', (route) => isFirstParty(route.request().url(), siteUrl)
|
|
51
|
+
? route.continue()
|
|
52
|
+
: route.fulfill({ status: 200, body: '' }));
|
|
53
|
+
}
|
|
54
|
+
const page = await context.newPage();
|
|
55
|
+
await network?.attachPage(page);
|
|
56
|
+
const response = await page.goto(url, {
|
|
57
|
+
waitUntil: 'load',
|
|
58
|
+
timeout: options.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
59
|
+
});
|
|
60
|
+
snapshot.finalUrl = page.url();
|
|
61
|
+
snapshot.status = response?.status() ?? null;
|
|
62
|
+
await page
|
|
63
|
+
.waitForLoadState('networkidle', {
|
|
64
|
+
timeout: options.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
65
|
+
})
|
|
66
|
+
.catch(() => diagnostics.add('timeout', page.url()));
|
|
67
|
+
await page.waitForTimeout(options.settleMs ?? DEFAULTS.settleMs);
|
|
68
|
+
snapshot.links = await page.$$eval('a[href]', (anchors) => anchors.map((a) => ({
|
|
69
|
+
href: a.href,
|
|
70
|
+
text: [a.textContent, a.getAttribute('aria-label'), a.getAttribute('title')]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' ')
|
|
73
|
+
.replace(/\s+/g, ' ')
|
|
74
|
+
.trim()
|
|
75
|
+
.slice(0, 200),
|
|
76
|
+
})));
|
|
77
|
+
snapshot.resourceHints = await page.$$eval('link[rel][href]', (elements) => elements.map((link) => ({
|
|
78
|
+
rel: (link.getAttribute('rel') ?? '').toLowerCase(),
|
|
79
|
+
href: link.href,
|
|
80
|
+
})));
|
|
81
|
+
// Offline adapter contexts cannot prove a connection from a preconnect hint alone.
|
|
82
|
+
if (controlled)
|
|
83
|
+
snapshot.resourceHints = [];
|
|
84
|
+
snapshot.cookies = (await context.cookies()).map(({ name, domain, path, expires }) => ({
|
|
85
|
+
name,
|
|
86
|
+
domain,
|
|
87
|
+
path,
|
|
88
|
+
expires,
|
|
89
|
+
}));
|
|
90
|
+
snapshot.requests = [...requests];
|
|
91
|
+
snapshot.requestCount = diagnostics.data.requests;
|
|
92
|
+
document = true;
|
|
93
|
+
beforeComplete = diagnostics.data.issueCount === 0 && snapshot.error === undefined;
|
|
94
|
+
if (checkConsent && options.consentCheck !== false) {
|
|
95
|
+
snapshot.consent = await observeConsent(page, context, requests, {
|
|
96
|
+
settleMs: options.settleMs ?? DEFAULTS.settleMs,
|
|
97
|
+
timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
98
|
+
signal,
|
|
99
|
+
onReject: () => {
|
|
100
|
+
diagnostics.phase = 'after-reject';
|
|
101
|
+
},
|
|
102
|
+
onTimeout: () => diagnostics.add('timeout', page.url()),
|
|
103
|
+
});
|
|
104
|
+
if (signal.aborted)
|
|
105
|
+
throw new DOMException('Capture ended', 'AbortError');
|
|
106
|
+
if (snapshot.consent.error && !diagnostics.hasIssues('after-reject')) {
|
|
107
|
+
diagnostics.add(classifyLoadError(snapshot.consent.error), page.url());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (snapshot.requests.length === 0)
|
|
113
|
+
snapshot.requests = [...requests];
|
|
114
|
+
snapshot.requestCount ??= diagnostics.data.requests;
|
|
115
|
+
const kind = signal.aborted
|
|
116
|
+
? options.signal?.aborted
|
|
117
|
+
? 'aborted'
|
|
118
|
+
: 'timeout'
|
|
119
|
+
: classifyLoadError(error);
|
|
120
|
+
snapshot.error = kind;
|
|
121
|
+
if (diagnostics.data.issueCount === 0)
|
|
122
|
+
diagnostics.add(kind, url);
|
|
123
|
+
if (checkConsent &&
|
|
124
|
+
options.consentCheck !== false &&
|
|
125
|
+
(snapshot.consent === undefined || signal.aborted)) {
|
|
126
|
+
snapshot.consent = {
|
|
127
|
+
...snapshot.consent,
|
|
128
|
+
bannerFound: snapshot.consent?.bannerFound ?? false,
|
|
129
|
+
rejected: false,
|
|
130
|
+
state: 'interaction-failed',
|
|
131
|
+
error: kind,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
diagnostics.stop();
|
|
137
|
+
controller.abort();
|
|
138
|
+
signal.removeEventListener('abort', closeOnAbort);
|
|
139
|
+
await Promise.all([closing ?? context.close(), network?.close()]);
|
|
140
|
+
}
|
|
141
|
+
if (options.signal?.aborted)
|
|
142
|
+
throw new DOMException('Scan aborted', 'AbortError');
|
|
143
|
+
beforeComplete = beforeComplete && !diagnostics.hasIssues('before-consent');
|
|
144
|
+
if (snapshot.consent?.rejected) {
|
|
145
|
+
snapshot.consent.afterRejectComplete = !diagnostics.hasIssues('after-reject');
|
|
146
|
+
}
|
|
147
|
+
snapshot.capture = {
|
|
148
|
+
complete: beforeComplete && diagnostics.data.issueCount === 0,
|
|
149
|
+
beforeConsentComplete: beforeComplete,
|
|
150
|
+
document,
|
|
151
|
+
diagnostics: diagnostics.data,
|
|
152
|
+
};
|
|
153
|
+
return snapshot;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAoB,MAAM,mBAAmB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,GAAW,EACX,OAAY,EACZ,OAAoB,EACpB,YAAqB;IAErB,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACvC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM;QACzC,cAAc,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;QAC9C,mFAAmF;QACnF,OAAO,EAAE,UAAU;KACpB,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,OAAO,CAAC,gBAAgB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtF,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC;QAC7B,UAAU,CAAC,MAAM;QACjB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,IAAI,WAAW,CACjC,OAAO,CAAC,cAAc,IAAI,GAAG,EAC7B,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CACtE,CAAC;IACF,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAC9C,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc;QACpC,CAAC,CAAC,IAAI,iBAAiB,CACnB,OAAO,CAAC,cAAc,EACtB,aAAa,CAAC,OAAO,CAAC,aAAa,CAAC,EACpC,WAAW,EACX,MAAM,CACP;QACH,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,OAAkC,CAAC;IACvC,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,OAAO,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAiB;QAC7B,GAAG;QACH,QAAQ,EAAE,GAAG;QACb,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,EAAE;QACT,aAAa,EAAE,EAAE;KAClB,CAAC;IACF,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,OAAO;YAAE,YAAY,EAAE,CAAC;QACnC,MAAM,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CACpC,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC;gBAC1C,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE;gBAClB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAC7C,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACpC,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,OAAO,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS;SACjD,CAAC,CAAC;QACH,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC/B,QAAQ,CAAC,MAAM,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC;QAC7C,MAAM,IAAI;aACP,gBAAgB,CAAC,aAAa,EAAE;YAC/B,OAAO,EAAE,OAAO,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS;SACjD,CAAC;aACD,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACjE,QAAQ,CAAC,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CACxD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClB,IAAI,EAAG,CAAuB,CAAC,IAAI;YACnC,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;iBACzE,MAAM,CAAC,OAAO,CAAC;iBACf,IAAI,CAAC,GAAG,CAAC;iBACT,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;iBACpB,IAAI,EAAE;iBACN,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SACjB,CAAC,CAAC,CACJ,CAAC;QACF,QAAQ,CAAC,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,EAAE,CACzE,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACtB,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE;YACnD,IAAI,EAAG,IAAwB,CAAC,IAAI;SACrC,CAAC,CAAC,CACJ,CAAC;QACF,mFAAmF;QACnF,IAAI,UAAU;YAAE,QAAQ,CAAC,aAAa,GAAG,EAAE,CAAC;QAC5C,QAAQ,CAAC,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;YACrF,IAAI;YACJ,MAAM;YACN,IAAI;YACJ,OAAO;SACR,CAAC,CAAC,CAAC;QACJ,QAAQ,CAAC,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QAClC,QAAQ,CAAC,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;QAClD,QAAQ,GAAG,IAAI,CAAC;QAChB,cAAc,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC;QACnF,IAAI,YAAY,IAAI,OAAO,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;YACnD,QAAQ,CAAC,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE;gBAC/D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ;gBAC/C,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS;gBAClD,MAAM;gBACN,QAAQ,EAAE,GAAG,EAAE;oBACb,WAAW,CAAC,KAAK,GAAG,cAAc,CAAC;gBACrC,CAAC;gBACD,SAAS,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;aACxD,CAAC,CAAC;YACH,IAAI,MAAM,CAAC,OAAO;gBAAE,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;YAC1E,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,cAAc,CAAC,EAAE,CAAC;gBACrE,WAAW,CAAC,GAAG,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,QAAQ,CAAC,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QACtE,QAAQ,CAAC,YAAY,KAAK,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO;YACzB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO;gBACvB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,SAAS;YACb,CAAC,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC7B,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC;QACtB,IAAI,WAAW,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC;YAAE,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAClE,IACE,YAAY;YACZ,OAAO,CAAC,YAAY,KAAK,KAAK;YAC9B,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,CAAC,EAClD,CAAC;YACD,QAAQ,CAAC,OAAO,GAAG;gBACjB,GAAG,QAAQ,CAAC,OAAO;gBACnB,WAAW,EAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,IAAI,KAAK;gBACnD,QAAQ,EAAE,KAAK;gBACf,KAAK,EAAE,oBAAoB;gBAC3B,KAAK,EAAE,IAAI;aACZ,CAAC;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,WAAW,CAAC,IAAI,EAAE,CAAC;QACnB,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAClD,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAClF,cAAc,GAAG,cAAc,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC5E,IAAI,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC;QAC/B,QAAQ,CAAC,OAAO,CAAC,mBAAmB,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAChF,CAAC;IACD,QAAQ,CAAC,OAAO,GAAG;QACjB,QAAQ,EAAE,cAAc,IAAI,WAAW,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC;QAC7D,qBAAqB,EAAE,cAAc;QACrC,QAAQ;QACR,WAAW,EAAE,WAAW,CAAC,IAAI;KAC9B,CAAC;IACF,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
+
import { diagnosticUrl } from './diagnostics.js';
|
|
6
|
+
import { LANGS } from './i18n.js';
|
|
7
|
+
import { REGIONS } from './legal.js';
|
|
5
8
|
import { VERSION } from './meta.js';
|
|
6
9
|
import { FORMATS, format, formatText } from './reporters/index.js';
|
|
7
10
|
import { defaultRules } from './rules/index.js';
|
|
@@ -29,6 +32,8 @@ Options
|
|
|
29
32
|
--settle <ms> Wait this long after load for late tags (default 2000)
|
|
30
33
|
--timeout <ms> Navigation timeout per page (default 30000)
|
|
31
34
|
--locale <locale> Browser locale (default de-DE)
|
|
35
|
+
--lang <lang> Language of the report: en | de (default en)
|
|
36
|
+
--region <region> Whose law to cite: de | at | ch | eu (default de)
|
|
32
37
|
--skip-consent Don't click "reject" on the consent banner
|
|
33
38
|
--headed Open a visible browser window instead of running headless
|
|
34
39
|
--channel <name> Use an installed browser: chrome or msedge
|
|
@@ -59,6 +64,8 @@ async function main(argv) {
|
|
|
59
64
|
settle: { type: 'string' },
|
|
60
65
|
timeout: { type: 'string' },
|
|
61
66
|
locale: { type: 'string' },
|
|
67
|
+
lang: { type: 'string', default: 'en' },
|
|
68
|
+
region: { type: 'string', default: 'de' },
|
|
62
69
|
'skip-consent': { type: 'boolean' },
|
|
63
70
|
headed: { type: 'boolean' },
|
|
64
71
|
channel: { type: 'string' },
|
|
@@ -91,12 +98,14 @@ async function main(argv) {
|
|
|
91
98
|
settleMs: wholeNumber(values.settle, '--settle', 0),
|
|
92
99
|
timeoutMs: wholeNumber(values.timeout, '--timeout', 1),
|
|
93
100
|
locale: values.locale,
|
|
101
|
+
lang: oneOf(values.lang, LANGS, '--lang'),
|
|
102
|
+
region: oneOf(values.region, REGIONS, '--region'),
|
|
94
103
|
consentCheck: values['skip-consent'] !== true,
|
|
95
104
|
headed: values.headed === true,
|
|
96
105
|
channel: values.channel === undefined ? undefined : oneOf(values.channel, CHANNELS, '--channel'),
|
|
97
106
|
onPage: (page) => process.stderr.write(page.error === undefined
|
|
98
|
-
? `scanned ${page.finalUrl}\n`
|
|
99
|
-
: `skipped ${page.url}: ${page.error}\n`),
|
|
107
|
+
? `scanned ${diagnosticUrl(page.finalUrl)}\n`
|
|
108
|
+
: `skipped ${diagnosticUrl(page.url)}: ${page.error}\n`),
|
|
100
109
|
});
|
|
101
110
|
const outputs = values.output ?? [];
|
|
102
111
|
for (const file of outputs) {
|
|
@@ -125,7 +134,7 @@ function parseTarget(positionals) {
|
|
|
125
134
|
return new URL(withScheme);
|
|
126
135
|
}
|
|
127
136
|
catch {
|
|
128
|
-
throw new Error(
|
|
137
|
+
throw new Error('Not a valid HTTP(S) URL.');
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
function oneOf(value, allowed, flag) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAe,MAAM,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGpC,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC;AAC3D,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAG/C,MAAM,OAAO,GAAG,CAAC,CAAC;AAClB,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,UAAU,GAAG,CAAC,CAAC;AAErB,MAAM,IAAI,GAAG,aAAa,OAAO;;;;;;;;;4BASL,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/consent.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { ConsentObservation, RecordedRequest } from './types.js';
|
|
|
3
3
|
export interface ConsentCheckOptions {
|
|
4
4
|
settleMs: number;
|
|
5
5
|
timeoutMs: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
onReject?: () => void;
|
|
8
|
+
onTimeout?: () => void;
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
11
|
* Looks for a consent banner, clicks its reject button if there is one,
|
package/dist/consent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyLoadError } from './diagnostics.js';
|
|
1
2
|
/**
|
|
2
3
|
* Button selectors of common consent managers. Best effort: vendors change
|
|
3
4
|
* their markup, so the label heuristic below is the fallback for everything
|
|
@@ -59,13 +60,16 @@ const CLICKABLE = 'button, [role="button"], a, input[type="button"], input[type=
|
|
|
59
60
|
* be loaded; `requests` is the live array the page's request listener writes to.
|
|
60
61
|
*/
|
|
61
62
|
export async function observeConsent(page, context, requests, options) {
|
|
63
|
+
if (options.signal?.aborted)
|
|
64
|
+
throw new DOMException('Consent check aborted', 'AbortError');
|
|
62
65
|
const banner = await findBanner(page);
|
|
63
66
|
if (banner === undefined) {
|
|
64
|
-
return { bannerFound: false, rejected: false };
|
|
67
|
+
return { bannerFound: false, rejected: false, state: 'not-detected' };
|
|
65
68
|
}
|
|
66
69
|
const observation = {
|
|
67
70
|
bannerFound: true,
|
|
68
71
|
rejected: false,
|
|
72
|
+
state: 'reject-unavailable',
|
|
69
73
|
...(banner.cmp === undefined ? {} : { cmp: banner.cmp }),
|
|
70
74
|
...(banner.acceptLabel === undefined ? {} : { acceptLabel: banner.acceptLabel }),
|
|
71
75
|
...(banner.rejectLabel === undefined ? {} : { rejectLabel: banner.rejectLabel }),
|
|
@@ -77,9 +81,12 @@ export async function observeConsent(page, context, requests, options) {
|
|
|
77
81
|
try {
|
|
78
82
|
const cookiesBefore = new Set((await context.cookies()).map(cookieKey));
|
|
79
83
|
const firstRequest = requests.length;
|
|
80
|
-
|
|
84
|
+
options.onReject?.();
|
|
85
|
+
await banner.reject.click({ timeout: Math.min(5_000, options.timeoutMs) });
|
|
81
86
|
await settle(page, options);
|
|
82
|
-
await page.reload({ waitUntil: 'load', timeout: options.timeoutMs });
|
|
87
|
+
const response = await page.reload({ waitUntil: 'load', timeout: options.timeoutMs });
|
|
88
|
+
if (response === null || response.status() >= 400)
|
|
89
|
+
throw new Error('HTTP reload failed');
|
|
83
90
|
await settle(page, options);
|
|
84
91
|
const cookies = (await context.cookies())
|
|
85
92
|
.filter((cookie) => !cookiesBefore.has(cookieKey(cookie)))
|
|
@@ -87,12 +94,12 @@ export async function observeConsent(page, context, requests, options) {
|
|
|
87
94
|
return {
|
|
88
95
|
...observation,
|
|
89
96
|
rejected: true,
|
|
97
|
+
state: 'reject-tested',
|
|
90
98
|
afterReject: { requests: requests.slice(firstRequest), cookies },
|
|
91
99
|
};
|
|
92
100
|
}
|
|
93
101
|
catch (error) {
|
|
94
|
-
|
|
95
|
-
return { ...observation, error: message.split('\n')[0] ?? message };
|
|
102
|
+
return { ...observation, state: 'interaction-failed', error: classifyLoadError(error) };
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
async function findBanner(page) {
|
|
@@ -177,7 +184,7 @@ async function label(locator) {
|
|
|
177
184
|
}
|
|
178
185
|
async function settle(page, options) {
|
|
179
186
|
await page.waitForLoadState('networkidle', { timeout: options.timeoutMs }).catch(() => {
|
|
180
|
-
|
|
187
|
+
options.onTimeout?.();
|
|
181
188
|
});
|
|
182
189
|
await page.waitForTimeout(options.settleMs);
|
|
183
190
|
}
|