@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.
- package/README.md +123 -333
- package/dist/auth/handshake.d.ts +19 -4
- package/dist/auth/handshake.d.ts.map +1 -1
- package/dist/auth/handshake.js +52 -15
- package/dist/auth/handshake.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/delegation/did-key-resolver.d.ts.map +1 -1
- package/dist/delegation/did-key-resolver.js +9 -6
- package/dist/delegation/did-key-resolver.js.map +1 -1
- package/dist/delegation/outbound-headers.d.ts +2 -4
- package/dist/delegation/outbound-headers.d.ts.map +1 -1
- package/dist/delegation/outbound-headers.js +2 -3
- package/dist/delegation/outbound-headers.js.map +1 -1
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +1 -1
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/delegation/vc-verifier.d.ts.map +1 -1
- package/dist/delegation/vc-verifier.js +2 -2
- package/dist/delegation/vc-verifier.js.map +1 -1
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/mcpi-transport.d.ts +39 -0
- package/dist/middleware/mcpi-transport.d.ts.map +1 -0
- package/dist/middleware/mcpi-transport.js +121 -0
- package/dist/middleware/mcpi-transport.js.map +1 -0
- package/dist/middleware/with-mcpi-server.d.ts +25 -9
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
- package/dist/middleware/with-mcpi-server.js +62 -47
- package/dist/middleware/with-mcpi-server.js.map +1 -1
- package/dist/middleware/with-mcpi.d.ts +26 -5
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +108 -10
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/dist/providers/memory.js +2 -2
- package/dist/providers/memory.js.map +1 -1
- package/dist/session/manager.d.ts +7 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +20 -4
- package/dist/session/manager.js.map +1 -1
- package/dist/utils/crypto-service.d.ts.map +1 -1
- package/dist/utils/crypto-service.js +11 -10
- package/dist/utils/crypto-service.js.map +1 -1
- package/dist/utils/did-helpers.d.ts +12 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +18 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/errors.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +1 -1
- package/src/__tests__/integration/mcp-enhance-server.test.ts +48 -5
- package/src/__tests__/integration/mcp-transport-context7.test.ts +19 -15
- package/src/__tests__/integration/mcp-transport.test.ts +13 -10
- package/src/__tests__/providers/base.test.ts +1 -1
- package/src/__tests__/providers/memory.test.ts +2 -2
- package/src/__tests__/utils/mock-providers.ts +2 -2
- package/src/auth/__tests__/handshake.test.ts +190 -0
- package/src/auth/handshake.ts +88 -21
- package/src/auth/index.ts +1 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +2 -2
- package/src/delegation/__tests__/outbound-headers.test.ts +16 -20
- package/src/delegation/__tests__/statuslist-manager.test.ts +120 -7
- package/src/delegation/__tests__/vc-verifier.test.ts +45 -3
- package/src/delegation/did-key-resolver.ts +11 -6
- package/src/delegation/outbound-headers.ts +1 -4
- package/src/delegation/statuslist-manager.ts +3 -1
- package/src/delegation/vc-verifier.ts +3 -2
- package/src/errors.ts +65 -0
- package/src/index.ts +10 -0
- package/src/middleware/__tests__/mcpi-transport.test.ts +150 -0
- package/src/middleware/__tests__/with-mcpi-server.test.ts +117 -0
- package/src/middleware/__tests__/with-mcpi.test.ts +124 -6
- package/src/middleware/index.ts +6 -0
- package/src/middleware/mcpi-transport.ts +162 -0
- package/src/middleware/with-mcpi-server.ts +83 -92
- package/src/middleware/with-mcpi.ts +147 -11
- package/src/proof/__tests__/errors.test.ts +79 -0
- package/src/proof/__tests__/verifier.test.ts +5 -5
- package/src/providers/memory.ts +2 -2
- package/src/session/__tests__/session-manager.test.ts +3 -3
- package/src/session/manager.ts +28 -6
- package/src/utils/crypto-service.ts +11 -10
- package/src/utils/did-helpers.ts +19 -0
package/src/auth/handshake.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
'
|
|
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: '
|
|
268
|
+
reason: 'Unknown agent — policy: require-consent',
|
|
228
269
|
};
|
|
229
270
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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:
|
|
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) ??
|
|
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
|
@@ -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}
|
|
202
|
-
expect(didDoc?.assertionMethod).toContain(`${didKey}
|
|
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}
|
|
31
|
+
serverKid = `${serverDid}#${serverDid.replace('did:key:', '')}`;
|
|
35
32
|
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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#
|
|
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#
|
|
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#
|
|
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
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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(
|
|
510
|
-
expect(result.checks?.statusValid).toBe(
|
|
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}
|
|
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}
|
|
140
|
-
assertionMethod: [`${did}
|
|
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}
|
|
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}
|
|
179
|
-
assertionMethod: [`${did}
|
|
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
|
-
* }
|
|
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
|
-
|
|
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:
|
|
354
|
-
reason:
|
|
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
|
}
|