@produtype/core 1.25.0 → 1.26.1
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.
|
@@ -8,7 +8,33 @@ function toEvidence(matches) {
|
|
|
8
8
|
return matches.map((m) => ({ type: 'snippet', value: m.snippet, file: m.file, line: m.line }));
|
|
9
9
|
}
|
|
10
10
|
async function detectGdpr(ctx) {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The consent vendors, who name themselves.
|
|
13
|
+
*
|
|
14
|
+
* `cookie-consent`, `consentGiven` and `gdprConsent` are four spellings of one
|
|
15
|
+
* English word, and a site whose banner says `consensoCookie` has none of them —
|
|
16
|
+
* the same failure the password reset had, in a capability where the whole audience
|
|
17
|
+
* is European and half of it does not write in English.
|
|
18
|
+
*
|
|
19
|
+
* What the audience does have is a vendor. Iubenda, Cookiebot, OneTrust,
|
|
20
|
+
* Usercentrics, CookieHub and Klaro all publish a banner you embed, and most sites
|
|
21
|
+
* embed the script rather than install a package — so the domain the script is
|
|
22
|
+
* fetched from is the anchor, beside the package for the ones that ship on npm.
|
|
23
|
+
* `cdn.iubenda.com` is Iubenda's; nobody else's site serves from it.
|
|
24
|
+
*/
|
|
25
|
+
const CONSENT_VENDORS = [
|
|
26
|
+
/cdn\.iubenda\.com|iubenda\.com\/(?:privacy-policy|cookie-policy)/i,
|
|
27
|
+
/consent\.cookiebot\.com|cookiebot/i,
|
|
28
|
+
/cdn\.cookielaw\.org|onetrust|OptanonWrapper/i,
|
|
29
|
+
/app\.usercentrics\.eu|usercentrics/i,
|
|
30
|
+
/cookiehub|cdn\.cookiehub\.eu/i,
|
|
31
|
+
/vanilla-cookieconsent|\bklaro\b|tarteaucitron|osano|termly/i,
|
|
32
|
+
/@axeptio|axeptio\.eu/i,
|
|
33
|
+
];
|
|
34
|
+
const consent = [
|
|
35
|
+
...await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/cookie[-_ ]?consent/i, /consentGiven/i, /privacyConsent/i, /gdprConsent/i], 20),
|
|
36
|
+
...await (0, textSearch_1.searchInFiles)(ctx.root, [...ctx.files.source, ...ctx.files.all.filter((f) => /\.html?$/i.test(f))], CONSENT_VENDORS, 10),
|
|
37
|
+
];
|
|
12
38
|
/**
|
|
13
39
|
* The same duty, in the words each ecosystem actually uses.
|
|
14
40
|
*
|
|
@@ -102,7 +128,7 @@ async function detectGdpr(ctx) {
|
|
|
102
128
|
* produced it: these are the same terms the patterns above match.
|
|
103
129
|
*/
|
|
104
130
|
const TERMS = {
|
|
105
|
-
consent: ['a consent record', ['cookie-consent', 'consentGiven', 'privacyConsent', 'gdprConsent']],
|
|
131
|
+
consent: ['a consent record', ['cookie-consent', 'consentGiven', 'privacyConsent', 'gdprConsent', 'Iubenda, Cookiebot, OneTrust, Usercentrics, CookieHub, Klaro or Axeptio']],
|
|
106
132
|
export: ['an export of personal data', ['/gdpr/export', 'exportUserData', 'personalDataExport', 'dataSubject', 'rightToAccess', '"data portability"']],
|
|
107
133
|
erasure: ['an erasure flow', ['erasure', '"delete account"', '"right to be forgotten"', '"delete user data"', '"delete personal data"']],
|
|
108
134
|
retention: ['a retention or purge policy', ['gdpr/privacy near retention/purge/delete', 'retention/purge/delete near personal data', '"delete personal data older than"']],
|
|
@@ -101,11 +101,29 @@ function deriveStatus(analysis, capability) {
|
|
|
101
101
|
case 'gdpr.consent': {
|
|
102
102
|
return detector(analysis, 'gdpr.consent.route')?.present ? 'present' : 'missing';
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Article 20 and article 17 are both routes, and both were read by their English
|
|
106
|
+
* names.
|
|
107
|
+
*
|
|
108
|
+
* `exportUserData`, `UserExport`, `erasure`, `"delete account"` — a gestionale
|
|
109
|
+
* whose route is `POST /cancella-account` has none of them, and was told at
|
|
110
|
+
* `high` that it offers neither. The one anchor that survives translation is a
|
|
111
|
+
* `DELETE` on `/account` or `/users/me`, which is HTTP's verb and a conventional
|
|
112
|
+
* path, and it is already read.
|
|
113
|
+
*
|
|
114
|
+
* Where even that did not fire and no route name was ever read in this project,
|
|
115
|
+
* these are the same unasked question the password reset is: the search could not
|
|
116
|
+
* reach a vocabulary it does not know.
|
|
117
|
+
*/
|
|
104
118
|
case 'gdpr.export': {
|
|
105
|
-
|
|
119
|
+
if (detector(analysis, 'gdpr.export.route')?.present)
|
|
120
|
+
return 'present';
|
|
121
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
106
122
|
}
|
|
107
123
|
case 'gdpr.erasure': {
|
|
108
|
-
|
|
124
|
+
if (detector(analysis, 'gdpr.erasure.route')?.present)
|
|
125
|
+
return 'present';
|
|
126
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
109
127
|
}
|
|
110
128
|
case 'gdpr.retention': {
|
|
111
129
|
return detector(analysis, 'gdpr.retention.job')?.present ? 'present' : 'missing';
|
|
@@ -431,6 +431,8 @@ function buildReport(analysis, options) {
|
|
|
431
431
|
* presented with the same confidence.
|
|
432
432
|
*/
|
|
433
433
|
readingDepth: (0, readingDepth_1.readingDepths)(analysis.files.source, analysis.files.unreadable, analysis.parsedStructure),
|
|
434
|
+
routeNamesUnread: analysis.detectors['auth.core']?.present === true
|
|
435
|
+
&& analysis.detectors['auth.routesAreReadable']?.present === false,
|
|
434
436
|
parsedStructure: analysis.parsedStructure,
|
|
435
437
|
detectors: detectorDiagnostics(analysis),
|
|
436
438
|
selectedProfile: requestedProfile,
|
|
@@ -278,6 +278,18 @@ function renderMarkdown(report) {
|
|
|
278
278
|
...((0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth, report.diagnostics.parsedStructure)
|
|
279
279
|
? [`- ${(0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth, report.diagnostics.parsedStructure)}`]
|
|
280
280
|
: []),
|
|
281
|
+
/**
|
|
282
|
+
* A vocabulary this could not read, said out loud.
|
|
283
|
+
*
|
|
284
|
+
* Four capabilities are found by the English words their routes are usually
|
|
285
|
+
* given. Where a project authenticates and no route name was ever read, those
|
|
286
|
+
* questions go unanswered rather than answered wrongly — and a reader whose
|
|
287
|
+
* routes are `/accedi` and `/recupero-password` is entitled to know that is what
|
|
288
|
+
* happened, instead of finding four questions quietly absent.
|
|
289
|
+
*/
|
|
290
|
+
...(report.diagnostics.routeNamesUnread
|
|
291
|
+
? ['- No route name in this repository matched /login, /register or /signin, so password reset, email verification, personal data export and erasure were not assessed: those are found by the words a route is given, and this project gives them others.']
|
|
292
|
+
: []),
|
|
281
293
|
`- Expectation mode: ${report.diagnostics.expectationMode}`,
|
|
282
294
|
`- ProdKit version: ${report.diagnostics.prodkitVersion}`,
|
|
283
295
|
`- Detector diagnostics: ${report.diagnostics.detectors.filter((d) => d.status === 'completed').length} completed / ${report.diagnostics.detectors.filter((d) => d.status === 'skipped').length} skipped`,
|
package/dist/report/types.d.ts
CHANGED
|
@@ -74,6 +74,19 @@ export interface ReportDiagnostics {
|
|
|
74
74
|
* fact, and the reader is entitled to know which one they have.
|
|
75
75
|
*/
|
|
76
76
|
readingDepth: LanguageReading[];
|
|
77
|
+
/**
|
|
78
|
+
* Whether a route name was ever read in this project, where it authenticates.
|
|
79
|
+
*
|
|
80
|
+
* Four capabilities — password reset, email verification, personal data export and
|
|
81
|
+
* erasure — are found by the English words their routes are usually given. Where
|
|
82
|
+
* none of `/login`, `/register` or `/signin` ever matched, those searches could not
|
|
83
|
+
* reach this project's vocabulary and answer `unknown` instead of `missing`.
|
|
84
|
+
*
|
|
85
|
+
* Withdrawing the claim silently would be its own failure: the reader of a project
|
|
86
|
+
* whose routes are `/accedi` and `/recupero-password` would see four questions
|
|
87
|
+
* simply absent, with nothing saying why. This is what the report says instead.
|
|
88
|
+
*/
|
|
89
|
+
routeNamesUnread: boolean;
|
|
77
90
|
/**
|
|
78
91
|
* Whether the optional TypeScript compiler was loaded for this reading.
|
|
79
92
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.1",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|