@primust/verifier 1.0.0 → 1.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 (49) hide show
  1. package/dist/chunk-LTWQK3HT.js +432 -0
  2. package/dist/chunk-NOADQWB6.js +3012 -0
  3. package/dist/cli.d.ts +3 -2
  4. package/dist/cli.js +309 -361
  5. package/dist/index.d.ts +335 -13
  6. package/dist/index.js +1181 -13
  7. package/dist/tsa-chain-7KSQ5LAH.js +235 -0
  8. package/dist/v29-envelope-GFVVA2S6.js +42 -0
  9. package/package.json +7 -8
  10. package/dist/bounded-trace.d.ts +0 -46
  11. package/dist/bounded-trace.d.ts.map +0 -1
  12. package/dist/bounded-trace.js +0 -558
  13. package/dist/bounded-trace.js.map +0 -1
  14. package/dist/cli.d.ts.map +0 -1
  15. package/dist/cli.js.map +0 -1
  16. package/dist/index.d.ts.map +0 -1
  17. package/dist/index.js.map +0 -1
  18. package/dist/key-cache.d.ts +0 -20
  19. package/dist/key-cache.d.ts.map +0 -1
  20. package/dist/key-cache.js +0 -68
  21. package/dist/key-cache.js.map +0 -1
  22. package/dist/scoped.d.ts +0 -35
  23. package/dist/scoped.d.ts.map +0 -1
  24. package/dist/scoped.js +0 -582
  25. package/dist/scoped.js.map +0 -1
  26. package/dist/types.d.ts +0 -60
  27. package/dist/types.d.ts.map +0 -1
  28. package/dist/types.js +0 -5
  29. package/dist/types.js.map +0 -1
  30. package/dist/upstream_resolver.d.ts +0 -60
  31. package/dist/upstream_resolver.d.ts.map +0 -1
  32. package/dist/upstream_resolver.js +0 -126
  33. package/dist/upstream_resolver.js.map +0 -1
  34. package/dist/v29-envelope.d.ts +0 -55
  35. package/dist/v29-envelope.d.ts.map +0 -1
  36. package/dist/v29-envelope.js +0 -450
  37. package/dist/v29-envelope.js.map +0 -1
  38. package/dist/verifier.d.ts +0 -36
  39. package/dist/verifier.d.ts.map +0 -1
  40. package/dist/verifier.js +0 -1235
  41. package/dist/verifier.js.map +0 -1
  42. package/dist/verifier.test.d.ts +0 -2
  43. package/dist/verifier.test.d.ts.map +0 -1
  44. package/dist/verifier.test.js +0 -395
  45. package/dist/verifier.test.js.map +0 -1
  46. package/dist/verify-html-template.d.ts +0 -45
  47. package/dist/verify-html-template.d.ts.map +0 -1
  48. package/dist/verify-html-template.js +0 -182
  49. package/dist/verify-html-template.js.map +0 -1
package/dist/key-cache.js DELETED
@@ -1,68 +0,0 @@
1
- /**
2
- * Public key cache for VPEC signature verification.
3
- *
4
- * Fetches public keys from trust anchor URLs and caches by key ID.
5
- * Supports pinned trust roots for offline/air-gapped verification.
6
- */
7
- import { readFileSync, existsSync } from 'node:fs';
8
- // In-memory cache: kid → base64url public key
9
- const cache = new Map();
10
- /**
11
- * Resolve a public key by key ID.
12
- *
13
- * @param kid - Key identifier from VPEC signature field
14
- * @param publicKeyUrl - URL to fetch the PEM/base64url key
15
- * @param trustRoot - Optional local PEM file path or content for offline mode
16
- */
17
- /**
18
- * Pre-seed the in-memory key cache. Used by evidence-pack assembler
19
- * to inject the signer's public key before verification runs.
20
- */
21
- export function seedKeyCache(kid, publicKey) {
22
- cache.set(kid, publicKey);
23
- }
24
- export async function getKey(kid, publicKeyUrl, trustRoot) {
25
- // Pinned trust root — zero network calls
26
- if (trustRoot) {
27
- // If it looks like a file path, read it
28
- if (existsSync(trustRoot)) {
29
- return readFileSync(trustRoot, 'utf-8').trim();
30
- }
31
- // Otherwise treat as inline key content
32
- return trustRoot.trim();
33
- }
34
- // Check cache
35
- const cached = cache.get(kid);
36
- if (cached)
37
- return cached;
38
- // Fetch from URL with retries
39
- if (!publicKeyUrl) {
40
- throw new Error(`No public_key_url for kid=${kid}`);
41
- }
42
- let lastError = null;
43
- for (let attempt = 0; attempt < 3; attempt++) {
44
- try {
45
- const resp = await fetch(publicKeyUrl, {
46
- headers: { Accept: 'application/x-pem-file' },
47
- signal: AbortSignal.timeout(10_000),
48
- });
49
- if (!resp.ok) {
50
- throw new Error(`HTTP ${resp.status}`);
51
- }
52
- const pem = (await resp.text()).trim();
53
- if (!pem) {
54
- throw new Error('Empty response');
55
- }
56
- cache.set(kid, pem);
57
- return pem;
58
- }
59
- catch (e) {
60
- lastError = e instanceof Error ? e : new Error(String(e));
61
- if (attempt < 2) {
62
- await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
63
- }
64
- }
65
- }
66
- throw new Error(`Failed to fetch key from ${publicKeyUrl} after 3 attempts: ${lastError?.message}`);
67
- }
68
- //# sourceMappingURL=key-cache.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"key-cache.js","sourceRoot":"","sources":["../src/key-cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEnD,8CAA8C;AAC9C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExC;;;;;;GAMG;AACH;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,SAAiB;IACzD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,GAAW,EACX,YAAoB,EACpB,SAAkB;IAElB,yCAAyC;IACzC,IAAI,SAAS,EAAE,CAAC;QACd,wCAAwC;QACxC,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,OAAO,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACjD,CAAC;QACD,wCAAwC;QACxC,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,cAAc;IACd,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,8BAA8B;IAC9B,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,SAAS,GAAiB,IAAI,CAAC;IACnC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,YAAY,EAAE;gBACrC,OAAO,EAAE,EAAE,MAAM,EAAE,wBAAwB,EAAE;gBAC7C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;aACpC,CAAC,CAAC;YAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACzC,CAAC;YAED,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;YACpC,CAAC;YAED,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACpB,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,SAAS,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,4BAA4B,YAAY,sBAAsB,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;AACtG,CAAC"}
package/dist/scoped.d.ts DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Reference TypeScript verifier for the `scoped_certificates` VPEC section.
3
- *
4
- * Ported from verifier-py/src/primust_verify/scoped.py. Validates the
5
- * top-level `scoped_certificates` array + matching
6
- * `scoped_certificates_commitment` emitted by the SDK's
7
- * `primust.scoped.bundle` module (SCOPED_CERT_SPEC_v27 §14).
8
- *
9
- * Trust-chain scope:
10
- * 1. Every entry carries a known `certificate_type` discriminator.
11
- * 2. Every entry has the required structural fields for its type.
12
- * 3. The array is in canonical order: lexicographic by
13
- * (certificate_type, canonical_sha256(payload)).
14
- * 4. The declared commitment byte-matches the recomputed Merkle root
15
- * over the ordered canonical-JSON leaves. Empty bundle uses the
16
- * well-known `sha256(canonical({scoped_certificates: []}))`.
17
- *
18
- * SI-3 (VPEC verifiability): this module's output MUST agree with the
19
- * Python reference (`primust_verify.scoped`) byte-for-byte across the
20
- * shared fixture corpus at
21
- * packages/verifier-py/tests/fixtures/scoped_v1/. The conformance runner
22
- * in scripts/scoped_conformance.py gates that in CI.
23
- */
24
- import { buildMerkleRoot, canonicalJson, canonicalJsonString } from "./bounded-trace";
25
- export declare const SCOPED_REASONS: readonly ["ok", "missing_section", "malformed_section", "missing_commitment", "malformed_commitment", "missing_discriminator", "unknown_certificate_type", "schema_validation_failed", "ordering_violation", "commitment_mismatch"];
26
- export type ScopedReason = (typeof SCOPED_REASONS)[number];
27
- export interface ScopedCertificatesResult {
28
- valid: boolean;
29
- reason: ScopedReason;
30
- entry_count: number;
31
- details: Record<string, unknown>;
32
- }
33
- export declare function verifyScopedCertificates(vpecLike: Record<string, unknown>): ScopedCertificatesResult;
34
- export { canonicalJson, canonicalJsonString, buildMerkleRoot };
35
- //# sourceMappingURL=scoped.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scoped.d.ts","sourceRoot":"","sources":["../src/scoped.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACpB,MAAM,iBAAiB,CAAC;AAmazB,eAAO,MAAM,cAAc,qOAWjB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AA+DD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,wBAAwB,CA8F1B;AAGD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,eAAe,EAAE,CAAC"}
package/dist/scoped.js DELETED
@@ -1,582 +0,0 @@
1
- /**
2
- * Reference TypeScript verifier for the `scoped_certificates` VPEC section.
3
- *
4
- * Ported from verifier-py/src/primust_verify/scoped.py. Validates the
5
- * top-level `scoped_certificates` array + matching
6
- * `scoped_certificates_commitment` emitted by the SDK's
7
- * `primust.scoped.bundle` module (SCOPED_CERT_SPEC_v27 §14).
8
- *
9
- * Trust-chain scope:
10
- * 1. Every entry carries a known `certificate_type` discriminator.
11
- * 2. Every entry has the required structural fields for its type.
12
- * 3. The array is in canonical order: lexicographic by
13
- * (certificate_type, canonical_sha256(payload)).
14
- * 4. The declared commitment byte-matches the recomputed Merkle root
15
- * over the ordered canonical-JSON leaves. Empty bundle uses the
16
- * well-known `sha256(canonical({scoped_certificates: []}))`.
17
- *
18
- * SI-3 (VPEC verifiability): this module's output MUST agree with the
19
- * Python reference (`primust_verify.scoped`) byte-for-byte across the
20
- * shared fixture corpus at
21
- * packages/verifier-py/tests/fixtures/scoped_v1/. The conformance runner
22
- * in scripts/scoped_conformance.py gates that in CI.
23
- */
24
- import { createHash } from "node:crypto";
25
- import { buildMerkleRoot, canonicalJson, canonicalJsonString, } from "./bounded-trace";
26
- // ── Per-certificate-type strict schema (mirrors verifier-py/scoped.py) ──
27
- //
28
- // Mirrors primust.scoped.schemas.py structurally. Zero SDK dep: schema
29
- // values are duplicated into this file so the verifier package stays
30
- // self-contained. When the SDK schemas change, update this table AND
31
- // primust_verify/scoped.py in lockstep — the scoped_conformance.py CI
32
- // gate catches drift via shared fixtures.
33
- // ── Helpers ──
34
- function canonicalSha256(obj) {
35
- const h = createHash("sha256");
36
- h.update(canonicalJson(obj));
37
- return "sha256:" + h.digest("hex");
38
- }
39
- const HASH_RE = /^sha256:[0-9a-f]{64}$/;
40
- function isCommitment(s) {
41
- return typeof s === "string" && HASH_RE.test(s);
42
- }
43
- const SURFACE_SCHEMA = {
44
- required: {
45
- kind: { enum: ["field", "field_group", "document", "process", "workflow_step"] },
46
- id: { type: "string" },
47
- },
48
- optional: {
49
- path: { type: "string", nullable: true },
50
- },
51
- };
52
- const SUPPORT_REF_SCHEMA = {
53
- required: {
54
- doc_id: { type: "string" },
55
- span_commitment: { type: "string", pattern: "commitment" },
56
- },
57
- optional: {},
58
- };
59
- const SHADOW_RESULT_SCHEMA = {
60
- required: {
61
- shadow_id: { type: "string" },
62
- result: { enum: ["certified", "not_certified", "abstain"] },
63
- },
64
- optional: {
65
- shadow_type: {
66
- enum: ["quantized", "distilled", "architecture_diverse", "base"],
67
- nullable: true,
68
- },
69
- score: { type: "number", nullable: true },
70
- },
71
- };
72
- const RETRIEVAL_REF_SCHEMA = {
73
- required: {
74
- chunk_id: { type: "string" },
75
- span_commitment: { type: "string", pattern: "commitment" },
76
- },
77
- optional: {},
78
- };
79
- const NAMED_SCHEMAS = {
80
- _surface: SURFACE_SCHEMA,
81
- _support_ref: SUPPORT_REF_SCHEMA,
82
- _shadow_result: SHADOW_RESULT_SCHEMA,
83
- _retrieval_ref: RETRIEVAL_REF_SCHEMA,
84
- };
85
- const CERTIFICATE_LEVELS = [
86
- "scoped_weak",
87
- "scoped_moderate",
88
- "scoped_strong",
89
- "scoped_asymptotic",
90
- "bounded_agreement",
91
- ];
92
- const ARTIFACT_SCHEMAS = {
93
- local_manifold: {
94
- required: {
95
- certificate_type: { literal: "local_manifold" },
96
- certificate_level: { enum: CERTIFICATE_LEVELS },
97
- calibration_epoch: { type: "string" },
98
- localizer_id: { type: "string" },
99
- manifold_id: { type: "string" },
100
- neighborhood_commitment: { type: "string", pattern: "commitment" },
101
- local_threshold: { type: "number", min: 0.0, max: 1.0 },
102
- surface: { schema: "_surface" },
103
- result: { enum: ["certified", "not_certified", "abstain"] },
104
- },
105
- optional: {
106
- fallback_to_global: { type: "boolean" },
107
- signature: { type: "string", nullable: true },
108
- },
109
- },
110
- hierarchical_output: {
111
- required: {
112
- certificate_type: { literal: "hierarchical_output" },
113
- hierarchy_id: { type: "string" },
114
- certified_paths: {
115
- type: "array",
116
- items: {
117
- type: "array",
118
- items: { type: "array", items: { type: "string" } },
119
- },
120
- },
121
- uncertified_paths: {
122
- type: "array",
123
- items: {
124
- type: "array",
125
- items: { type: "array", items: { type: "string" } },
126
- },
127
- },
128
- },
129
- optional: {
130
- uncertified_remainder: { type: "object", nullable: true },
131
- signature: { type: "string", nullable: true },
132
- },
133
- },
134
- model_continuity: {
135
- required: {
136
- certificate_type: { literal: "model_continuity" },
137
- service_id: { type: "string" },
138
- continuity_epoch: { type: "string" },
139
- envelope_id: { type: "string" },
140
- continuity_state: {
141
- enum: [
142
- "within_envelope",
143
- "outside_envelope",
144
- "epoch_change",
145
- "insufficient_probes",
146
- ],
147
- },
148
- probe_suite_id: { type: "string" },
149
- probe_suite_commitment: { type: "string", pattern: "commitment" },
150
- probe_response_commitment: { type: "string", pattern: "commitment" },
151
- },
152
- optional: {
153
- signature: { type: "string", nullable: true },
154
- },
155
- },
156
- retrieval_grounding: {
157
- required: {
158
- certificate_type: { literal: "retrieval_grounding" },
159
- surface: { schema: "_surface" },
160
- support_set: { type: "array", items: { schema: "_support_ref" } },
161
- grounded: { type: "boolean" },
162
- },
163
- optional: {
164
- per_stage_fn_bounds: {
165
- type: "array",
166
- items: { type: "number", min: 0.0, max: 1.0 },
167
- nullable: true,
168
- },
169
- aggregation_model: { type: "string", nullable: true },
170
- signature: { type: "string", nullable: true },
171
- },
172
- },
173
- workflow_composition: {
174
- required: {
175
- certificate_type: { literal: "workflow_composition" },
176
- workflow_id: { type: "string" },
177
- step_vpec_ids: { type: "array", items: { type: "string" } },
178
- hard_relevant_steps: { type: "array", items: { type: "string" } },
179
- composition_rule: { literal: "weakest_link_hard" },
180
- composed_result: { enum: CERTIFICATE_LEVELS },
181
- },
182
- optional: {
183
- informational_summary: { type: "object", nullable: true },
184
- signature: { type: "string", nullable: true },
185
- },
186
- },
187
- proof_of_absence: {
188
- required: {
189
- certificate_type: { literal: "proof_of_absence" },
190
- surface: { schema: "_surface" },
191
- absence_class: {
192
- enum: [
193
- "no_phi",
194
- "no_restricted_identifier",
195
- "no_prohibited_financial_claim",
196
- ],
197
- },
198
- result: { type: "boolean" },
199
- },
200
- optional: {
201
- detector_family: { type: "string", nullable: true },
202
- per_stage_fn_bounds: {
203
- type: "array",
204
- items: { type: "number", min: 0.0, max: 1.0 },
205
- nullable: true,
206
- },
207
- signature: { type: "string", nullable: true },
208
- },
209
- },
210
- calibration_epoch: {
211
- required: {
212
- certificate_type: { literal: "calibration_epoch" },
213
- calibration_epoch: { type: "string" },
214
- drift_state: { enum: ["stable", "drifting", "break"] },
215
- source_mix: { type: "object" },
216
- },
217
- optional: {
218
- parent_epoch: { type: "string", nullable: true },
219
- signature: { type: "string", nullable: true },
220
- },
221
- },
222
- shadow_committee: {
223
- required: {
224
- certificate_type: { literal: "shadow_committee" },
225
- committee_rule: { type: "string" },
226
- shadow_results: { type: "array", items: { schema: "_shadow_result" } },
227
- committee_result: { enum: ["certified", "not_certified", "abstain"] },
228
- },
229
- optional: {
230
- disagreement: { type: "boolean" },
231
- },
232
- },
233
- decision_context: {
234
- required: {
235
- certificate_type: { literal: "decision_context" },
236
- primitive_type: { literal: "DCE" },
237
- scope: { type: "string" },
238
- capture_mode: {
239
- enum: [
240
- "reasoning_block",
241
- "planner_output",
242
- "prompt_and_output_binding",
243
- "declared_context_only",
244
- ],
245
- },
246
- rationale_commitment: { type: "string", pattern: "commitment" },
247
- context_commitment: { type: "string", pattern: "commitment" },
248
- source_binding: { type: "string" },
249
- proof_level_achieved: { enum: ["execution", "witnessed", "attestation"] },
250
- scope_disclosure: { type: "string" },
251
- },
252
- optional: {
253
- retrieval_references: { type: "array", items: { schema: "_retrieval_ref" } },
254
- tool_output_commitments: {
255
- type: "array",
256
- items: { type: "string", pattern: "commitment" },
257
- },
258
- intent_declaration_ids: { type: "array", items: { type: "string" } },
259
- constraints: { type: "array", items: { type: "string" } },
260
- },
261
- },
262
- temporal_comparison: {
263
- required: {
264
- certificate_type: { literal: "temporal_comparison" },
265
- pack_a_id: { type: "string" },
266
- pack_b_id: { type: "string" },
267
- pack_a_commitment: { type: "string", pattern: "commitment" },
268
- pack_b_commitment: { type: "string", pattern: "commitment" },
269
- pack_a_period_start: { type: "string" },
270
- pack_a_period_end: { type: "string" },
271
- pack_b_period_start: { type: "string" },
272
- pack_b_period_end: { type: "string" },
273
- checks_added: { type: "array", items: { type: "string" } },
274
- checks_removed: { type: "array", items: { type: "string" } },
275
- checks_retained_count: { type: "integer" },
276
- coverage_changes_count: { type: "integer" },
277
- performance_changes_count: { type: "integer" },
278
- redaction_consistent: { type: "boolean" },
279
- diff_commitment: { type: "string", pattern: "commitment" },
280
- },
281
- optional: {
282
- redaction_profile_a: { type: "string", nullable: true },
283
- redaction_profile_b: { type: "string", nullable: true },
284
- signature: { type: "string", nullable: true },
285
- },
286
- },
287
- };
288
- function isPlainObject(v) {
289
- return typeof v === "object" && v !== null && !Array.isArray(v);
290
- }
291
- function checkValue(value, spec) {
292
- if (spec.nullable && value === null)
293
- return null;
294
- if ("literal" in spec && spec.literal !== undefined) {
295
- if (value !== spec.literal)
296
- return `expected literal ${JSON.stringify(spec.literal)}, got ${JSON.stringify(value)}`;
297
- return null;
298
- }
299
- if (spec.enum) {
300
- if (!spec.enum.includes(value)) {
301
- return `value ${JSON.stringify(value)} not in enum ${JSON.stringify(spec.enum)}`;
302
- }
303
- return null;
304
- }
305
- if (spec.schema) {
306
- const sub = NAMED_SCHEMAS[spec.schema];
307
- return checkObject(value, sub);
308
- }
309
- switch (spec.type) {
310
- case "string":
311
- if (typeof value !== "string")
312
- return `expected string, got ${typeof value}`;
313
- if (spec.pattern === "commitment" && !isCommitment(value)) {
314
- return "expected sha256:[64-hex] commitment";
315
- }
316
- return null;
317
- case "number":
318
- if (typeof value !== "number" || Number.isNaN(value))
319
- return `expected number, got ${typeof value}`;
320
- if (spec.min !== undefined && value < spec.min)
321
- return `value ${value} below minimum ${spec.min}`;
322
- if (spec.max !== undefined && value > spec.max)
323
- return `value ${value} above maximum ${spec.max}`;
324
- return null;
325
- case "integer":
326
- if (typeof value !== "number" || !Number.isInteger(value))
327
- return `expected integer, got ${typeof value}`;
328
- return null;
329
- case "boolean":
330
- if (typeof value !== "boolean")
331
- return `expected boolean, got ${typeof value}`;
332
- return null;
333
- case "object":
334
- if (!isPlainObject(value))
335
- return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
336
- return null;
337
- case "array":
338
- if (!Array.isArray(value))
339
- return `expected array, got ${typeof value}`;
340
- if (spec.items) {
341
- for (let i = 0; i < value.length; i++) {
342
- const err = checkValue(value[i], spec.items);
343
- if (err)
344
- return `[${i}]: ${err}`;
345
- }
346
- }
347
- return null;
348
- }
349
- return `unknown field spec`;
350
- }
351
- function checkObject(value, schema) {
352
- if (!isPlainObject(value)) {
353
- return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
354
- }
355
- const known = new Set([...Object.keys(schema.required), ...Object.keys(schema.optional)]);
356
- const extras = Object.keys(value).filter((k) => !known.has(k));
357
- if (extras.length > 0) {
358
- extras.sort();
359
- return `unexpected extra fields: ${JSON.stringify(extras)}`;
360
- }
361
- for (const [name, spec] of Object.entries(schema.required)) {
362
- if (!(name in value))
363
- return `missing required field '${name}'`;
364
- const err = checkValue(value[name], spec);
365
- if (err)
366
- return `${name}: ${err}`;
367
- }
368
- for (const [name, spec] of Object.entries(schema.optional)) {
369
- if (name in value) {
370
- const err = checkValue(value[name], spec);
371
- if (err)
372
- return `${name}: ${err}`;
373
- }
374
- }
375
- return null;
376
- }
377
- function checkWorkflowCompositionSubset(entry) {
378
- // WorkflowCompositionArtifact.hard_relevant_steps must be a subset of
379
- // step_vpec_ids (mirrors the SDK's model_validator + Python verifier's
380
- // _check_workflow_composition_subset). Without this an artifact can
381
- // declare a "hard relevant" step that isn't in the step list —
382
- // structurally impossible since weakest-link-hard cannot evaluate it.
383
- const stepIds = entry.step_vpec_ids;
384
- const hard = entry.hard_relevant_steps;
385
- if (!Array.isArray(stepIds) || !Array.isArray(hard))
386
- return null;
387
- const stepSet = new Set(stepIds);
388
- const extras = hard.filter((s) => !stepSet.has(s));
389
- if (extras.length > 0) {
390
- return `hard_relevant_steps must be a subset of step_vpec_ids; unknown step ids: ${JSON.stringify(extras)}`;
391
- }
392
- return null;
393
- }
394
- function checkSourceMix(value) {
395
- // CalibrationEpoch.source_mix must be dict[str, float] summing to
396
- // 1.0 ± 1e-6 with every value FINITE and NON-NEGATIVE (mirrors SDK
397
- // Pydantic validator + the post-Phase-1-#3 review hardening). Without
398
- // the finite/non-negative checks {"a": 1.2, "b": -0.2} would sum to
399
- // 1.0 but represent an invalid distribution.
400
- if (!isPlainObject(value))
401
- return "source_mix is not an object";
402
- let total = 0;
403
- for (const [k, v] of Object.entries(value)) {
404
- if (typeof k !== "string")
405
- return "source_mix has non-string key";
406
- if (typeof v !== "number" || Number.isNaN(v)) {
407
- return `source_mix[${JSON.stringify(k)}] is not a number`;
408
- }
409
- if (!Number.isFinite(v)) {
410
- return `source_mix[${JSON.stringify(k)}] is not finite`;
411
- }
412
- if (v < 0) {
413
- return `source_mix[${JSON.stringify(k)}] is negative`;
414
- }
415
- total += v;
416
- }
417
- if (Math.abs(total - 1.0) > 1e-6)
418
- return `source_mix sums to ${total}, expected 1.0`;
419
- return null;
420
- }
421
- // ── Result type + reason codes ──
422
- export const SCOPED_REASONS = [
423
- "ok",
424
- "missing_section",
425
- "malformed_section",
426
- "missing_commitment",
427
- "malformed_commitment",
428
- "missing_discriminator",
429
- "unknown_certificate_type",
430
- "schema_validation_failed",
431
- "ordering_violation",
432
- "commitment_mismatch",
433
- ];
434
- // ── Verification ──
435
- function emptyBundleCommitment() {
436
- return canonicalSha256({ scoped_certificates: [] });
437
- }
438
- function validateEntry(entry) {
439
- if (!isPlainObject(entry)) {
440
- return {
441
- ok: false,
442
- reason: "schema_validation_failed",
443
- details: { detail: "entry is not an object" },
444
- };
445
- }
446
- const ct = entry.certificate_type;
447
- if (typeof ct !== "string" || ct.length === 0) {
448
- return { ok: false, reason: "missing_discriminator", details: {} };
449
- }
450
- const schema = ARTIFACT_SCHEMAS[ct];
451
- if (!schema) {
452
- return {
453
- ok: false,
454
- reason: "unknown_certificate_type",
455
- details: { certificate_type: ct },
456
- };
457
- }
458
- const err = checkObject(entry, schema);
459
- if (err) {
460
- return {
461
- ok: false,
462
- reason: "schema_validation_failed",
463
- details: { certificate_type: ct, detail: err },
464
- };
465
- }
466
- if (ct === "calibration_epoch") {
467
- const smErr = checkSourceMix(entry.source_mix);
468
- if (smErr) {
469
- return {
470
- ok: false,
471
- reason: "schema_validation_failed",
472
- details: { certificate_type: ct, detail: smErr },
473
- };
474
- }
475
- }
476
- if (ct === "workflow_composition") {
477
- const subErr = checkWorkflowCompositionSubset(entry);
478
- if (subErr) {
479
- return {
480
- ok: false,
481
- reason: "schema_validation_failed",
482
- details: { certificate_type: ct, detail: subErr },
483
- };
484
- }
485
- }
486
- return { ok: true, certificateType: ct };
487
- }
488
- export function verifyScopedCertificates(vpecLike) {
489
- if (!("scoped_certificates" in vpecLike)) {
490
- return { valid: false, reason: "missing_section", entry_count: 0, details: {} };
491
- }
492
- const entries = vpecLike.scoped_certificates;
493
- if (!Array.isArray(entries)) {
494
- // Present-but-not-an-array is a malformed section, NOT informational.
495
- // Returning missing_section here would let a credential with
496
- // `scoped_certificates: "not-an-array"` slip past verification.
497
- return {
498
- valid: false,
499
- reason: "malformed_section",
500
- entry_count: 0,
501
- details: {
502
- detail: "scoped_certificates is not an array",
503
- got_type: typeof entries,
504
- },
505
- };
506
- }
507
- if (!("scoped_certificates_commitment" in vpecLike)) {
508
- return {
509
- valid: false,
510
- reason: "missing_commitment",
511
- entry_count: entries.length,
512
- details: {},
513
- };
514
- }
515
- const declared = vpecLike.scoped_certificates_commitment;
516
- if (!isCommitment(declared)) {
517
- return {
518
- valid: false,
519
- reason: "malformed_commitment",
520
- entry_count: entries.length,
521
- details: { declared },
522
- };
523
- }
524
- // Per-entry structural validation + discriminator check.
525
- const orderKeys = [];
526
- for (let i = 0; i < entries.length; i++) {
527
- const res = validateEntry(entries[i]);
528
- if (!res.ok) {
529
- return {
530
- valid: false,
531
- reason: res.reason,
532
- entry_count: entries.length,
533
- details: { index: i, ...res.details },
534
- };
535
- }
536
- const payloadHash = canonicalSha256(entries[i]);
537
- orderKeys.push([res.certificateType, payloadHash]);
538
- }
539
- // Canonical ordering check.
540
- const sortedKeys = [...orderKeys].sort((a, b) => {
541
- if (a[0] !== b[0])
542
- return a[0] < b[0] ? -1 : 1;
543
- if (a[1] !== b[1])
544
- return a[1] < b[1] ? -1 : 1;
545
- return 0;
546
- });
547
- for (let i = 0; i < orderKeys.length; i++) {
548
- if (orderKeys[i][0] !== sortedKeys[i][0] || orderKeys[i][1] !== sortedKeys[i][1]) {
549
- return {
550
- valid: false,
551
- reason: "ordering_violation",
552
- entry_count: entries.length,
553
- details: {
554
- index: i,
555
- got: orderKeys[i],
556
- expected: sortedKeys[i],
557
- },
558
- };
559
- }
560
- }
561
- // Commitment recomputation.
562
- let recomputed;
563
- if (entries.length === 0) {
564
- recomputed = emptyBundleCommitment();
565
- }
566
- else {
567
- const leaves = entries.map((e) => canonicalJson(e));
568
- recomputed = buildMerkleRoot(leaves);
569
- }
570
- if (recomputed !== declared) {
571
- return {
572
- valid: false,
573
- reason: "commitment_mismatch",
574
- entry_count: entries.length,
575
- details: { declared, recomputed },
576
- };
577
- }
578
- return { valid: true, reason: "ok", entry_count: entries.length, details: {} };
579
- }
580
- // Re-export canonical helpers for consumers that want to compose them.
581
- export { canonicalJson, canonicalJsonString, buildMerkleRoot };
582
- //# sourceMappingURL=scoped.js.map