@mcp-i/core 1.1.0-canary.2 → 1.1.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 (94) hide show
  1. package/README.md +123 -333
  2. package/dist/auth/handshake.d.ts +19 -4
  3. package/dist/auth/handshake.d.ts.map +1 -1
  4. package/dist/auth/handshake.js +52 -15
  5. package/dist/auth/handshake.js.map +1 -1
  6. package/dist/auth/index.d.ts +1 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/delegation/did-key-resolver.d.ts.map +1 -1
  10. package/dist/delegation/did-key-resolver.js +9 -6
  11. package/dist/delegation/did-key-resolver.js.map +1 -1
  12. package/dist/delegation/outbound-headers.d.ts +2 -4
  13. package/dist/delegation/outbound-headers.d.ts.map +1 -1
  14. package/dist/delegation/outbound-headers.js +2 -3
  15. package/dist/delegation/outbound-headers.js.map +1 -1
  16. package/dist/delegation/statuslist-manager.d.ts.map +1 -1
  17. package/dist/delegation/statuslist-manager.js +1 -1
  18. package/dist/delegation/statuslist-manager.js.map +1 -1
  19. package/dist/delegation/vc-verifier.d.ts.map +1 -1
  20. package/dist/delegation/vc-verifier.js +2 -2
  21. package/dist/delegation/vc-verifier.js.map +1 -1
  22. package/dist/errors.d.ts +42 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +45 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +1 -0
  31. package/dist/middleware/index.d.ts.map +1 -1
  32. package/dist/middleware/index.js +1 -0
  33. package/dist/middleware/index.js.map +1 -1
  34. package/dist/middleware/mcpi-transport.d.ts +39 -0
  35. package/dist/middleware/mcpi-transport.d.ts.map +1 -0
  36. package/dist/middleware/mcpi-transport.js +121 -0
  37. package/dist/middleware/mcpi-transport.js.map +1 -0
  38. package/dist/middleware/with-mcpi-server.d.ts +25 -9
  39. package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
  40. package/dist/middleware/with-mcpi-server.js +62 -47
  41. package/dist/middleware/with-mcpi-server.js.map +1 -1
  42. package/dist/middleware/with-mcpi.d.ts +26 -5
  43. package/dist/middleware/with-mcpi.d.ts.map +1 -1
  44. package/dist/middleware/with-mcpi.js +108 -10
  45. package/dist/middleware/with-mcpi.js.map +1 -1
  46. package/dist/providers/memory.js +2 -2
  47. package/dist/providers/memory.js.map +1 -1
  48. package/dist/session/manager.d.ts +7 -1
  49. package/dist/session/manager.d.ts.map +1 -1
  50. package/dist/session/manager.js +20 -4
  51. package/dist/session/manager.js.map +1 -1
  52. package/dist/utils/crypto-service.d.ts.map +1 -1
  53. package/dist/utils/crypto-service.js +11 -10
  54. package/dist/utils/crypto-service.js.map +1 -1
  55. package/dist/utils/did-helpers.d.ts +12 -0
  56. package/dist/utils/did-helpers.d.ts.map +1 -1
  57. package/dist/utils/did-helpers.js +18 -0
  58. package/dist/utils/did-helpers.js.map +1 -1
  59. package/package.json +3 -3
  60. package/src/__tests__/errors.test.ts +56 -0
  61. package/src/__tests__/integration/full-flow.test.ts +1 -1
  62. package/src/__tests__/integration/mcp-enhance-server.test.ts +48 -5
  63. package/src/__tests__/integration/mcp-transport-context7.test.ts +19 -15
  64. package/src/__tests__/integration/mcp-transport.test.ts +13 -10
  65. package/src/__tests__/providers/base.test.ts +1 -1
  66. package/src/__tests__/providers/memory.test.ts +2 -2
  67. package/src/__tests__/utils/mock-providers.ts +2 -2
  68. package/src/auth/__tests__/handshake.test.ts +190 -0
  69. package/src/auth/handshake.ts +88 -21
  70. package/src/auth/index.ts +1 -0
  71. package/src/delegation/__tests__/did-key-resolver.test.ts +2 -2
  72. package/src/delegation/__tests__/outbound-headers.test.ts +16 -20
  73. package/src/delegation/__tests__/statuslist-manager.test.ts +120 -7
  74. package/src/delegation/__tests__/vc-verifier.test.ts +45 -3
  75. package/src/delegation/did-key-resolver.ts +11 -6
  76. package/src/delegation/outbound-headers.ts +1 -4
  77. package/src/delegation/statuslist-manager.ts +3 -1
  78. package/src/delegation/vc-verifier.ts +3 -2
  79. package/src/errors.ts +65 -0
  80. package/src/index.ts +10 -0
  81. package/src/middleware/__tests__/mcpi-transport.test.ts +150 -0
  82. package/src/middleware/__tests__/with-mcpi-server.test.ts +117 -0
  83. package/src/middleware/__tests__/with-mcpi.test.ts +124 -6
  84. package/src/middleware/index.ts +6 -0
  85. package/src/middleware/mcpi-transport.ts +162 -0
  86. package/src/middleware/with-mcpi-server.ts +83 -92
  87. package/src/middleware/with-mcpi.ts +147 -11
  88. package/src/proof/__tests__/errors.test.ts +79 -0
  89. package/src/proof/__tests__/verifier.test.ts +5 -5
  90. package/src/providers/memory.ts +2 -2
  91. package/src/session/__tests__/session-manager.test.ts +3 -3
  92. package/src/session/manager.ts +28 -6
  93. package/src/utils/crypto-service.ts +11 -10
  94. package/src/utils/did-helpers.ts +19 -0
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ PROOF_VERIFICATION_ERROR_CODES,
4
+ ProofVerificationError,
5
+ createProofVerificationError,
6
+ } from "../errors.js";
7
+
8
+ describe("PROOF_VERIFICATION_ERROR_CODES", () => {
9
+ it("should define all expected error code categories", () => {
10
+ const codes = PROOF_VERIFICATION_ERROR_CODES;
11
+
12
+ // Proof structure
13
+ expect(codes.INVALID_PROOF_STRUCTURE).toBeDefined();
14
+ expect(codes.MISSING_REQUIRED_FIELD).toBeDefined();
15
+
16
+ // Security
17
+ expect(codes.NONCE_REPLAY_DETECTED).toBeDefined();
18
+ expect(codes.TIMESTAMP_SKEW_EXCEEDED).toBeDefined();
19
+
20
+ // JWS
21
+ expect(codes.INVALID_JWS_SIGNATURE).toBeDefined();
22
+ expect(codes.INVALID_JWS_FORMAT).toBeDefined();
23
+
24
+ // JWK
25
+ expect(codes.INVALID_JWK_FORMAT).toBeDefined();
26
+
27
+ // DID
28
+ expect(codes.DID_RESOLUTION_FAILED).toBeDefined();
29
+ expect(codes.DID_DOCUMENT_NOT_FOUND).toBeDefined();
30
+ });
31
+ });
32
+
33
+ describe("ProofVerificationError", () => {
34
+ it("should extend Error", () => {
35
+ const err = new ProofVerificationError(
36
+ PROOF_VERIFICATION_ERROR_CODES.NONCE_REPLAY_DETECTED,
37
+ "Nonce reused",
38
+ );
39
+
40
+ expect(err).toBeInstanceOf(Error);
41
+ expect(err.name).toBe("ProofVerificationError");
42
+ expect(err.message).toBe("Nonce reused");
43
+ expect(err.code).toBe("NONCE_REPLAY_DETECTED");
44
+ });
45
+
46
+ it("should include optional details", () => {
47
+ const err = new ProofVerificationError(
48
+ PROOF_VERIFICATION_ERROR_CODES.DID_DOCUMENT_NOT_FOUND,
49
+ "DID not found",
50
+ { did: "did:key:z6MkTest" },
51
+ );
52
+
53
+ expect(err.details).toEqual({ did: "did:key:z6MkTest" });
54
+ });
55
+
56
+ it("should have undefined details when not provided", () => {
57
+ const err = new ProofVerificationError(
58
+ PROOF_VERIFICATION_ERROR_CODES.INVALID_JWS_FORMAT,
59
+ "Bad format",
60
+ );
61
+
62
+ expect(err.details).toBeUndefined();
63
+ });
64
+ });
65
+
66
+ describe("createProofVerificationError", () => {
67
+ it("should create a ProofVerificationError instance", () => {
68
+ const err = createProofVerificationError(
69
+ PROOF_VERIFICATION_ERROR_CODES.TIMESTAMP_SKEW_EXCEEDED,
70
+ "Clock skew too large",
71
+ { skew: 300 },
72
+ );
73
+
74
+ expect(err).toBeInstanceOf(ProofVerificationError);
75
+ expect(err.code).toBe("TIMESTAMP_SKEW_EXCEEDED");
76
+ expect(err.message).toBe("Clock skew too large");
77
+ expect(err.details).toEqual({ skew: 300 });
78
+ });
79
+ });
@@ -35,7 +35,7 @@ describe('ProofVerifier Security', () => {
35
35
  kty: 'OKP',
36
36
  crv: 'Ed25519',
37
37
  x: 'VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ',
38
- kid: 'did:key:z123#keys-1',
38
+ kid: 'did:key:z123#z123',
39
39
  };
40
40
 
41
41
  const createValidProof = (): DetachedProof => {
@@ -64,7 +64,7 @@ describe('ProofVerifier Security', () => {
64
64
  jws,
65
65
  meta: {
66
66
  did: 'did:key:z123',
67
- kid: 'did:key:z123#keys-1',
67
+ kid: 'did:key:z123#z123',
68
68
  ts: Math.floor(Date.now() / 1000),
69
69
  nonce: 'nonce123',
70
70
  audience: 'test-audience',
@@ -104,7 +104,7 @@ describe('ProofVerifier Security', () => {
104
104
  mockFetchProvider = {
105
105
  resolveDID: vi.fn().mockResolvedValue({
106
106
  verificationMethod: [{
107
- id: 'did:key:z123#keys-1',
107
+ id: 'did:key:z123#z123',
108
108
  publicKeyJwk: validJwk,
109
109
  }],
110
110
  }),
@@ -417,7 +417,7 @@ describe('ProofVerifier Security', () => {
417
417
 
418
418
  describe('fetchPublicKeyFromDID', () => {
419
419
  it('should fetch public key from DID document', async () => {
420
- const jwk = await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'keys-1');
420
+ const jwk = await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'z123');
421
421
 
422
422
  expect(jwk).toEqual(validJwk);
423
423
  expect(mockFetchProvider.resolveDID).toHaveBeenCalledWith('did:key:z123');
@@ -462,7 +462,7 @@ describe('ProofVerifier Security', () => {
462
462
  it('should throw ProofVerificationError if JWK is not Ed25519', async () => {
463
463
  mockFetchProvider.resolveDID = vi.fn().mockResolvedValue({
464
464
  verificationMethod: [{
465
- id: 'did:key:z123#keys-1',
465
+ id: 'did:key:z123#z123',
466
466
  publicKeyJwk: {
467
467
  kty: 'RSA',
468
468
  crv: 'RS256',
@@ -11,7 +11,7 @@ import {
11
11
  IdentityProvider,
12
12
  type AgentIdentity,
13
13
  } from './base.js';
14
- import { generateDidKeyFromBase64 } from '../utils/did-helpers.js';
14
+ import { generateDidKeyFromBase64, didKeyFragment } from '../utils/did-helpers.js';
15
15
 
16
16
  export class MemoryStorageProvider extends StorageProvider {
17
17
  private store: Map<string, string> = new Map();
@@ -116,7 +116,7 @@ export class MemoryIdentityProvider extends IdentityProvider {
116
116
 
117
117
  return {
118
118
  did,
119
- kid: `${did}#keys-1`,
119
+ kid: `${did}#${didKeyFragment(did)}`,
120
120
  privateKey: keyPair.privateKey,
121
121
  publicKey: keyPair.publicKey,
122
122
  createdAt: new Date().toISOString(),
@@ -88,7 +88,7 @@ describe("SessionManager", () => {
88
88
  const result = await manager.validateHandshake(request);
89
89
 
90
90
  expect(result.success).toBe(false);
91
- expect(result.error?.code).toBe("XMCP_I_EHANDSHAKE");
91
+ expect(result.error?.code).toBe("handshake_failed");
92
92
  });
93
93
 
94
94
  it("should accept request within timestamp skew", async () => {
@@ -106,7 +106,7 @@ describe("SessionManager", () => {
106
106
  // Same request again (same nonce)
107
107
  const second = await manager.validateHandshake(request);
108
108
  expect(second.success).toBe(false);
109
- expect(second.error?.code).toBe("XMCP_I_EHANDSHAKE");
109
+ expect(second.error?.code).toBe("handshake_failed");
110
110
  });
111
111
 
112
112
  it("should accept different nonces in succession", async () => {
@@ -261,7 +261,7 @@ describe("SessionManager", () => {
261
261
  const result = await sm.validateHandshake(request);
262
262
 
263
263
  expect(result.success).toBe(false);
264
- expect(result.error?.code).toBe("MCPI_AUDIENCE_MISMATCH");
264
+ expect(result.error?.code).toBe("handshake_failed");
265
265
  expect(result.error?.message).toContain("Audience mismatch");
266
266
  });
267
267
 
@@ -9,6 +9,7 @@
9
9
  * Cloudflare Workers) to remain synchronous without platform-specific imports.
10
10
  */
11
11
 
12
+ import { MCPI_ERROR_CODES, type MCPIErrorCode } from "../errors.js";
12
13
  import type {
13
14
  HandshakeRequest,
14
15
  SessionContext,
@@ -24,28 +25,33 @@ export interface SessionConfig {
24
25
  absoluteSessionLifetime?: number;
25
26
  nonceCache?: NonceCache;
26
27
  serverDid?: string;
28
+ /** Maximum number of concurrent sessions. Oldest sessions are evicted when exceeded. Default: 10000 */
29
+ maxSessions?: number;
27
30
  }
28
31
 
29
32
  export interface HandshakeResult {
30
33
  success: boolean;
31
34
  session?: SessionContext;
32
35
  error?: {
33
- code: string;
36
+ code: MCPIErrorCode;
34
37
  message: string;
35
38
  remediation?: string;
36
39
  };
37
40
  }
38
41
 
39
42
  export class SessionManager {
40
- private config: Required<Omit<SessionConfig, 'absoluteSessionLifetime' | 'serverDid'>> & {
43
+ private config: Required<Omit<SessionConfig, 'absoluteSessionLifetime' | 'serverDid' | 'maxSessions'>> & {
41
44
  absoluteSessionLifetime?: number;
42
45
  serverDid?: string;
43
46
  };
44
47
  private cryptoProvider: CryptoProvider;
45
48
  private sessions = new Map<string, SessionContext>();
49
+ private sessionInsertionOrder: string[] = [];
50
+ private maxSessions: number;
46
51
 
47
52
  constructor(cryptoProvider: CryptoProvider, config: SessionConfig = {}) {
48
53
  this.cryptoProvider = cryptoProvider;
54
+ this.maxSessions = config.maxSessions ?? 10_000;
49
55
  this.config = {
50
56
  timestampSkewSeconds: config.timestampSkewSeconds ?? 120,
51
57
  sessionTtlMinutes: config.sessionTtlMinutes ?? 30,
@@ -89,7 +95,7 @@ export class SessionManager {
89
95
  return {
90
96
  success: false,
91
97
  error: {
92
- code: 'XMCP_I_EHANDSHAKE',
98
+ code: MCPI_ERROR_CODES.handshake_failed,
93
99
  message: `Timestamp outside acceptable range (±${this.config.timestampSkewSeconds}s)`,
94
100
  remediation: `Check NTP sync on client and server. Current server time: ${now}, received: ${request.timestamp}, diff: ${timeDiff}s. Adjust timestampSkewSeconds if needed.`,
95
101
  },
@@ -101,7 +107,7 @@ export class SessionManager {
101
107
  return {
102
108
  success: false,
103
109
  error: {
104
- code: 'MCPI_AUDIENCE_MISMATCH',
110
+ code: MCPI_ERROR_CODES.handshake_failed,
105
111
  message: `Audience mismatch: expected ${this.config.serverDid}, got ${request.audience}`,
106
112
  },
107
113
  };
@@ -115,7 +121,7 @@ export class SessionManager {
115
121
  return {
116
122
  success: false,
117
123
  error: {
118
- code: 'XMCP_I_EHANDSHAKE',
124
+ code: MCPI_ERROR_CODES.handshake_failed,
119
125
  message: 'Nonce already used (replay attack prevention)',
120
126
  remediation: 'Generate a new unique nonce for each request',
121
127
  },
@@ -146,14 +152,16 @@ export class SessionManager {
146
152
  ...(clientInfo && { clientInfo }),
147
153
  };
148
154
 
155
+ this.evictIfNeeded();
149
156
  this.sessions.set(sessionId, session);
157
+ this.sessionInsertionOrder.push(sessionId);
150
158
 
151
159
  return { success: true, session };
152
160
  } catch (error) {
153
161
  return {
154
162
  success: false,
155
163
  error: {
156
- code: 'XMCP_I_EHANDSHAKE',
164
+ code: MCPI_ERROR_CODES.handshake_failed,
157
165
  message: `Handshake validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
158
166
  },
159
167
  };
@@ -262,6 +270,15 @@ export class SessionManager {
262
270
  .replace(/=/g, '');
263
271
  }
264
272
 
273
+ private evictIfNeeded(): void {
274
+ while (this.sessions.size >= this.maxSessions && this.sessionInsertionOrder.length > 0) {
275
+ const oldest = this.sessionInsertionOrder.shift();
276
+ if (oldest) {
277
+ this.sessions.delete(oldest);
278
+ }
279
+ }
280
+ }
281
+
265
282
  async cleanup(): Promise<void> {
266
283
  const now = Math.floor(Date.now() / 1000);
267
284
 
@@ -281,6 +298,10 @@ export class SessionManager {
281
298
  }
282
299
  }
283
300
 
301
+ this.sessionInsertionOrder = this.sessionInsertionOrder.filter(
302
+ id => this.sessions.has(id)
303
+ );
304
+
284
305
  await this.config.nonceCache.cleanup();
285
306
  }
286
307
 
@@ -306,6 +327,7 @@ export class SessionManager {
306
327
 
307
328
  clearSessions(): void {
308
329
  this.sessions.clear();
330
+ this.sessionInsertionOrder = [];
309
331
  }
310
332
  }
311
333
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { CryptoProvider } from '../providers/base.js';
9
+ import { logger } from '../logging/index.js';
9
10
  import {
10
11
  base64urlDecodeToString,
11
12
  base64urlDecodeToBytes,
@@ -43,7 +44,7 @@ export class CryptoService {
43
44
  const result = await this.cryptoProvider.verify(data, signature, publicKey);
44
45
  return result === true;
45
46
  } catch (error) {
46
- console.error('[CryptoService] Ed25519 verification error:', error);
47
+ logger.error('[CryptoService] Ed25519 verification error:', error);
47
48
  return false;
48
49
  }
49
50
  }
@@ -101,12 +102,12 @@ export class CryptoService {
101
102
  ): Promise<boolean> {
102
103
  try {
103
104
  if (!this.isValidEd25519JWK(publicKeyJwk)) {
104
- console.error('[CryptoService] Invalid Ed25519 JWK format');
105
+ logger.error('[CryptoService] Invalid Ed25519 JWK format');
105
106
  return false;
106
107
  }
107
108
 
108
109
  if (options?.expectedKid && publicKeyJwk.kid !== options.expectedKid) {
109
- console.error('[CryptoService] Key ID mismatch');
110
+ logger.error('[CryptoService] Key ID mismatch');
110
111
  return false;
111
112
  }
112
113
 
@@ -126,22 +127,22 @@ export class CryptoService {
126
127
  const signatureBytes = base64urlDecodeToBytes(signatureB64);
127
128
  parsed = { header, payload: undefined, signatureBytes, signingInput: '' };
128
129
  } catch {
129
- console.error('[CryptoService] Invalid detached JWS format');
130
+ logger.error('[CryptoService] Invalid detached JWS format');
130
131
  return false;
131
132
  }
132
133
  } else {
133
- console.error('[CryptoService] Invalid JWS format:', error);
134
+ logger.error('[CryptoService] Invalid JWS format:', error);
134
135
  return false;
135
136
  }
136
137
  } else {
137
- console.error('[CryptoService] Invalid JWS format:', error);
138
+ logger.error('[CryptoService] Invalid JWS format:', error);
138
139
  return false;
139
140
  }
140
141
  }
141
142
 
142
143
  const expectedAlg = options?.alg || 'EdDSA';
143
144
  if (parsed.header['alg'] !== expectedAlg) {
144
- console.error(
145
+ logger.error(
145
146
  `[CryptoService] Unsupported algorithm: ${parsed.header['alg']}, expected ${expectedAlg}`
146
147
  );
147
148
  return false;
@@ -164,7 +165,7 @@ export class CryptoService {
164
165
  signingInputBytes = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
165
166
  } else {
166
167
  if (!parsed.signingInput) {
167
- console.error('[CryptoService] Missing signing input for compact JWS');
168
+ logger.error('[CryptoService] Missing signing input for compact JWS');
168
169
  return false;
169
170
  }
170
171
  signingInputBytes = new TextEncoder().encode(parsed.signingInput);
@@ -174,13 +175,13 @@ export class CryptoService {
174
175
  try {
175
176
  publicKeyBase64 = this.jwkToBase64PublicKey(publicKeyJwk);
176
177
  } catch (error) {
177
- console.error('[CryptoService] Failed to extract public key:', error);
178
+ logger.error('[CryptoService] Failed to extract public key:', error);
178
179
  return false;
179
180
  }
180
181
 
181
182
  return await this.verifyEd25519(signingInputBytes, parsed.signatureBytes, publicKeyBase64);
182
183
  } catch (error) {
183
- console.error('[CryptoService] JWS verification error:', error);
184
+ logger.error('[CryptoService] JWS verification error:', error);
184
185
  return false;
185
186
  }
186
187
  }
@@ -208,3 +208,22 @@ export function generateDidKeyFromBase64(publicKeyBase64: string): string {
208
208
  );
209
209
  return generateDidKeyFromBytes(publicKeyBytes);
210
210
  }
211
+
212
+ /**
213
+ * Get the spec-compliant fragment identifier for a did:key DID.
214
+ *
215
+ * Per the did:key spec (W3C CCG), the fragment equals the multibase-encoded
216
+ * public key value (the DID-specific-id). For example:
217
+ * did:key:z6MkABC... → z6MkABC...
218
+ *
219
+ * @see https://w3c-ccg.github.io/did-key-spec/#document-creation-algorithm
220
+ * @param did - A did:key DID string
221
+ * @returns The fragment identifier (multibase value), or 'keys-1' as fallback for non-did:key
222
+ */
223
+ export function didKeyFragment(did: string): string {
224
+ if (did.startsWith('did:key:')) {
225
+ return did.slice('did:key:'.length);
226
+ }
227
+ // Fallback for non-did:key methods
228
+ return 'keys-1';
229
+ }