@primust/verifier 1.1.0 → 1.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.
@@ -2,6 +2,7 @@
2
2
  import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
3
3
  var ENVELOPE_VERSION = "v0.1";
4
4
  var ALLOWED_KINDS = /* @__PURE__ */ new Set([
5
+ // v1.0 kinds (DECOMPOSITION_PROOF_SPEC v1.0 — locked Apr 26):
5
6
  "governance_check",
6
7
  "outbound_action",
7
8
  "token_authority_snapshot",
@@ -10,8 +11,14 @@ var ALLOWED_KINDS = /* @__PURE__ */ new Set([
10
11
  "scope_claim_lifecycle",
11
12
  "scope_claim_emitted",
12
13
  "turn_context",
13
- "tool_execution"
14
+ "tool_execution",
15
+ // v1.1 additions (Controls Harness Execution Mode, mig 156).
16
+ // See docs/v29/foundation/CONTROLS_HARNESS_EXECUTION_SPEC_v0_1.md §2.4.
17
+ "enforcement_action",
18
+ "redaction",
19
+ "notification"
14
20
  ]);
21
+ var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["host_captured", "customer_supplied"]);
15
22
  var ALLOWED_TRUST_EDGES = /* @__PURE__ */ new Set(["A2T", "A2M", "A2H", "A2A", "A2S", "A2D"]);
16
23
  var PROOF_TIER_HIERARCHY = [
17
24
  "attestation",
@@ -412,6 +419,7 @@ function verifyV29(opts) {
412
419
  export {
413
420
  ENVELOPE_VERSION,
414
421
  ALLOWED_KINDS,
422
+ ALLOWED_PROVENANCE,
415
423
  ALLOWED_TRUST_EDGES,
416
424
  PROOF_TIER_HIERARCHY,
417
425
  v29Pass,
@@ -1220,7 +1220,11 @@ function verify2(document, signatureEnvelope, publicKeyB64Url) {
1220
1220
  } catch {
1221
1221
  return false;
1222
1222
  }
1223
- for (const canonFn of [canonical, canonicalLegacy]) {
1223
+ const LEGACY_CUTOFF = "2026-04-19";
1224
+ const signedAt = signatureEnvelope.signed_at ?? "";
1225
+ const legacyAllowed = signedAt !== "" && signedAt.slice(0, 10) < LEGACY_CUTOFF;
1226
+ const candidates = legacyAllowed ? [canonical, canonicalLegacy] : [canonical];
1227
+ for (const canonFn of candidates) {
1224
1228
  try {
1225
1229
  const canonicalStr = canonFn(document);
1226
1230
  const hashBytes = sha2562(new TextEncoder().encode(canonicalStr));
@@ -2441,14 +2445,22 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2441
2445
  const failedArtifacts = proofArtifacts.filter(
2442
2446
  (artifactEntry) => artifactEntry.verification_status === "failed"
2443
2447
  ).length;
2448
+ const unresolvedArtifacts = proofArtifacts.filter(
2449
+ (artifactEntry) => artifactEntry.verification_status === "unresolved_at_seal"
2450
+ ).length;
2444
2451
  const verifiedArtifacts = proofArtifacts.filter(
2445
2452
  (artifactEntry) => artifactEntry.verification_status === "verified"
2446
2453
  ).length;
2454
+ if (failedArtifacts > 0) {
2455
+ result.errors.push(`proof_artifacts_failed:${failedArtifacts}`);
2456
+ }
2447
2457
  if (pendingArtifacts > 0) {
2448
- result.warnings.push(`proof_artifacts_pending:${pendingArtifacts}`);
2458
+ result.errors.push(`proof_artifacts_pending:${pendingArtifacts}`);
2449
2459
  }
2450
- if (failedArtifacts > 0) {
2451
- result.warnings.push(`proof_artifacts_failed:${failedArtifacts}`);
2460
+ if (unresolvedArtifacts > 0) {
2461
+ result.errors.push(
2462
+ `proof_artifacts_unresolved_at_seal:${unresolvedArtifacts}; proof unresolved at seal; counterparty must consult live API to determine status`
2463
+ );
2452
2464
  }
2453
2465
  if (!artifact.zk_proof) {
2454
2466
  result.warnings.push(`proof_artifacts_present:${verifiedArtifacts}`);
@@ -2477,7 +2489,7 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2477
2489
  }
2478
2490
  if (artifact.envelope_version != null && (artifact.run_header || artifact.records)) {
2479
2491
  try {
2480
- const { verifyV29 } = await import("./v29-envelope-GFVVA2S6.js");
2492
+ const { verifyV29 } = await import("./v29-envelope-JVSI5N3L.js");
2481
2493
  const shapeEnvelope = {
2482
2494
  envelope_version: artifact.envelope_version,
2483
2495
  run_header: artifact.run_header ?? {},
@@ -2503,9 +2515,38 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2503
2515
  result.warnings.push(`v29_conformance_error:${e.message}`);
2504
2516
  }
2505
2517
  }
2518
+ if (options.revocation_check_url && result.vpec_id) {
2519
+ await checkRevocationOnline(
2520
+ result,
2521
+ options.revocation_check_url.replace(/\/+$/, ""),
2522
+ options.revocation_check_timeout_ms ?? 5e3
2523
+ );
2524
+ }
2506
2525
  result.valid = result.errors.length === 0;
2507
2526
  return result;
2508
2527
  }
2528
+ async function checkRevocationOnline(result, baseUrl, timeoutMs) {
2529
+ const url = `${baseUrl}/api/v1/vpecs/${result.vpec_id}/revocation`;
2530
+ const controller = new AbortController();
2531
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2532
+ try {
2533
+ const resp = await fetch(url, { signal: controller.signal });
2534
+ if (!resp.ok) {
2535
+ result.warnings.push(`revocation_check_http_${resp.status}`);
2536
+ return;
2537
+ }
2538
+ const body = await resp.json();
2539
+ if (body.revoked === true) {
2540
+ const reason = body.revocation_reason ?? "unspecified";
2541
+ result.errors.push(`credential_revoked: ${reason}`);
2542
+ }
2543
+ } catch (e) {
2544
+ const msg = e.message ?? String(e);
2545
+ result.warnings.push(`revocation_check_unavailable: ${msg}`);
2546
+ } finally {
2547
+ clearTimeout(timer);
2548
+ }
2549
+ }
2509
2550
  function computeMerkleRoot(hashes) {
2510
2551
  let leaves = hashes.map((h2) => {
2511
2552
  const hex = h2.startsWith("sha256:") ? h2.slice(7) : h2;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createUpstreamRootResolver,
4
4
  verify
5
- } from "./chunk-NOADQWB6.js";
5
+ } from "./chunk-UB2YSMJ6.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { readFileSync } from "fs";
@@ -64,7 +64,7 @@ function formatHumanCertificate(artifact, result) {
64
64
  const ps = artifact.provable_surface;
65
65
  lines.push(` Provable surface: ${ps != null ? `${(ps * 100).toFixed(1)}%` : "\u2014"}`);
66
66
  const records = artifact.records ?? [];
67
- lines.push(` Checks run: ${records.length}`);
67
+ lines.push(` Controls run: ${records.length}`);
68
68
  lines.push(` Open gaps: ${result.gaps.length}`);
69
69
  const ac = artifact.action_coverage;
70
70
  if (ac) {
@@ -88,7 +88,7 @@ function formatHumanCertificate(artifact, result) {
88
88
  const tname = u.tool_name;
89
89
  let label = ` #${idx} ${atype}`;
90
90
  if (tname) label += `: ${tname}`;
91
- label += cuIndices.includes(idx) ? " \u26A0 consequential \u2014 no check ran" : " \u2014 no check ran";
91
+ label += cuIndices.includes(idx) ? " \u26A0 consequential \u2014 no control ran" : " \u2014 no control ran";
92
92
  lines.push(label);
93
93
  }
94
94
  }
@@ -106,14 +106,14 @@ function formatHumanCertificate(artifact, result) {
106
106
  const metadataMode = artifact.metadata_mode;
107
107
  if (records.length > 0 && metadataMode === "rich") {
108
108
  lines.push("");
109
- lines.push("GOVERNANCE CHECKS");
109
+ lines.push("CONTROLS RUN");
110
110
  const shown = records.slice(0, 10);
111
111
  for (const r of shown) {
112
- let checkId = r.manifest_id ?? r.check_id ?? "unknown";
113
- if (checkId.includes("@")) checkId = checkId.split("@")[0];
114
- const checkResult = r.check_result ?? "unknown";
112
+ let controlId = r.manifest_id ?? r.control_id ?? r.check_id ?? "unknown";
113
+ if (controlId.includes("@")) controlId = controlId.split("@")[0];
114
+ const controlResult = r.control_result ?? r.check_result ?? "unknown";
115
115
  const em = r.evidence_metadata;
116
- let line = ` ${checkId}: ${checkResult}`;
116
+ let line = ` ${controlId}: ${controlResult}`;
117
117
  if (em) {
118
118
  const details = [];
119
119
  if ("entity_count" in em) details.push(`${em.entity_count} entities`);
@@ -124,17 +124,17 @@ function formatHumanCertificate(artifact, result) {
124
124
  lines.push(line);
125
125
  }
126
126
  if (records.length > 10) {
127
- lines.push(` ... and ${records.length - 10} more checks`);
127
+ lines.push(` ... and ${records.length - 10} more controls`);
128
128
  }
129
129
  }
130
130
  lines.push("");
131
131
  lines.push("WHAT THIS PROVES");
132
- lines.push(" Governance checks ran on the actions listed in this credential");
133
- lines.push(" at the stated proof levels. This credential was issued at the");
134
- lines.push(" time of the governed run and has not been altered since.");
132
+ lines.push(" Controls ran on the actions listed in this credential at the");
133
+ lines.push(" stated proof levels. This credential was issued at the time");
134
+ lines.push(" of the governed run and has not been altered since.");
135
135
  lines.push("");
136
136
  lines.push("WHAT THIS DOES NOT PROVE");
137
- lines.push(" That the checks run constitute sufficient evidence for any");
137
+ lines.push(" That the controls run constitute sufficient evidence for any");
138
138
  lines.push(" specific regulatory requirement. That determination belongs");
139
139
  lines.push(" to the customer, their compliance function, and their auditors.");
140
140
  lines.push("");
package/dist/index.d.ts CHANGED
@@ -28,6 +28,20 @@ interface VerifyOptions {
28
28
  skip_network?: boolean;
29
29
  /** Path to custom public key PEM (for enterprise self-hosting). */
30
30
  trust_root?: string;
31
+ /**
32
+ * CRL: when set, the verifier consults the public revocation endpoint
33
+ * and treats revoked credentials as invalid. Default undefined preserves
34
+ * the offline-first invariant — verify() makes no network calls unless
35
+ * this is provided. Pass e.g. "https://primust-api.fly.dev" (no trailing
36
+ * slash); the verifier appends "/api/v1/vpecs/{vpec_id}/revocation".
37
+ */
38
+ revocation_check_url?: string;
39
+ /**
40
+ * Timeout for the CRL fetch in milliseconds. On timeout or network
41
+ * error the verifier adds a warning but does NOT fail (revocation
42
+ * check is advisory; absence of a record is the default state).
43
+ */
44
+ revocation_check_timeout_ms?: number;
31
45
  }
32
46
  interface VerificationResult {
33
47
  vpec_id: string;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createUpstreamRootResolver,
4
4
  seedKeyCache,
5
5
  verify
6
- } from "./chunk-NOADQWB6.js";
6
+ } from "./chunk-UB2YSMJ6.js";
7
7
  import {
8
8
  ALLOWED_KINDS,
9
9
  ALLOWED_TRUST_EDGES,
@@ -20,9 +20,15 @@ import {
20
20
  validateRecordsAgainstHarness,
21
21
  validateRuntimeBindingHash,
22
22
  verifyV29
23
- } from "./chunk-LTWQK3HT.js";
23
+ } from "./chunk-BWJUVMT7.js";
24
24
 
25
25
  // src/verify-html-template.ts
26
+ function escapeHtml(value) {
27
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
28
+ }
29
+ function safePackJson(packData) {
30
+ return JSON.stringify(packData).replace(/</g, "\\u003c");
31
+ }
26
32
  function generateVerifyHtml(options) {
27
33
  const { publicKeyB64, packData } = options;
28
34
  return `<!DOCTYPE html>
@@ -61,7 +67,7 @@ function generateVerifyHtml(options) {
61
67
  </head>
62
68
  <body>
63
69
  <h1>Primust Evidence Pack Verification</h1>
64
- <div class="subtitle">Pack: ${packData.pack_id} &middot; Period: ${packData.period_start} to ${packData.period_end}</div>
70
+ <div class="subtitle">Pack: ${escapeHtml(packData.pack_id)} &middot; Period: ${escapeHtml(packData.period_start)} to ${escapeHtml(packData.period_end)}</div>
65
71
 
66
72
  <div id="status" class="status loading">Verifying...</div>
67
73
 
@@ -106,8 +112,8 @@ function generateVerifyHtml(options) {
106
112
  </div>
107
113
 
108
114
  <script>
109
- const PRIMUST_PUBLIC_KEY_B64 = "${publicKeyB64}";
110
- const PACK_DATA = ${JSON.stringify(packData)};
115
+ const PRIMUST_PUBLIC_KEY_B64 = "${escapeHtml(publicKeyB64)}";
116
+ const PACK_DATA = ${safePackJson(packData)};
111
117
 
112
118
  async function importKey(b64) {
113
119
  const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
@@ -139,20 +145,49 @@ function generateVerifyHtml(options) {
139
145
  if (!valid) allValid = false;
140
146
 
141
147
  const row = document.createElement('tr');
142
- row.innerHTML =
143
- '<td><span class="badge ' + (valid ? 'badge-pass' : 'badge-fail') + '">' + (valid ? 'VERIFIED' : 'NOT VERIFIED') + '</span></td>' +
144
- '<td><code>' + vpec.vpec_id + '</code></td>' +
145
- '<td>' + vpec.proof_level_floor + '</td>' +
146
- '<td>' + (vpec.provable_surface * 100).toFixed(0) + '%</td>' +
147
- '<td>' + vpec.gap_count + '</td>' +
148
- '<td>' + vpec.issued_at + '</td>';
148
+
149
+ const statusTd = document.createElement('td');
150
+ const statusBadge = document.createElement('span');
151
+ statusBadge.className = 'badge ' + (valid ? 'badge-pass' : 'badge-fail');
152
+ statusBadge.textContent = valid ? 'VERIFIED' : 'NOT VERIFIED';
153
+ statusTd.appendChild(statusBadge);
154
+ row.appendChild(statusTd);
155
+
156
+ const idTd = document.createElement('td');
157
+ const idCode = document.createElement('code');
158
+ idCode.textContent = vpec.vpec_id;
159
+ idTd.appendChild(idCode);
160
+ row.appendChild(idTd);
161
+
162
+ const proofTd = document.createElement('td');
163
+ proofTd.textContent = vpec.proof_level_floor;
164
+ row.appendChild(proofTd);
165
+
166
+ const surfaceTd = document.createElement('td');
167
+ surfaceTd.textContent = (vpec.provable_surface * 100).toFixed(0) + '%';
168
+ row.appendChild(surfaceTd);
169
+
170
+ const gapsTd = document.createElement('td');
171
+ gapsTd.textContent = String(vpec.gap_count);
172
+ row.appendChild(gapsTd);
173
+
174
+ const issuedTd = document.createElement('td');
175
+ issuedTd.textContent = vpec.issued_at;
176
+ row.appendChild(issuedTd);
177
+
149
178
  tbody.appendChild(row);
150
179
  }
151
180
 
152
181
  // Render upstream chain if present
153
182
  if (PACK_DATA.upstream_vpecs && PACK_DATA.upstream_vpecs.length > 0) {
154
183
  const container = document.getElementById('chain-container');
155
- container.innerHTML = '<h2 style="font-size:1.1rem;margin-bottom:1rem;">Cross-Organization Chain</h2>';
184
+
185
+ // Chain header (static, no PACK_DATA \u2014 safe to construct directly).
186
+ const heading = document.createElement('h2');
187
+ heading.style.fontSize = '1.1rem';
188
+ heading.style.marginBottom = '1rem';
189
+ heading.textContent = 'Cross-Organization Chain';
190
+ container.appendChild(heading);
156
191
 
157
192
  for (const uv of PACK_DATA.upstream_vpecs) {
158
193
  let uvValid = false;
@@ -162,9 +197,25 @@ function generateVerifyHtml(options) {
162
197
 
163
198
  const node = document.createElement('div');
164
199
  node.className = 'chain-node';
165
- node.innerHTML =
166
- '<span class="badge ' + (uvValid ? 'badge-pass' : 'badge-fail') + '">' + (uvValid ? 'VERIFIED' : 'NOT VERIFIED') + '</span> ' +
167
- '<strong>' + uv.org_id + '</strong> &mdash; <code>' + uv.vpec_id + '</code> (' + uv.proof_level_floor + ')';
200
+
201
+ const badge = document.createElement('span');
202
+ badge.className = 'badge ' + (uvValid ? 'badge-pass' : 'badge-fail');
203
+ badge.textContent = uvValid ? 'VERIFIED' : 'NOT VERIFIED';
204
+ node.appendChild(badge);
205
+ node.appendChild(document.createTextNode(' '));
206
+
207
+ const orgStrong = document.createElement('strong');
208
+ orgStrong.textContent = uv.org_id;
209
+ node.appendChild(orgStrong);
210
+
211
+ node.appendChild(document.createTextNode(' \\u2014 '));
212
+
213
+ const idCode = document.createElement('code');
214
+ idCode.textContent = uv.vpec_id;
215
+ node.appendChild(idCode);
216
+
217
+ node.appendChild(document.createTextNode(' (' + uv.proof_level_floor + ')'));
218
+
168
219
  container.appendChild(node);
169
220
 
170
221
  const arrow = document.createElement('div');
@@ -175,7 +226,17 @@ function generateVerifyHtml(options) {
175
226
 
176
227
  const currentNode = document.createElement('div');
177
228
  currentNode.className = 'chain-node current';
178
- currentNode.innerHTML = '<strong>This Pack</strong> &mdash; <code>' + PACK_DATA.pack_id + '</code>';
229
+
230
+ const labelStrong = document.createElement('strong');
231
+ labelStrong.textContent = 'This Pack';
232
+ currentNode.appendChild(labelStrong);
233
+
234
+ currentNode.appendChild(document.createTextNode(' \\u2014 '));
235
+
236
+ const packCode = document.createElement('code');
237
+ packCode.textContent = PACK_DATA.pack_id;
238
+ currentNode.appendChild(packCode);
239
+
179
240
  container.appendChild(currentNode);
180
241
  }
181
242
 
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ALLOWED_KINDS,
3
+ ALLOWED_PROVENANCE,
3
4
  ALLOWED_TRUST_EDGES,
4
5
  ENVELOPE_VERSION,
5
6
  PROOF_TIER_HIERARCHY,
@@ -18,9 +19,10 @@ import {
18
19
  validateRuntimeBindingHash,
19
20
  validateRuntimeBindingSignature,
20
21
  verifyV29
21
- } from "./chunk-LTWQK3HT.js";
22
+ } from "./chunk-BWJUVMT7.js";
22
23
  export {
23
24
  ALLOWED_KINDS,
25
+ ALLOWED_PROVENANCE,
24
26
  ALLOWED_TRUST_EDGES,
25
27
  ENVELOPE_VERSION,
26
28
  PROOF_TIER_HIERARCHY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primust/verifier",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Offline and CLI verifier for Primust VPECs. Free forever. No account required.",
5
5
  "homepage": "https://primust.com",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "tsx": "^4.19.0",
35
35
  "typescript": "^5.6.0",
36
36
  "vitest": "^3.0.0",
37
- "@primust/artifact-core": "^1.0.0",
37
+ "@primust/artifact-core": "workspace:*",
38
38
  "@noble/hashes": "^1.8.0",
39
39
  "@zkpassport/poseidon2": "^0.6.2"
40
40
  },
@@ -42,6 +42,9 @@
42
42
  "dist"
43
43
  ],
44
44
  "license": "Apache-2.0",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
45
48
  "exports": {
46
49
  ".": {
47
50
  "import": "./dist/index.js",