@mcp-i/core 0.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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -0
  3. package/dist/auth/handshake.d.ts +104 -0
  4. package/dist/auth/handshake.d.ts.map +1 -0
  5. package/dist/auth/handshake.js +230 -0
  6. package/dist/auth/handshake.js.map +1 -0
  7. package/dist/auth/index.d.ts +3 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +2 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/types.d.ts +31 -0
  12. package/dist/auth/types.d.ts.map +1 -0
  13. package/dist/auth/types.js +7 -0
  14. package/dist/auth/types.js.map +1 -0
  15. package/dist/delegation/audience-validator.d.ts +9 -0
  16. package/dist/delegation/audience-validator.d.ts.map +1 -0
  17. package/dist/delegation/audience-validator.js +17 -0
  18. package/dist/delegation/audience-validator.js.map +1 -0
  19. package/dist/delegation/bitstring.d.ts +37 -0
  20. package/dist/delegation/bitstring.d.ts.map +1 -0
  21. package/dist/delegation/bitstring.js +117 -0
  22. package/dist/delegation/bitstring.js.map +1 -0
  23. package/dist/delegation/cascading-revocation.d.ts +45 -0
  24. package/dist/delegation/cascading-revocation.d.ts.map +1 -0
  25. package/dist/delegation/cascading-revocation.js +148 -0
  26. package/dist/delegation/cascading-revocation.js.map +1 -0
  27. package/dist/delegation/delegation-graph.d.ts +49 -0
  28. package/dist/delegation/delegation-graph.d.ts.map +1 -0
  29. package/dist/delegation/delegation-graph.js +99 -0
  30. package/dist/delegation/delegation-graph.js.map +1 -0
  31. package/dist/delegation/did-key-resolver.d.ts +64 -0
  32. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  33. package/dist/delegation/did-key-resolver.js +154 -0
  34. package/dist/delegation/did-key-resolver.js.map +1 -0
  35. package/dist/delegation/did-web-resolver.d.ts +83 -0
  36. package/dist/delegation/did-web-resolver.d.ts.map +1 -0
  37. package/dist/delegation/did-web-resolver.js +218 -0
  38. package/dist/delegation/did-web-resolver.js.map +1 -0
  39. package/dist/delegation/index.d.ts +21 -0
  40. package/dist/delegation/index.d.ts.map +1 -0
  41. package/dist/delegation/index.js +21 -0
  42. package/dist/delegation/index.js.map +1 -0
  43. package/dist/delegation/outbound-headers.d.ts +81 -0
  44. package/dist/delegation/outbound-headers.d.ts.map +1 -0
  45. package/dist/delegation/outbound-headers.js +139 -0
  46. package/dist/delegation/outbound-headers.js.map +1 -0
  47. package/dist/delegation/outbound-proof.d.ts +43 -0
  48. package/dist/delegation/outbound-proof.d.ts.map +1 -0
  49. package/dist/delegation/outbound-proof.js +52 -0
  50. package/dist/delegation/outbound-proof.js.map +1 -0
  51. package/dist/delegation/statuslist-manager.d.ts +44 -0
  52. package/dist/delegation/statuslist-manager.d.ts.map +1 -0
  53. package/dist/delegation/statuslist-manager.js +126 -0
  54. package/dist/delegation/statuslist-manager.js.map +1 -0
  55. package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
  56. package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
  57. package/dist/delegation/storage/memory-graph-storage.js +145 -0
  58. package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
  59. package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
  60. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
  61. package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
  62. package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
  63. package/dist/delegation/utils.d.ts +49 -0
  64. package/dist/delegation/utils.d.ts.map +1 -0
  65. package/dist/delegation/utils.js +131 -0
  66. package/dist/delegation/utils.js.map +1 -0
  67. package/dist/delegation/vc-issuer.d.ts +56 -0
  68. package/dist/delegation/vc-issuer.d.ts.map +1 -0
  69. package/dist/delegation/vc-issuer.js +80 -0
  70. package/dist/delegation/vc-issuer.js.map +1 -0
  71. package/dist/delegation/vc-verifier.d.ts +112 -0
  72. package/dist/delegation/vc-verifier.d.ts.map +1 -0
  73. package/dist/delegation/vc-verifier.js +280 -0
  74. package/dist/delegation/vc-verifier.js.map +1 -0
  75. package/dist/index.d.ts +45 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +53 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/logging/index.d.ts +2 -0
  80. package/dist/logging/index.d.ts.map +1 -0
  81. package/dist/logging/index.js +2 -0
  82. package/dist/logging/index.js.map +1 -0
  83. package/dist/logging/logger.d.ts +23 -0
  84. package/dist/logging/logger.d.ts.map +1 -0
  85. package/dist/logging/logger.js +82 -0
  86. package/dist/logging/logger.js.map +1 -0
  87. package/dist/middleware/index.d.ts +7 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +7 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/with-mcpi.d.ts +152 -0
  92. package/dist/middleware/with-mcpi.d.ts.map +1 -0
  93. package/dist/middleware/with-mcpi.js +472 -0
  94. package/dist/middleware/with-mcpi.js.map +1 -0
  95. package/dist/proof/errors.d.ts +49 -0
  96. package/dist/proof/errors.d.ts.map +1 -0
  97. package/dist/proof/errors.js +61 -0
  98. package/dist/proof/errors.js.map +1 -0
  99. package/dist/proof/generator.d.ts +65 -0
  100. package/dist/proof/generator.d.ts.map +1 -0
  101. package/dist/proof/generator.js +163 -0
  102. package/dist/proof/generator.js.map +1 -0
  103. package/dist/proof/index.d.ts +4 -0
  104. package/dist/proof/index.d.ts.map +1 -0
  105. package/dist/proof/index.js +4 -0
  106. package/dist/proof/index.js.map +1 -0
  107. package/dist/proof/verifier.d.ts +108 -0
  108. package/dist/proof/verifier.d.ts.map +1 -0
  109. package/dist/proof/verifier.js +299 -0
  110. package/dist/proof/verifier.js.map +1 -0
  111. package/dist/providers/base.d.ts +64 -0
  112. package/dist/providers/base.d.ts.map +1 -0
  113. package/dist/providers/base.js +19 -0
  114. package/dist/providers/base.js.map +1 -0
  115. package/dist/providers/index.d.ts +3 -0
  116. package/dist/providers/index.d.ts.map +1 -0
  117. package/dist/providers/index.js +3 -0
  118. package/dist/providers/index.js.map +1 -0
  119. package/dist/providers/memory.d.ts +33 -0
  120. package/dist/providers/memory.d.ts.map +1 -0
  121. package/dist/providers/memory.js +102 -0
  122. package/dist/providers/memory.js.map +1 -0
  123. package/dist/session/index.d.ts +2 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +2 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/session/manager.d.ts +77 -0
  128. package/dist/session/manager.d.ts.map +1 -0
  129. package/dist/session/manager.js +251 -0
  130. package/dist/session/manager.js.map +1 -0
  131. package/dist/types/protocol.d.ts +320 -0
  132. package/dist/types/protocol.d.ts.map +1 -0
  133. package/dist/types/protocol.js +229 -0
  134. package/dist/types/protocol.js.map +1 -0
  135. package/dist/utils/base58.d.ts +31 -0
  136. package/dist/utils/base58.d.ts.map +1 -0
  137. package/dist/utils/base58.js +104 -0
  138. package/dist/utils/base58.js.map +1 -0
  139. package/dist/utils/base64.d.ts +13 -0
  140. package/dist/utils/base64.d.ts.map +1 -0
  141. package/dist/utils/base64.js +99 -0
  142. package/dist/utils/base64.js.map +1 -0
  143. package/dist/utils/crypto-service.d.ts +37 -0
  144. package/dist/utils/crypto-service.d.ts.map +1 -0
  145. package/dist/utils/crypto-service.js +153 -0
  146. package/dist/utils/crypto-service.js.map +1 -0
  147. package/dist/utils/did-helpers.d.ts +156 -0
  148. package/dist/utils/did-helpers.d.ts.map +1 -0
  149. package/dist/utils/did-helpers.js +193 -0
  150. package/dist/utils/did-helpers.js.map +1 -0
  151. package/dist/utils/ed25519-constants.d.ts +18 -0
  152. package/dist/utils/ed25519-constants.d.ts.map +1 -0
  153. package/dist/utils/ed25519-constants.js +21 -0
  154. package/dist/utils/ed25519-constants.js.map +1 -0
  155. package/dist/utils/index.d.ts +5 -0
  156. package/dist/utils/index.d.ts.map +1 -0
  157. package/dist/utils/index.js +5 -0
  158. package/dist/utils/index.js.map +1 -0
  159. package/package.json +105 -0
  160. package/src/__tests__/integration/full-flow.test.ts +362 -0
  161. package/src/__tests__/providers/base.test.ts +173 -0
  162. package/src/__tests__/providers/memory.test.ts +332 -0
  163. package/src/__tests__/utils/mock-providers.ts +319 -0
  164. package/src/__tests__/utils/node-crypto-provider.ts +93 -0
  165. package/src/auth/handshake.ts +411 -0
  166. package/src/auth/index.ts +11 -0
  167. package/src/auth/types.ts +40 -0
  168. package/src/delegation/__tests__/audience-validator.test.ts +110 -0
  169. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  170. package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
  171. package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
  172. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  173. package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
  174. package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
  175. package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
  176. package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
  177. package/src/delegation/__tests__/utils.test.ts +185 -0
  178. package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
  179. package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
  180. package/src/delegation/audience-validator.ts +24 -0
  181. package/src/delegation/bitstring.ts +160 -0
  182. package/src/delegation/cascading-revocation.ts +224 -0
  183. package/src/delegation/delegation-graph.ts +143 -0
  184. package/src/delegation/did-key-resolver.ts +181 -0
  185. package/src/delegation/did-web-resolver.ts +270 -0
  186. package/src/delegation/index.ts +33 -0
  187. package/src/delegation/outbound-headers.ts +193 -0
  188. package/src/delegation/outbound-proof.ts +90 -0
  189. package/src/delegation/statuslist-manager.ts +219 -0
  190. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  191. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  192. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  193. package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
  194. package/src/delegation/utils.ts +189 -0
  195. package/src/delegation/vc-issuer.ts +137 -0
  196. package/src/delegation/vc-verifier.ts +440 -0
  197. package/src/index.ts +264 -0
  198. package/src/logging/__tests__/logger.test.ts +366 -0
  199. package/src/logging/index.ts +6 -0
  200. package/src/logging/logger.ts +91 -0
  201. package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
  202. package/src/middleware/index.ts +16 -0
  203. package/src/middleware/with-mcpi.ts +766 -0
  204. package/src/proof/__tests__/proof-generator.test.ts +483 -0
  205. package/src/proof/__tests__/verifier.test.ts +488 -0
  206. package/src/proof/errors.ts +75 -0
  207. package/src/proof/generator.ts +255 -0
  208. package/src/proof/index.ts +22 -0
  209. package/src/proof/verifier.ts +449 -0
  210. package/src/providers/base.ts +68 -0
  211. package/src/providers/index.ts +15 -0
  212. package/src/providers/memory.ts +130 -0
  213. package/src/session/__tests__/session-manager.test.ts +342 -0
  214. package/src/session/index.ts +7 -0
  215. package/src/session/manager.ts +332 -0
  216. package/src/types/protocol.ts +596 -0
  217. package/src/utils/__tests__/base58.test.ts +281 -0
  218. package/src/utils/__tests__/base64.test.ts +239 -0
  219. package/src/utils/__tests__/crypto-service.test.ts +530 -0
  220. package/src/utils/__tests__/did-helpers.test.ts +156 -0
  221. package/src/utils/base58.ts +115 -0
  222. package/src/utils/base64.ts +116 -0
  223. package/src/utils/crypto-service.ts +209 -0
  224. package/src/utils/did-helpers.ts +210 -0
  225. package/src/utils/ed25519-constants.ts +23 -0
  226. package/src/utils/index.ts +9 -0
@@ -0,0 +1,467 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import {
3
+ DidWebResolver,
4
+ createDidWebResolver,
5
+ isDidWeb,
6
+ parseDidWeb,
7
+ didWebToUrl,
8
+ } from '../did-web-resolver.js';
9
+ import type { FetchProvider } from '../../providers/base.js';
10
+ import type { DIDDocument } from '../vc-verifier.js';
11
+
12
+ /**
13
+ * Tests for did:web resolver
14
+ *
15
+ * These tests verify the did:web resolution functionality:
16
+ * - URL construction for root domain DIDs
17
+ * - URL construction for path-based DIDs
18
+ * - Successful resolution with mocked fetch
19
+ * - Null return on 404
20
+ * - Null return on invalid JSON
21
+ * - Null return on missing `id` field in response
22
+ */
23
+
24
+ describe('did:web URL Construction', () => {
25
+ describe('isDidWeb', () => {
26
+ it('should return true for did:web DIDs', () => {
27
+ expect(isDidWeb('did:web:example.com')).toBe(true);
28
+ expect(isDidWeb('did:web:example.com:path')).toBe(true);
29
+ expect(isDidWeb('did:web:sub.example.com')).toBe(true);
30
+ });
31
+
32
+ it('should return false for non-did:web DIDs', () => {
33
+ expect(isDidWeb('did:key:z6Mk...')).toBe(false);
34
+ expect(isDidWeb('did:example:123')).toBe(false);
35
+ expect(isDidWeb('not-a-did')).toBe(false);
36
+ expect(isDidWeb('')).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe('parseDidWeb', () => {
41
+ it('should parse root domain DID', () => {
42
+ const result = parseDidWeb('did:web:example.com');
43
+ expect(result).not.toBeNull();
44
+ expect(result?.domain).toBe('example.com');
45
+ expect(result?.path).toEqual([]);
46
+ });
47
+
48
+ it('should parse path-based DID', () => {
49
+ const result = parseDidWeb('did:web:example.com:path:to:doc');
50
+ expect(result).not.toBeNull();
51
+ expect(result?.domain).toBe('example.com');
52
+ expect(result?.path).toEqual(['path', 'to', 'doc']);
53
+ });
54
+
55
+ it('should handle URL-encoded components', () => {
56
+ // %3A is URL-encoded colon, which would be used for port numbers
57
+ const result = parseDidWeb('did:web:example.com%3A8080');
58
+ expect(result).not.toBeNull();
59
+ expect(result?.domain).toBe('example.com:8080');
60
+ expect(result?.path).toEqual([]);
61
+ });
62
+
63
+ it('should return null for invalid DIDs', () => {
64
+ expect(parseDidWeb('did:key:z6Mk...')).toBeNull();
65
+ expect(parseDidWeb('did:web:')).toBeNull();
66
+ expect(parseDidWeb('')).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe('didWebToUrl', () => {
71
+ it('should convert root domain DID to .well-known URL', () => {
72
+ const url = didWebToUrl('did:web:example.com');
73
+ expect(url).toBe('https://example.com/.well-known/did.json');
74
+ });
75
+
76
+ it('should convert subdomain DID to .well-known URL', () => {
77
+ const url = didWebToUrl('did:web:agents.example.com');
78
+ expect(url).toBe('https://agents.example.com/.well-known/did.json');
79
+ });
80
+
81
+ it('should convert single path component DID to path URL', () => {
82
+ const url = didWebToUrl('did:web:example.com:user');
83
+ expect(url).toBe('https://example.com/user/did.json');
84
+ });
85
+
86
+ it('should convert multi-path DID to path URL', () => {
87
+ const url = didWebToUrl('did:web:example.com:path:to:doc');
88
+ expect(url).toBe('https://example.com/path/to/doc/did.json');
89
+ });
90
+
91
+ it('should handle agents path pattern', () => {
92
+ const url = didWebToUrl('did:web:example.com:agents:bot1');
93
+ expect(url).toBe('https://example.com/agents/bot1/did.json');
94
+ });
95
+
96
+ it('should handle URL-encoded port number', () => {
97
+ const url = didWebToUrl('did:web:example.com%3A8080');
98
+ expect(url).toBe('https://example.com:8080/.well-known/did.json');
99
+ });
100
+
101
+ it('should handle URL-encoded port number with path', () => {
102
+ const url = didWebToUrl('did:web:example.com%3A3000:users:alice');
103
+ expect(url).toBe('https://example.com:3000/users/alice/did.json');
104
+ });
105
+
106
+ it('should return null for invalid DIDs', () => {
107
+ expect(didWebToUrl('did:key:z6Mk...')).toBeNull();
108
+ expect(didWebToUrl('did:web:')).toBeNull();
109
+ expect(didWebToUrl('')).toBeNull();
110
+ });
111
+ });
112
+ });
113
+
114
+ describe('DidWebResolver', () => {
115
+ let mockFetchProvider: FetchProvider;
116
+ let resolver: DidWebResolver;
117
+
118
+ const validDIDDocument: DIDDocument = {
119
+ id: 'did:web:example.com',
120
+ verificationMethod: [
121
+ {
122
+ id: 'did:web:example.com#key-1',
123
+ type: 'Ed25519VerificationKey2020',
124
+ controller: 'did:web:example.com',
125
+ publicKeyJwk: {
126
+ kty: 'OKP',
127
+ crv: 'Ed25519',
128
+ x: 'test-public-key-base64url',
129
+ },
130
+ },
131
+ ],
132
+ authentication: ['did:web:example.com#key-1'],
133
+ assertionMethod: ['did:web:example.com#key-1'],
134
+ };
135
+
136
+ const createMockFetchProvider = (
137
+ responseBody: unknown,
138
+ status = 200
139
+ ): FetchProvider => {
140
+ return {
141
+ resolveDID: vi.fn(),
142
+ fetchStatusList: vi.fn(),
143
+ fetchDelegationChain: vi.fn(),
144
+ fetch: vi.fn().mockResolvedValue({
145
+ ok: status >= 200 && status < 300,
146
+ status,
147
+ json: vi.fn().mockResolvedValue(responseBody),
148
+ }),
149
+ };
150
+ };
151
+
152
+ const createMockFetchProviderWithJsonError = (): FetchProvider => {
153
+ return {
154
+ resolveDID: vi.fn(),
155
+ fetchStatusList: vi.fn(),
156
+ fetchDelegationChain: vi.fn(),
157
+ fetch: vi.fn().mockResolvedValue({
158
+ ok: true,
159
+ status: 200,
160
+ json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
161
+ }),
162
+ };
163
+ };
164
+
165
+ const createMockFetchProviderWithNetworkError = (): FetchProvider => {
166
+ return {
167
+ resolveDID: vi.fn(),
168
+ fetchStatusList: vi.fn(),
169
+ fetchDelegationChain: vi.fn(),
170
+ fetch: vi.fn().mockRejectedValue(new Error('Network error')),
171
+ };
172
+ };
173
+
174
+ beforeEach(() => {
175
+ mockFetchProvider = createMockFetchProvider(validDIDDocument);
176
+ resolver = new DidWebResolver(mockFetchProvider, { cacheTtl: 1000 });
177
+ });
178
+
179
+ describe('successful resolution', () => {
180
+ it('should resolve did:web:example.com', async () => {
181
+ const result = await resolver.resolve('did:web:example.com');
182
+
183
+ expect(result).not.toBeNull();
184
+ expect(result?.id).toBe('did:web:example.com');
185
+ expect(result?.verificationMethod).toHaveLength(1);
186
+ expect(result?.verificationMethod?.[0]?.type).toBe('Ed25519VerificationKey2020');
187
+ expect(mockFetchProvider.fetch).toHaveBeenCalledWith(
188
+ 'https://example.com/.well-known/did.json'
189
+ );
190
+ });
191
+
192
+ it('should resolve path-based DID', async () => {
193
+ const pathDIDDocument: DIDDocument = {
194
+ ...validDIDDocument,
195
+ id: 'did:web:example.com:agents:bot1',
196
+ verificationMethod: [
197
+ {
198
+ ...validDIDDocument.verificationMethod![0]!,
199
+ id: 'did:web:example.com:agents:bot1#key-1',
200
+ controller: 'did:web:example.com:agents:bot1',
201
+ },
202
+ ],
203
+ };
204
+
205
+ mockFetchProvider = createMockFetchProvider(pathDIDDocument);
206
+ resolver = new DidWebResolver(mockFetchProvider);
207
+
208
+ const result = await resolver.resolve('did:web:example.com:agents:bot1');
209
+
210
+ expect(result).not.toBeNull();
211
+ expect(result?.id).toBe('did:web:example.com:agents:bot1');
212
+ expect(mockFetchProvider.fetch).toHaveBeenCalledWith(
213
+ 'https://example.com/agents/bot1/did.json'
214
+ );
215
+ });
216
+
217
+ it('should cache successful resolutions', async () => {
218
+ await resolver.resolve('did:web:example.com');
219
+ await resolver.resolve('did:web:example.com');
220
+
221
+ // Should only fetch once due to caching
222
+ expect(mockFetchProvider.fetch).toHaveBeenCalledTimes(1);
223
+ });
224
+
225
+ it('should return cached result', async () => {
226
+ const result1 = await resolver.resolve('did:web:example.com');
227
+ const result2 = await resolver.resolve('did:web:example.com');
228
+
229
+ expect(result1).toEqual(result2);
230
+ });
231
+ });
232
+
233
+ describe('null return on 404', () => {
234
+ it('should return null for 404 response', async () => {
235
+ mockFetchProvider = createMockFetchProvider({}, 404);
236
+ resolver = new DidWebResolver(mockFetchProvider);
237
+
238
+ const result = await resolver.resolve('did:web:example.com');
239
+
240
+ expect(result).toBeNull();
241
+ });
242
+
243
+ it('should return null for 500 response', async () => {
244
+ mockFetchProvider = createMockFetchProvider({}, 500);
245
+ resolver = new DidWebResolver(mockFetchProvider);
246
+
247
+ const result = await resolver.resolve('did:web:example.com');
248
+
249
+ expect(result).toBeNull();
250
+ });
251
+ });
252
+
253
+ describe('null return on invalid JSON', () => {
254
+ it('should return null when JSON parsing fails', async () => {
255
+ mockFetchProvider = createMockFetchProviderWithJsonError();
256
+ resolver = new DidWebResolver(mockFetchProvider);
257
+
258
+ const result = await resolver.resolve('did:web:example.com');
259
+
260
+ expect(result).toBeNull();
261
+ });
262
+ });
263
+
264
+ describe('null return on missing id field', () => {
265
+ it('should return null when id field is missing', async () => {
266
+ const invalidDocument = {
267
+ verificationMethod: [],
268
+ };
269
+
270
+ mockFetchProvider = createMockFetchProvider(invalidDocument);
271
+ resolver = new DidWebResolver(mockFetchProvider);
272
+
273
+ const result = await resolver.resolve('did:web:example.com');
274
+
275
+ expect(result).toBeNull();
276
+ });
277
+
278
+ it('should return null when id field is empty string', async () => {
279
+ const invalidDocument = {
280
+ id: '',
281
+ verificationMethod: [],
282
+ };
283
+
284
+ mockFetchProvider = createMockFetchProvider(invalidDocument);
285
+ resolver = new DidWebResolver(mockFetchProvider);
286
+
287
+ const result = await resolver.resolve('did:web:example.com');
288
+
289
+ expect(result).toBeNull();
290
+ });
291
+
292
+ it('should return null when id does not match requested DID', async () => {
293
+ const mismatchedDocument: DIDDocument = {
294
+ ...validDIDDocument,
295
+ id: 'did:web:other.com',
296
+ };
297
+
298
+ mockFetchProvider = createMockFetchProvider(mismatchedDocument);
299
+ resolver = new DidWebResolver(mockFetchProvider);
300
+
301
+ const result = await resolver.resolve('did:web:example.com');
302
+
303
+ expect(result).toBeNull();
304
+ });
305
+ });
306
+
307
+ describe('null return on network error', () => {
308
+ it('should return null when fetch throws', async () => {
309
+ mockFetchProvider = createMockFetchProviderWithNetworkError();
310
+ resolver = new DidWebResolver(mockFetchProvider);
311
+
312
+ const result = await resolver.resolve('did:web:example.com');
313
+
314
+ expect(result).toBeNull();
315
+ });
316
+ });
317
+
318
+ describe('null return for non-did:web', () => {
319
+ it('should return null for did:key', async () => {
320
+ const result = await resolver.resolve(
321
+ 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'
322
+ );
323
+
324
+ expect(result).toBeNull();
325
+ expect(mockFetchProvider.fetch).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it('should return null for invalid DID format', async () => {
329
+ const result = await resolver.resolve('not-a-did');
330
+
331
+ expect(result).toBeNull();
332
+ expect(mockFetchProvider.fetch).not.toHaveBeenCalled();
333
+ });
334
+ });
335
+
336
+ describe('invalid DID document structure', () => {
337
+ it('should return null when response is not an object', async () => {
338
+ mockFetchProvider = createMockFetchProvider('not an object');
339
+ resolver = new DidWebResolver(mockFetchProvider);
340
+
341
+ const result = await resolver.resolve('did:web:example.com');
342
+
343
+ expect(result).toBeNull();
344
+ });
345
+
346
+ it('should return null when response is null', async () => {
347
+ mockFetchProvider = createMockFetchProvider(null);
348
+ resolver = new DidWebResolver(mockFetchProvider);
349
+
350
+ const result = await resolver.resolve('did:web:example.com');
351
+
352
+ expect(result).toBeNull();
353
+ });
354
+
355
+ it('should return null when verificationMethod is not an array', async () => {
356
+ const invalidDocument = {
357
+ id: 'did:web:example.com',
358
+ verificationMethod: 'not an array',
359
+ };
360
+
361
+ mockFetchProvider = createMockFetchProvider(invalidDocument);
362
+ resolver = new DidWebResolver(mockFetchProvider);
363
+
364
+ const result = await resolver.resolve('did:web:example.com');
365
+
366
+ expect(result).toBeNull();
367
+ });
368
+
369
+ it('should return null when verificationMethod entry is invalid', async () => {
370
+ const invalidDocument = {
371
+ id: 'did:web:example.com',
372
+ verificationMethod: [
373
+ {
374
+ // Missing required id field
375
+ type: 'Ed25519VerificationKey2020',
376
+ controller: 'did:web:example.com',
377
+ },
378
+ ],
379
+ };
380
+
381
+ mockFetchProvider = createMockFetchProvider(invalidDocument);
382
+ resolver = new DidWebResolver(mockFetchProvider);
383
+
384
+ const result = await resolver.resolve('did:web:example.com');
385
+
386
+ expect(result).toBeNull();
387
+ });
388
+ });
389
+
390
+ describe('cache management', () => {
391
+ it('should clear all cache entries', async () => {
392
+ await resolver.resolve('did:web:example.com');
393
+
394
+ resolver.clearCache();
395
+
396
+ // Next call should fetch again
397
+ await resolver.resolve('did:web:example.com');
398
+
399
+ expect(mockFetchProvider.fetch).toHaveBeenCalledTimes(2);
400
+ });
401
+
402
+ it('should clear specific cache entry', async () => {
403
+ await resolver.resolve('did:web:example.com');
404
+
405
+ resolver.clearCacheEntry('did:web:example.com');
406
+
407
+ // Next call should fetch again
408
+ await resolver.resolve('did:web:example.com');
409
+
410
+ expect(mockFetchProvider.fetch).toHaveBeenCalledTimes(2);
411
+ });
412
+
413
+ it('should expire cached entries after TTL', async () => {
414
+ // Use very short TTL
415
+ resolver = new DidWebResolver(mockFetchProvider, { cacheTtl: 10 });
416
+
417
+ await resolver.resolve('did:web:example.com');
418
+
419
+ // Wait for cache to expire
420
+ await new Promise((resolve) => setTimeout(resolve, 15));
421
+
422
+ await resolver.resolve('did:web:example.com');
423
+
424
+ expect(mockFetchProvider.fetch).toHaveBeenCalledTimes(2);
425
+ });
426
+ });
427
+ });
428
+
429
+ describe('createDidWebResolver', () => {
430
+ it('should create a resolver instance', () => {
431
+ const mockFetchProvider: FetchProvider = {
432
+ resolveDID: vi.fn(),
433
+ fetchStatusList: vi.fn(),
434
+ fetchDelegationChain: vi.fn(),
435
+ fetch: vi.fn(),
436
+ };
437
+
438
+ const resolver = createDidWebResolver(mockFetchProvider);
439
+
440
+ expect(resolver).toBeDefined();
441
+ expect(typeof resolver.resolve).toBe('function');
442
+ });
443
+
444
+ it('should pass options to resolver', async () => {
445
+ const validDIDDocument: DIDDocument = {
446
+ id: 'did:web:example.com',
447
+ verificationMethod: [],
448
+ };
449
+
450
+ const mockFetchProvider: FetchProvider = {
451
+ resolveDID: vi.fn(),
452
+ fetchStatusList: vi.fn(),
453
+ fetchDelegationChain: vi.fn(),
454
+ fetch: vi.fn().mockResolvedValue({
455
+ ok: true,
456
+ status: 200,
457
+ json: vi.fn().mockResolvedValue(validDIDDocument),
458
+ }),
459
+ };
460
+
461
+ const resolver = createDidWebResolver(mockFetchProvider, { cacheTtl: 5000 });
462
+
463
+ const result = await resolver.resolve('did:web:example.com');
464
+
465
+ expect(result).not.toBeNull();
466
+ });
467
+ });
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Outbound Delegation Headers Tests
3
+ *
4
+ * Tests for buildOutboundDelegationHeaders utility.
5
+ */
6
+
7
+ import { describe, it, expect, beforeAll } from 'vitest';
8
+ import { decodeJwt, decodeProtectedHeader } from 'jose';
9
+ import {
10
+ buildOutboundDelegationHeaders,
11
+ type OutboundDelegationContext,
12
+ } from '../outbound-headers.js';
13
+ import type { SessionContext, DelegationRecord } from '../../types/protocol.js';
14
+ import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
15
+ import { generateDidKeyFromBase64 } from '../../utils/did-helpers.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Test fixtures
19
+ // ---------------------------------------------------------------------------
20
+
21
+ let cryptoProvider: NodeCryptoProvider;
22
+ let serverKeyPair: { privateKey: string; publicKey: string };
23
+ let serverDid: string;
24
+ let serverKid: string;
25
+ let agentKeyPair: { privateKey: string; publicKey: string };
26
+ let agentDid: string;
27
+
28
+ beforeAll(async () => {
29
+ cryptoProvider = new NodeCryptoProvider();
30
+
31
+ // Generate server identity
32
+ serverKeyPair = await cryptoProvider.generateKeyPair();
33
+ serverDid = generateDidKeyFromBase64(serverKeyPair.publicKey);
34
+ serverKid = `${serverDid}#keys-1`;
35
+
36
+ // Generate agent identity
37
+ agentKeyPair = await cryptoProvider.generateKeyPair();
38
+ agentDid = generateDidKeyFromBase64(agentKeyPair.publicKey);
39
+ });
40
+
41
+ function createTestSession(overrides: Partial<SessionContext> = {}): SessionContext {
42
+ return {
43
+ sessionId: 'mcpi_test-session-123',
44
+ audience: 'did:web:my-mcp-server.example.com',
45
+ nonce: 'test-nonce-abc',
46
+ timestamp: Math.floor(Date.now() / 1000),
47
+ createdAt: Date.now(),
48
+ lastActivity: Date.now(),
49
+ ttlMinutes: 30,
50
+ identityState: 'authenticated',
51
+ agentDid,
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ function createTestDelegation(overrides: Partial<DelegationRecord> = {}): DelegationRecord {
57
+ return {
58
+ id: 'del-test-123',
59
+ issuerDid: serverDid,
60
+ subjectDid: agentDid,
61
+ vcId: 'urn:uuid:vc-test-456',
62
+ constraints: {
63
+ scopes: ['tool:execute', 'resource:read'],
64
+ },
65
+ signature: 'test-signature',
66
+ status: 'active',
67
+ createdAt: Date.now(),
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ function createTestContext(
73
+ overrides: Partial<OutboundDelegationContext> = {}
74
+ ): OutboundDelegationContext {
75
+ return {
76
+ session: createTestSession(),
77
+ delegation: createTestDelegation(),
78
+ serverIdentity: {
79
+ did: serverDid,
80
+ kid: serverKid,
81
+ privateKey: serverKeyPair.privateKey,
82
+ },
83
+ targetUrl: 'https://downstream-api.example.com/resource',
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Tests
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe('buildOutboundDelegationHeaders', () => {
93
+ it('builds correct headers from a valid session + delegation', async () => {
94
+ const context = createTestContext();
95
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
96
+
97
+ expect(headers).toHaveProperty('X-Agent-DID');
98
+ expect(headers).toHaveProperty('X-Delegation-Chain');
99
+ expect(headers).toHaveProperty('X-Session-ID');
100
+ expect(headers).toHaveProperty('X-Delegation-Proof');
101
+ });
102
+
103
+ it('X-Agent-DID matches session.agentDid', async () => {
104
+ const context = createTestContext();
105
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
106
+
107
+ expect(headers['X-Agent-DID']).toBe(context.session.agentDid);
108
+ });
109
+
110
+ it('X-Delegation-Chain matches delegation.vcId', async () => {
111
+ const context = createTestContext();
112
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
113
+
114
+ expect(headers['X-Delegation-Chain']).toBe(context.delegation.vcId);
115
+ });
116
+
117
+ it('X-Session-ID matches session.sessionId', async () => {
118
+ const context = createTestContext();
119
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
120
+
121
+ expect(headers['X-Session-ID']).toBe(context.session.sessionId);
122
+ });
123
+
124
+ it('X-Delegation-Proof is a valid JWT with correct claims', async () => {
125
+ const context = createTestContext();
126
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
127
+
128
+ const jwt = headers['X-Delegation-Proof'];
129
+
130
+ // Verify it's a valid JWT format (3 parts)
131
+ expect(jwt.split('.')).toHaveLength(3);
132
+
133
+ // Verify header
134
+ const header = decodeProtectedHeader(jwt);
135
+ expect(header.alg).toBe('EdDSA');
136
+ expect(header.kid).toBe(serverKid);
137
+
138
+ // Verify payload claims
139
+ const payload = decodeJwt(jwt);
140
+ expect(payload.iss).toBe(serverDid); // server forwarding
141
+ expect(payload.sub).toBe(agentDid); // original agent
142
+ expect(payload.aud).toBe('downstream-api.example.com'); // target hostname
143
+ expect(payload.scope).toBe('delegation:propagate');
144
+ expect(typeof payload.iat).toBe('number');
145
+ expect(typeof payload.exp).toBe('number');
146
+ expect(typeof payload.jti).toBe('string');
147
+ });
148
+
149
+ it('extracts hostname correctly from full URL for aud claim', async () => {
150
+ const context = createTestContext({
151
+ targetUrl: 'https://api.service.example.com:8443/v1/resource?query=test',
152
+ });
153
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
154
+
155
+ const payload = decodeJwt(headers['X-Delegation-Proof']);
156
+ expect(payload.aud).toBe('api.service.example.com');
157
+ });
158
+
159
+ it('works with http:// URLs', async () => {
160
+ const context = createTestContext({
161
+ targetUrl: 'http://internal-service.local/api',
162
+ });
163
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
164
+
165
+ const payload = decodeJwt(headers['X-Delegation-Proof']);
166
+ expect(payload.aud).toBe('internal-service.local');
167
+ });
168
+
169
+ it('works with https:// URLs', async () => {
170
+ const context = createTestContext({
171
+ targetUrl: 'https://secure.example.org/endpoint',
172
+ });
173
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
174
+
175
+ const payload = decodeJwt(headers['X-Delegation-Proof']);
176
+ expect(payload.aud).toBe('secure.example.org');
177
+ });
178
+
179
+ it('JWT exp is 60 seconds from iat', async () => {
180
+ const context = createTestContext();
181
+ const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
182
+
183
+ const payload = decodeJwt(headers['X-Delegation-Proof']);
184
+ expect((payload.exp as number) - (payload.iat as number)).toBe(60);
185
+ });
186
+
187
+ it('throws when session is missing agentDid', async () => {
188
+ const context = createTestContext({
189
+ session: createTestSession({ agentDid: undefined }),
190
+ });
191
+
192
+ await expect(
193
+ buildOutboundDelegationHeaders(context, cryptoProvider)
194
+ ).rejects.toThrow('Session must have agentDid');
195
+ });
196
+
197
+ it('throws when session is missing sessionId', async () => {
198
+ const context = createTestContext({
199
+ session: createTestSession({ sessionId: '' }),
200
+ });
201
+
202
+ await expect(
203
+ buildOutboundDelegationHeaders(context, cryptoProvider)
204
+ ).rejects.toThrow('Session must have sessionId');
205
+ });
206
+
207
+ it('throws when delegation is missing vcId', async () => {
208
+ const context = createTestContext({
209
+ delegation: createTestDelegation({ vcId: '' }),
210
+ });
211
+
212
+ await expect(
213
+ buildOutboundDelegationHeaders(context, cryptoProvider)
214
+ ).rejects.toThrow('Delegation must have vcId');
215
+ });
216
+
217
+ it('throws for non-did:key server DID', async () => {
218
+ const context = createTestContext({
219
+ serverIdentity: {
220
+ did: 'did:web:server.example.com',
221
+ kid: 'did:web:server.example.com#key-1',
222
+ privateKey: serverKeyPair.privateKey,
223
+ },
224
+ });
225
+
226
+ await expect(
227
+ buildOutboundDelegationHeaders(context, cryptoProvider)
228
+ ).rejects.toThrow('Server DID must be did:key with Ed25519');
229
+ });
230
+ });