@bounded-systems/conformance-kit 0.2.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/LICENSE +21 -0
- package/README.md +143 -0
- package/emitters/index.mjs +68 -0
- package/gates/axe-gate.mjs +325 -0
- package/gates/baseline-gate.mjs +112 -0
- package/gates/commonmark-runner.mjs +98 -0
- package/gates/conformance/conformance.mjs +389 -0
- package/gates/conformance/web-build.mjs +568 -0
- package/gates/conformance-report.mjs +128 -0
- package/gates/html-validator-gate.mjs +120 -0
- package/gates/readability-gate.mjs +134 -0
- package/gates/sbom/check-sbom.mjs +112 -0
- package/gates/sbom/gen-sbom.mjs +167 -0
- package/gates/semantic/deno.json +7 -0
- package/gates/semantic/gate.ts +34 -0
- package/gates/seo-gate.mjs +208 -0
- package/gates/shacl-runner.mjs +160 -0
- package/gates/vuln-gate.mjs +101 -0
- package/generators/gen-cid.mjs +144 -0
- package/generators/gen-identity.mjs +120 -0
- package/generators/gen-snapshots.mjs +108 -0
- package/generators/openapi.mjs +61 -0
- package/integrity/gen-provenance.mjs +137 -0
- package/integrity/gen-sitemanifest.mjs +66 -0
- package/integrity/http-probe.mjs +131 -0
- package/integrity/structure-audit/audit.mjs +159 -0
- package/integrity/structure-audit/package.json +12 -0
- package/integrity/verify/README.md +40 -0
- package/integrity/verify/verify.mjs +107 -0
- package/integrity/verify-site.mjs +160 -0
- package/lib/config.mjs +36 -0
- package/lib/schema-validate.mjs +68 -0
- package/package.json +71 -0
- package/provenance.json +41 -0
- package/vendor.example.json +25 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
// gates/conformance/web-build.mjs
|
|
2
|
+
//
|
|
3
|
+
// A zero-dependency Node port of lone's web-build conformance STANDARD
|
|
4
|
+
// (`jsr:@bounded-systems/lone@^0.4`, `src/standard/web_build.ts`). It is the typed
|
|
5
|
+
// data the aggregator in `./conformance.mjs` folds evidence into: the criteria, the
|
|
6
|
+
// strong COMPACT_CLAIM, the standard name/version, the Core Web Vitals thresholds,
|
|
7
|
+
// and a hand-rolled validator for the external-evidence envelope.
|
|
8
|
+
//
|
|
9
|
+
// Why a port and not an import: lone is a Deno/JSR package (its DOM blessing runs in
|
|
10
|
+
// the `gates/semantic/` Deno gate). The conformance MODEL, by contrast, is a pure
|
|
11
|
+
// data function with no DOM — so it is mirrored here as plain Node, zero-dep, so a
|
|
12
|
+
// consumer's hermetic, offline `node build.mjs` can compute the report without
|
|
13
|
+
// pulling Deno/zod/JSR into the pure build. Kept byte-faithful to lone's criteria,
|
|
14
|
+
// claim string, tiers, and evidence shapes; pinned to STANDARD_VERSION below. lone
|
|
15
|
+
// remains the source of truth — when it bumps the standard, re-port this file.
|
|
16
|
+
//
|
|
17
|
+
// The whole point is OVERCLAIM-AVOIDANCE: the strong compact claim is emitted only
|
|
18
|
+
// when every gating (tier-1 `required`) criterion has passing evidence. Absent
|
|
19
|
+
// external evidence is reported as `not-assessed`, never silently treated as met.
|
|
20
|
+
|
|
21
|
+
/** Core Web Vitals thresholds (good, at p75). */
|
|
22
|
+
export const CWV_THRESHOLDS = {
|
|
23
|
+
lcpMs: 2500,
|
|
24
|
+
inpMs: 200,
|
|
25
|
+
cls: 0.1,
|
|
26
|
+
percentile: 75,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The strong compact claim. Emitted by `conformance()` ONLY when every gating
|
|
31
|
+
* criterion is `met`. Never assemble this string by hand.
|
|
32
|
+
*/
|
|
33
|
+
export const COMPACT_CLAIM =
|
|
34
|
+
"Conforms to WCAG 2.2 AA, HTML and WAI-ARIA author requirements, " +
|
|
35
|
+
"OWASP ASVS 5.0 Level 2, passes Core Web Vitals at p75, and targets " +
|
|
36
|
+
"Baseline Widely Available.";
|
|
37
|
+
|
|
38
|
+
export const STANDARD_NAME = "Bounded Systems Web-Build Conformance Standard";
|
|
39
|
+
export const STANDARD_VERSION = "1.0.0";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The criteria, as typed data. Ordered by area for stable reporting. Mirrors
|
|
43
|
+
* lone's `CRITERIA`. `evidence: "lone"` criteria are checked statically from a DOM
|
|
44
|
+
* subtree (the absence of error-severity findings under `loneCodes`); everything
|
|
45
|
+
* else is external evidence, supplied + threshold-checked, never fabricated.
|
|
46
|
+
*
|
|
47
|
+
* Only tier-1 `required` criteria gate the COMPACT_CLAIM; tier-2/tier-3/cognitive
|
|
48
|
+
* criteria are reported + summarised per-area but never widen the headline claim.
|
|
49
|
+
*/
|
|
50
|
+
export const CRITERIA = [
|
|
51
|
+
// ── HTML — HTML Living Standard ──────────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
id: "html.dom-author-requirements",
|
|
54
|
+
area: "html",
|
|
55
|
+
label: "HTML author requirements",
|
|
56
|
+
standard: "HTML Living Standard",
|
|
57
|
+
target: "DOM subtree meets HTML author requirements (valid semantics & structure).",
|
|
58
|
+
level: "author conformance",
|
|
59
|
+
evidence: "lone",
|
|
60
|
+
required: true,
|
|
61
|
+
loneCodes: ["LONE_SEMANTIC_"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "html.validator-clean",
|
|
65
|
+
area: "html",
|
|
66
|
+
label: "Nu HTML Checker errors",
|
|
67
|
+
standard: "Nu Html Checker",
|
|
68
|
+
target: "Zero HTML validator (Nu) errors over the rendered page.",
|
|
69
|
+
level: "zero errors",
|
|
70
|
+
evidence: "external",
|
|
71
|
+
required: true,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// ── Accessibility — WCAG 2.2 / WAI-ARIA 1.2 / axe ────────────────────────
|
|
75
|
+
{
|
|
76
|
+
id: "a11y.aria-author",
|
|
77
|
+
area: "accessibility",
|
|
78
|
+
label: "WAI-ARIA author requirements",
|
|
79
|
+
standard: "WAI-ARIA 1.2",
|
|
80
|
+
target: "Valid roles/states/properties/relationships; prefer native HTML semantics.",
|
|
81
|
+
level: "author conformance",
|
|
82
|
+
evidence: "lone",
|
|
83
|
+
required: true,
|
|
84
|
+
loneCodes: ["LONE_ARIA_"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "a11y.wcag22-aa-auto",
|
|
88
|
+
area: "accessibility",
|
|
89
|
+
label: "WCAG 2.2 AA (automated subset)",
|
|
90
|
+
standard: "WCAG 2.2",
|
|
91
|
+
target: "Automatable WCAG 2.2 AA checks pass (names, text alternatives, contrast, keyboard, SR content).",
|
|
92
|
+
level: "AA (automated subset)",
|
|
93
|
+
evidence: "lone",
|
|
94
|
+
required: true,
|
|
95
|
+
loneCodes: [
|
|
96
|
+
"LONE_NAME_",
|
|
97
|
+
"LONE_TEXT_",
|
|
98
|
+
"LONE_SR_",
|
|
99
|
+
"LONE_KEYBOARD_",
|
|
100
|
+
"LONE_COLOR_",
|
|
101
|
+
"LONE_READER_",
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "a11y.axe-serious-critical",
|
|
106
|
+
area: "accessibility",
|
|
107
|
+
label: "axe serious/critical violations",
|
|
108
|
+
standard: "axe-core",
|
|
109
|
+
target: "Zero serious/critical accessibility violations on the rendered page.",
|
|
110
|
+
level: "serious/critical",
|
|
111
|
+
evidence: "external",
|
|
112
|
+
required: true,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "a11y.wcag22-aa-manual",
|
|
116
|
+
area: "accessibility",
|
|
117
|
+
label: "WCAG 2.2 AA (manual audit)",
|
|
118
|
+
standard: "WCAG 2.2",
|
|
119
|
+
target: "Complete-flow manual audit incl. keyboard + screen-reader testing of critical flows.",
|
|
120
|
+
level: "AA (manual)",
|
|
121
|
+
evidence: "external",
|
|
122
|
+
required: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "a11y.wcag22-aaa-selected",
|
|
126
|
+
area: "accessibility",
|
|
127
|
+
label: "WCAG 2.2 AAA (selected)",
|
|
128
|
+
standard: "WCAG 2.2",
|
|
129
|
+
target: "Selected AAA success criteria met.",
|
|
130
|
+
level: "AAA (selected)",
|
|
131
|
+
evidence: "external",
|
|
132
|
+
required: false,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── Security — OWASP ASVS 5.0.0 ──────────────────────────────────────────
|
|
136
|
+
{
|
|
137
|
+
id: "security.asvs",
|
|
138
|
+
area: "security",
|
|
139
|
+
label: "OWASP ASVS Level 2",
|
|
140
|
+
standard: "OWASP ASVS 5.0.0",
|
|
141
|
+
target: "Verified to Level 2 (Level 3 for highly sensitive applications).",
|
|
142
|
+
level: "L2",
|
|
143
|
+
evidence: "external",
|
|
144
|
+
required: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "security.no-critical-vulns",
|
|
148
|
+
area: "security",
|
|
149
|
+
label: "known critical/high vulns",
|
|
150
|
+
standard: "OWASP ASVS 5.0.0",
|
|
151
|
+
target: "Zero known critical/high exploitable vulnerabilities.",
|
|
152
|
+
level: "zero critical/high",
|
|
153
|
+
evidence: "external",
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
// External grader — Chromium-maintained HSTS preload list. Independent by
|
|
158
|
+
// construction, so it needs no `verifiedBy`. Recommended (non-gating).
|
|
159
|
+
id: "security.hsts-preload",
|
|
160
|
+
area: "security",
|
|
161
|
+
label: "HSTS preload",
|
|
162
|
+
standard: "RFC 6797 / hstspreload.org",
|
|
163
|
+
target: "Origin is on the HSTS preload list (HTTPS enforced before first byte).",
|
|
164
|
+
level: "preloaded",
|
|
165
|
+
evidence: "external",
|
|
166
|
+
required: false,
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// ── Performance — Core Web Vitals ────────────────────────────────────────
|
|
170
|
+
{
|
|
171
|
+
id: "performance.core-web-vitals",
|
|
172
|
+
area: "performance",
|
|
173
|
+
label: "Core Web Vitals (p75)",
|
|
174
|
+
standard: "Core Web Vitals",
|
|
175
|
+
target: "LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1 at p75 on mobile AND desktop (field data).",
|
|
176
|
+
level: "p75 mobile + desktop",
|
|
177
|
+
evidence: "external",
|
|
178
|
+
required: true,
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// ── Compatibility — Baseline ─────────────────────────────────────────────
|
|
182
|
+
{
|
|
183
|
+
id: "compatibility.baseline",
|
|
184
|
+
area: "compatibility",
|
|
185
|
+
label: "Baseline Widely Available",
|
|
186
|
+
standard: "Baseline",
|
|
187
|
+
target: "Baseline Widely Available (interoperable ≥30 months), or a tested fallback for newer features.",
|
|
188
|
+
level: "Widely Available",
|
|
189
|
+
evidence: "external",
|
|
190
|
+
required: true,
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// ── Reliability — runtime ────────────────────────────────────────────────
|
|
194
|
+
{
|
|
195
|
+
id: "reliability.runtime",
|
|
196
|
+
area: "reliability",
|
|
197
|
+
label: "runtime reliability",
|
|
198
|
+
standard: "Bounded Systems reliability bar",
|
|
199
|
+
target: "No uncaught browser errors; no broken internal links; critical journeys covered by e2e tests.",
|
|
200
|
+
level: "—",
|
|
201
|
+
evidence: "external",
|
|
202
|
+
required: true,
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// ══ TIER 2 — machine-readable structured content + technical SEO ═══════════
|
|
206
|
+
{
|
|
207
|
+
id: "semantic.jsonld-shacl",
|
|
208
|
+
area: "semantic",
|
|
209
|
+
label: "JSON-LD 1.1 + SHACL conformance",
|
|
210
|
+
standard: "JSON-LD 1.1 / SHACL",
|
|
211
|
+
target: "Structured data parses as JSON-LD 1.1 and conforms to its SHACL shapes (zero violating blocks).",
|
|
212
|
+
level: "conforms",
|
|
213
|
+
evidence: "external",
|
|
214
|
+
required: true,
|
|
215
|
+
tier: 2,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "seo.technical",
|
|
219
|
+
area: "seo",
|
|
220
|
+
label: "Technical SEO",
|
|
221
|
+
standard: "Search-engine technical guidelines / RFC 9309",
|
|
222
|
+
target: "Canonical URLs correct, titles unique, robots.txt RFC 9309-valid, sitemap resolves, zero broken internal links.",
|
|
223
|
+
level: "clean",
|
|
224
|
+
evidence: "external",
|
|
225
|
+
required: true,
|
|
226
|
+
tier: 2,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "semantic.commonmark",
|
|
230
|
+
area: "semantic",
|
|
231
|
+
label: "CommonMark conformance",
|
|
232
|
+
standard: "CommonMark",
|
|
233
|
+
target: "Authored Markdown parses cleanly under the CommonMark spec.",
|
|
234
|
+
level: "conforms",
|
|
235
|
+
evidence: "external",
|
|
236
|
+
required: true,
|
|
237
|
+
tier: 2,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "semantic.ai-readability",
|
|
241
|
+
area: "semantic",
|
|
242
|
+
label: "AI-readability",
|
|
243
|
+
standard: "llms.txt convention",
|
|
244
|
+
target: "llms.txt present, its links resolve, and HTML pages expose Markdown siblings for machine consumption.",
|
|
245
|
+
level: "recommended",
|
|
246
|
+
evidence: "external",
|
|
247
|
+
required: false,
|
|
248
|
+
tier: 2,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "semantic.openapi",
|
|
252
|
+
area: "semantic",
|
|
253
|
+
label: "OpenAPI 3.2 + JSON Schema 2020-12",
|
|
254
|
+
standard: "OpenAPI 3.2 / JSON Schema 2020-12",
|
|
255
|
+
target: "Published OpenAPI document is valid and responses match their declared JSON Schemas. Only applies if an API is published.",
|
|
256
|
+
level: "conditional",
|
|
257
|
+
evidence: "external",
|
|
258
|
+
required: false,
|
|
259
|
+
tier: 2,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "semantic.feeds",
|
|
263
|
+
area: "semantic",
|
|
264
|
+
label: "Atom feed (RFC 4287)",
|
|
265
|
+
standard: "RFC 4287",
|
|
266
|
+
target: "Published feed is a valid Atom 1.0 document.",
|
|
267
|
+
level: "recommended",
|
|
268
|
+
evidence: "external",
|
|
269
|
+
required: false,
|
|
270
|
+
tier: 2,
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// ══ TIER 3 — integrity / provenance / reproducibility ══════════════════════
|
|
274
|
+
{
|
|
275
|
+
id: "integrity.slsa-provenance",
|
|
276
|
+
area: "integrity",
|
|
277
|
+
label: "SLSA provenance + in-toto",
|
|
278
|
+
standard: "SLSA / in-toto",
|
|
279
|
+
target: "Build emits in-toto/SLSA provenance that is present, signed, and verifies against the artifact.",
|
|
280
|
+
level: "present + signed + verified",
|
|
281
|
+
evidence: "external",
|
|
282
|
+
required: true,
|
|
283
|
+
tier: 3,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "integrity.reproducible-build",
|
|
287
|
+
area: "integrity",
|
|
288
|
+
label: "Reproducible build",
|
|
289
|
+
standard: "Reproducible Builds",
|
|
290
|
+
target: "Re-running the build from source yields byte-identical artifacts.",
|
|
291
|
+
level: "reproducible",
|
|
292
|
+
evidence: "external",
|
|
293
|
+
required: true,
|
|
294
|
+
tier: 3,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "integrity.sbom",
|
|
298
|
+
area: "integrity",
|
|
299
|
+
label: "SPDX SBOM",
|
|
300
|
+
standard: "SPDX",
|
|
301
|
+
target: "An SPDX SBOM is present, valid, complete (covers all components), and signed.",
|
|
302
|
+
level: "present + valid + complete + signed",
|
|
303
|
+
evidence: "external",
|
|
304
|
+
required: true,
|
|
305
|
+
tier: 3,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "integrity.content-digests",
|
|
309
|
+
area: "integrity",
|
|
310
|
+
label: "Content digests (RFC 9530)",
|
|
311
|
+
standard: "RFC 9530",
|
|
312
|
+
target: "Responses carry Repr-Digest (RFC 9530) representation digests.",
|
|
313
|
+
level: "recommended",
|
|
314
|
+
evidence: "external",
|
|
315
|
+
required: false,
|
|
316
|
+
tier: 3,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: "integrity.signed-release-manifest",
|
|
320
|
+
area: "integrity",
|
|
321
|
+
label: "Signed release manifest",
|
|
322
|
+
standard: "Bounded Systems release bar",
|
|
323
|
+
target: "Each release ships a manifest of artifact digests that is present and signed.",
|
|
324
|
+
level: "present + signed",
|
|
325
|
+
evidence: "external",
|
|
326
|
+
required: true,
|
|
327
|
+
tier: 3,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "integrity.ipfs-cid",
|
|
331
|
+
area: "integrity",
|
|
332
|
+
label: "IPFS CID recorded",
|
|
333
|
+
standard: "IPFS / CIDv1",
|
|
334
|
+
target: "The release records a content-addressed IPFS CID for the artifact.",
|
|
335
|
+
level: "recommended",
|
|
336
|
+
evidence: "external",
|
|
337
|
+
required: false,
|
|
338
|
+
tier: 3,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "integrity.http-rfc9110",
|
|
342
|
+
area: "integrity",
|
|
343
|
+
label: "HTTP correctness (RFC 9110)",
|
|
344
|
+
standard: "RFC 9110",
|
|
345
|
+
target: "Responses are semantically correct per RFC 9110 HTTP semantics.",
|
|
346
|
+
level: "recommended",
|
|
347
|
+
evidence: "external",
|
|
348
|
+
required: false,
|
|
349
|
+
tier: 3,
|
|
350
|
+
},
|
|
351
|
+
// ── Integrity — external graders (independent by construction) ────────────
|
|
352
|
+
{
|
|
353
|
+
// OpenSSF Scorecard — automated third-party grader of repo security posture
|
|
354
|
+
// (0–10). Independent, so no `verifiedBy`. Recommended (non-gating).
|
|
355
|
+
id: "integrity.scorecard",
|
|
356
|
+
area: "integrity",
|
|
357
|
+
label: "OpenSSF Scorecard",
|
|
358
|
+
standard: "OpenSSF Scorecard",
|
|
359
|
+
target: "Repository scores ≥ 7.0 on the OpenSSF Scorecard.",
|
|
360
|
+
level: "score ≥ 7.0",
|
|
361
|
+
evidence: "external",
|
|
362
|
+
required: false,
|
|
363
|
+
tier: 3,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
// SLSA build LEVEL achieved (distinct from `integrity.slsa-provenance`, which
|
|
367
|
+
// only checks provenance is present/signed/verified). Recommended (non-gating).
|
|
368
|
+
id: "integrity.slsa-level",
|
|
369
|
+
area: "integrity",
|
|
370
|
+
label: "SLSA build level",
|
|
371
|
+
standard: "SLSA",
|
|
372
|
+
target: "Build achieves the targeted SLSA build level (default L3).",
|
|
373
|
+
level: "≥ target (default L3)",
|
|
374
|
+
evidence: "external",
|
|
375
|
+
required: false,
|
|
376
|
+
tier: 3,
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// ══ COGNITIVE ACCESSIBILITY — W3C COGA ═════════════════════════════════════
|
|
380
|
+
// HONEST LABELING: an INTERFACE-COMPLEXITY BUDGET (W3C COGA-derived), explicitly
|
|
381
|
+
// NOT a "cognitive-load measurement". Reported + summarised but non-gating.
|
|
382
|
+
{
|
|
383
|
+
id: "cognitive.complexity-budget",
|
|
384
|
+
area: "cognitive",
|
|
385
|
+
label: "Interface-complexity budget (W3C COGA-derived)",
|
|
386
|
+
standard: "W3C COGA (derived)",
|
|
387
|
+
target:
|
|
388
|
+
"Rendered DOM stays within an interface-complexity budget: choice density, " +
|
|
389
|
+
"primary-action count, heading depth, clear link purpose, interruptions, " +
|
|
390
|
+
"form/memory burden, motion, progressive disclosure. " +
|
|
391
|
+
"This is an interface-complexity budget, NOT a cognitive-load measurement.",
|
|
392
|
+
level: "budget (recommended)",
|
|
393
|
+
evidence: "lone",
|
|
394
|
+
required: false,
|
|
395
|
+
tier: "cognitive",
|
|
396
|
+
loneCodes: ["LONE_COGA_"],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "cognitive.coga-usability-testing",
|
|
400
|
+
area: "cognitive",
|
|
401
|
+
label: "COGA usability testing",
|
|
402
|
+
standard: "W3C COGA",
|
|
403
|
+
target: "Usability testing conducted with people with cognitive disabilities; critical tasks pass.",
|
|
404
|
+
level: "manual (recommended)",
|
|
405
|
+
evidence: "external",
|
|
406
|
+
required: false,
|
|
407
|
+
tier: "cognitive",
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
// ── External-evidence envelope validation (zero-dep, mirrors lone's Zod) ──────
|
|
412
|
+
// lone verifies the SHAPE and THRESHOLDS of supplied evidence and THROWS on a
|
|
413
|
+
// malformed envelope ("lone refuses to guess"); absent fields stay absent and are
|
|
414
|
+
// reported `not-assessed` by the aggregator. This is a hand-rolled equivalent of
|
|
415
|
+
// lone's `ExternalEvidence.parse()`: per-field type checks, defaults applied,
|
|
416
|
+
// unknown top-level keys stripped (as Zod `.object()` does), throw on type mismatch.
|
|
417
|
+
|
|
418
|
+
class EvidenceError extends Error {}
|
|
419
|
+
const fail = (path, msg) => {
|
|
420
|
+
throw new EvidenceError(`external evidence: ${path} — ${msg}`);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
424
|
+
|
|
425
|
+
// Field validator combinators. Each is (value, path) => parsedValue (throws on bad).
|
|
426
|
+
const vBool = (v, p) => (typeof v === "boolean" ? v : fail(p, "expected a boolean"));
|
|
427
|
+
const vStr = (v, p) => (typeof v === "string" ? v : fail(p, "expected a string"));
|
|
428
|
+
const vInt0 = (v, p) =>
|
|
429
|
+
Number.isInteger(v) && v >= 0 ? v : fail(p, "expected an integer ≥ 0");
|
|
430
|
+
const vNum = (min, max) => (v, p) => {
|
|
431
|
+
if (typeof v !== "number" || Number.isNaN(v)) fail(p, "expected a number");
|
|
432
|
+
if (min != null && v < min) fail(p, `expected ≥ ${min}`);
|
|
433
|
+
if (max != null && v > max) fail(p, `expected ≤ ${max}`);
|
|
434
|
+
return v;
|
|
435
|
+
};
|
|
436
|
+
const vEnum = (...vals) => (v, p) =>
|
|
437
|
+
vals.includes(v) ? v : fail(p, `expected one of ${vals.map((x) => JSON.stringify(x)).join(", ")}`);
|
|
438
|
+
const vArrayOf = (inner) => (v, p) => {
|
|
439
|
+
if (!Array.isArray(v)) fail(p, "expected an array");
|
|
440
|
+
return v.map((x, i) => inner(x, `${p}[${i}]`));
|
|
441
|
+
};
|
|
442
|
+
// A nested object schema.
|
|
443
|
+
const vObject = (shape) => (v, p) => parseShape(shape, v, p);
|
|
444
|
+
|
|
445
|
+
// Field descriptors: { val, optional?, default? }. `req(val)` required; `opt(val)`
|
|
446
|
+
// optional (absent → omitted); `def(val, d)` optional with a default.
|
|
447
|
+
const req = (val) => ({ val });
|
|
448
|
+
const opt = (val) => ({ val, optional: true });
|
|
449
|
+
const def = (val, d) => ({ val, default: d });
|
|
450
|
+
|
|
451
|
+
function parseShape(shape, input, path) {
|
|
452
|
+
if (input === undefined || input === null) input = {};
|
|
453
|
+
if (!isPlainObject(input)) fail(path, "expected an object");
|
|
454
|
+
const out = {};
|
|
455
|
+
for (const [key, spec] of Object.entries(shape)) {
|
|
456
|
+
const here = path ? `${path}.${key}` : key;
|
|
457
|
+
if (key in input && input[key] !== undefined) {
|
|
458
|
+
out[key] = spec.val(input[key], here);
|
|
459
|
+
} else if ("default" in spec) {
|
|
460
|
+
out[key] = spec.default;
|
|
461
|
+
} else if (!spec.optional) {
|
|
462
|
+
fail(here, "is required");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out; // unknown keys stripped (as Zod .object() does)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const CWV_SAMPLE_SHAPE = {
|
|
469
|
+
formFactor: req(vEnum("mobile", "desktop")),
|
|
470
|
+
percentile: req(vNum(0, 100)),
|
|
471
|
+
lcpMs: req(vNum(0)),
|
|
472
|
+
inpMs: req(vNum(0)),
|
|
473
|
+
cls: req(vNum(0)),
|
|
474
|
+
source: def(vEnum("field", "lab"), "field"),
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// The full external-evidence envelope. Every field optional; mirrors lone.
|
|
478
|
+
const ENVELOPE = {
|
|
479
|
+
// tier-1
|
|
480
|
+
htmlValidator: opt(vObject({ errors: req(vInt0), warnings: opt(vInt0) })),
|
|
481
|
+
// `verifiedBy` (an independent assessor) is REQUIRED for `a11y.wcag22-aa-manual`
|
|
482
|
+
// to reach `met`; absent it the criterion is `not-assessed` — a self-attested
|
|
483
|
+
// manual audit never gates the compact claim.
|
|
484
|
+
manualA11y: opt(vObject({
|
|
485
|
+
wcag22AA: req(vBool),
|
|
486
|
+
keyboardTested: req(vBool),
|
|
487
|
+
screenReaderTested: req(vBool),
|
|
488
|
+
completeFlows: req(vBool),
|
|
489
|
+
verifiedBy: opt(vStr),
|
|
490
|
+
})),
|
|
491
|
+
wcag22AAA: opt(vObject({ criteria: def(vArrayOf(vStr), []), met: req(vBool) })),
|
|
492
|
+
axe: opt(vObject({ serious: req(vInt0), critical: req(vInt0) })),
|
|
493
|
+
// OWASP ASVS attestation — the self-graded part. `verifiedBy` (an independent
|
|
494
|
+
// assessor) is REQUIRED for `security.asvs` to reach `met`; absent it the
|
|
495
|
+
// criterion is `not-assessed` (self-attestation never gates the compact claim).
|
|
496
|
+
asvs: opt(vObject({
|
|
497
|
+
standard: def(vStr, "OWASP ASVS"),
|
|
498
|
+
version: def(vStr, "5.0.0"),
|
|
499
|
+
achievedLevel: req(vEnum(1, 2, 3)),
|
|
500
|
+
targetLevel: def(vEnum(1, 2, 3), 2),
|
|
501
|
+
verifiedBy: opt(vStr),
|
|
502
|
+
})),
|
|
503
|
+
// Known critical/high vulnerabilities — the TOOL-measured part (e.g. `npm audit`,
|
|
504
|
+
// OSV). Decoupled from `asvs` so an objective vuln count can be supplied WITHOUT
|
|
505
|
+
// also self-grading an ASVS level.
|
|
506
|
+
vulns: opt(vObject({ knownCriticalOrHighVulns: req(vInt0) })),
|
|
507
|
+
// External grader — Chromium HSTS preload list. Independent; no `verifiedBy`.
|
|
508
|
+
hstsPreload: opt(vObject({ preloaded: req(vBool) })),
|
|
509
|
+
coreWebVitals: opt(vArrayOf(vObject(CWV_SAMPLE_SHAPE))),
|
|
510
|
+
baseline: opt(vObject({
|
|
511
|
+
status: req(vEnum("widely", "newly", "limited")),
|
|
512
|
+
fallbackTested: def(vBool, false),
|
|
513
|
+
})),
|
|
514
|
+
reliability: opt(vObject({
|
|
515
|
+
uncaughtErrors: req(vInt0),
|
|
516
|
+
brokenInternalLinks: req(vInt0),
|
|
517
|
+
e2eCriticalJourneys: req(vBool),
|
|
518
|
+
})),
|
|
519
|
+
// tier-2
|
|
520
|
+
jsonLdShacl: opt(vObject({ conforms: req(vBool), blocks: req(vInt0) })),
|
|
521
|
+
seoTechnical: opt(vObject({
|
|
522
|
+
canonicalOk: req(vBool),
|
|
523
|
+
titlesUnique: req(vBool),
|
|
524
|
+
robotsRfc9309Ok: req(vBool),
|
|
525
|
+
sitemapResolves: req(vBool),
|
|
526
|
+
brokenInternalLinks: req(vInt0),
|
|
527
|
+
})),
|
|
528
|
+
commonMark: opt(vObject({ conforms: req(vBool) })),
|
|
529
|
+
aiReadability: opt(vObject({
|
|
530
|
+
llmsTxtPresent: req(vBool),
|
|
531
|
+
linksResolve: req(vBool),
|
|
532
|
+
markdownSiblings: req(vBool),
|
|
533
|
+
})),
|
|
534
|
+
openApi: opt(vObject({ openapiValid: req(vBool), responsesMatchSchemas: req(vBool) })),
|
|
535
|
+
feeds: opt(vObject({ atomValid: req(vBool) })),
|
|
536
|
+
// tier-3
|
|
537
|
+
slsaProvenance: opt(vObject({ present: req(vBool), signed: req(vBool), verified: req(vBool) })),
|
|
538
|
+
// External grader — SLSA build LEVEL achieved (distinct from slsaProvenance).
|
|
539
|
+
slsaLevel: opt(vObject({ level: req(vEnum(0, 1, 2, 3)), target: def(vEnum(1, 2, 3), 3) })),
|
|
540
|
+
// External grader — OpenSSF Scorecard score (0–10). Independent; no `verifiedBy`.
|
|
541
|
+
scorecard: opt(vObject({ score: req(vNum(0, 10)) })),
|
|
542
|
+
reproducibleBuild: opt(vObject({ reproducible: req(vBool) })),
|
|
543
|
+
sbom: opt(vObject({ present: req(vBool), valid: req(vBool), complete: req(vBool), signed: req(vBool) })),
|
|
544
|
+
contentDigests: opt(vObject({ reprDigestHeaders: req(vBool) })),
|
|
545
|
+
signedReleaseManifest: opt(vObject({ present: req(vBool), signed: req(vBool) })),
|
|
546
|
+
ipfsCid: opt(vObject({ cidRecorded: req(vBool) })),
|
|
547
|
+
httpRfc9110: opt(vObject({ conforms: req(vBool) })),
|
|
548
|
+
// cognitive
|
|
549
|
+
cogaUsability: opt(vObject({
|
|
550
|
+
conducted: req(vBool),
|
|
551
|
+
withCognitiveDisabilities: req(vBool),
|
|
552
|
+
criticalTasksPassed: req(vBool),
|
|
553
|
+
})),
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Validate + normalise the external-evidence envelope. Throws an `Error` on a
|
|
558
|
+
* malformed envelope (wrong types) — lone refuses to guess. Absent fields are
|
|
559
|
+
* simply omitted (the aggregator reports them `not-assessed`).
|
|
560
|
+
*
|
|
561
|
+
* @param {object} [evidence]
|
|
562
|
+
* @returns {object} the parsed envelope (defaults applied, unknown keys stripped)
|
|
563
|
+
*/
|
|
564
|
+
export function parseExternalEvidence(evidence) {
|
|
565
|
+
return parseShape(ENVELOPE, evidence ?? {}, "");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export { EvidenceError };
|