@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
@@ -23,13 +23,22 @@ export type { DelegationVerifier, VerifyDelegationResult };
23
23
 
24
24
  export interface AgentReputation {
25
25
  agentDid: string;
26
- score: number;
26
+ score: number | null;
27
27
  totalInteractions: number;
28
28
  successRate: number;
29
29
  riskLevel: 'low' | 'medium' | 'high' | 'unknown';
30
30
  updatedAt: number;
31
31
  }
32
32
 
33
+ /**
34
+ * Policy for handling agents with no reputation history.
35
+ *
36
+ * - 'deny' — reject unknown agents outright (strict environments)
37
+ * - 'require-consent' — route to the consent/authorization flow (default)
38
+ * - 'allow' — let unknown agents through (reputation is advisory only)
39
+ */
40
+ export type UnknownAgentPolicy = 'deny' | 'require-consent' | 'allow';
41
+
33
42
  export interface AuthHandshakeConfig {
34
43
  delegationVerifier: DelegationVerifier;
35
44
  resumeTokenStore: ResumeTokenStore;
@@ -41,7 +50,15 @@ export interface AuthHandshakeConfig {
41
50
  authorization: {
42
51
  authorizationUrl: string;
43
52
  resumeTokenTtl?: number;
44
- requireAuthForUnknown?: boolean;
53
+ /**
54
+ * How to handle agents with no reputation history (404 from reputation
55
+ * service, network error, or first-time agent).
56
+ *
57
+ * - 'deny' — reject outright
58
+ * - 'require-consent' — route to consent flow (default)
59
+ * - 'allow' — skip reputation gate for unknowns
60
+ */
61
+ unknownAgentPolicy?: UnknownAgentPolicy;
45
62
  minReputationScore?: number;
46
63
  };
47
64
  debug?: boolean;
@@ -118,7 +135,10 @@ export class MemoryResumeTokenStore implements ResumeTokenStore {
118
135
  scopes: string[],
119
136
  metadata?: Record<string, unknown>
120
137
  ): Promise<string> {
121
- const token = `rt_${Date.now()}_${Math.random().toString(36).substring(2, 18)}`;
138
+ const bytes = new Uint8Array(16);
139
+ globalThis.crypto.getRandomValues(bytes);
140
+ const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
141
+ const token = `rt_${hex}`;
122
142
  const now = Date.now();
123
143
 
124
144
  this.tokens.set(token, {
@@ -182,14 +202,12 @@ export class MemoryResumeTokenStore implements ResumeTokenStore {
182
202
  * @param agentDid - The agent's DID to verify
183
203
  * @param scopes - Required scopes for the operation
184
204
  * @param config - Authorization configuration including verifier, token store, etc.
185
- * @param _resumeToken - Optional resume token from previous authorization attempt
186
205
  * @returns Result indicating authorization status, delegation, or auth hints
187
206
  */
188
207
  export async function verifyOrHints(
189
208
  agentDid: string,
190
209
  scopes: string[],
191
210
  config: AuthHandshakeConfig,
192
- _resumeToken?: string
193
211
  ): Promise<VerifyOrHintsResult> {
194
212
  const startTime = Date.now();
195
213
 
@@ -199,36 +217,85 @@ export async function verifyOrHints(
199
217
 
200
218
  let reputation: AgentReputation | undefined;
201
219
  if (config.reputationService && config.authorization.minReputationScore !== undefined) {
220
+ const unknownPolicy = config.authorization.unknownAgentPolicy ?? 'require-consent';
221
+
202
222
  try {
203
223
  reputation = await fetchAgentReputation(agentDid, config.reputationService);
224
+ } catch (error) {
225
+ logger.error('[AuthHandshake] Reputation service unreachable, treating agent as unknown:', error);
226
+ reputation = {
227
+ agentDid,
228
+ score: null,
229
+ totalInteractions: 0,
230
+ successRate: 0,
231
+ riskLevel: 'unknown',
232
+ updatedAt: Date.now(),
233
+ };
234
+ }
204
235
 
205
- if (config.debug) {
206
- logger.debug(`[AuthHandshake] Reputation score: ${reputation.score}`);
207
- }
208
-
209
- if (reputation.score < config.authorization.minReputationScore) {
210
- if (config.debug) {
211
- logger.debug(
212
- `[AuthHandshake] Reputation ${reputation.score} < ${config.authorization.minReputationScore}, requiring authorization`
213
- );
214
- }
236
+ if (config.debug) {
237
+ logger.debug(`[AuthHandshake] Reputation score: ${reputation.score}`);
238
+ }
215
239
 
240
+ // Unknown agent (no reputation data)
241
+ if (reputation.score === null) {
242
+ if (unknownPolicy === 'deny') {
216
243
  const authError = await buildNeedsAuthorizationError(
217
244
  agentDid,
218
245
  scopes,
219
246
  config,
220
- 'Agent reputation score below threshold'
247
+ 'Unknown agent denied by policy'
221
248
  );
249
+ return {
250
+ authorized: false,
251
+ authError,
252
+ reputation,
253
+ reason: 'Unknown agent — policy: deny',
254
+ };
255
+ }
222
256
 
257
+ if (unknownPolicy === 'require-consent') {
258
+ const authError = await buildNeedsAuthorizationError(
259
+ agentDid,
260
+ scopes,
261
+ config,
262
+ 'Unknown agent requires consent'
263
+ );
223
264
  return {
224
265
  authorized: false,
225
266
  authError,
226
267
  reputation,
227
- reason: 'Low reputation score',
268
+ reason: 'Unknown agent — policy: require-consent',
228
269
  };
229
270
  }
230
- } catch (error) {
231
- logger.warn('[AuthHandshake] Failed to check reputation:', error);
271
+
272
+ // unknownPolicy === 'allow' skip reputation gate, continue to delegation check
273
+ if (config.debug) {
274
+ logger.debug('[AuthHandshake] Unknown agent allowed by policy, skipping reputation gate');
275
+ }
276
+ }
277
+
278
+ // Known agent with score below threshold
279
+ if (reputation.score !== null && reputation.score < config.authorization.minReputationScore) {
280
+ if (config.debug) {
281
+ logger.debug(
282
+ `[AuthHandshake] Reputation ${reputation.score} < ${config.authorization.minReputationScore}, requiring authorization`
283
+ );
284
+ }
285
+
286
+ const authError = await buildNeedsAuthorizationError(
287
+ agentDid,
288
+ scopes,
289
+ config,
290
+ 'Agent reputation score below threshold'
291
+ );
292
+
293
+ return {
294
+ authorized: false,
295
+ authError,
296
+ reputation,
297
+ reason: 'Low reputation score',
298
+ };
232
299
  }
233
300
  }
234
301
 
@@ -322,7 +389,7 @@ async function fetchAgentReputation(
322
389
  if (response.status === 404) {
323
390
  return {
324
391
  agentDid,
325
- score: 50,
392
+ score: null,
326
393
  totalInteractions: 0,
327
394
  successRate: 0,
328
395
  riskLevel: 'unknown',
@@ -334,7 +401,7 @@ async function fetchAgentReputation(
334
401
 
335
402
  const data = (await response.json()) as Record<string, unknown>;
336
403
 
337
- const score = (data['score'] as number | undefined) ?? 50;
404
+ const score = (data['score'] as number | undefined) ?? 0;
338
405
  const levelRaw = (
339
406
  (data['level'] as string | undefined) ??
340
407
  (data['riskLevel'] as string | undefined) ??
package/src/auth/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  type VerifyOrHintsResult,
7
7
  type AgentReputation,
8
8
  type ResumeTokenStore,
9
+ type UnknownAgentPolicy,
9
10
  } from './handshake.js';
10
11
 
11
12
  export type { DelegationVerifier, VerifyDelegationResult } from './types.js';
@@ -198,8 +198,8 @@ describe("did:key Resolver", () => {
198
198
  expect(didDoc?.verificationMethod?.[0].type).toBe("Ed25519VerificationKey2020");
199
199
  expect(didDoc?.verificationMethod?.[0].controller).toBe(didKey);
200
200
  expect(didDoc?.verificationMethod?.[0].publicKeyJwk).toBeDefined();
201
- expect(didDoc?.authentication).toContain(`${didKey}#keys-1`);
202
- expect(didDoc?.assertionMethod).toContain(`${didKey}#keys-1`);
201
+ expect(didDoc?.authentication).toContain(`${didKey}#${didKey.replace('did:key:', '')}`);
202
+ expect(didDoc?.assertionMethod).toContain(`${didKey}#${didKey.replace('did:key:', '')}`);
203
203
  });
204
204
 
205
205
  it("should return null for non-Ed25519 did:key", async () => {
@@ -18,23 +18,19 @@ import { generateDidKeyFromBase64 } from '../../utils/did-helpers.js';
18
18
  // Test fixtures
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
- let cryptoProvider: NodeCryptoProvider;
22
21
  let serverKeyPair: { privateKey: string; publicKey: string };
23
22
  let serverDid: string;
24
23
  let serverKid: string;
25
- let agentKeyPair: { privateKey: string; publicKey: string };
26
24
  let agentDid: string;
27
25
 
28
26
  beforeAll(async () => {
29
- cryptoProvider = new NodeCryptoProvider();
27
+ const cryptoProvider = new NodeCryptoProvider();
30
28
 
31
- // Generate server identity
32
29
  serverKeyPair = await cryptoProvider.generateKeyPair();
33
30
  serverDid = generateDidKeyFromBase64(serverKeyPair.publicKey);
34
- serverKid = `${serverDid}#keys-1`;
31
+ serverKid = `${serverDid}#${serverDid.replace('did:key:', '')}`;
35
32
 
36
- // Generate agent identity
37
- agentKeyPair = await cryptoProvider.generateKeyPair();
33
+ const agentKeyPair = await cryptoProvider.generateKeyPair();
38
34
  agentDid = generateDidKeyFromBase64(agentKeyPair.publicKey);
39
35
  });
40
36
 
@@ -92,7 +88,7 @@ function createTestContext(
92
88
  describe('buildOutboundDelegationHeaders', () => {
93
89
  it('builds correct headers from a valid session + delegation', async () => {
94
90
  const context = createTestContext();
95
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
91
+ const headers = await buildOutboundDelegationHeaders(context);
96
92
 
97
93
  expect(headers).toHaveProperty('X-Agent-DID');
98
94
  expect(headers).toHaveProperty('X-Delegation-Chain');
@@ -102,28 +98,28 @@ describe('buildOutboundDelegationHeaders', () => {
102
98
 
103
99
  it('X-Agent-DID matches session.agentDid', async () => {
104
100
  const context = createTestContext();
105
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
101
+ const headers = await buildOutboundDelegationHeaders(context);
106
102
 
107
103
  expect(headers['X-Agent-DID']).toBe(context.session.agentDid);
108
104
  });
109
105
 
110
106
  it('X-Delegation-Chain matches delegation.vcId', async () => {
111
107
  const context = createTestContext();
112
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
108
+ const headers = await buildOutboundDelegationHeaders(context);
113
109
 
114
110
  expect(headers['X-Delegation-Chain']).toBe(context.delegation.vcId);
115
111
  });
116
112
 
117
113
  it('X-Session-ID matches session.sessionId', async () => {
118
114
  const context = createTestContext();
119
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
115
+ const headers = await buildOutboundDelegationHeaders(context);
120
116
 
121
117
  expect(headers['X-Session-ID']).toBe(context.session.sessionId);
122
118
  });
123
119
 
124
120
  it('X-Delegation-Proof is a valid JWT with correct claims', async () => {
125
121
  const context = createTestContext();
126
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
122
+ const headers = await buildOutboundDelegationHeaders(context);
127
123
 
128
124
  const jwt = headers['X-Delegation-Proof'];
129
125
 
@@ -150,7 +146,7 @@ describe('buildOutboundDelegationHeaders', () => {
150
146
  const context = createTestContext({
151
147
  targetUrl: 'https://api.service.example.com:8443/v1/resource?query=test',
152
148
  });
153
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
149
+ const headers = await buildOutboundDelegationHeaders(context);
154
150
 
155
151
  const payload = decodeJwt(headers['X-Delegation-Proof']);
156
152
  expect(payload.aud).toBe('api.service.example.com');
@@ -160,7 +156,7 @@ describe('buildOutboundDelegationHeaders', () => {
160
156
  const context = createTestContext({
161
157
  targetUrl: 'http://internal-service.local/api',
162
158
  });
163
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
159
+ const headers = await buildOutboundDelegationHeaders(context);
164
160
 
165
161
  const payload = decodeJwt(headers['X-Delegation-Proof']);
166
162
  expect(payload.aud).toBe('internal-service.local');
@@ -170,7 +166,7 @@ describe('buildOutboundDelegationHeaders', () => {
170
166
  const context = createTestContext({
171
167
  targetUrl: 'https://secure.example.org/endpoint',
172
168
  });
173
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
169
+ const headers = await buildOutboundDelegationHeaders(context);
174
170
 
175
171
  const payload = decodeJwt(headers['X-Delegation-Proof']);
176
172
  expect(payload.aud).toBe('secure.example.org');
@@ -178,7 +174,7 @@ describe('buildOutboundDelegationHeaders', () => {
178
174
 
179
175
  it('JWT exp is 60 seconds from iat', async () => {
180
176
  const context = createTestContext();
181
- const headers = await buildOutboundDelegationHeaders(context, cryptoProvider);
177
+ const headers = await buildOutboundDelegationHeaders(context);
182
178
 
183
179
  const payload = decodeJwt(headers['X-Delegation-Proof']);
184
180
  expect((payload.exp as number) - (payload.iat as number)).toBe(60);
@@ -190,7 +186,7 @@ describe('buildOutboundDelegationHeaders', () => {
190
186
  });
191
187
 
192
188
  await expect(
193
- buildOutboundDelegationHeaders(context, cryptoProvider)
189
+ buildOutboundDelegationHeaders(context)
194
190
  ).rejects.toThrow('Session must have agentDid');
195
191
  });
196
192
 
@@ -200,7 +196,7 @@ describe('buildOutboundDelegationHeaders', () => {
200
196
  });
201
197
 
202
198
  await expect(
203
- buildOutboundDelegationHeaders(context, cryptoProvider)
199
+ buildOutboundDelegationHeaders(context)
204
200
  ).rejects.toThrow('Session must have sessionId');
205
201
  });
206
202
 
@@ -210,7 +206,7 @@ describe('buildOutboundDelegationHeaders', () => {
210
206
  });
211
207
 
212
208
  await expect(
213
- buildOutboundDelegationHeaders(context, cryptoProvider)
209
+ buildOutboundDelegationHeaders(context)
214
210
  ).rejects.toThrow('Delegation must have vcId');
215
211
  });
216
212
 
@@ -224,7 +220,7 @@ describe('buildOutboundDelegationHeaders', () => {
224
220
  });
225
221
 
226
222
  await expect(
227
- buildOutboundDelegationHeaders(context, cryptoProvider)
223
+ buildOutboundDelegationHeaders(context)
228
224
  ).rejects.toThrow('Server DID must be did:key with Ed25519');
229
225
  });
230
226
  });
@@ -59,7 +59,7 @@ describe("StatusList2021Manager", () => {
59
59
  proof: {
60
60
  type: "Ed25519Signature2020",
61
61
  created: new Date().toISOString(),
62
- verificationMethod: "did:key:z6MkIssuer#keys-1",
62
+ verificationMethod: "did:key:z6MkIssuer#z6MkIssuer",
63
63
  proofPurpose: "assertionMethod",
64
64
  proofValue: "mock-proof-value",
65
65
  },
@@ -74,13 +74,13 @@ describe("StatusList2021Manager", () => {
74
74
 
75
75
  mockIdentity = {
76
76
  getDid: vi.fn().mockReturnValue("did:key:z6MkTestIssuer"),
77
- getKeyId: vi.fn().mockReturnValue("did:key:z6MkTestIssuer#keys-1"),
77
+ getKeyId: vi.fn().mockReturnValue("did:key:z6MkTestIssuer#z6MkTestIssuer"),
78
78
  };
79
79
 
80
80
  mockSigningFunction = vi.fn().mockResolvedValue({
81
81
  type: "Ed25519Signature2020",
82
82
  created: new Date().toISOString(),
83
- verificationMethod: "did:key:z6MkTestIssuer#keys-1",
83
+ verificationMethod: "did:key:z6MkTestIssuer#z6MkTestIssuer",
84
84
  proofPurpose: "assertionMethod",
85
85
  proofValue: "mock-signature",
86
86
  });
@@ -405,7 +405,7 @@ describe("StatusList2021Manager", () => {
405
405
  expect(isRevoked).toBe(true);
406
406
  });
407
407
 
408
- it("should return false if status list doesn't exist", async () => {
408
+ it("should throw if status list doesn't exist (fail closed)", async () => {
409
409
  const manager = new StatusList2021Manager(
410
410
  mockStorage,
411
411
  mockIdentity,
@@ -424,9 +424,9 @@ describe("StatusList2021Manager", () => {
424
424
  statusListCredential: "https://status.example.com/revocation/v1",
425
425
  };
426
426
 
427
- const isRevoked = await manager.checkStatus(credentialStatus);
428
-
429
- expect(isRevoked).toBe(false);
427
+ await expect(
428
+ manager.checkStatus(credentialStatus)
429
+ ).rejects.toThrow("Status list not found");
430
430
  });
431
431
  });
432
432
 
@@ -512,4 +512,117 @@ describe("StatusList2021Manager", () => {
512
512
  expect(manager.getStatusListBaseUrl()).toBe("https://status.example.com");
513
513
  });
514
514
  });
515
+
516
+ describe("checkStatus fail-closed behavior", () => {
517
+ it("should throw with the status list URL in the error message", async () => {
518
+ const manager = new StatusList2021Manager(
519
+ mockStorage,
520
+ mockIdentity,
521
+ mockSigningFunction,
522
+ mockCompressor,
523
+ mockDecompressor
524
+ );
525
+
526
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
527
+
528
+ const statusListUrl = "https://status.example.com/revocation/v1";
529
+ const credentialStatus: CredentialStatus = {
530
+ id: `${statusListUrl}#5`,
531
+ type: "StatusList2021Entry",
532
+ statusPurpose: "revocation",
533
+ statusListIndex: "5",
534
+ statusListCredential: statusListUrl,
535
+ };
536
+
537
+ await expect(manager.checkStatus(credentialStatus)).rejects.toThrow(
538
+ statusListUrl
539
+ );
540
+ });
541
+
542
+ it("should throw when storage provider rejects", async () => {
543
+ const manager = new StatusList2021Manager(
544
+ mockStorage,
545
+ mockIdentity,
546
+ mockSigningFunction,
547
+ mockCompressor,
548
+ mockDecompressor
549
+ );
550
+
551
+ vi.mocked(mockStorage.getStatusList).mockRejectedValue(
552
+ new Error("Redis connection refused")
553
+ );
554
+
555
+ const credentialStatus: CredentialStatus = {
556
+ id: "https://status.example.com/revocation/v1#5",
557
+ type: "StatusList2021Entry",
558
+ statusPurpose: "revocation",
559
+ statusListIndex: "5",
560
+ statusListCredential: "https://status.example.com/revocation/v1",
561
+ };
562
+
563
+ await expect(manager.checkStatus(credentialStatus)).rejects.toThrow(
564
+ "Redis connection refused"
565
+ );
566
+ });
567
+
568
+ it("should still return false for non-revoked credential with valid storage", async () => {
569
+ const manager = new StatusList2021Manager(
570
+ mockStorage,
571
+ mockIdentity,
572
+ mockSigningFunction,
573
+ mockCompressor,
574
+ mockDecompressor
575
+ );
576
+
577
+ const statusListId = "https://status.example.com/revocation/v1";
578
+ const existingCredential = createStatusListCredential(
579
+ statusListId,
580
+ "revocation"
581
+ );
582
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(existingCredential);
583
+
584
+ const credentialStatus: CredentialStatus = {
585
+ id: `${statusListId}#3`,
586
+ type: "StatusList2021Entry",
587
+ statusPurpose: "revocation",
588
+ statusListIndex: "3",
589
+ statusListCredential: statusListId,
590
+ };
591
+
592
+ const isRevoked = await manager.checkStatus(credentialStatus);
593
+ expect(isRevoked).toBe(false);
594
+ });
595
+
596
+ it("should still return true for revoked credential with valid storage", async () => {
597
+ const manager = new StatusList2021Manager(
598
+ mockStorage,
599
+ mockIdentity,
600
+ mockSigningFunction,
601
+ mockCompressor,
602
+ mockDecompressor
603
+ );
604
+
605
+ const statusListId = "https://status.example.com/revocation/v1";
606
+ const bytes = new Uint8Array(2);
607
+ bytes[0] = 0b00001000; // Bit 3 set
608
+ const encodedList = Buffer.from(bytes).toString("base64url");
609
+ const existingCredential = createStatusListCredential(
610
+ statusListId,
611
+ "revocation",
612
+ encodedList
613
+ );
614
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(existingCredential);
615
+
616
+ const credentialStatus: CredentialStatus = {
617
+ id: `${statusListId}#3`,
618
+ type: "StatusList2021Entry",
619
+ statusPurpose: "revocation",
620
+ statusListIndex: "3",
621
+ statusListCredential: statusListId,
622
+ };
623
+
624
+ const isRevoked = await manager.checkStatus(credentialStatus);
625
+ expect(isRevoked).toBe(true);
626
+ });
627
+ });
515
628
  });
@@ -471,7 +471,7 @@ describe("DelegationCredentialVerifier", () => {
471
471
  expect(mockStatusListResolver.checkStatus).not.toHaveBeenCalled();
472
472
  });
473
473
 
474
- it("should skip status checking when no resolver available", async () => {
474
+ it("should fail closed when no status resolver available but credential has credentialStatus", async () => {
475
475
  await setupDefaultContractsMocks();
476
476
  const verifierWithoutResolver = new DelegationCredentialVerifier({
477
477
  didResolver: mockDidResolver,
@@ -506,8 +506,9 @@ describe("DelegationCredentialVerifier", () => {
506
506
  const result =
507
507
  await verifierWithoutResolver.verifyDelegationCredential(vcWithStatus);
508
508
 
509
- expect(result.valid).toBe(true);
510
- expect(result.checks?.statusValid).toBe(true); // Trust but don't verify
509
+ expect(result.valid).toBe(false);
510
+ expect(result.checks?.statusValid).toBe(false);
511
+ expect(result.reason).toContain("no status list resolver is configured");
511
512
  });
512
513
 
513
514
  it("should fail when credential is revoked", async () => {
@@ -1026,4 +1027,45 @@ describe("DelegationCredentialVerifier", () => {
1026
1027
  });
1027
1028
  });
1028
1029
  });
1030
+
1031
+ describe("E2E: StatusList2021 missing storage → verifier rejects", () => {
1032
+ it("should return valid: false when status list resolver throws (missing storage)", async () => {
1033
+ await setupDefaultContractsMocks();
1034
+ const vcWithStatus = {
1035
+ ...mockValidVC,
1036
+ credentialStatus: {
1037
+ id: "https://example.com/status#123",
1038
+ type: "StatusList2021Entry" as const,
1039
+ statusPurpose: "revocation" as const,
1040
+ statusListIndex: "123",
1041
+ statusListCredential: "https://example.com/status",
1042
+ },
1043
+ };
1044
+
1045
+ mockDidResolver.resolve.mockResolvedValue({
1046
+ id: "did:web:example.com:issuer",
1047
+ verificationMethod: [
1048
+ {
1049
+ id: "did:web:example.com:issuer#key-1",
1050
+ type: "Ed25519VerificationKey2020",
1051
+ controller: "did:web:example.com:issuer",
1052
+ publicKeyJwk: { kty: "OKP", crv: "Ed25519", x: "mock-key" },
1053
+ },
1054
+ ],
1055
+ });
1056
+ mockSignatureVerifier.mockResolvedValue({ valid: true });
1057
+
1058
+ // Simulate StatusList2021Manager.checkStatus throwing on missing storage
1059
+ mockStatusListResolver.checkStatus.mockRejectedValue(
1060
+ new Error("Status list not found: https://example.com/status — cannot determine revocation status")
1061
+ );
1062
+
1063
+ const result = await verifier.verifyDelegationCredential(vcWithStatus);
1064
+
1065
+ expect(result.valid).toBe(false);
1066
+ expect(result.reason).toContain("Status list not found");
1067
+ expect(result.reason).toContain("cannot determine revocation status");
1068
+ expect(result.checks?.statusValid).toBe(false);
1069
+ });
1070
+ });
1029
1071
  });
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { base58Decode } from '../utils/base58.js';
18
+ import { didKeyFragment } from '../utils/did-helpers.js';
18
19
  import { base64urlEncodeFromBytes } from '../utils/base64.js';
19
20
  import type { DIDResolver, DIDDocument, VerificationMethod } from './vc-verifier.js';
20
21
  import { logger } from '../logging/index.js';
@@ -124,8 +125,10 @@ export function createDidKeyResolver(): DIDResolver {
124
125
  const multibaseKey = did.replace('did:key:', '');
125
126
 
126
127
  // Construct the verification method
128
+ const fragment = didKeyFragment(did);
129
+
127
130
  const verificationMethod: VerificationMethod = {
128
- id: `${did}#keys-1`,
131
+ id: `${did}#${fragment}`,
129
132
  type: 'Ed25519VerificationKey2020',
130
133
  controller: did,
131
134
  publicKeyJwk,
@@ -136,8 +139,8 @@ export function createDidKeyResolver(): DIDResolver {
136
139
  return {
137
140
  id: did,
138
141
  verificationMethod: [verificationMethod],
139
- authentication: [`${did}#keys-1`],
140
- assertionMethod: [`${did}#keys-1`],
142
+ authentication: [`${did}#${fragment}`],
143
+ assertionMethod: [`${did}#${fragment}`],
141
144
  };
142
145
  },
143
146
  };
@@ -164,8 +167,10 @@ export function resolveDidKeySync(did: string): DIDDocument | null {
164
167
  const publicKeyJwk = publicKeyToJwk(publicKeyBytes);
165
168
  const multibaseKey = did.replace('did:key:', '');
166
169
 
170
+ const fragment = didKeyFragment(did);
171
+
167
172
  const verificationMethod: VerificationMethod = {
168
- id: `${did}#keys-1`,
173
+ id: `${did}#${fragment}`,
169
174
  type: 'Ed25519VerificationKey2020',
170
175
  controller: did,
171
176
  publicKeyJwk,
@@ -175,7 +180,7 @@ export function resolveDidKeySync(did: string): DIDDocument | null {
175
180
  return {
176
181
  id: did,
177
182
  verificationMethod: [verificationMethod],
178
- authentication: [`${did}#keys-1`],
179
- assertionMethod: [`${did}#keys-1`],
183
+ authentication: [`${did}#${fragment}`],
184
+ assertionMethod: [`${did}#${fragment}`],
180
185
  };
181
186
  }
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  import type { SessionContext, DelegationRecord } from '../types/protocol.js';
17
- import type { CryptoProvider } from '../providers/base.js';
18
17
  import { buildDelegationProofJWT, type Ed25519PrivateJWK } from './outbound-proof.js';
19
18
  import { extractPublicKeyFromDidKey, isEd25519DidKey } from './did-key-resolver.js';
20
19
  import { base64ToBytes, base64urlEncodeFromBytes } from '../utils/base64.js';
@@ -112,7 +111,6 @@ function buildPrivateKeyJwk(
112
111
  * downstream service can independently verify the delegation chain.
113
112
  *
114
113
  * @param context - The delegation context including session, delegation, and server identity
115
- * @param _cryptoProvider - CryptoProvider (reserved for future use)
116
114
  * @returns Headers object to attach to the outbound request
117
115
  *
118
116
  * @throws {Error} If session is missing agentDid or sessionId
@@ -126,7 +124,7 @@ function buildPrivateKeyJwk(
126
124
  * delegation,
127
125
  * serverIdentity: { did: serverDid, kid: serverKid, privateKey },
128
126
  * targetUrl: 'https://downstream-api.example.com/resource',
129
- * }, cryptoProvider);
127
+ * });
130
128
  *
131
129
  * // Attach headers to your HTTP request
132
130
  * fetch(targetUrl, { headers });
@@ -134,7 +132,6 @@ function buildPrivateKeyJwk(
134
132
  */
135
133
  export async function buildOutboundDelegationHeaders(
136
134
  context: OutboundDelegationContext,
137
- _cryptoProvider: CryptoProvider
138
135
  ): Promise<OutboundDelegationHeaders> {
139
136
  const { session, delegation, serverIdentity, targetUrl } = context;
140
137
 
@@ -112,7 +112,9 @@ export class StatusList2021Manager {
112
112
 
113
113
  const statusList = await this.storage.getStatusList(statusListCredential);
114
114
  if (!statusList) {
115
- return false;
115
+ throw new Error(
116
+ `Status list not found: ${statusListCredential} — cannot determine revocation status`
117
+ );
116
118
  }
117
119
 
118
120
  const manager = await BitstringManager.decode(
@@ -350,8 +350,9 @@ export class DelegationCredentialVerifier {
350
350
  try {
351
351
  if (!statusListResolver) {
352
352
  return {
353
- valid: true,
354
- reason: "No status list resolver available, skipping status check",
353
+ valid: false,
354
+ reason:
355
+ "Credential has credentialStatus but no status list resolver is configured — cannot verify revocation status",
355
356
  durationMs: Date.now() - startTime,
356
357
  };
357
358
  }