@mcp-i/core 1.1.1 → 1.2.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.
Files changed (39) hide show
  1. package/dist/delegation/cascading-revocation.d.ts.map +1 -1
  2. package/dist/delegation/cascading-revocation.js +3 -1
  3. package/dist/delegation/cascading-revocation.js.map +1 -1
  4. package/dist/delegation/outbound-headers.d.ts +12 -12
  5. package/dist/delegation/outbound-headers.d.ts.map +1 -1
  6. package/dist/delegation/outbound-headers.js +12 -12
  7. package/dist/delegation/outbound-headers.js.map +1 -1
  8. package/dist/delegation/outbound-proof.d.ts +1 -1
  9. package/dist/delegation/outbound-proof.js +1 -1
  10. package/dist/delegation/statuslist-manager.d.ts +3 -0
  11. package/dist/delegation/statuslist-manager.d.ts.map +1 -1
  12. package/dist/delegation/statuslist-manager.js +13 -0
  13. package/dist/delegation/statuslist-manager.js.map +1 -1
  14. package/dist/middleware/with-mcpi-server.d.ts +1 -1
  15. package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
  16. package/dist/middleware/with-mcpi-server.js.map +1 -1
  17. package/dist/middleware/with-mcpi.d.ts +14 -0
  18. package/dist/middleware/with-mcpi.d.ts.map +1 -1
  19. package/dist/middleware/with-mcpi.js +12 -0
  20. package/dist/middleware/with-mcpi.js.map +1 -1
  21. package/package.json +3 -2
  22. package/src/__tests__/audit/canonicalization-integrity.test.ts +243 -0
  23. package/src/__tests__/audit/graph-revocation-roundtrip.test.ts +280 -0
  24. package/src/__tests__/audit/helpers/crypto-helpers.ts +245 -0
  25. package/src/__tests__/audit/proof-boundary.test.ts +269 -0
  26. package/src/__tests__/audit/statuslist-bitstring-roundtrip.test.ts +135 -0
  27. package/src/__tests__/audit/vc-roundtrip.test.ts +290 -0
  28. package/src/delegation/__tests__/outbound-headers.test.ts +16 -16
  29. package/src/delegation/__tests__/transitive-access.test.ts +1233 -0
  30. package/src/delegation/__tests__/vc-issuer.integration.test.ts +136 -0
  31. package/src/delegation/__tests__/vc-jwt.test.ts +318 -0
  32. package/src/delegation/__tests__/vc-verifier.integration.test.ts +199 -0
  33. package/src/delegation/cascading-revocation.ts +3 -1
  34. package/src/delegation/outbound-headers.ts +16 -16
  35. package/src/delegation/outbound-proof.ts +1 -1
  36. package/src/delegation/statuslist-manager.ts +17 -0
  37. package/src/middleware/with-mcpi-server.ts +1 -5
  38. package/src/middleware/with-mcpi.ts +29 -0
  39. package/src/proof/__tests__/verifier.integration.test.ts +181 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * VC Round-Trip Audit Tests
3
+ *
4
+ * Tests the full lifecycle: issue a DelegationCredential with real Ed25519
5
+ * signing, then verify it through the 3-stage verification pipeline.
6
+ * No mocks on crypto, canonicalization, or signing.
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll } from 'vitest';
10
+ import { DelegationCredentialIssuer } from '../../delegation/vc-issuer.js';
11
+ import {
12
+ DelegationCredentialVerifier,
13
+ type DIDResolver,
14
+ } from '../../delegation/vc-verifier.js';
15
+ import { createDidKeyResolver } from '../../delegation/did-key-resolver.js';
16
+ import type { AgentIdentity } from '../../providers/base.js';
17
+ import type { DelegationRecord, DelegationCredential } from '../../types/protocol.js';
18
+ import {
19
+ createRealCryptoProvider,
20
+ createRealIdentity,
21
+ createRealSigningFunction,
22
+ createRealSignatureVerifier,
23
+ } from './helpers/crypto-helpers.js';
24
+ import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
25
+
26
+ describe('VC Round-Trip Audit', () => {
27
+ let crypto: NodeCryptoProvider;
28
+ let issuerIdentity: AgentIdentity;
29
+ let subjectIdentity: AgentIdentity;
30
+ let issuer: DelegationCredentialIssuer;
31
+ let verifier: DelegationCredentialVerifier;
32
+ let didResolver: DIDResolver;
33
+
34
+ beforeAll(async () => {
35
+ crypto = createRealCryptoProvider();
36
+ issuerIdentity = await createRealIdentity(crypto);
37
+ subjectIdentity = await createRealIdentity(crypto);
38
+
39
+ const signingFunction = createRealSigningFunction(crypto, issuerIdentity);
40
+ const signatureVerifier = createRealSignatureVerifier(crypto);
41
+ didResolver = createDidKeyResolver();
42
+
43
+ const issuerIdProvider = {
44
+ getDid: () => issuerIdentity.did,
45
+ getKeyId: () => issuerIdentity.kid,
46
+ getPrivateKey: () => issuerIdentity.privateKey,
47
+ };
48
+
49
+ issuer = new DelegationCredentialIssuer(issuerIdProvider, signingFunction);
50
+
51
+ verifier = new DelegationCredentialVerifier({
52
+ didResolver,
53
+ signatureVerifier,
54
+ });
55
+ });
56
+
57
+ function makeDelegationRecord(overrides?: Partial<DelegationRecord>): DelegationRecord {
58
+ return {
59
+ id: 'test-delegation-001',
60
+ issuerDid: issuerIdentity.did,
61
+ subjectDid: subjectIdentity.did,
62
+ vcId: 'urn:uuid:test-vc-001',
63
+ constraints: {
64
+ scopes: ['tools:read', 'tools:write'],
65
+ notBefore: Math.floor(Date.now() / 1000) - 3600,
66
+ notAfter: Math.floor(Date.now() / 1000) + 3600,
67
+ },
68
+ signature: '',
69
+ status: 'active',
70
+ createdAt: Date.now(),
71
+ ...overrides,
72
+ };
73
+ }
74
+
75
+ // ── Round-Trip Tests ──────────────────────────────────────────
76
+
77
+ it('should issue and verify a delegation credential round-trip', async () => {
78
+ const delegation = makeDelegationRecord();
79
+ const vc = await issuer.issueDelegationCredential(delegation);
80
+
81
+ const result = await verifier.verifyDelegationCredential(vc, {
82
+ skipStatus: true,
83
+ skipCache: true,
84
+ });
85
+
86
+ expect(result.valid).toBe(true);
87
+ expect(result.stage).toBe('complete');
88
+ expect(result.checks?.basicValid).toBe(true);
89
+ expect(result.checks?.signatureValid).toBe(true);
90
+ expect(result.reason).toBeUndefined();
91
+ });
92
+
93
+ it('should reject VC with tampered credentialSubject', async () => {
94
+ const delegation = makeDelegationRecord();
95
+ const vc = await issuer.issueDelegationCredential(delegation);
96
+
97
+ // Tamper with the scopes after signing
98
+ const tampered = structuredClone(vc);
99
+ tampered.credentialSubject.delegation.scopes = ['admin:*'];
100
+
101
+ const result = await verifier.verifyDelegationCredential(tampered, {
102
+ skipStatus: true,
103
+ skipCache: true,
104
+ });
105
+
106
+ expect(result.valid).toBe(false);
107
+ expect(result.checks?.signatureValid).toBe(false);
108
+ });
109
+
110
+ it('should reject VC with swapped issuer DID', async () => {
111
+ const delegation = makeDelegationRecord();
112
+ const vc = await issuer.issueDelegationCredential(delegation);
113
+
114
+ // Swap issuer to subject's DID — resolver will return subject's key,
115
+ // which won't match the issuer's signature
116
+ const tampered = structuredClone(vc);
117
+ tampered.issuer = subjectIdentity.did;
118
+
119
+ const result = await verifier.verifyDelegationCredential(tampered, {
120
+ skipStatus: true,
121
+ skipCache: true,
122
+ });
123
+
124
+ expect(result.valid).toBe(false);
125
+ });
126
+
127
+ it('should reject expired VC', async () => {
128
+ const delegation = makeDelegationRecord({
129
+ constraints: {
130
+ scopes: ['tools:read'],
131
+ notAfter: Math.floor(Date.now() / 1000) - 1, // 1 second in the past
132
+ },
133
+ });
134
+ const vc = await issuer.issueDelegationCredential(delegation);
135
+
136
+ const result = await verifier.verifyDelegationCredential(vc, {
137
+ skipStatus: true,
138
+ skipCache: true,
139
+ });
140
+
141
+ expect(result.valid).toBe(false);
142
+ expect(result.stage).toBe('basic');
143
+ expect(result.reason).toContain('expired');
144
+ });
145
+
146
+ it('should reject VC with revoked status field', async () => {
147
+ const delegation = makeDelegationRecord({ status: 'revoked' });
148
+ const vc = await issuer.issueDelegationCredential(delegation);
149
+
150
+ const result = await verifier.verifyDelegationCredential(vc, {
151
+ skipStatus: true,
152
+ skipCache: true,
153
+ });
154
+
155
+ expect(result.valid).toBe(false);
156
+ expect(result.stage).toBe('basic');
157
+ expect(result.reason).toContain('revoked');
158
+ });
159
+
160
+ it('should reject VC without proof field', async () => {
161
+ const delegation = makeDelegationRecord();
162
+ const vc = await issuer.issueDelegationCredential(delegation);
163
+
164
+ // Remove proof entirely
165
+ const noProof = { ...vc } as Record<string, unknown>;
166
+ delete noProof['proof'];
167
+
168
+ const result = await verifier.verifyDelegationCredential(
169
+ noProof as DelegationCredential,
170
+ { skipStatus: true, skipCache: true }
171
+ );
172
+
173
+ expect(result.valid).toBe(false);
174
+ expect(result.stage).toBe('basic');
175
+ expect(result.reason).toContain('proof');
176
+ });
177
+
178
+ it('should reject VC when DID resolver returns null', async () => {
179
+ const delegation = makeDelegationRecord();
180
+ const vc = await issuer.issueDelegationCredential(delegation);
181
+
182
+ const nullResolver: DIDResolver = {
183
+ resolve: async () => null,
184
+ };
185
+
186
+ const strictVerifier = new DelegationCredentialVerifier({
187
+ didResolver: nullResolver,
188
+ signatureVerifier: createRealSignatureVerifier(crypto),
189
+ });
190
+
191
+ const result = await strictVerifier.verifyDelegationCredential(vc, {
192
+ skipStatus: true,
193
+ skipCache: true,
194
+ });
195
+
196
+ expect(result.valid).toBe(false);
197
+ expect(result.reason).toContain('resolve');
198
+ });
199
+
200
+ // ── Caching Tests ─────────────────────────────────────────────
201
+
202
+ it('should return cached result on second verification', async () => {
203
+ const delegation = makeDelegationRecord();
204
+ const vc = await issuer.issueDelegationCredential(delegation);
205
+
206
+ const cachingVerifier = new DelegationCredentialVerifier({
207
+ didResolver,
208
+ signatureVerifier: createRealSignatureVerifier(crypto),
209
+ cacheTtl: 60_000,
210
+ });
211
+
212
+ const first = await cachingVerifier.verifyDelegationCredential(vc, {
213
+ skipStatus: true,
214
+ });
215
+ expect(first.valid).toBe(true);
216
+ expect(first.cached).toBeUndefined();
217
+
218
+ const second = await cachingVerifier.verifyDelegationCredential(vc, {
219
+ skipStatus: true,
220
+ });
221
+ expect(second.valid).toBe(true);
222
+ expect(second.cached).toBe(true);
223
+ });
224
+
225
+ it('should evict oldest cache entry when maxCacheSize is exceeded', async () => {
226
+ const cachingVerifier = new DelegationCredentialVerifier({
227
+ didResolver,
228
+ signatureVerifier: createRealSignatureVerifier(crypto),
229
+ cacheTtl: 60_000,
230
+ maxCacheSize: 2,
231
+ });
232
+
233
+ // Issue 3 distinct VCs
234
+ const vcs: DelegationCredential[] = [];
235
+ for (let i = 0; i < 3; i++) {
236
+ const delegation = makeDelegationRecord({
237
+ id: `cache-test-${i}`,
238
+ vcId: `urn:uuid:cache-test-${i}`,
239
+ });
240
+ vcs.push(await issuer.issueDelegationCredential(delegation));
241
+ }
242
+
243
+ // Verify all 3 — first should be evicted
244
+ for (const vc of vcs) {
245
+ await cachingVerifier.verifyDelegationCredential(vc, { skipStatus: true });
246
+ }
247
+
248
+ // First VC should NOT be cached (evicted)
249
+ const first = await cachingVerifier.verifyDelegationCredential(vcs[0]!, {
250
+ skipStatus: true,
251
+ });
252
+ expect(first.cached).toBeUndefined();
253
+
254
+ // Third VC should still be cached
255
+ const third = await cachingVerifier.verifyDelegationCredential(vcs[2]!, {
256
+ skipStatus: true,
257
+ });
258
+ expect(third.cached).toBe(true);
259
+ });
260
+
261
+ // ── Signature Integrity ───────────────────────────────────────
262
+
263
+ it('should produce different signatures for different delegations', async () => {
264
+ const vc1 = await issuer.issueDelegationCredential(
265
+ makeDelegationRecord({ id: 'del-A' })
266
+ );
267
+ const vc2 = await issuer.issueDelegationCredential(
268
+ makeDelegationRecord({ id: 'del-B' })
269
+ );
270
+
271
+ expect(vc1.proof?.proofValue).not.toBe(vc2.proof?.proofValue);
272
+ });
273
+
274
+ it('should produce identical canonicalization for same delegation regardless of field order', async () => {
275
+ const delegation = makeDelegationRecord();
276
+ const vc1 = await issuer.issueDelegationCredential(delegation);
277
+ const vc2 = await issuer.issueDelegationCredential(delegation);
278
+
279
+ // Same input should produce same unsigned VC structure
280
+ // (proof timestamps differ, but the unsigned portion should canonicalize the same)
281
+ const strip = (vc: DelegationCredential) => {
282
+ const copy = { ...vc } as Record<string, unknown>;
283
+ delete copy['proof'];
284
+ return copy;
285
+ };
286
+
287
+ const { canonicalizeJSON } = await import('../../delegation/utils.js');
288
+ expect(canonicalizeJSON(strip(vc1))).toBe(canonicalizeJSON(strip(vc2)));
289
+ });
290
+ });
@@ -90,38 +90,38 @@ describe('buildOutboundDelegationHeaders', () => {
90
90
  const context = createTestContext();
91
91
  const headers = await buildOutboundDelegationHeaders(context);
92
92
 
93
- expect(headers).toHaveProperty('X-Agent-DID');
94
- expect(headers).toHaveProperty('X-Delegation-Chain');
95
- expect(headers).toHaveProperty('X-Session-ID');
96
- expect(headers).toHaveProperty('X-Delegation-Proof');
93
+ expect(headers).toHaveProperty('KYA-Agent-DID');
94
+ expect(headers).toHaveProperty('KYA-Delegation-Chain');
95
+ expect(headers).toHaveProperty('KYA-Session-Id');
96
+ expect(headers).toHaveProperty('KYA-Delegation-Proof');
97
97
  });
98
98
 
99
- it('X-Agent-DID matches session.agentDid', async () => {
99
+ it('KYA-Agent-DID matches session.agentDid', async () => {
100
100
  const context = createTestContext();
101
101
  const headers = await buildOutboundDelegationHeaders(context);
102
102
 
103
- expect(headers['X-Agent-DID']).toBe(context.session.agentDid);
103
+ expect(headers['KYA-Agent-DID']).toBe(context.session.agentDid);
104
104
  });
105
105
 
106
- it('X-Delegation-Chain matches delegation.vcId', async () => {
106
+ it('KYA-Delegation-Chain matches delegation.vcId', async () => {
107
107
  const context = createTestContext();
108
108
  const headers = await buildOutboundDelegationHeaders(context);
109
109
 
110
- expect(headers['X-Delegation-Chain']).toBe(context.delegation.vcId);
110
+ expect(headers['KYA-Delegation-Chain']).toBe(context.delegation.vcId);
111
111
  });
112
112
 
113
- it('X-Session-ID matches session.sessionId', async () => {
113
+ it('KYA-Session-Id matches session.sessionId', async () => {
114
114
  const context = createTestContext();
115
115
  const headers = await buildOutboundDelegationHeaders(context);
116
116
 
117
- expect(headers['X-Session-ID']).toBe(context.session.sessionId);
117
+ expect(headers['KYA-Session-Id']).toBe(context.session.sessionId);
118
118
  });
119
119
 
120
- it('X-Delegation-Proof is a valid JWT with correct claims', async () => {
120
+ it('KYA-Delegation-Proof is a valid JWT with correct claims', async () => {
121
121
  const context = createTestContext();
122
122
  const headers = await buildOutboundDelegationHeaders(context);
123
123
 
124
- const jwt = headers['X-Delegation-Proof'];
124
+ const jwt = headers['KYA-Delegation-Proof'];
125
125
 
126
126
  // Verify it's a valid JWT format (3 parts)
127
127
  expect(jwt.split('.')).toHaveLength(3);
@@ -148,7 +148,7 @@ describe('buildOutboundDelegationHeaders', () => {
148
148
  });
149
149
  const headers = await buildOutboundDelegationHeaders(context);
150
150
 
151
- const payload = decodeJwt(headers['X-Delegation-Proof']);
151
+ const payload = decodeJwt(headers['KYA-Delegation-Proof']);
152
152
  expect(payload.aud).toBe('api.service.example.com');
153
153
  });
154
154
 
@@ -158,7 +158,7 @@ describe('buildOutboundDelegationHeaders', () => {
158
158
  });
159
159
  const headers = await buildOutboundDelegationHeaders(context);
160
160
 
161
- const payload = decodeJwt(headers['X-Delegation-Proof']);
161
+ const payload = decodeJwt(headers['KYA-Delegation-Proof']);
162
162
  expect(payload.aud).toBe('internal-service.local');
163
163
  });
164
164
 
@@ -168,7 +168,7 @@ describe('buildOutboundDelegationHeaders', () => {
168
168
  });
169
169
  const headers = await buildOutboundDelegationHeaders(context);
170
170
 
171
- const payload = decodeJwt(headers['X-Delegation-Proof']);
171
+ const payload = decodeJwt(headers['KYA-Delegation-Proof']);
172
172
  expect(payload.aud).toBe('secure.example.org');
173
173
  });
174
174
 
@@ -176,7 +176,7 @@ describe('buildOutboundDelegationHeaders', () => {
176
176
  const context = createTestContext();
177
177
  const headers = await buildOutboundDelegationHeaders(context);
178
178
 
179
- const payload = decodeJwt(headers['X-Delegation-Proof']);
179
+ const payload = decodeJwt(headers['KYA-Delegation-Proof']);
180
180
  expect((payload.exp as number) - (payload.iat as number)).toBe(60);
181
181
  });
182
182