@typedstandards/verify-core 0.6.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ references are to `npstorey/civic-ai-tools-website` (#119 is the offline-crypto
6
6
  hardening arc; #116 is the standalone-verifier arc this package was extracted
7
7
  in).
8
8
 
9
+ ## 0.7.0 — 2026-06-16
10
+
11
+ - #1 envelope integrity is now TRI-STATE. `VerifyResult` gains an
12
+ `envelopeIntegrity: { status: 'verified' | 'altered' | 'unavailable'; reason? }`
13
+ field. A null `package` (content not fetched) previously surfaced only as
14
+ `hashMatch: false`, which a consumer could not distinguish from real tampering;
15
+ it now resolves to `status: 'unavailable'`, distinct from the `altered`
16
+ (bytes-present, hash-mismatch) case. `VerifyInput` gains an optional
17
+ `contentUnavailableReason: 'private' | 'unfetchable'` so the caller can say WHY
18
+ the content is absent — `private` (the commitment redacted the location for a
19
+ sealed/committed record, integrity N/A) vs. `unfetchable` (a present location
20
+ whose bytes could not be retrieved). Fixes the verifier false-alarming
21
+ "Contents changed since signing" on a perfectly valid content-private package
22
+ (npstorey/typedstandards#21). Additive and back-compatible: `hashMatch` is
23
+ unchanged (still `false` in both the `altered` and `unavailable` cases), so
24
+ existing consumers reading only `hashMatch` are unaffected.
25
+
9
26
  ## 0.6.1 — 2026-06-10
10
27
 
11
28
  - Documentation only; no code changes. Corrects the README's stale "Check
package/dist/verify.d.ts CHANGED
@@ -11,6 +11,34 @@ export interface VerifySignatureEnvelope {
11
11
  algorithm?: string;
12
12
  kid?: string;
13
13
  }
14
+ /**
15
+ * Why a package's content was unavailable for envelope recomputation (#1).
16
+ * - `private` — the commitment OMITTED the content location: a creator-only /
17
+ * unlisted record whose bytes are private by design. Integrity is N/A, never a
18
+ * failure; the public commitment (signature, key, timestamp, transparency log)
19
+ * is still fully verifiable.
20
+ * - `unfetchable` — a location WAS present but its bytes could not be retrieved
21
+ * (404 / network). An availability problem, surfaced as unconfirmed rather than
22
+ * altered.
23
+ */
24
+ export type ContentUnavailableReason = 'private' | 'unfetchable';
25
+ /**
26
+ * Tri-state envelope integrity (#1). Replaces the boolean `hashMatch` lens, which
27
+ * collapsed "content unavailable" onto `false` — so a private (content-redacted)
28
+ * record read as tampered.
29
+ * - `verified` — bytes present; the recomputed hash matches the signed hash.
30
+ * - `altered` — bytes present; the hash MISMATCHES. Real tampering.
31
+ * - `unavailable` — no bytes to recompute from; `reason` says whether the content
32
+ * was private by design or merely unfetchable.
33
+ * `hashMatch` is retained UNCHANGED for back-compat (it is `false` in BOTH the
34
+ * `altered` and `unavailable` cases); a consumer that must not conflate tampering
35
+ * with unavailability reads THIS field.
36
+ */
37
+ export interface EnvelopeIntegrityResult {
38
+ status: 'verified' | 'altered' | 'unavailable';
39
+ /** Present only when `status === 'unavailable'`. */
40
+ reason?: ContentUnavailableReason;
41
+ }
14
42
  /** Lifecycle STATE as carried by the WS1 sidecar (`CommitmentLifecycle`). */
15
43
  export interface CommitmentLifecycleState {
16
44
  status: 'active' | 'withdrawn';
@@ -24,9 +52,11 @@ export interface CommitmentLifecycleState {
24
52
  * the package JSON the sidecar's `packageUrl` resolves to.
25
53
  */
26
54
  export interface VerifyInput {
27
- /** The canonical package JSON (the signed envelope). Null mirrors the server's
28
- * "blob unavailable" path: integrity fails and the package-derived checks
29
- * report null. */
55
+ /** The canonical package JSON (the signed envelope). Null when the content is
56
+ * unavailable — its bytes were not fetched, so envelope integrity (#1) cannot be
57
+ * recomputed and the package-derived checks report null. `contentUnavailableReason`
58
+ * says WHY it is null (private-by-design vs. unfetchable); the two read very
59
+ * differently and must not be conflated with tampering. */
30
60
  package: Record<string, unknown> | null;
31
61
  /** The claimed envelope hash (`sidecar.packageHash` / `basePackageHash`). */
32
62
  packageHash: string;
@@ -55,6 +85,13 @@ export interface VerifyInput {
55
85
  /** Legacy external SHA-256 for the pre-v0.1 content-hash relabel. Defaults to
56
86
  * `packageHash` (they coincide for pre-v0.1 packages). */
57
87
  legacyExternalHash?: string;
88
+ /** When `package` is null, why: `private` (the commitment redacted the content
89
+ * location — a sealed/committed record) or `unfetchable` (a location was present
90
+ * but its bytes could not be retrieved). Drives the `unavailable` reason on
91
+ * `envelopeIntegrity`. Ignored when `package` is non-null. Absent ⇒ the
92
+ * conservative `unfetchable` (a null package with no stated reason is treated as
93
+ * an unconfirmed availability problem, never silently as a by-design redaction). */
94
+ contentUnavailableReason?: ContentUnavailableReason;
58
95
  }
59
96
  export interface VerifyDeps {
60
97
  /** Parsed trust registry (server: loaded; browser: fetched from the sidecar's
@@ -72,6 +109,10 @@ export interface VerifyDeps {
72
109
  * can surface them directly. */
73
110
  export interface VerifyResult {
74
111
  hashMatch: boolean;
112
+ /** Tri-state envelope integrity (#1) — the corrected lens consumers SHOULD render
113
+ * instead of branching on `hashMatch`, which cannot express "content unavailable,
114
+ * so integrity is N/A" and reads it as tampering. See `EnvelopeIntegrityResult`. */
115
+ envelopeIntegrity: EnvelopeIntegrityResult;
75
116
  recomputedHash: string | null;
76
117
  /** = recomputedHash (check #13 nodeId). */
77
118
  nodeId: string | null;
package/dist/verify.js CHANGED
@@ -38,12 +38,24 @@ import { resolveLifecycleFromLegacyColumns, } from "./lifecycle.js";
38
38
  */
39
39
  export async function verifyEvidence(input, deps) {
40
40
  const { package: pkg, packageHash } = input;
41
- // Step 1 — recompute the envelope hash (checks #1 + #13).
41
+ // Step 1 — recompute the envelope hash (checks #1 + #13). Tri-state: a present
42
+ // package recomputes to `verified` / `altered`; a null package is `unavailable`
43
+ // (NOT `altered`) — its content was never fetched, so there is nothing to compare,
44
+ // and `contentUnavailableReason` distinguishes a by-design redaction (private)
45
+ // from a fetch failure (unfetchable). `hashMatch` stays a boolean for back-compat.
42
46
  let hashMatch = false;
43
47
  let recomputedHash = null;
48
+ let envelopeIntegrity;
44
49
  if (pkg) {
45
50
  recomputedHash = recomputePackageHash(pkg);
46
51
  hashMatch = recomputedHash === packageHash;
52
+ envelopeIntegrity = { status: hashMatch ? 'verified' : 'altered' };
53
+ }
54
+ else {
55
+ envelopeIntegrity = {
56
+ status: 'unavailable',
57
+ reason: input.contentUnavailableReason ?? 'unfetchable',
58
+ };
47
59
  }
48
60
  // Step 2 — signature (check #2), dispatching on the stored algorithm (#111).
49
61
  let signatureValid = null;
@@ -144,6 +156,7 @@ export async function verifyEvidence(input, deps) {
144
156
  });
145
157
  return {
146
158
  hashMatch,
159
+ envelopeIntegrity,
147
160
  recomputedHash,
148
161
  nodeId: recomputedHash,
149
162
  signatureValid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/verify-core",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Portable, browser-safe verification core for Typed Standards evidence packages — the §9.2 check suite, single-sourced so it verifies identically on a server and in the browser.",
5
5
  "license": "MIT",
6
6
  "author": "Nathan Storey",