@mcp-i/core 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/cascading-revocation.d.ts.map +1 -1
- package/dist/delegation/cascading-revocation.js +3 -1
- package/dist/delegation/cascading-revocation.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 +14 -16
- package/dist/delegation/outbound-headers.d.ts.map +1 -1
- package/dist/delegation/outbound-headers.js +14 -15
- package/dist/delegation/outbound-headers.js.map +1 -1
- package/dist/delegation/outbound-proof.d.ts +1 -1
- package/dist/delegation/outbound-proof.js +1 -1
- package/dist/delegation/statuslist-manager.d.ts +3 -0
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +14 -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 +40 -5
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +120 -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 +2 -2
- package/src/__tests__/audit/canonicalization-integrity.test.ts +243 -0
- package/src/__tests__/audit/graph-revocation-roundtrip.test.ts +280 -0
- package/src/__tests__/audit/helpers/crypto-helpers.ts +245 -0
- package/src/__tests__/audit/proof-boundary.test.ts +269 -0
- package/src/__tests__/audit/statuslist-bitstring-roundtrip.test.ts +135 -0
- package/src/__tests__/audit/vc-roundtrip.test.ts +290 -0
- package/src/delegation/__tests__/outbound-headers.test.ts +16 -16
- package/src/delegation/__tests__/transitive-access.test.ts +1233 -0
- package/src/delegation/__tests__/vc-issuer.integration.test.ts +136 -0
- package/src/delegation/__tests__/vc-jwt.test.ts +318 -0
- package/src/delegation/__tests__/vc-verifier.integration.test.ts +199 -0
- package/src/delegation/cascading-revocation.ts +3 -1
- package/src/delegation/outbound-headers.ts +16 -16
- package/src/delegation/outbound-proof.ts +1 -1
- package/src/delegation/statuslist-manager.ts +17 -0
- package/src/middleware/with-mcpi.ts +29 -0
- package/src/proof/__tests__/verifier.integration.test.ts +181 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-Revocation Round-Trip Audit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the delegation graph → cascading revocation pipeline with real
|
|
5
|
+
* StatusList2021 bitstring encoding/decoding and real gzip compression.
|
|
6
|
+
* No mocking of graph storage, status lists, or compression.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, beforeAll } from 'vitest';
|
|
10
|
+
import { DelegationGraphManager } from '../../delegation/delegation-graph.js';
|
|
11
|
+
import { CascadingRevocationManager } from '../../delegation/cascading-revocation.js';
|
|
12
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
13
|
+
import {
|
|
14
|
+
createRealCryptoProvider,
|
|
15
|
+
createRealIdentity,
|
|
16
|
+
createRealStatusListManager,
|
|
17
|
+
MemoryDelegationGraphStorage,
|
|
18
|
+
type RealStatusListSetup,
|
|
19
|
+
} from './helpers/crypto-helpers.js';
|
|
20
|
+
import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
|
|
21
|
+
|
|
22
|
+
describe('Graph-Revocation Round-Trip Audit', () => {
|
|
23
|
+
let crypto: NodeCryptoProvider;
|
|
24
|
+
let issuerIdentity: AgentIdentity;
|
|
25
|
+
let graph: DelegationGraphManager;
|
|
26
|
+
let graphStorage: MemoryDelegationGraphStorage;
|
|
27
|
+
let statusListSetup: RealStatusListSetup;
|
|
28
|
+
let revocationManager: CascadingRevocationManager;
|
|
29
|
+
|
|
30
|
+
// Create identities for chain: issuer -> agentA -> agentB -> agentC
|
|
31
|
+
let agentA: AgentIdentity;
|
|
32
|
+
let agentB: AgentIdentity;
|
|
33
|
+
let agentC: AgentIdentity;
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
crypto = createRealCryptoProvider();
|
|
37
|
+
issuerIdentity = await createRealIdentity(crypto);
|
|
38
|
+
agentA = await createRealIdentity(crypto);
|
|
39
|
+
agentB = await createRealIdentity(crypto);
|
|
40
|
+
agentC = await createRealIdentity(crypto);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
graphStorage = new MemoryDelegationGraphStorage();
|
|
45
|
+
graph = new DelegationGraphManager(graphStorage);
|
|
46
|
+
|
|
47
|
+
statusListSetup = createRealStatusListManager(crypto, issuerIdentity, {
|
|
48
|
+
statusListBaseUrl: 'https://status.test.example.com',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
revocationManager = new CascadingRevocationManager(graph, statusListSetup.manager);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Helper: register a delegation and allocate a status entry
|
|
55
|
+
async function registerWithStatus(
|
|
56
|
+
id: string,
|
|
57
|
+
parentId: string | null,
|
|
58
|
+
issuerDid: string,
|
|
59
|
+
subjectDid: string
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
const statusEntry = await statusListSetup.manager.allocateStatusEntry('revocation');
|
|
62
|
+
await graph.registerDelegation({
|
|
63
|
+
id,
|
|
64
|
+
parentId,
|
|
65
|
+
issuerDid,
|
|
66
|
+
subjectDid,
|
|
67
|
+
credentialStatusId: statusEntry.id,
|
|
68
|
+
});
|
|
69
|
+
return statusEntry.id;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Chain Validation ──────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
describe('chain validation with real graph', () => {
|
|
75
|
+
it('should validate a correctly chained delegation', async () => {
|
|
76
|
+
// issuer -> agentA -> agentB -> agentC
|
|
77
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
78
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
79
|
+
await registerWithStatus('del-grandchild', 'del-child', agentB.did, agentC.did);
|
|
80
|
+
|
|
81
|
+
const result = await graph.validateChain('del-grandchild');
|
|
82
|
+
expect(result.valid).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should reject chain with issuer/subject mismatch', async () => {
|
|
86
|
+
// root: issuer -> agentA, child: agentC -> agentB (mismatch: agentC != agentA)
|
|
87
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
88
|
+
await registerWithStatus('del-child', 'del-root', agentC.did, agentB.did);
|
|
89
|
+
|
|
90
|
+
const result = await graph.validateChain('del-child');
|
|
91
|
+
expect(result.valid).toBe(false);
|
|
92
|
+
expect(result.reason).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Cascading Revocation ──────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('cascading revocation with real StatusList2021', () => {
|
|
99
|
+
it('should revoke root and cascade to all descendants via StatusList', async () => {
|
|
100
|
+
const rootStatusId = await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
101
|
+
const childStatusId = await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
102
|
+
const grandchildStatusId = await registerWithStatus('del-gc', 'del-child', agentB.did, agentC.did);
|
|
103
|
+
|
|
104
|
+
const events = await revocationManager.revokeDelegation('del-root');
|
|
105
|
+
|
|
106
|
+
expect(events.length).toBe(3);
|
|
107
|
+
expect(events[0]!.isRoot).toBe(true);
|
|
108
|
+
|
|
109
|
+
// Verify via real StatusList that all are revoked
|
|
110
|
+
const rootRevoked = await revocationManager.isRevoked('del-root');
|
|
111
|
+
const childRevoked = await revocationManager.isRevoked('del-child');
|
|
112
|
+
const gcRevoked = await revocationManager.isRevoked('del-gc');
|
|
113
|
+
|
|
114
|
+
expect(rootRevoked.revoked).toBe(true);
|
|
115
|
+
expect(childRevoked.revoked).toBe(true);
|
|
116
|
+
expect(gcRevoked.revoked).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should only revoke descendants, not siblings or ancestors', async () => {
|
|
120
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
121
|
+
await registerWithStatus('del-child1', 'del-root', agentA.did, agentB.did);
|
|
122
|
+
await registerWithStatus('del-child2', 'del-root', agentA.did, agentC.did);
|
|
123
|
+
|
|
124
|
+
// Revoke child1 only
|
|
125
|
+
await revocationManager.revokeDelegation('del-child1');
|
|
126
|
+
|
|
127
|
+
const child1 = await revocationManager.isRevoked('del-child1');
|
|
128
|
+
const child2 = await revocationManager.isRevoked('del-child2');
|
|
129
|
+
const root = await revocationManager.isRevoked('del-root');
|
|
130
|
+
|
|
131
|
+
expect(child1.revoked).toBe(true);
|
|
132
|
+
expect(child2.revoked).toBe(false);
|
|
133
|
+
expect(root.revoked).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should detect ancestor revocation through chain walk', async () => {
|
|
137
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
138
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
139
|
+
|
|
140
|
+
await revocationManager.revokeDelegation('del-root');
|
|
141
|
+
|
|
142
|
+
const childStatus = await revocationManager.isRevoked('del-child');
|
|
143
|
+
expect(childStatus.revoked).toBe(true);
|
|
144
|
+
expect(childStatus.revokedAncestor).toBe('del-root');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── Restore Behavior ──────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe('restore delegation', () => {
|
|
151
|
+
it('should restore root but NOT restore cascaded children', async () => {
|
|
152
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
153
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
154
|
+
|
|
155
|
+
// Revoke root (cascades to child)
|
|
156
|
+
await revocationManager.revokeDelegation('del-root');
|
|
157
|
+
|
|
158
|
+
const rootBefore = await revocationManager.isRevoked('del-root');
|
|
159
|
+
const childBefore = await revocationManager.isRevoked('del-child');
|
|
160
|
+
expect(rootBefore.revoked).toBe(true);
|
|
161
|
+
expect(childBefore.revoked).toBe(true);
|
|
162
|
+
|
|
163
|
+
// Restore root only
|
|
164
|
+
await revocationManager.restoreDelegation('del-root');
|
|
165
|
+
|
|
166
|
+
const rootAfter = await revocationManager.isRevoked('del-root');
|
|
167
|
+
const childAfter = await revocationManager.isRevoked('del-child');
|
|
168
|
+
|
|
169
|
+
expect(rootAfter.revoked).toBe(false);
|
|
170
|
+
// Child should STILL be revoked — restore is not recursive
|
|
171
|
+
expect(childAfter.revoked).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ── Dry-Run ───────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe('dry-run mode', () => {
|
|
178
|
+
it('should not modify StatusList bits in dry-run', async () => {
|
|
179
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
180
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
181
|
+
|
|
182
|
+
const events = await revocationManager.revokeDelegation('del-root', { dryRun: true });
|
|
183
|
+
|
|
184
|
+
expect(events.length).toBe(2);
|
|
185
|
+
|
|
186
|
+
// Nothing should actually be revoked
|
|
187
|
+
const root = await revocationManager.isRevoked('del-root');
|
|
188
|
+
const child = await revocationManager.isRevoked('del-child');
|
|
189
|
+
|
|
190
|
+
expect(root.revoked).toBe(false);
|
|
191
|
+
expect(child.revoked).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Deep Chain ────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
describe('deep chain cascading', () => {
|
|
198
|
+
it('should cascade through a depth-10 chain', async () => {
|
|
199
|
+
// Build chain of depth 10
|
|
200
|
+
const dids = [issuerIdentity, agentA, agentB, agentC];
|
|
201
|
+
let parentId: string | null = null;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < 10; i++) {
|
|
204
|
+
const issuerIdx = i % dids.length;
|
|
205
|
+
const subjectIdx = (i + 1) % dids.length;
|
|
206
|
+
const id = `del-depth-${i}`;
|
|
207
|
+
|
|
208
|
+
await registerWithStatus(
|
|
209
|
+
id,
|
|
210
|
+
parentId,
|
|
211
|
+
dids[issuerIdx]!.did,
|
|
212
|
+
dids[subjectIdx]!.did
|
|
213
|
+
);
|
|
214
|
+
parentId = id;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Revoke root — all 9 descendants should be revoked
|
|
218
|
+
const events = await revocationManager.revokeDelegation('del-depth-0');
|
|
219
|
+
expect(events.length).toBe(10);
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < 10; i++) {
|
|
222
|
+
const status = await revocationManager.isRevoked(`del-depth-${i}`);
|
|
223
|
+
expect(status.revoked).toBe(true);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ── Concurrent Revocation ─────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
describe('concurrent revocation', () => {
|
|
231
|
+
it('should handle concurrent revocation of sibling subtrees', async () => {
|
|
232
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
233
|
+
await registerWithStatus('del-child1', 'del-root', agentA.did, agentB.did);
|
|
234
|
+
await registerWithStatus('del-gc1', 'del-child1', agentB.did, agentC.did);
|
|
235
|
+
await registerWithStatus('del-child2', 'del-root', agentA.did, agentC.did);
|
|
236
|
+
await registerWithStatus('del-gc2', 'del-child2', agentC.did, agentB.did);
|
|
237
|
+
|
|
238
|
+
// Revoke both subtrees concurrently
|
|
239
|
+
const [events1, events2] = await Promise.all([
|
|
240
|
+
revocationManager.revokeDelegation('del-child1'),
|
|
241
|
+
revocationManager.revokeDelegation('del-child2'),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
expect(events1.length).toBeGreaterThanOrEqual(2);
|
|
245
|
+
expect(events2.length).toBeGreaterThanOrEqual(2);
|
|
246
|
+
|
|
247
|
+
// All 4 nodes should be revoked
|
|
248
|
+
for (const id of ['del-child1', 'del-gc1', 'del-child2', 'del-gc2']) {
|
|
249
|
+
const status = await revocationManager.isRevoked(id);
|
|
250
|
+
expect(status.revoked).toBe(true);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Root should not be revoked
|
|
254
|
+
const root = await revocationManager.isRevoked('del-root');
|
|
255
|
+
expect(root.revoked).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ── validateDelegation ────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
describe('validateDelegation', () => {
|
|
262
|
+
it('should return valid for non-revoked, well-formed chain', async () => {
|
|
263
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
264
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
265
|
+
|
|
266
|
+
const result = await revocationManager.validateDelegation('del-child');
|
|
267
|
+
expect(result.valid).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return invalid after ancestor revocation', async () => {
|
|
271
|
+
await registerWithStatus('del-root', null, issuerIdentity.did, agentA.did);
|
|
272
|
+
await registerWithStatus('del-child', 'del-root', agentA.did, agentB.did);
|
|
273
|
+
|
|
274
|
+
await revocationManager.revokeDelegation('del-root');
|
|
275
|
+
|
|
276
|
+
const result = await revocationManager.validateDelegation('del-child');
|
|
277
|
+
expect(result.valid).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Test Helpers for Audit Tests
|
|
3
|
+
*
|
|
4
|
+
* Provides real (non-mocked) crypto, clock, and signing utilities
|
|
5
|
+
* for round-trip and boundary testing. All helpers use NodeCryptoProvider
|
|
6
|
+
* for actual Ed25519 operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as zlib from 'node:zlib';
|
|
10
|
+
import { NodeCryptoProvider } from '../../utils/node-crypto-provider.js';
|
|
11
|
+
import { MemoryIdentityProvider, MemoryNonceCacheProvider } from '../../../providers/memory.js';
|
|
12
|
+
import { ClockProvider, FetchProvider } from '../../../providers/base.js';
|
|
13
|
+
import type { AgentIdentity } from '../../../providers/base.js';
|
|
14
|
+
import type { Proof, StatusList2021Credential, DelegationRecord } from '../../../types/protocol.js';
|
|
15
|
+
import type { VCSigningFunction } from '../../../delegation/vc-issuer.js';
|
|
16
|
+
import type { SignatureVerificationFunction, DIDDocument } from '../../../delegation/vc-verifier.js';
|
|
17
|
+
import { canonicalizeJSON } from '../../../delegation/utils.js';
|
|
18
|
+
import { createDidKeyResolver } from '../../../delegation/did-key-resolver.js';
|
|
19
|
+
import type { CompressionFunction, DecompressionFunction } from '../../../delegation/bitstring.js';
|
|
20
|
+
import { StatusList2021Manager } from '../../../delegation/statuslist-manager.js';
|
|
21
|
+
import { MemoryStatusListStorage } from '../../../delegation/storage/memory-statuslist-storage.js';
|
|
22
|
+
|
|
23
|
+
// ── Crypto ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export function createRealCryptoProvider(): NodeCryptoProvider {
|
|
26
|
+
return new NodeCryptoProvider();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createRealIdentity(crypto: NodeCryptoProvider): Promise<AgentIdentity> {
|
|
30
|
+
const provider = new MemoryIdentityProvider(crypto);
|
|
31
|
+
return provider.getIdentity();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Clock Providers ─────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export class RealClockProvider extends ClockProvider {
|
|
37
|
+
now(): number {
|
|
38
|
+
return Date.now();
|
|
39
|
+
}
|
|
40
|
+
isWithinSkew(timestampMs: number, skewSeconds: number): boolean {
|
|
41
|
+
return Math.abs(Date.now() - timestampMs) <= skewSeconds * 1000;
|
|
42
|
+
}
|
|
43
|
+
hasExpired(expiresAt: number): boolean {
|
|
44
|
+
return Date.now() > expiresAt;
|
|
45
|
+
}
|
|
46
|
+
calculateExpiry(ttlSeconds: number): number {
|
|
47
|
+
return Date.now() + ttlSeconds * 1000;
|
|
48
|
+
}
|
|
49
|
+
format(timestamp: number): string {
|
|
50
|
+
return new Date(timestamp).toISOString();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clock provider with a controllable "now" for precise boundary testing.
|
|
56
|
+
* `setNow(ms)` moves the clock to an exact millisecond.
|
|
57
|
+
*/
|
|
58
|
+
export class ControllableClockProvider extends ClockProvider {
|
|
59
|
+
private currentMs: number;
|
|
60
|
+
|
|
61
|
+
constructor(nowMs: number = Date.now()) {
|
|
62
|
+
super();
|
|
63
|
+
this.currentMs = nowMs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setNow(ms: number): void {
|
|
67
|
+
this.currentMs = ms;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
advance(ms: number): void {
|
|
71
|
+
this.currentMs += ms;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
now(): number {
|
|
75
|
+
return this.currentMs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isWithinSkew(timestampMs: number, skewSeconds: number): boolean {
|
|
79
|
+
return Math.abs(this.currentMs - timestampMs) <= skewSeconds * 1000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hasExpired(expiresAt: number): boolean {
|
|
83
|
+
return this.currentMs > expiresAt;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
calculateExpiry(ttlSeconds: number): number {
|
|
87
|
+
return this.currentMs + ttlSeconds * 1000;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
format(timestamp: number): string {
|
|
91
|
+
return new Date(timestamp).toISOString();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Fetch Provider ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export class RealFetchProvider extends FetchProvider {
|
|
98
|
+
private didResolver = createDidKeyResolver();
|
|
99
|
+
|
|
100
|
+
async resolveDID(did: string): Promise<DIDDocument | null> {
|
|
101
|
+
return this.didResolver.resolve(did);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async fetchStatusList(_url: string): Promise<StatusList2021Credential | null> {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async fetchDelegationChain(_id: string): Promise<DelegationRecord[]> {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async fetch(_url: string, _options?: unknown): Promise<Response> {
|
|
113
|
+
throw new Error('Not implemented');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Signing & Verification ──────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a real VCSigningFunction that produces Ed25519Signature2020 proofs.
|
|
121
|
+
* The signature is over the canonicalized VC (the `canonicalVC` string passed in).
|
|
122
|
+
*/
|
|
123
|
+
export function createRealSigningFunction(
|
|
124
|
+
crypto: NodeCryptoProvider,
|
|
125
|
+
identity: AgentIdentity
|
|
126
|
+
): VCSigningFunction {
|
|
127
|
+
return async (canonicalVC: string, issuerDid: string, kid: string): Promise<Proof> => {
|
|
128
|
+
const data = new TextEncoder().encode(canonicalVC);
|
|
129
|
+
const signature = await crypto.sign(data, identity.privateKey);
|
|
130
|
+
const proofValue = Buffer.from(signature).toString('base64url');
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
type: 'Ed25519Signature2020',
|
|
134
|
+
created: new Date().toISOString(),
|
|
135
|
+
verificationMethod: kid,
|
|
136
|
+
proofPurpose: 'assertionMethod',
|
|
137
|
+
proofValue,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates a real SignatureVerificationFunction for the VC verifier.
|
|
144
|
+
* Strips `proof`, canonicalizes, and verifies the Ed25519 signature.
|
|
145
|
+
*/
|
|
146
|
+
export function createRealSignatureVerifier(
|
|
147
|
+
crypto: NodeCryptoProvider
|
|
148
|
+
): SignatureVerificationFunction {
|
|
149
|
+
return async (vc, publicKeyJwk) => {
|
|
150
|
+
try {
|
|
151
|
+
const jwk = publicKeyJwk as { x?: string };
|
|
152
|
+
if (!jwk.x) {
|
|
153
|
+
return { valid: false, reason: 'Missing public key x coordinate' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Decode the public key from base64url (JWK x parameter)
|
|
157
|
+
const publicKeyBase64 = Buffer.from(jwk.x, 'base64url').toString('base64');
|
|
158
|
+
|
|
159
|
+
// Strip proof and canonicalize
|
|
160
|
+
const vcWithoutProof = { ...vc } as Record<string, unknown>;
|
|
161
|
+
delete vcWithoutProof['proof'];
|
|
162
|
+
const canonicalVC = canonicalizeJSON(vcWithoutProof);
|
|
163
|
+
const data = new TextEncoder().encode(canonicalVC);
|
|
164
|
+
|
|
165
|
+
// Extract signature from proof
|
|
166
|
+
const proofValue = vc.proof?.proofValue;
|
|
167
|
+
if (!proofValue) {
|
|
168
|
+
return { valid: false, reason: 'Missing proofValue' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const signature = Buffer.from(proofValue as string, 'base64url');
|
|
172
|
+
const isValid = await crypto.verify(data, new Uint8Array(signature), publicKeyBase64);
|
|
173
|
+
|
|
174
|
+
return { valid: isValid, reason: isValid ? undefined : 'Signature verification failed' };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
valid: false,
|
|
178
|
+
reason: `Verification error: ${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Compression (real gzip via Node.js zlib) ────────────────────
|
|
185
|
+
|
|
186
|
+
export const nodeCompressor: CompressionFunction = {
|
|
187
|
+
compress(data: Uint8Array): Promise<Uint8Array> {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
zlib.gzip(Buffer.from(data), (err, result) => {
|
|
190
|
+
if (err) reject(err);
|
|
191
|
+
else resolve(new Uint8Array(result));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const nodeDecompressor: DecompressionFunction = {
|
|
198
|
+
decompress(data: Uint8Array): Promise<Uint8Array> {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
zlib.gunzip(Buffer.from(data), (err, result) => {
|
|
201
|
+
if (err) reject(err);
|
|
202
|
+
else resolve(new Uint8Array(result));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// ── StatusList2021 Manager (real) ───────────────────────────────
|
|
209
|
+
|
|
210
|
+
export interface RealStatusListSetup {
|
|
211
|
+
manager: StatusList2021Manager;
|
|
212
|
+
storage: MemoryStatusListStorage;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createRealStatusListManager(
|
|
216
|
+
crypto: NodeCryptoProvider,
|
|
217
|
+
identity: AgentIdentity,
|
|
218
|
+
options?: { statusListBaseUrl?: string; defaultListSize?: number }
|
|
219
|
+
): RealStatusListSetup {
|
|
220
|
+
const storage = new MemoryStatusListStorage();
|
|
221
|
+
const signingFunction = createRealSigningFunction(crypto, identity);
|
|
222
|
+
|
|
223
|
+
const identityProvider = {
|
|
224
|
+
getDid: () => identity.did,
|
|
225
|
+
getKeyId: () => identity.kid,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const manager = new StatusList2021Manager(
|
|
229
|
+
storage,
|
|
230
|
+
identityProvider,
|
|
231
|
+
signingFunction,
|
|
232
|
+
nodeCompressor,
|
|
233
|
+
nodeDecompressor,
|
|
234
|
+
options
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return { manager, storage };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Re-exports for convenience ──────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export { MemoryNonceCacheProvider } from '../../../providers/memory.js';
|
|
243
|
+
export { MemoryIdentityProvider } from '../../../providers/memory.js';
|
|
244
|
+
export { MemoryDelegationGraphStorage } from '../../../delegation/storage/memory-graph-storage.js';
|
|
245
|
+
export { MemoryStatusListStorage } from '../../../delegation/storage/memory-statuslist-storage.js';
|