@typedstandards/verify-core 0.4.0 → 0.5.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.
@@ -81,3 +81,40 @@ export declare function resolveLifecycleFromLegacyColumns(columns: {
81
81
  reinstatedAt?: string | null;
82
82
  reinstatedReason?: string | null;
83
83
  }): LifecycleResolution;
84
+ /** A signed lifecycle attestation, carried in the bundle so the chain resolves with
85
+ * NO reference-implementation dependency (civic-ai-tools-website#119 P3). The server
86
+ * fetches these from the DB + blob; an offline verifier reads them from the bundle. */
87
+ export interface CarriedLifecycleNode {
88
+ /** The signed attestation node JSON (the envelope) — recomputed + signature-checked. */
89
+ node: Record<string, unknown>;
90
+ /** The stored nodeId (envelope hash) the recomputed hash must match. */
91
+ nodeId: string;
92
+ /** The signature envelope over the node (`{signature, publicKey, algorithm}`). */
93
+ signature?: {
94
+ signature?: string;
95
+ publicKey?: string;
96
+ algorithm?: string;
97
+ } | null;
98
+ /** Presence flags (surfaced; per-attestation TSA/Rekor depth is a follow-up). */
99
+ hasTimestamp?: boolean;
100
+ hasRekor?: boolean;
101
+ }
102
+ /**
103
+ * Resolve lifecycle (#10) INDEPENDENTLY from carried signed attestation nodes —
104
+ * the browser/offline path that reaches `source: 'attestation-chain'` with no
105
+ * reference-implementation dependency (civic-ai-tools-website#119 P3).
106
+ *
107
+ * Unlike the server (whose DB query guarantees the rows target this node and whose
108
+ * rows are platform-signed), an independent verifier must NOT trust the carrier, so
109
+ * each node is gated before it can affect status:
110
+ * - REACHABILITY (§9.2 #13): `node.targetNodeId` MUST equal the content node's id;
111
+ * an attestation about a different node is not part of this lifecycle.
112
+ * - INTEGRITY: the recomputed envelope hash MUST equal the stored `nodeId`.
113
+ * - SIGNATURE: the Ed25519(ph) signature MUST verify over that hash.
114
+ * A node failing any of these is EXCLUDED (a forged/tampered/misdirected transition
115
+ * cannot move the status). Surviving nodes go to `resolveLifecycleFromChain`, which
116
+ * applies the §8.10.3 retention asymmetry (a valid but non-signer-matched attestation
117
+ * is surfaced in the chain yet does NOT move the publisher's status). That shared
118
+ * resolver is unchanged, so the server route's output stays byte-identical.
119
+ */
120
+ export declare function verifyLifecycleChain(carried: CarriedLifecycleNode[], contentNodeId: string, targetSignerIdentifier: string): LifecycleResolution;
package/dist/lifecycle.js CHANGED
@@ -111,3 +111,53 @@ export function resolveLifecycleFromLegacyColumns(columns) {
111
111
  : {}),
112
112
  };
113
113
  }
114
+ function pickString(v) {
115
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
116
+ }
117
+ /**
118
+ * Resolve lifecycle (#10) INDEPENDENTLY from carried signed attestation nodes —
119
+ * the browser/offline path that reaches `source: 'attestation-chain'` with no
120
+ * reference-implementation dependency (civic-ai-tools-website#119 P3).
121
+ *
122
+ * Unlike the server (whose DB query guarantees the rows target this node and whose
123
+ * rows are platform-signed), an independent verifier must NOT trust the carrier, so
124
+ * each node is gated before it can affect status:
125
+ * - REACHABILITY (§9.2 #13): `node.targetNodeId` MUST equal the content node's id;
126
+ * an attestation about a different node is not part of this lifecycle.
127
+ * - INTEGRITY: the recomputed envelope hash MUST equal the stored `nodeId`.
128
+ * - SIGNATURE: the Ed25519(ph) signature MUST verify over that hash.
129
+ * A node failing any of these is EXCLUDED (a forged/tampered/misdirected transition
130
+ * cannot move the status). Surviving nodes go to `resolveLifecycleFromChain`, which
131
+ * applies the §8.10.3 retention asymmetry (a valid but non-signer-matched attestation
132
+ * is surfaced in the chain yet does NOT move the publisher's status). That shared
133
+ * resolver is unchanged, so the server route's output stays byte-identical.
134
+ */
135
+ export function verifyLifecycleChain(carried, contentNodeId, targetSignerIdentifier) {
136
+ const views = [];
137
+ for (const entry of carried) {
138
+ // Reachability: the attestation must reference THIS content node.
139
+ if (pickString(entry.node['targetNodeId']) !== contentNodeId)
140
+ continue;
141
+ const verdict = verifyAttestationNode(entry.node, entry.nodeId, entry.signature ?? null);
142
+ // Independent crypto gate: integrity + a valid signature, or it cannot count.
143
+ if (!verdict.nodeIdMatches || verdict.signatureValid !== true)
144
+ continue;
145
+ const signer = entry.node['signer'];
146
+ const metadata = entry.node['metadata'];
147
+ views.push({
148
+ nodeId: entry.nodeId,
149
+ type: pickString(entry.node['type']) ?? '',
150
+ signer,
151
+ createdAt: pickString(metadata?.['createdAt']) ?? '',
152
+ reason: pickString(entry.node['reason']),
153
+ effectiveAt: pickString(entry.node['effectiveAt']),
154
+ priorWithdrawalNodeId: pickString(entry.node['priorWithdrawalNodeId']),
155
+ signatureValid: verdict.signatureValid,
156
+ nodeIdMatches: verdict.nodeIdMatches,
157
+ hasTimestamp: !!entry.hasTimestamp,
158
+ hasRekor: !!entry.hasRekor,
159
+ signerMatchesTarget: !!signer && signer.identifier === targetSignerIdentifier,
160
+ });
161
+ }
162
+ return resolveLifecycleFromChain(views);
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/verify-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",