@fides-anima/fpp-protocol-core 1.0.2
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/LICENSE +95 -0
- package/README.md +41 -0
- package/SKILL.md +348 -0
- package/constitution.json +102 -0
- package/constitution.yaml +106 -0
- package/dist/adoption-disclosure.d.ts +35 -0
- package/dist/adoption-disclosure.d.ts.map +1 -0
- package/dist/adoption-disclosure.js +67 -0
- package/dist/adoption-disclosure.js.map +1 -0
- package/dist/adoption.d.ts +56 -0
- package/dist/adoption.d.ts.map +1 -0
- package/dist/adoption.js +97 -0
- package/dist/adoption.js.map +1 -0
- package/dist/canonical-json.d.ts +23 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +60 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/capsules.d.ts +63 -0
- package/dist/capsules.d.ts.map +1 -0
- package/dist/capsules.js +94 -0
- package/dist/capsules.js.map +1 -0
- package/dist/claims.d.ts +62 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +99 -0
- package/dist/claims.js.map +1 -0
- package/dist/digest.d.ts +38 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +52 -0
- package/dist/digest.js.map +1 -0
- package/dist/disposition.d.ts +34 -0
- package/dist/disposition.d.ts.map +1 -0
- package/dist/disposition.js +51 -0
- package/dist/disposition.js.map +1 -0
- package/dist/emergency-override.d.ts +73 -0
- package/dist/emergency-override.d.ts.map +1 -0
- package/dist/emergency-override.js +102 -0
- package/dist/emergency-override.js.map +1 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +30 -0
- package/dist/evidence.js.map +1 -0
- package/dist/freshness.d.ts +41 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +66 -0
- package/dist/freshness.js.map +1 -0
- package/dist/governance.d.ts +87 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +94 -0
- package/dist/governance.js.map +1 -0
- package/dist/identity.d.ts +43 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +93 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/mandates.d.ts +80 -0
- package/dist/mandates.d.ts.map +1 -0
- package/dist/mandates.js +127 -0
- package/dist/mandates.js.map +1 -0
- package/dist/merkle.d.ts +31 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +95 -0
- package/dist/merkle.js.map +1 -0
- package/dist/quorum.d.ts +133 -0
- package/dist/quorum.d.ts.map +1 -0
- package/dist/quorum.js +128 -0
- package/dist/quorum.js.map +1 -0
- package/dist/receipts.d.ts +77 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +104 -0
- package/dist/receipts.js.map +1 -0
- package/dist/steward-authorization.d.ts +200 -0
- package/dist/steward-authorization.d.ts.map +1 -0
- package/dist/steward-authorization.js +483 -0
- package/dist/steward-authorization.js.map +1 -0
- package/dist/workspace-profile.d.ts +43 -0
- package/dist/workspace-profile.d.ts.map +1 -0
- package/dist/workspace-profile.js +91 -0
- package/dist/workspace-profile.js.map +1 -0
- package/package.json +56 -0
- package/src/adoption-disclosure.ts +120 -0
- package/src/adoption.ts +152 -0
- package/src/canonical-json.ts +70 -0
- package/src/capsules.ts +161 -0
- package/src/claims.ts +147 -0
- package/src/digest.ts +63 -0
- package/src/disposition.ts +74 -0
- package/src/emergency-override.ts +168 -0
- package/src/evidence.ts +48 -0
- package/src/freshness.ts +103 -0
- package/src/governance.ts +152 -0
- package/src/identity.ts +119 -0
- package/src/index.ts +270 -0
- package/src/mandates.ts +201 -0
- package/src/merkle.ts +138 -0
- package/src/quorum.ts +193 -0
- package/src/receipts.ts +150 -0
- package/src/steward-authorization.ts +657 -0
- package/src/workspace-profile.ts +134 -0
package/src/merkle.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned Merkle tree primitives.
|
|
3
|
+
*
|
|
4
|
+
* V1 preserves the historical SHA-256(utf8(left+right)) pairing used by
|
|
5
|
+
* audit proofs. V2 domain-separates leaf and internal-node hashes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
9
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
|
|
10
|
+
import { DIGEST_DOMAINS } from "./digest.js";
|
|
11
|
+
|
|
12
|
+
export type MerkleProofStep = {
|
|
13
|
+
hash: string;
|
|
14
|
+
position: "left" | "right";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type MerkleProof = {
|
|
18
|
+
leaf: string;
|
|
19
|
+
index: number;
|
|
20
|
+
path: MerkleProofStep[];
|
|
21
|
+
root: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function sha256Hex(data: string): string {
|
|
25
|
+
return bytesToHex(sha256(utf8ToBytes(data)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Legacy pair hash: SHA-256(utf8(a + b)) with no domain separator. */
|
|
29
|
+
export function hashPairV1(a: string, b: string): string {
|
|
30
|
+
return sha256Hex(a + b);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** V2 pair hash: domain-separated internal node. */
|
|
34
|
+
export function hashPairV2(a: string, b: string): string {
|
|
35
|
+
return sha256Hex(DIGEST_DOMAINS.node + "\0" + a + "\0" + b);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function computeRoot(
|
|
39
|
+
leaves: string[],
|
|
40
|
+
hashPair: (a: string, b: string) => string,
|
|
41
|
+
): string {
|
|
42
|
+
if (leaves.length === 0) return "0".repeat(64);
|
|
43
|
+
if (leaves.length === 1) return leaves[0]!;
|
|
44
|
+
|
|
45
|
+
const level: string[] = [];
|
|
46
|
+
for (let i = 0; i < leaves.length; i += 2) {
|
|
47
|
+
const left = leaves[i]!;
|
|
48
|
+
const right = leaves[i + 1] ?? left;
|
|
49
|
+
level.push(hashPair(left, right));
|
|
50
|
+
}
|
|
51
|
+
return computeRoot(level, hashPair);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createProof(
|
|
55
|
+
leaves: string[],
|
|
56
|
+
index: number,
|
|
57
|
+
hashPair: (a: string, b: string) => string,
|
|
58
|
+
): MerkleProof | null {
|
|
59
|
+
if (index < 0 || index >= leaves.length || leaves.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
const path: MerkleProofStep[] = [];
|
|
62
|
+
let currentLevel = [...leaves];
|
|
63
|
+
let idx = index;
|
|
64
|
+
|
|
65
|
+
while (currentLevel.length > 1) {
|
|
66
|
+
if (currentLevel.length % 2 === 1) {
|
|
67
|
+
currentLevel.push(currentLevel[currentLevel.length - 1]!);
|
|
68
|
+
}
|
|
69
|
+
const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
|
|
70
|
+
path.push({
|
|
71
|
+
hash: currentLevel[siblingIdx]!,
|
|
72
|
+
position: idx % 2 === 0 ? "right" : "left",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const nextLevel: string[] = [];
|
|
76
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
77
|
+
nextLevel.push(hashPair(currentLevel[i]!, currentLevel[i + 1]!));
|
|
78
|
+
}
|
|
79
|
+
currentLevel = nextLevel;
|
|
80
|
+
idx = Math.floor(idx / 2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
leaf: leaves[index]!,
|
|
85
|
+
index,
|
|
86
|
+
path,
|
|
87
|
+
root: currentLevel[0]!,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verifyProof(
|
|
92
|
+
proof: MerkleProof,
|
|
93
|
+
hashPair: (a: string, b: string) => string,
|
|
94
|
+
): boolean {
|
|
95
|
+
let current = proof.leaf;
|
|
96
|
+
for (const step of proof.path) {
|
|
97
|
+
current =
|
|
98
|
+
step.position === "left"
|
|
99
|
+
? hashPair(step.hash, current)
|
|
100
|
+
: hashPair(current, step.hash);
|
|
101
|
+
}
|
|
102
|
+
return current === proof.root;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function computeMerkleRootV1(leaves: string[]): string {
|
|
106
|
+
return computeRoot(leaves, hashPairV1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createMerkleProofV1(
|
|
110
|
+
leaves: string[],
|
|
111
|
+
index: number,
|
|
112
|
+
): MerkleProof | null {
|
|
113
|
+
return createProof(leaves, index, hashPairV1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function verifyMerkleProofV1(proof: MerkleProof): boolean {
|
|
117
|
+
return verifyProof(proof, hashPairV1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function computeMerkleRootV2(leaves: string[]): string {
|
|
121
|
+
return computeRoot(leaves, hashPairV2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createMerkleProofV2(
|
|
125
|
+
leaves: string[],
|
|
126
|
+
index: number,
|
|
127
|
+
): MerkleProof | null {
|
|
128
|
+
return createProof(leaves, index, hashPairV2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function verifyMerkleProofV2(proof: MerkleProof): boolean {
|
|
132
|
+
return verifyProof(proof, hashPairV2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Compatibility aliases matching historical script/plugin names (v1). */
|
|
136
|
+
export const computeMerkleRoot = computeMerkleRootV1;
|
|
137
|
+
export const createMerkleProof = createMerkleProofV1;
|
|
138
|
+
export const verifyMerkleProof = verifyMerkleProofV1;
|
package/src/quorum.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quorum proposal, ballot, and evidence-package schemas.
|
|
3
|
+
* Quorum finalization produces StandingMandateV1 with evidenceRef
|
|
4
|
+
* pointing at the evidence digest — not constitutional ratification.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
8
|
+
import { Value } from "@sinclair/typebox/value";
|
|
9
|
+
import { digest, DIGEST_DOMAINS } from "./digest.js";
|
|
10
|
+
|
|
11
|
+
export const QUORUM_CLASSES = ["peer-quorum", "steward-quorum"] as const;
|
|
12
|
+
export type QuorumClass = (typeof QUORUM_CLASSES)[number];
|
|
13
|
+
|
|
14
|
+
export const QUORUM_VOTES = ["aye", "nay", "abstain"] as const;
|
|
15
|
+
export type QuorumVote = (typeof QUORUM_VOTES)[number];
|
|
16
|
+
|
|
17
|
+
const ScopeSchema = Type.Object(
|
|
18
|
+
{
|
|
19
|
+
classifications: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
20
|
+
capabilities: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
21
|
+
},
|
|
22
|
+
{ additionalProperties: false },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const BudgetsSchema = Type.Object(
|
|
26
|
+
{
|
|
27
|
+
maxActions: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
28
|
+
remainingActions: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
29
|
+
},
|
|
30
|
+
{ additionalProperties: false },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const QuorumProposalV1Schema = Type.Object(
|
|
34
|
+
{
|
|
35
|
+
schemaVersion: Type.Literal(1),
|
|
36
|
+
proposalId: Type.String({ minLength: 1 }),
|
|
37
|
+
quorumClass: Type.Union([
|
|
38
|
+
Type.Literal("peer-quorum"),
|
|
39
|
+
Type.Literal("steward-quorum"),
|
|
40
|
+
]),
|
|
41
|
+
proposerId: Type.String({ minLength: 1 }),
|
|
42
|
+
/** Digest of the intended unsigned mandate payload. */
|
|
43
|
+
mandateDigest: Type.String({ minLength: 1 }),
|
|
44
|
+
scope: ScopeSchema,
|
|
45
|
+
budgets: BudgetsSchema,
|
|
46
|
+
mandateValidFrom: Type.String({ minLength: 1 }),
|
|
47
|
+
mandateValidTo: Type.String({ minLength: 1 }),
|
|
48
|
+
proposedAt: Type.String({ minLength: 1 }),
|
|
49
|
+
expiresAt: Type.String({ minLength: 1 }),
|
|
50
|
+
publicKey: Type.String({ minLength: 1 }),
|
|
51
|
+
signature: Type.String({ minLength: 1 }),
|
|
52
|
+
},
|
|
53
|
+
{ additionalProperties: false },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export type QuorumProposalV1 = Static<typeof QuorumProposalV1Schema>;
|
|
57
|
+
|
|
58
|
+
export const QuorumBallotV1Schema = Type.Object(
|
|
59
|
+
{
|
|
60
|
+
schemaVersion: Type.Literal(1),
|
|
61
|
+
ballotId: Type.String({ minLength: 1 }),
|
|
62
|
+
proposalId: Type.String({ minLength: 1 }),
|
|
63
|
+
voterId: Type.String({ minLength: 1 }),
|
|
64
|
+
vote: Type.Union([
|
|
65
|
+
Type.Literal("aye"),
|
|
66
|
+
Type.Literal("nay"),
|
|
67
|
+
Type.Literal("abstain"),
|
|
68
|
+
]),
|
|
69
|
+
/** Must match the proposal's mandateDigest. */
|
|
70
|
+
mandateDigest: Type.String({ minLength: 1 }),
|
|
71
|
+
castAt: Type.String({ minLength: 1 }),
|
|
72
|
+
publicKey: Type.String({ minLength: 1 }),
|
|
73
|
+
signature: Type.String({ minLength: 1 }),
|
|
74
|
+
},
|
|
75
|
+
{ additionalProperties: false },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export type QuorumBallotV1 = Static<typeof QuorumBallotV1Schema>;
|
|
79
|
+
|
|
80
|
+
export const QuorumEvidencePackageV1Schema = Type.Object(
|
|
81
|
+
{
|
|
82
|
+
schemaVersion: Type.Literal(1),
|
|
83
|
+
proposal: QuorumProposalV1Schema,
|
|
84
|
+
ballots: Type.Array(QuorumBallotV1Schema, { minItems: 1 }),
|
|
85
|
+
finalizedAt: Type.String({ minLength: 1 }),
|
|
86
|
+
},
|
|
87
|
+
{ additionalProperties: false },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export type QuorumEvidencePackageV1 = Static<
|
|
91
|
+
typeof QuorumEvidencePackageV1Schema
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
export type QuorumParseResult<T> =
|
|
95
|
+
| { ok: true; value: T }
|
|
96
|
+
| { ok: false; error: string };
|
|
97
|
+
|
|
98
|
+
export type QuorumProposalParseResult =
|
|
99
|
+
| { ok: true; proposal: QuorumProposalV1 }
|
|
100
|
+
| { ok: false; error: string };
|
|
101
|
+
|
|
102
|
+
export type QuorumBallotParseResult =
|
|
103
|
+
| { ok: true; ballot: QuorumBallotV1 }
|
|
104
|
+
| { ok: false; error: string };
|
|
105
|
+
|
|
106
|
+
export type QuorumEvidenceParseResult =
|
|
107
|
+
| { ok: true; package: QuorumEvidencePackageV1 }
|
|
108
|
+
| { ok: false; error: string };
|
|
109
|
+
|
|
110
|
+
export type BallotMatchResult =
|
|
111
|
+
| { ok: true }
|
|
112
|
+
| { ok: false; error: string };
|
|
113
|
+
|
|
114
|
+
function firstSchemaError(schema: unknown, input: unknown): string {
|
|
115
|
+
const errors = [...Value.Errors(schema as never, input)];
|
|
116
|
+
return errors[0] !== undefined
|
|
117
|
+
? `${errors[0].path}: ${errors[0].message}`
|
|
118
|
+
: "invalid input";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseQuorumProposal(input: unknown): QuorumProposalParseResult {
|
|
122
|
+
if (!Value.Check(QuorumProposalV1Schema, input)) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: firstSchemaError(QuorumProposalV1Schema, input),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, proposal: input };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function parseQuorumBallot(input: unknown): QuorumBallotParseResult {
|
|
132
|
+
if (!Value.Check(QuorumBallotV1Schema, input)) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: firstSchemaError(QuorumBallotV1Schema, input),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, ballot: input };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseQuorumEvidencePackage(
|
|
142
|
+
input: unknown,
|
|
143
|
+
): QuorumEvidenceParseResult {
|
|
144
|
+
if (!Value.Check(QuorumEvidencePackageV1Schema, input)) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: firstSchemaError(QuorumEvidencePackageV1Schema, input),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, package: input };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Ballot must reference the same proposal and mandateDigest.
|
|
155
|
+
* Signature cryptographic verification is left to the plugin layer.
|
|
156
|
+
*/
|
|
157
|
+
export function validateBallotAgainstProposal(
|
|
158
|
+
ballot: QuorumBallotV1,
|
|
159
|
+
proposal: QuorumProposalV1,
|
|
160
|
+
): BallotMatchResult {
|
|
161
|
+
if (ballot.proposalId !== proposal.proposalId) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: `proposalId mismatch: ballot=${ballot.proposalId} proposal=${proposal.proposalId}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (ballot.mandateDigest !== proposal.mandateDigest) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: "mandateDigest mismatch between ballot and proposal",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Stable digest of a quorum evidence package for StandingMandateV1.evidenceRef.
|
|
178
|
+
* Domain-separated so it cannot be confused with claim/receipt digests.
|
|
179
|
+
*/
|
|
180
|
+
export function computeQuorumEvidenceDigest(
|
|
181
|
+
pkg: QuorumEvidencePackageV1,
|
|
182
|
+
): string {
|
|
183
|
+
return digest({
|
|
184
|
+
version: 2,
|
|
185
|
+
domain: DIGEST_DOMAINS.quorum,
|
|
186
|
+
value: {
|
|
187
|
+
schemaVersion: pkg.schemaVersion,
|
|
188
|
+
proposal: pkg.proposal,
|
|
189
|
+
ballots: pkg.ballots,
|
|
190
|
+
finalizedAt: pkg.finalizedAt,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
package/src/receipts.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance receipt schema (emission deferred to later plans).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
6
|
+
import { Value } from "@sinclair/typebox/value";
|
|
7
|
+
import {
|
|
8
|
+
GovernanceEpochSchema,
|
|
9
|
+
GovernanceModeSchema,
|
|
10
|
+
} from "./governance.js";
|
|
11
|
+
|
|
12
|
+
const DigestHexSchema = Type.String({ pattern: "^[0-9a-f]{64}$" });
|
|
13
|
+
|
|
14
|
+
export const ReceiptInclusionEvidenceV1Schema = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
evidenceVersion: Type.Literal(1),
|
|
17
|
+
logKind: Type.Literal("conformance-receipt"),
|
|
18
|
+
entry: Type.Object(
|
|
19
|
+
{
|
|
20
|
+
previousHash: DigestHexSchema,
|
|
21
|
+
timestamp: Type.String({ minLength: 1 }),
|
|
22
|
+
kind: Type.Literal("conformance-receipt"),
|
|
23
|
+
receipt: Type.Unknown(),
|
|
24
|
+
hash: DigestHexSchema,
|
|
25
|
+
},
|
|
26
|
+
{ additionalProperties: false },
|
|
27
|
+
),
|
|
28
|
+
proof: Type.Object(
|
|
29
|
+
{
|
|
30
|
+
leaf: DigestHexSchema,
|
|
31
|
+
index: Type.Integer({ minimum: 0 }),
|
|
32
|
+
path: Type.Array(
|
|
33
|
+
Type.Object(
|
|
34
|
+
{
|
|
35
|
+
hash: DigestHexSchema,
|
|
36
|
+
position: Type.Union([
|
|
37
|
+
Type.Literal("left"),
|
|
38
|
+
Type.Literal("right"),
|
|
39
|
+
]),
|
|
40
|
+
},
|
|
41
|
+
{ additionalProperties: false },
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
root: DigestHexSchema,
|
|
45
|
+
},
|
|
46
|
+
{ additionalProperties: false },
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
{ additionalProperties: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export type ReceiptInclusionEvidenceV1 = Static<
|
|
53
|
+
typeof ReceiptInclusionEvidenceV1Schema
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export type ReceiptInclusionEvidenceParseResult =
|
|
57
|
+
| { ok: true; evidence: ReceiptInclusionEvidenceV1 }
|
|
58
|
+
| { ok: false; error: string };
|
|
59
|
+
|
|
60
|
+
export function parseReceiptInclusionEvidence(
|
|
61
|
+
input: unknown,
|
|
62
|
+
): ReceiptInclusionEvidenceParseResult {
|
|
63
|
+
if (!Value.Check(ReceiptInclusionEvidenceV1Schema, input)) {
|
|
64
|
+
return { ok: false, error: "invalid ReceiptInclusionEvidenceV1" };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, evidence: input };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const ConformanceReceiptV1Schema = Type.Union([
|
|
70
|
+
Type.Object(
|
|
71
|
+
{
|
|
72
|
+
schemaVersion: Type.Literal(1),
|
|
73
|
+
receiptClass: Type.Literal("conformance"),
|
|
74
|
+
actionDigest: DigestHexSchema,
|
|
75
|
+
policyId: Type.String({ minLength: 1 }),
|
|
76
|
+
policyVersion: Type.String({ minLength: 1 }),
|
|
77
|
+
implementationVersion: Type.String({ minLength: 1 }),
|
|
78
|
+
disposition: Type.Union([
|
|
79
|
+
Type.Literal("allow"),
|
|
80
|
+
Type.Literal("deny"),
|
|
81
|
+
Type.Literal("require_approval"),
|
|
82
|
+
Type.Literal("abstain"),
|
|
83
|
+
Type.Literal("allow_staged"),
|
|
84
|
+
Type.Literal("allow_minimal"),
|
|
85
|
+
]),
|
|
86
|
+
authorization: Type.String({ minLength: 1 }),
|
|
87
|
+
outcome: Type.String({ minLength: 1 }),
|
|
88
|
+
issuedAt: Type.String({ minLength: 1 }),
|
|
89
|
+
constitutionHash: Type.Optional(DigestHexSchema),
|
|
90
|
+
classifierRulesetHash: Type.Optional(DigestHexSchema),
|
|
91
|
+
effectiveConfigHash: Type.Optional(DigestHexSchema),
|
|
92
|
+
// Forbidden when unpaired — both keys must be absent in this branch.
|
|
93
|
+
governanceEpoch: Type.Optional(Type.Never()),
|
|
94
|
+
governanceMode: Type.Optional(Type.Never()),
|
|
95
|
+
},
|
|
96
|
+
{ additionalProperties: true },
|
|
97
|
+
),
|
|
98
|
+
Type.Object(
|
|
99
|
+
{
|
|
100
|
+
schemaVersion: Type.Literal(1),
|
|
101
|
+
receiptClass: Type.Literal("conformance"),
|
|
102
|
+
actionDigest: DigestHexSchema,
|
|
103
|
+
policyId: Type.String({ minLength: 1 }),
|
|
104
|
+
policyVersion: Type.String({ minLength: 1 }),
|
|
105
|
+
implementationVersion: Type.String({ minLength: 1 }),
|
|
106
|
+
disposition: Type.Union([
|
|
107
|
+
Type.Literal("allow"),
|
|
108
|
+
Type.Literal("deny"),
|
|
109
|
+
Type.Literal("require_approval"),
|
|
110
|
+
Type.Literal("abstain"),
|
|
111
|
+
Type.Literal("allow_staged"),
|
|
112
|
+
Type.Literal("allow_minimal"),
|
|
113
|
+
]),
|
|
114
|
+
authorization: Type.String({ minLength: 1 }),
|
|
115
|
+
outcome: Type.String({ minLength: 1 }),
|
|
116
|
+
issuedAt: Type.String({ minLength: 1 }),
|
|
117
|
+
constitutionHash: Type.Optional(DigestHexSchema),
|
|
118
|
+
classifierRulesetHash: Type.Optional(DigestHexSchema),
|
|
119
|
+
effectiveConfigHash: Type.Optional(DigestHexSchema),
|
|
120
|
+
/** Gateway governance binding — both fields required together. */
|
|
121
|
+
governanceEpoch: GovernanceEpochSchema,
|
|
122
|
+
governanceMode: GovernanceModeSchema,
|
|
123
|
+
},
|
|
124
|
+
{ additionalProperties: true },
|
|
125
|
+
),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
export type ConformanceReceiptV1 = Static<typeof ConformanceReceiptV1Schema>;
|
|
129
|
+
|
|
130
|
+
export type ReceiptParseResult =
|
|
131
|
+
| { ok: true; receipt: ConformanceReceiptV1 }
|
|
132
|
+
| { ok: false; error: string };
|
|
133
|
+
|
|
134
|
+
export function parseConformanceReceipt(input: unknown): ReceiptParseResult {
|
|
135
|
+
if (!Value.Check(ConformanceReceiptV1Schema, input)) {
|
|
136
|
+
return { ok: false, error: "invalid ConformanceReceiptV1" };
|
|
137
|
+
}
|
|
138
|
+
const issuedAt = (input as ConformanceReceiptV1).issuedAt;
|
|
139
|
+
const issuedAtMs = Date.parse(issuedAt);
|
|
140
|
+
if (
|
|
141
|
+
!Number.isFinite(issuedAtMs) ||
|
|
142
|
+
new Date(issuedAtMs).toISOString() !== issuedAt
|
|
143
|
+
) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: "invalid ConformanceReceiptV1: issuedAt must be canonical ISO-8601",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, receipt: input as ConformanceReceiptV1 };
|
|
150
|
+
}
|