@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.
- package/dist/lifecycle.d.ts +37 -0
- package/dist/lifecycle.js +50 -0
- package/package.json +1 -1
package/dist/lifecycle.d.ts
CHANGED
|
@@ -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.
|
|
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",
|