@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
@@ -5,10 +5,10 @@
5
5
  * delegation context to downstream services.
6
6
  *
7
7
  * Headers (MCP-I §7):
8
- * - X-Agent-DID: the original agent's DID
9
- * - X-Delegation-Chain: the delegation chain ID (vcId of the root delegation)
10
- * - X-Session-ID: the current session ID
11
- * - X-Delegation-Proof: a signed JWT proving the delegation is being forwarded
8
+ * - KYA-Agent-DID: the original agent's DID
9
+ * - KYA-Delegation-Chain: the delegation chain ID (vcId of the root delegation)
10
+ * - KYA-Session-Id: the current session ID
11
+ * - KYA-Delegation-Proof: a signed JWT proving the delegation is being forwarded
12
12
  *
13
13
  * Related Spec: MCP-I §7 — Outbound Delegation Propagation
14
14
  */
@@ -23,10 +23,10 @@ import { logger } from '../logging/index.js';
23
23
  * Header names for outbound delegation propagation
24
24
  */
25
25
  export const OUTBOUND_HEADER_NAMES = {
26
- AGENT_DID: 'X-Agent-DID',
27
- DELEGATION_CHAIN: 'X-Delegation-Chain',
28
- SESSION_ID: 'X-Session-ID',
29
- DELEGATION_PROOF: 'X-Delegation-Proof',
26
+ AGENT_DID: 'KYA-Agent-DID',
27
+ DELEGATION_CHAIN: 'KYA-Delegation-Chain',
28
+ SESSION_ID: 'KYA-Session-Id',
29
+ DELEGATION_PROOF: 'KYA-Delegation-Proof',
30
30
  } as const;
31
31
 
32
32
  /**
@@ -51,10 +51,10 @@ export interface OutboundDelegationContext {
51
51
  * Outbound delegation headers to attach to downstream requests
52
52
  */
53
53
  export interface OutboundDelegationHeaders {
54
- 'X-Agent-DID': string;
55
- 'X-Delegation-Chain': string;
56
- 'X-Session-ID': string;
57
- 'X-Delegation-Proof': string;
54
+ 'KYA-Agent-DID': string;
55
+ 'KYA-Delegation-Chain': string;
56
+ 'KYA-Session-Id': string;
57
+ 'KYA-Delegation-Proof': string;
58
58
  }
59
59
 
60
60
  /**
@@ -182,9 +182,9 @@ export async function buildOutboundDelegationHeaders(
182
182
  });
183
183
 
184
184
  return {
185
- 'X-Agent-DID': session.agentDid,
186
- 'X-Delegation-Chain': delegation.vcId,
187
- 'X-Session-ID': session.sessionId,
188
- 'X-Delegation-Proof': jwt,
185
+ 'KYA-Agent-DID': session.agentDid,
186
+ 'KYA-Delegation-Chain': delegation.vcId,
187
+ 'KYA-Session-Id': session.sessionId,
188
+ 'KYA-Delegation-Proof': jwt,
189
189
  };
190
190
  }
@@ -5,7 +5,7 @@
5
5
  * Enables downstream services to independently verify the delegation chain.
6
6
  *
7
7
  * Wire format: signed compact EdDSA JWT (60s TTL, per-call jti)
8
- * Header injection: X-Delegation-Id, X-Delegation-Chain, X-Delegation-Proof, X-Scopes
8
+ * Header injection: KYA-Delegation-Id, KYA-Delegation-Chain, KYA-Delegation-Proof, KYA-Granted-Scopes
9
9
  *
10
10
  * Related Spec: MCP-I §2 — Outbound Delegation Propagation
11
11
  */
@@ -28,6 +28,8 @@ export interface StatusListIdentityProvider {
28
28
  export class StatusList2021Manager {
29
29
  private statusListBaseUrl: string;
30
30
  private defaultListSize: number;
31
+ /** Per-status-list mutex to serialize updateStatus calls and prevent race conditions. */
32
+ private updateLocks = new Map<string, Promise<void>>();
31
33
 
32
34
  constructor(
33
35
  private storage: StatusListStorageProvider,
@@ -63,6 +65,21 @@ export class StatusList2021Manager {
63
65
  }
64
66
 
65
67
  async updateStatus(credentialStatus: CredentialStatus, revoked: boolean): Promise<void> {
68
+ const { statusListCredential } = credentialStatus;
69
+
70
+ // Serialize updates per status list to prevent concurrent read-modify-write races.
71
+ // Each call chains on the previous operation for the same list.
72
+ const previous = this.updateLocks.get(statusListCredential) ?? Promise.resolve();
73
+ const operation = previous.then(() => this.doUpdateStatus(credentialStatus, revoked));
74
+
75
+ // Store a non-rejecting version so the chain continues even if one update fails
76
+ this.updateLocks.set(statusListCredential, operation.catch(() => {}));
77
+
78
+ // Propagate the actual error to the caller
79
+ await operation;
80
+ }
81
+
82
+ private async doUpdateStatus(credentialStatus: CredentialStatus, revoked: boolean): Promise<void> {
66
83
  const { statusListCredential, statusListIndex } = credentialStatus;
67
84
 
68
85
  const statusList = await this.storage.getStatusList(statusListCredential);
@@ -71,11 +71,7 @@ export async function generateIdentity(
71
71
  */
72
72
  interface McpServerLike {
73
73
  connect(transport: Transport): Promise<unknown>;
74
- registerTool(
75
- name: string,
76
- config: Record<string, unknown>,
77
- handler: (args: unknown) => Promise<unknown>,
78
- ): void;
74
+ registerTool(...args: unknown[]): void;
79
75
  }
80
76
 
81
77
  /**
@@ -85,6 +85,20 @@ export interface MCPIDelegationConfig {
85
85
  resolveDelegationChain?: (
86
86
  leafCredential: DelegationCredential,
87
87
  ) => Promise<DelegationCredential[]>;
88
+ /**
89
+ * When true, re-delegations (credentials with a `parentId`) MUST include
90
+ * an `audience` constraint binding them to the verifying server's DID.
91
+ *
92
+ * This prevents confused-deputy attacks where a delegated credential is
93
+ * forwarded to an unintended server. Without audience binding, any server
94
+ * that receives the credential will accept it.
95
+ *
96
+ * Recommended for production. See: Alan Karp's transitive access analysis
97
+ * and MCP-I §11.6 (Confused Deputy Attacks).
98
+ *
99
+ * Default is false for backward compatibility.
100
+ */
101
+ requireAudienceOnRedelegation?: boolean;
88
102
  /**
89
103
  * Compatibility mode for legacy integrations that cannot yet provide
90
104
  * full delegation-chain and status-list resolvers.
@@ -789,6 +803,21 @@ export function createMCPIMiddleware(
789
803
  };
790
804
  }
791
805
 
806
+ // When requireAudienceOnRedelegation is enabled, every non-root
807
+ // credential in the chain MUST have an audience constraint.
808
+ // This prevents confused-deputy attacks where re-delegated
809
+ // credentials are forwarded to unintended servers.
810
+ if (
811
+ delegationConfig?.requireAudienceOnRedelegation &&
812
+ delegation.parentId &&
813
+ !delegation.constraints.audience
814
+ ) {
815
+ return {
816
+ valid: false,
817
+ reason: `Delegation ${delegation.id} is a re-delegation (parentId: ${delegation.parentId}) but has no audience constraint. Re-delegations must include an audience when requireAudienceOnRedelegation is enabled`,
818
+ };
819
+ }
820
+
792
821
  if (!previousDelegation || !previousCredential) {
793
822
  if (delegation.parentId) {
794
823
  return {
@@ -0,0 +1,181 @@
1
+ /**
2
+ * ProofVerifier Integration Tests (Real Crypto)
3
+ *
4
+ * Companion to verifier.test.ts — these tests use real Ed25519 signing,
5
+ * real nonce caching, and real clock providers instead of mocking.
6
+ *
7
+ * The mocked unit tests verify pipeline logic and error code propagation.
8
+ * These integration tests verify that real proofs are correctly verified
9
+ * and that security properties hold with actual cryptographic operations.
10
+ */
11
+
12
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
13
+ import { ProofGenerator } from '../generator.js';
14
+ import { ProofVerifier } from '../verifier.js';
15
+ import type { AgentIdentity } from '../../providers/base.js';
16
+ import { extractPublicKeyFromDidKey, publicKeyToJwk } from '../../delegation/did-key-resolver.js';
17
+ import type { Ed25519JWK } from '../../utils/crypto-service.js';
18
+ import {
19
+ createRealCryptoProvider,
20
+ createRealIdentity,
21
+ RealClockProvider,
22
+ RealFetchProvider,
23
+ MemoryNonceCacheProvider,
24
+ } from '../../__tests__/audit/helpers/crypto-helpers.js';
25
+ import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
26
+
27
+ describe('ProofVerifier (real crypto)', () => {
28
+ let crypto: NodeCryptoProvider;
29
+ let agent: AgentIdentity;
30
+ let otherAgent: AgentIdentity;
31
+
32
+ beforeAll(async () => {
33
+ crypto = createRealCryptoProvider();
34
+ agent = await createRealIdentity(crypto);
35
+ otherAgent = await createRealIdentity(crypto);
36
+ });
37
+
38
+ function makeVerifier(): { verifier: ProofVerifier; nonceCache: MemoryNonceCacheProvider } {
39
+ const nonceCache = new MemoryNonceCacheProvider();
40
+ const verifier = new ProofVerifier({
41
+ cryptoProvider: crypto,
42
+ clockProvider: new RealClockProvider(),
43
+ nonceCacheProvider: nonceCache,
44
+ fetchProvider: new RealFetchProvider(),
45
+ timestampSkewSeconds: 300,
46
+ });
47
+ return { verifier, nonceCache };
48
+ }
49
+
50
+ function getJwk(identity: AgentIdentity): Ed25519JWK {
51
+ const raw = extractPublicKeyFromDidKey(identity.did);
52
+ const jwk = publicKeyToJwk(raw!);
53
+ jwk.kid = identity.kid;
54
+ return jwk as Ed25519JWK;
55
+ }
56
+
57
+ async function generateProof(identity: AgentIdentity) {
58
+ const gen = new ProofGenerator(
59
+ { did: identity.did, kid: identity.kid, privateKey: identity.privateKey, publicKey: identity.publicKey },
60
+ crypto
61
+ );
62
+ return gen.generateProof(
63
+ { method: 'tools/call', params: { name: 'test-tool' } },
64
+ { data: { output: 'result' } },
65
+ {
66
+ sessionId: 'sess_integration',
67
+ audience: 'did:web:server.example.com',
68
+ nonce: `nonce-${Date.now()}-${Math.random().toString(36).slice(2)}`,
69
+ timestamp: Math.floor(Date.now() / 1000),
70
+ createdAt: Math.floor(Date.now() / 1000),
71
+ lastActivity: Math.floor(Date.now() / 1000),
72
+ ttlMinutes: 30,
73
+ identityState: 'anonymous',
74
+ }
75
+ );
76
+ }
77
+
78
+ // ── Core Verification ─────────────────────────────────────────
79
+
80
+ it('should verify a valid proof with real Ed25519 signature', async () => {
81
+ const { verifier } = makeVerifier();
82
+ const proof = await generateProof(agent);
83
+ const jwk = getJwk(agent);
84
+
85
+ const result = await verifier.verifyProof(proof, jwk);
86
+
87
+ expect(result.valid).toBe(true);
88
+ });
89
+
90
+ it('should reject proof signed by a different key', async () => {
91
+ const { verifier } = makeVerifier();
92
+ const proof = await generateProof(agent);
93
+ const wrongJwk = getJwk(otherAgent);
94
+
95
+ const result = await verifier.verifyProof(proof, wrongJwk);
96
+
97
+ expect(result.valid).toBe(false);
98
+ });
99
+
100
+ // ── Nonce Replay (real cache) ─────────────────────────────────
101
+
102
+ it('should prevent nonce replay with real MemoryNonceCacheProvider', async () => {
103
+ const { verifier } = makeVerifier();
104
+ const proof = await generateProof(agent);
105
+ const jwk = getJwk(agent);
106
+
107
+ const first = await verifier.verifyProof(proof, jwk);
108
+ expect(first.valid).toBe(true);
109
+
110
+ const replay = await verifier.verifyProof(proof, jwk);
111
+ expect(replay.valid).toBe(false);
112
+ expect(replay.reason).toContain('replay');
113
+ });
114
+
115
+ it('should scope nonces per agent DID', async () => {
116
+ const { verifier } = makeVerifier();
117
+
118
+ const proofA = await generateProof(agent);
119
+ const proofB = await generateProof(otherAgent);
120
+ const jwkA = getJwk(agent);
121
+ const jwkB = getJwk(otherAgent);
122
+
123
+ const resultA = await verifier.verifyProof(proofA, jwkA);
124
+ const resultB = await verifier.verifyProof(proofB, jwkB);
125
+
126
+ expect(resultA.valid).toBe(true);
127
+ expect(resultB.valid).toBe(true);
128
+ });
129
+
130
+ // ── Detached Verification ─────────────────────────────────────
131
+
132
+ it('should verify proof via verifyProofDetached with string payload', async () => {
133
+ const { verifier } = makeVerifier();
134
+ const proof = await generateProof(agent);
135
+ const jwk = getJwk(agent);
136
+ const canonical = verifier.buildCanonicalPayload(proof.meta);
137
+
138
+ const result = await verifier.verifyProofDetached(proof, canonical, jwk);
139
+
140
+ expect(result.valid).toBe(true);
141
+ });
142
+
143
+ it('should verify proof via verifyProofDetached with Uint8Array payload', async () => {
144
+ const { verifier } = makeVerifier();
145
+ const proof = await generateProof(agent);
146
+ const jwk = getJwk(agent);
147
+ const canonical = new TextEncoder().encode(verifier.buildCanonicalPayload(proof.meta));
148
+
149
+ const result = await verifier.verifyProofDetached(proof, canonical, jwk);
150
+
151
+ expect(result.valid).toBe(true);
152
+ });
153
+
154
+ // ── Proof Structure Rejection ─────────────────────────────────
155
+
156
+ it('should reject malformed proof structure', async () => {
157
+ const { verifier } = makeVerifier();
158
+ const jwk = getJwk(agent);
159
+
160
+ const result = await verifier.verifyProof(
161
+ { jws: 'not-valid', meta: { did: 'x' } } as any,
162
+ jwk
163
+ );
164
+
165
+ expect(result.valid).toBe(false);
166
+ expect(result.reason).toContain('Invalid proof structure');
167
+ });
168
+
169
+ // ── fetchPublicKeyFromDID (real DID resolution) ───────────────
170
+
171
+ it('should resolve a real did:key to a valid Ed25519 JWK', async () => {
172
+ const { verifier } = makeVerifier();
173
+
174
+ const jwk = await verifier.fetchPublicKeyFromDID(agent.did);
175
+
176
+ expect(jwk).toBeDefined();
177
+ expect(jwk!.kty).toBe('OKP');
178
+ expect(jwk!.crv).toBe('Ed25519');
179
+ expect(jwk!.x).toBeTruthy();
180
+ });
181
+ });