@primust/verifier 1.1.0 → 1.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.
@@ -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));
@@ -1957,6 +1961,9 @@ var F3 = getPoseidon2BN254().primeField;
1957
1961
  // ../artifact-core/dist/commitment.js
1958
1962
  var BN254_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1959
1963
 
1964
+ // ../artifact-core/dist/hierarchical_leaf.js
1965
+ var _utf8Encoder = new TextEncoder();
1966
+
1960
1967
  // ../artifact-core/dist/validate-artifact.js
1961
1968
  var PROOF_LEVELS = [
1962
1969
  "mathematical",
@@ -2441,14 +2448,22 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2441
2448
  const failedArtifacts = proofArtifacts.filter(
2442
2449
  (artifactEntry) => artifactEntry.verification_status === "failed"
2443
2450
  ).length;
2451
+ const unresolvedArtifacts = proofArtifacts.filter(
2452
+ (artifactEntry) => artifactEntry.verification_status === "unresolved_at_seal"
2453
+ ).length;
2444
2454
  const verifiedArtifacts = proofArtifacts.filter(
2445
2455
  (artifactEntry) => artifactEntry.verification_status === "verified"
2446
2456
  ).length;
2457
+ if (failedArtifacts > 0) {
2458
+ result.errors.push(`proof_artifacts_failed:${failedArtifacts}`);
2459
+ }
2447
2460
  if (pendingArtifacts > 0) {
2448
- result.warnings.push(`proof_artifacts_pending:${pendingArtifacts}`);
2461
+ result.errors.push(`proof_artifacts_pending:${pendingArtifacts}`);
2449
2462
  }
2450
- if (failedArtifacts > 0) {
2451
- result.warnings.push(`proof_artifacts_failed:${failedArtifacts}`);
2463
+ if (unresolvedArtifacts > 0) {
2464
+ result.errors.push(
2465
+ `proof_artifacts_unresolved_at_seal:${unresolvedArtifacts}; proof unresolved at seal; counterparty must consult live API to determine status`
2466
+ );
2452
2467
  }
2453
2468
  if (!artifact.zk_proof) {
2454
2469
  result.warnings.push(`proof_artifacts_present:${verifiedArtifacts}`);
@@ -2477,7 +2492,7 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2477
2492
  }
2478
2493
  if (artifact.envelope_version != null && (artifact.run_header || artifact.records)) {
2479
2494
  try {
2480
- const { verifyV29 } = await import("./v29-envelope-GFVVA2S6.js");
2495
+ const { verifyV29 } = await import("./v29-envelope-JVSI5N3L.js");
2481
2496
  const shapeEnvelope = {
2482
2497
  envelope_version: artifact.envelope_version,
2483
2498
  run_header: artifact.run_header ?? {},
@@ -2503,9 +2518,38 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2503
2518
  result.warnings.push(`v29_conformance_error:${e.message}`);
2504
2519
  }
2505
2520
  }
2521
+ if (options.revocation_check_url && result.vpec_id) {
2522
+ await checkRevocationOnline(
2523
+ result,
2524
+ options.revocation_check_url.replace(/\/+$/, ""),
2525
+ options.revocation_check_timeout_ms ?? 5e3
2526
+ );
2527
+ }
2506
2528
  result.valid = result.errors.length === 0;
2507
2529
  return result;
2508
2530
  }
2531
+ async function checkRevocationOnline(result, baseUrl, timeoutMs) {
2532
+ const url = `${baseUrl}/api/v1/vpecs/${result.vpec_id}/revocation`;
2533
+ const controller = new AbortController();
2534
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2535
+ try {
2536
+ const resp = await fetch(url, { signal: controller.signal });
2537
+ if (!resp.ok) {
2538
+ result.warnings.push(`revocation_check_http_${resp.status}`);
2539
+ return;
2540
+ }
2541
+ const body = await resp.json();
2542
+ if (body.revoked === true) {
2543
+ const reason = body.revocation_reason ?? "unspecified";
2544
+ result.errors.push(`credential_revoked: ${reason}`);
2545
+ }
2546
+ } catch (e) {
2547
+ const msg = e.message ?? String(e);
2548
+ result.warnings.push(`revocation_check_unavailable: ${msg}`);
2549
+ } finally {
2550
+ clearTimeout(timer);
2551
+ }
2552
+ }
2509
2553
  function computeMerkleRoot(hashes) {
2510
2554
  let leaves = hashes.map((h2) => {
2511
2555
  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-XDQXBEIS.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-XDQXBEIS.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@primust/verifier",
3
- "version": "1.1.0",
4
- "description": "Offline and CLI verifier for Primust VPECs. Free forever. No account required.",
3
+ "version": "1.3.0",
4
+ "description": "Offline and CLI verifier for Primust Governed-Execution Credentials. Free forever. No account required.",
5
5
  "homepage": "https://primust.com",
6
6
  "repository": {
7
7
  "type": "git",
@@ -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",