@kya-os/contracts 1.8.0 → 1.9.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.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Test Vector Loader
3
+ *
4
+ * This module provides functions to load parity test vectors.
5
+ * These vectors ensure TypeScript and Rust implementations produce identical results.
6
+ */
7
+
8
+ import { readFileSync } from "fs";
9
+ import { join } from "path";
10
+
11
+ // Types for test vectors
12
+ export interface TestVector<TInput, TExpected> {
13
+ id: string;
14
+ description: string;
15
+ input: TInput;
16
+ expected: TExpected;
17
+ note?: string;
18
+ }
19
+
20
+ export interface TestVectorFile<TInput, TExpected> {
21
+ $schema?: string;
22
+ name: string;
23
+ description: string;
24
+ version: string;
25
+ vectors: TestVector<TInput, TExpected>[];
26
+ }
27
+
28
+ // did:key vector types
29
+ export interface DidKeyInput {
30
+ did: string;
31
+ keyId?: string;
32
+ }
33
+
34
+ export interface DidKeyExpected {
35
+ success: boolean;
36
+ keyType?: string;
37
+ publicKeyHex?: string;
38
+ keyId?: string;
39
+ keyIdValid?: boolean;
40
+ errorType?: string;
41
+ }
42
+
43
+ // Scope matching vector types
44
+ export interface ScopeMatchingInput {
45
+ requested: string;
46
+ granted: string[];
47
+ notBefore?: number | null;
48
+ notAfter?: number | null;
49
+ currentTime?: number;
50
+ }
51
+
52
+ export interface ScopeMatchingExpected {
53
+ permitted: boolean;
54
+ matchType?: "exact" | "wildcard" | "prefix" | "fullAccess" | "none";
55
+ matchedBy?: string | null;
56
+ error?: boolean;
57
+ errorType?: string;
58
+ }
59
+
60
+ export interface ScopeMatchingVectorFile
61
+ extends TestVectorFile<ScopeMatchingInput, ScopeMatchingExpected> {
62
+ timeBoundaryVectors?: TestVector<ScopeMatchingInput, ScopeMatchingExpected>[];
63
+ }
64
+
65
+ // VC-JWT vector types
66
+ export interface VcJwtInput {
67
+ jwt: string;
68
+ publicKeyHex?: string;
69
+ }
70
+
71
+ export interface VcJwtExpected {
72
+ success: boolean;
73
+ header?: {
74
+ alg: string;
75
+ typ?: string | null;
76
+ };
77
+ payload?: Record<string, unknown>;
78
+ issuer?: string;
79
+ verified?: boolean;
80
+ errorType?: string;
81
+ errorMessage?: string;
82
+ }
83
+
84
+ export interface VcJwtVectorFile
85
+ extends TestVectorFile<VcJwtInput, VcJwtExpected> {
86
+ signatureVerificationVectors?: TestVector<VcJwtInput, VcJwtExpected>[];
87
+ }
88
+
89
+ /**
90
+ * Load test vectors from JSON file
91
+ */
92
+ function loadVectors<T>(filename: string): T {
93
+ const filepath = join(__dirname, filename);
94
+ const content = readFileSync(filepath, "utf-8");
95
+ return JSON.parse(content) as T;
96
+ }
97
+
98
+ /**
99
+ * Load did:key resolution test vectors
100
+ */
101
+ export function loadDidKeyVectors(): TestVectorFile<
102
+ DidKeyInput,
103
+ DidKeyExpected
104
+ > {
105
+ return loadVectors<TestVectorFile<DidKeyInput, DidKeyExpected>>(
106
+ "did-key.json"
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Load scope matching test vectors
112
+ */
113
+ export function loadScopeMatchingVectors(): ScopeMatchingVectorFile {
114
+ return loadVectors<ScopeMatchingVectorFile>("scope-matching.json");
115
+ }
116
+
117
+ /**
118
+ * Load VC-JWT verification test vectors
119
+ */
120
+ export function loadVcJwtVectors(): VcJwtVectorFile {
121
+ return loadVectors<VcJwtVectorFile>("vc-jwt.json");
122
+ }
123
+
124
+ // ============================================================================
125
+ // Delegation chain conformance vectors (E3.5 / issue #2904)
126
+ //
127
+ // These vectors codify the reference delegation semantics defined in E3.1
128
+ // (mcp-i-core: cascading-revocation, delegation-graph) plus the CRISP per-hop
129
+ // attenuation primitive (contracts: areChildConstraintsValid), so the TS cores
130
+ // and the reduced-profile Rust edge engine verify against one shared corpus.
131
+ //
132
+ // Each vector carries TWO expected outcomes:
133
+ // - `core`: the full reference semantics a complete verifier must produce.
134
+ // - `edge`: the reduced-profile kya-os-engine (WASM/WASI) verdict. The edge
135
+ // engine formally verifies single-hop delegation only; multi-hop chains
136
+ // fail closed (see issue #2904 "reduced edge profile").
137
+ // `profiles` lists which profiles must satisfy the vector directly; `category`
138
+ // selects which reference primitive owns the vector's `core` assertion.
139
+ // ============================================================================
140
+
141
+ /** Selects which reference primitive owns a vector's `core` assertion. */
142
+ export type DelegationCategory =
143
+ | "cascading-revocation"
144
+ | "chain-integrity"
145
+ | "per-hop-attenuation"
146
+ | "multi-hop-valid"
147
+ | "scope-match";
148
+
149
+ export type DelegationProfile = "core" | "edge";
150
+
151
+ /** Minimal CRISP constraint shape the attenuation primitive consumes. */
152
+ export interface DelegationConstraintsInput {
153
+ notBefore?: number;
154
+ notAfter?: number;
155
+ scopes?: string[];
156
+ crisp?: {
157
+ budget?: { unit: "USD" | "ops" | "points"; cap: number };
158
+ scopes: Array<{ resource: string; matcher: "exact" | "prefix" | "regex" }>;
159
+ };
160
+ }
161
+
162
+ /** One hop in a delegation chain (root→leaf order). */
163
+ export interface DelegationHopInput {
164
+ /** Delegation id (unique within the chain). */
165
+ id: string;
166
+ /** Parent delegation id, or null for the root hop. */
167
+ parentId: string | null;
168
+ /** Issuer (delegator) DID. */
169
+ issuerDid: string;
170
+ /** Subject (delegatee) DID. */
171
+ subjectDid: string;
172
+ /** Flat granted scopes — what the reduced-profile engine's DelegationStep carries. */
173
+ grantedScopes: string[];
174
+ /** StatusList2021 pointer "<statusListUrl>#<index>" for revocation, if any. */
175
+ credentialStatusId?: string;
176
+ /** CRISP constraints — required for per-hop-attenuation vectors. */
177
+ constraints?: DelegationConstraintsInput;
178
+ }
179
+
180
+ export interface DelegationChainInput {
181
+ /** The delegation chain, root→leaf. length===1 ⇒ single-hop; >1 ⇒ multi-hop. */
182
+ chain: DelegationHopInput[];
183
+ /** Scope the leaf agent requests (the engine derives this from payload.aud). */
184
+ requestedScope: string;
185
+ /** credentialStatusIds that are currently revoked (drives the cascade check). */
186
+ revokedStatusIds?: string[];
187
+ /** Evaluation time (Unix seconds) for time-bounded checks. */
188
+ currentTime?: number;
189
+ }
190
+
191
+ /** Full reference-semantics outcome (the TS cores). */
192
+ export interface DelegationCoreExpected {
193
+ valid: boolean;
194
+ failureReason?:
195
+ | "cascaded-revocation"
196
+ | "direct-revocation"
197
+ | "invalid-chain"
198
+ | "attenuation-violation";
199
+ /** For cascaded revocation: the revoked ancestor's id. */
200
+ revokedAncestor?: string;
201
+ }
202
+
203
+ /** Canonical engine Decision wire shape (kya-os-engine types.rs). */
204
+ export type EngineDecision =
205
+ | { kind: "Permit" }
206
+ | { kind: "Block"; reason: { kind: string; [k: string]: unknown } };
207
+
208
+ /** Reduced-profile edge verdict (kya-os-engine). */
209
+ export interface DelegationEdgeExpected {
210
+ /**
211
+ * 'evaluated' — single-hop / L1: the engine computes the real verdict.
212
+ * 'fail-closed' — multi-hop: the engine refuses (Block PolicyDenied) unless a
213
+ * higher-priority reason (Revoked/Expired) fires first.
214
+ */
215
+ mode: "evaluated" | "fail-closed";
216
+ decision: EngineDecision;
217
+ }
218
+
219
+ export interface DelegationExpected {
220
+ /** Omitted for edge-only vectors (single-hop scope-match), where the engine is itself the reference. */
221
+ core?: DelegationCoreExpected;
222
+ edge: DelegationEdgeExpected;
223
+ }
224
+
225
+ export interface DelegationVector
226
+ extends TestVector<DelegationChainInput, DelegationExpected> {
227
+ category: DelegationCategory;
228
+ profiles: DelegationProfile[];
229
+ }
230
+
231
+ export interface DelegationVectorFile
232
+ extends TestVectorFile<DelegationChainInput, DelegationExpected> {
233
+ vectors: DelegationVector[];
234
+ }
235
+
236
+ /**
237
+ * Load delegation chain conformance test vectors
238
+ */
239
+ export function loadDelegationVectors(): DelegationVectorFile {
240
+ return loadVectors<DelegationVectorFile>("delegation-conformance.json");
241
+ }
242
+
243
+ /**
244
+ * Get all test vectors as a combined object
245
+ */
246
+ export function loadAllVectors() {
247
+ return {
248
+ didKey: loadDidKeyVectors(),
249
+ scopeMatching: loadScopeMatchingVectors(),
250
+ vcJwt: loadVcJwtVectors(),
251
+ delegation: loadDelegationVectors(),
252
+ };
253
+ }
@@ -0,0 +1,330 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "name": "Scope matching test vectors",
4
+ "description": "Test vectors for delegation scope matching parity between TypeScript and Rust",
5
+ "version": "1.0.0",
6
+ "vectors": [
7
+ {
8
+ "id": "exact-match",
9
+ "description": "Exact scope match",
10
+ "input": {
11
+ "requested": "read:email",
12
+ "granted": ["read:email"]
13
+ },
14
+ "expected": {
15
+ "permitted": true,
16
+ "matchType": "exact",
17
+ "matchedBy": "read:email"
18
+ }
19
+ },
20
+ {
21
+ "id": "exact-no-match",
22
+ "description": "Exact scope does not match",
23
+ "input": {
24
+ "requested": "write:email",
25
+ "granted": ["read:email"]
26
+ },
27
+ "expected": {
28
+ "permitted": false,
29
+ "matchType": "none",
30
+ "matchedBy": null
31
+ }
32
+ },
33
+ {
34
+ "id": "full-access",
35
+ "description": "Full access wildcard matches anything",
36
+ "input": {
37
+ "requested": "any:thing:at:all",
38
+ "granted": ["*"]
39
+ },
40
+ "expected": {
41
+ "permitted": true,
42
+ "matchType": "fullAccess",
43
+ "matchedBy": "*"
44
+ }
45
+ },
46
+ {
47
+ "id": "wildcard-action",
48
+ "description": "Wildcard action matches any action in resource",
49
+ "input": {
50
+ "requested": "read:email",
51
+ "granted": ["read:*"]
52
+ },
53
+ "expected": {
54
+ "permitted": true,
55
+ "matchType": "wildcard",
56
+ "matchedBy": "read:*"
57
+ }
58
+ },
59
+ {
60
+ "id": "wildcard-resource",
61
+ "description": "Wildcard resource matches any resource for action",
62
+ "input": {
63
+ "requested": "read:email",
64
+ "granted": ["*:email"]
65
+ },
66
+ "expected": {
67
+ "permitted": true,
68
+ "matchType": "wildcard",
69
+ "matchedBy": "*:email"
70
+ }
71
+ },
72
+ {
73
+ "id": "wildcard-depth-mismatch",
74
+ "description": "Wildcard does not match different depth",
75
+ "input": {
76
+ "requested": "read:email:inbox",
77
+ "granted": ["read:*"]
78
+ },
79
+ "expected": {
80
+ "permitted": false,
81
+ "matchType": "none",
82
+ "matchedBy": null
83
+ }
84
+ },
85
+ {
86
+ "id": "prefix-match-three-segments",
87
+ "description": "Prefix match with trailing wildcard (3 segments)",
88
+ "input": {
89
+ "requested": "read:email:inbox",
90
+ "granted": ["read:email:*"]
91
+ },
92
+ "expected": {
93
+ "permitted": true,
94
+ "matchType": "prefix",
95
+ "matchedBy": "read:email:*"
96
+ }
97
+ },
98
+ {
99
+ "id": "prefix-match-four-segments",
100
+ "description": "Prefix match with trailing wildcard (4 segments)",
101
+ "input": {
102
+ "requested": "read:email:inbox:unread",
103
+ "granted": ["read:email:inbox:*"]
104
+ },
105
+ "expected": {
106
+ "permitted": true,
107
+ "matchType": "prefix",
108
+ "matchedBy": "read:email:inbox:*"
109
+ }
110
+ },
111
+ {
112
+ "id": "deep-prefix-one-level",
113
+ "description": "Deep prefix wildcard matches one level below",
114
+ "input": {
115
+ "requested": "read:email",
116
+ "granted": ["read:**"]
117
+ },
118
+ "expected": {
119
+ "permitted": true,
120
+ "matchType": "prefix",
121
+ "matchedBy": "read:**"
122
+ }
123
+ },
124
+ {
125
+ "id": "deep-prefix-two-levels",
126
+ "description": "Deep prefix wildcard matches two levels below",
127
+ "input": {
128
+ "requested": "read:email:inbox",
129
+ "granted": ["read:**"]
130
+ },
131
+ "expected": {
132
+ "permitted": true,
133
+ "matchType": "prefix",
134
+ "matchedBy": "read:**"
135
+ }
136
+ },
137
+ {
138
+ "id": "deep-prefix-many-levels",
139
+ "description": "Deep prefix wildcard matches many levels below",
140
+ "input": {
141
+ "requested": "read:email:inbox:unread:important",
142
+ "granted": ["read:**"]
143
+ },
144
+ "expected": {
145
+ "permitted": true,
146
+ "matchType": "prefix",
147
+ "matchedBy": "read:**"
148
+ }
149
+ },
150
+ {
151
+ "id": "deep-prefix-no-self-match",
152
+ "description": "Deep prefix wildcard does NOT match same level (strictly below)",
153
+ "input": {
154
+ "requested": "read",
155
+ "granted": ["read:**"]
156
+ },
157
+ "expected": {
158
+ "permitted": false,
159
+ "matchType": "none",
160
+ "matchedBy": null
161
+ }
162
+ },
163
+ {
164
+ "id": "deep-prefix-wrong-prefix",
165
+ "description": "Deep prefix wildcard does not match wrong prefix",
166
+ "input": {
167
+ "requested": "write:email",
168
+ "granted": ["read:**"]
169
+ },
170
+ "expected": {
171
+ "permitted": false,
172
+ "matchType": "none",
173
+ "matchedBy": null
174
+ }
175
+ },
176
+ {
177
+ "id": "multiple-scopes-first-match",
178
+ "description": "Multiple granted scopes, first match wins",
179
+ "input": {
180
+ "requested": "read:email",
181
+ "granted": ["write:calendar", "read:email"]
182
+ },
183
+ "expected": {
184
+ "permitted": true,
185
+ "matchType": "exact",
186
+ "matchedBy": "read:email"
187
+ }
188
+ },
189
+ {
190
+ "id": "multiple-scopes-full-access-wins",
191
+ "description": "Full access wins when present",
192
+ "input": {
193
+ "requested": "read:email",
194
+ "granted": ["write:calendar", "*"]
195
+ },
196
+ "expected": {
197
+ "permitted": true,
198
+ "matchType": "fullAccess",
199
+ "matchedBy": "*"
200
+ }
201
+ },
202
+ {
203
+ "id": "empty-scope-error",
204
+ "description": "Empty scope should error",
205
+ "input": {
206
+ "requested": "",
207
+ "granted": ["read:email"]
208
+ },
209
+ "expected": {
210
+ "permitted": false,
211
+ "error": true,
212
+ "errorType": "EmptyScope"
213
+ }
214
+ },
215
+ {
216
+ "id": "wildcard-middle-segment",
217
+ "description": "Wildcard in middle segment",
218
+ "input": {
219
+ "requested": "read:email:inbox",
220
+ "granted": ["read:*:inbox"]
221
+ },
222
+ "expected": {
223
+ "permitted": true,
224
+ "matchType": "wildcard",
225
+ "matchedBy": "read:*:inbox"
226
+ }
227
+ },
228
+ {
229
+ "id": "wildcard-first-segment",
230
+ "description": "Wildcard in first segment",
231
+ "input": {
232
+ "requested": "read:email:inbox",
233
+ "granted": ["*:email:inbox"]
234
+ },
235
+ "expected": {
236
+ "permitted": true,
237
+ "matchType": "wildcard",
238
+ "matchedBy": "*:email:inbox"
239
+ }
240
+ },
241
+ {
242
+ "id": "empty-granted-no-match",
243
+ "description": "No granted scopes means no match",
244
+ "input": {
245
+ "requested": "read:email",
246
+ "granted": []
247
+ },
248
+ "expected": {
249
+ "permitted": false,
250
+ "matchType": "none",
251
+ "matchedBy": null
252
+ }
253
+ },
254
+ {
255
+ "id": "single-segment-scope",
256
+ "description": "Single segment scope exact match",
257
+ "input": {
258
+ "requested": "admin",
259
+ "granted": ["admin"]
260
+ },
261
+ "expected": {
262
+ "permitted": true,
263
+ "matchType": "exact",
264
+ "matchedBy": "admin"
265
+ }
266
+ }
267
+ ],
268
+ "timeBoundaryVectors": [
269
+ {
270
+ "id": "valid-time-window",
271
+ "description": "Current time within valid window",
272
+ "input": {
273
+ "requested": "read:email",
274
+ "granted": ["read:email"],
275
+ "notBefore": 1704067200,
276
+ "notAfter": 1767225600,
277
+ "currentTime": 1735689600
278
+ },
279
+ "expected": {
280
+ "permitted": true
281
+ }
282
+ },
283
+ {
284
+ "id": "expired-delegation",
285
+ "description": "Current time after notAfter",
286
+ "input": {
287
+ "requested": "read:email",
288
+ "granted": ["read:email"],
289
+ "notBefore": null,
290
+ "notAfter": 1704067200,
291
+ "currentTime": 1735689600
292
+ },
293
+ "expected": {
294
+ "permitted": false,
295
+ "error": true,
296
+ "errorType": "Expired"
297
+ }
298
+ },
299
+ {
300
+ "id": "not-yet-valid",
301
+ "description": "Current time before notBefore",
302
+ "input": {
303
+ "requested": "read:email",
304
+ "granted": ["read:email"],
305
+ "notBefore": 1735689600,
306
+ "notAfter": null,
307
+ "currentTime": 1704067200
308
+ },
309
+ "expected": {
310
+ "permitted": false,
311
+ "error": true,
312
+ "errorType": "NotYetValid"
313
+ }
314
+ },
315
+ {
316
+ "id": "skip-time-check",
317
+ "description": "Time check skipped when currentTime is 0",
318
+ "input": {
319
+ "requested": "read:email",
320
+ "granted": ["read:email"],
321
+ "notBefore": 9999999999,
322
+ "notAfter": 1,
323
+ "currentTime": 0
324
+ },
325
+ "expected": {
326
+ "permitted": true
327
+ }
328
+ }
329
+ ]
330
+ }