@mcp-i/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -0
  3. package/dist/auth/handshake.d.ts +104 -0
  4. package/dist/auth/handshake.d.ts.map +1 -0
  5. package/dist/auth/handshake.js +230 -0
  6. package/dist/auth/handshake.js.map +1 -0
  7. package/dist/auth/index.d.ts +3 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +2 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/types.d.ts +31 -0
  12. package/dist/auth/types.d.ts.map +1 -0
  13. package/dist/auth/types.js +7 -0
  14. package/dist/auth/types.js.map +1 -0
  15. package/dist/delegation/audience-validator.d.ts +9 -0
  16. package/dist/delegation/audience-validator.d.ts.map +1 -0
  17. package/dist/delegation/audience-validator.js +17 -0
  18. package/dist/delegation/audience-validator.js.map +1 -0
  19. package/dist/delegation/bitstring.d.ts +37 -0
  20. package/dist/delegation/bitstring.d.ts.map +1 -0
  21. package/dist/delegation/bitstring.js +117 -0
  22. package/dist/delegation/bitstring.js.map +1 -0
  23. package/dist/delegation/cascading-revocation.d.ts +45 -0
  24. package/dist/delegation/cascading-revocation.d.ts.map +1 -0
  25. package/dist/delegation/cascading-revocation.js +148 -0
  26. package/dist/delegation/cascading-revocation.js.map +1 -0
  27. package/dist/delegation/delegation-graph.d.ts +49 -0
  28. package/dist/delegation/delegation-graph.d.ts.map +1 -0
  29. package/dist/delegation/delegation-graph.js +99 -0
  30. package/dist/delegation/delegation-graph.js.map +1 -0
  31. package/dist/delegation/did-key-resolver.d.ts +64 -0
  32. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  33. package/dist/delegation/did-key-resolver.js +154 -0
  34. package/dist/delegation/did-key-resolver.js.map +1 -0
  35. package/dist/delegation/did-web-resolver.d.ts +83 -0
  36. package/dist/delegation/did-web-resolver.d.ts.map +1 -0
  37. package/dist/delegation/did-web-resolver.js +218 -0
  38. package/dist/delegation/did-web-resolver.js.map +1 -0
  39. package/dist/delegation/index.d.ts +21 -0
  40. package/dist/delegation/index.d.ts.map +1 -0
  41. package/dist/delegation/index.js +21 -0
  42. package/dist/delegation/index.js.map +1 -0
  43. package/dist/delegation/outbound-headers.d.ts +81 -0
  44. package/dist/delegation/outbound-headers.d.ts.map +1 -0
  45. package/dist/delegation/outbound-headers.js +139 -0
  46. package/dist/delegation/outbound-headers.js.map +1 -0
  47. package/dist/delegation/outbound-proof.d.ts +43 -0
  48. package/dist/delegation/outbound-proof.d.ts.map +1 -0
  49. package/dist/delegation/outbound-proof.js +52 -0
  50. package/dist/delegation/outbound-proof.js.map +1 -0
  51. package/dist/delegation/statuslist-manager.d.ts +44 -0
  52. package/dist/delegation/statuslist-manager.d.ts.map +1 -0
  53. package/dist/delegation/statuslist-manager.js +126 -0
  54. package/dist/delegation/statuslist-manager.js.map +1 -0
  55. package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
  56. package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
  57. package/dist/delegation/storage/memory-graph-storage.js +145 -0
  58. package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
  59. package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
  60. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
  61. package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
  62. package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
  63. package/dist/delegation/utils.d.ts +49 -0
  64. package/dist/delegation/utils.d.ts.map +1 -0
  65. package/dist/delegation/utils.js +131 -0
  66. package/dist/delegation/utils.js.map +1 -0
  67. package/dist/delegation/vc-issuer.d.ts +56 -0
  68. package/dist/delegation/vc-issuer.d.ts.map +1 -0
  69. package/dist/delegation/vc-issuer.js +80 -0
  70. package/dist/delegation/vc-issuer.js.map +1 -0
  71. package/dist/delegation/vc-verifier.d.ts +112 -0
  72. package/dist/delegation/vc-verifier.d.ts.map +1 -0
  73. package/dist/delegation/vc-verifier.js +280 -0
  74. package/dist/delegation/vc-verifier.js.map +1 -0
  75. package/dist/index.d.ts +45 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +53 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/logging/index.d.ts +2 -0
  80. package/dist/logging/index.d.ts.map +1 -0
  81. package/dist/logging/index.js +2 -0
  82. package/dist/logging/index.js.map +1 -0
  83. package/dist/logging/logger.d.ts +23 -0
  84. package/dist/logging/logger.d.ts.map +1 -0
  85. package/dist/logging/logger.js +82 -0
  86. package/dist/logging/logger.js.map +1 -0
  87. package/dist/middleware/index.d.ts +7 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +7 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/with-mcpi.d.ts +152 -0
  92. package/dist/middleware/with-mcpi.d.ts.map +1 -0
  93. package/dist/middleware/with-mcpi.js +472 -0
  94. package/dist/middleware/with-mcpi.js.map +1 -0
  95. package/dist/proof/errors.d.ts +49 -0
  96. package/dist/proof/errors.d.ts.map +1 -0
  97. package/dist/proof/errors.js +61 -0
  98. package/dist/proof/errors.js.map +1 -0
  99. package/dist/proof/generator.d.ts +65 -0
  100. package/dist/proof/generator.d.ts.map +1 -0
  101. package/dist/proof/generator.js +163 -0
  102. package/dist/proof/generator.js.map +1 -0
  103. package/dist/proof/index.d.ts +4 -0
  104. package/dist/proof/index.d.ts.map +1 -0
  105. package/dist/proof/index.js +4 -0
  106. package/dist/proof/index.js.map +1 -0
  107. package/dist/proof/verifier.d.ts +108 -0
  108. package/dist/proof/verifier.d.ts.map +1 -0
  109. package/dist/proof/verifier.js +299 -0
  110. package/dist/proof/verifier.js.map +1 -0
  111. package/dist/providers/base.d.ts +64 -0
  112. package/dist/providers/base.d.ts.map +1 -0
  113. package/dist/providers/base.js +19 -0
  114. package/dist/providers/base.js.map +1 -0
  115. package/dist/providers/index.d.ts +3 -0
  116. package/dist/providers/index.d.ts.map +1 -0
  117. package/dist/providers/index.js +3 -0
  118. package/dist/providers/index.js.map +1 -0
  119. package/dist/providers/memory.d.ts +33 -0
  120. package/dist/providers/memory.d.ts.map +1 -0
  121. package/dist/providers/memory.js +102 -0
  122. package/dist/providers/memory.js.map +1 -0
  123. package/dist/session/index.d.ts +2 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +2 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/session/manager.d.ts +77 -0
  128. package/dist/session/manager.d.ts.map +1 -0
  129. package/dist/session/manager.js +251 -0
  130. package/dist/session/manager.js.map +1 -0
  131. package/dist/types/protocol.d.ts +320 -0
  132. package/dist/types/protocol.d.ts.map +1 -0
  133. package/dist/types/protocol.js +229 -0
  134. package/dist/types/protocol.js.map +1 -0
  135. package/dist/utils/base58.d.ts +31 -0
  136. package/dist/utils/base58.d.ts.map +1 -0
  137. package/dist/utils/base58.js +104 -0
  138. package/dist/utils/base58.js.map +1 -0
  139. package/dist/utils/base64.d.ts +13 -0
  140. package/dist/utils/base64.d.ts.map +1 -0
  141. package/dist/utils/base64.js +99 -0
  142. package/dist/utils/base64.js.map +1 -0
  143. package/dist/utils/crypto-service.d.ts +37 -0
  144. package/dist/utils/crypto-service.d.ts.map +1 -0
  145. package/dist/utils/crypto-service.js +153 -0
  146. package/dist/utils/crypto-service.js.map +1 -0
  147. package/dist/utils/did-helpers.d.ts +156 -0
  148. package/dist/utils/did-helpers.d.ts.map +1 -0
  149. package/dist/utils/did-helpers.js +193 -0
  150. package/dist/utils/did-helpers.js.map +1 -0
  151. package/dist/utils/ed25519-constants.d.ts +18 -0
  152. package/dist/utils/ed25519-constants.d.ts.map +1 -0
  153. package/dist/utils/ed25519-constants.js +21 -0
  154. package/dist/utils/ed25519-constants.js.map +1 -0
  155. package/dist/utils/index.d.ts +5 -0
  156. package/dist/utils/index.d.ts.map +1 -0
  157. package/dist/utils/index.js +5 -0
  158. package/dist/utils/index.js.map +1 -0
  159. package/package.json +105 -0
  160. package/src/__tests__/integration/full-flow.test.ts +362 -0
  161. package/src/__tests__/providers/base.test.ts +173 -0
  162. package/src/__tests__/providers/memory.test.ts +332 -0
  163. package/src/__tests__/utils/mock-providers.ts +319 -0
  164. package/src/__tests__/utils/node-crypto-provider.ts +93 -0
  165. package/src/auth/handshake.ts +411 -0
  166. package/src/auth/index.ts +11 -0
  167. package/src/auth/types.ts +40 -0
  168. package/src/delegation/__tests__/audience-validator.test.ts +110 -0
  169. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  170. package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
  171. package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
  172. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  173. package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
  174. package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
  175. package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
  176. package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
  177. package/src/delegation/__tests__/utils.test.ts +185 -0
  178. package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
  179. package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
  180. package/src/delegation/audience-validator.ts +24 -0
  181. package/src/delegation/bitstring.ts +160 -0
  182. package/src/delegation/cascading-revocation.ts +224 -0
  183. package/src/delegation/delegation-graph.ts +143 -0
  184. package/src/delegation/did-key-resolver.ts +181 -0
  185. package/src/delegation/did-web-resolver.ts +270 -0
  186. package/src/delegation/index.ts +33 -0
  187. package/src/delegation/outbound-headers.ts +193 -0
  188. package/src/delegation/outbound-proof.ts +90 -0
  189. package/src/delegation/statuslist-manager.ts +219 -0
  190. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  191. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  192. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  193. package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
  194. package/src/delegation/utils.ts +189 -0
  195. package/src/delegation/vc-issuer.ts +137 -0
  196. package/src/delegation/vc-verifier.ts +440 -0
  197. package/src/index.ts +264 -0
  198. package/src/logging/__tests__/logger.test.ts +366 -0
  199. package/src/logging/index.ts +6 -0
  200. package/src/logging/logger.ts +91 -0
  201. package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
  202. package/src/middleware/index.ts +16 -0
  203. package/src/middleware/with-mcpi.ts +766 -0
  204. package/src/proof/__tests__/proof-generator.test.ts +483 -0
  205. package/src/proof/__tests__/verifier.test.ts +488 -0
  206. package/src/proof/errors.ts +75 -0
  207. package/src/proof/generator.ts +255 -0
  208. package/src/proof/index.ts +22 -0
  209. package/src/proof/verifier.ts +449 -0
  210. package/src/providers/base.ts +68 -0
  211. package/src/providers/index.ts +15 -0
  212. package/src/providers/memory.ts +130 -0
  213. package/src/session/__tests__/session-manager.test.ts +342 -0
  214. package/src/session/index.ts +7 -0
  215. package/src/session/manager.ts +332 -0
  216. package/src/types/protocol.ts +596 -0
  217. package/src/utils/__tests__/base58.test.ts +281 -0
  218. package/src/utils/__tests__/base64.test.ts +239 -0
  219. package/src/utils/__tests__/crypto-service.test.ts +530 -0
  220. package/src/utils/__tests__/did-helpers.test.ts +156 -0
  221. package/src/utils/base58.ts +115 -0
  222. package/src/utils/base64.ts +116 -0
  223. package/src/utils/crypto-service.ts +209 -0
  224. package/src/utils/did-helpers.ts +210 -0
  225. package/src/utils/ed25519-constants.ts +23 -0
  226. package/src/utils/index.ts +9 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Outbound Delegation Proof
3
+ *
4
+ * Builds signed delegation proof JWTs for injection on outbound HTTP requests.
5
+ * Enables downstream services to independently verify the delegation chain.
6
+ *
7
+ * Wire format: signed compact EdDSA JWT (60s TTL, per-call jti)
8
+ * Header injection: X-Delegation-Id, X-Delegation-Chain, X-Delegation-Proof, X-Scopes
9
+ *
10
+ * Related Spec: MCP-I §2 — Outbound Delegation Propagation
11
+ */
12
+
13
+ import { SignJWT, importJWK } from 'jose';
14
+ import type { DelegationRecord } from '../types/protocol.js';
15
+
16
+ export interface Ed25519PrivateJWK {
17
+ kty: 'OKP';
18
+ crv: 'Ed25519';
19
+ x: string;
20
+ d: string;
21
+ kid?: string;
22
+ use?: string;
23
+ }
24
+
25
+ export interface DelegationProofOptions {
26
+ agentDid: string;
27
+ userDid: string;
28
+ delegationId: string;
29
+ delegationChain: string;
30
+ scopes: string[];
31
+ privateKeyJwk: Ed25519PrivateJWK;
32
+ kid: string;
33
+ targetHostname: string;
34
+ }
35
+
36
+ /**
37
+ * Build a signed delegation proof JWT for outbound HTTP requests.
38
+ *
39
+ * Creates a short-lived (60s) EdDSA-signed JWT containing delegation context
40
+ * that can be verified by downstream services without access to the MCP server.
41
+ *
42
+ * @param options - Proof options including DIDs, delegation info, scopes, and signing key
43
+ * @returns Compact JWS string (header.payload.signature)
44
+ * @throws {Error} If key import or signing fails
45
+ */
46
+ export async function buildDelegationProofJWT(
47
+ options: DelegationProofOptions
48
+ ): Promise<string> {
49
+ const {
50
+ agentDid,
51
+ userDid,
52
+ delegationId,
53
+ delegationChain,
54
+ scopes,
55
+ privateKeyJwk,
56
+ kid,
57
+ targetHostname,
58
+ } = options;
59
+
60
+ const privateKey = await importJWK(privateKeyJwk, 'EdDSA');
61
+
62
+ const iat = Math.floor(Date.now() / 1000);
63
+ const exp = iat + 60;
64
+
65
+ const jwt = await new SignJWT({
66
+ delegation_id: delegationId,
67
+ delegation_chain: delegationChain,
68
+ scope: scopes.join(','),
69
+ })
70
+ .setProtectedHeader({ alg: 'EdDSA', kid })
71
+ .setIssuer(agentDid)
72
+ .setSubject(userDid)
73
+ .setJti(crypto.randomUUID())
74
+ .setAudience(targetHostname)
75
+ .setIssuedAt(iat)
76
+ .setExpirationTime(exp)
77
+ .sign(privateKey);
78
+
79
+ return jwt;
80
+ }
81
+
82
+ export function buildChainString(delegation: DelegationRecord): string {
83
+ if (!delegation.id && !delegation.vcId) {
84
+ return '';
85
+ }
86
+ if (!delegation.vcId) {
87
+ return delegation.id;
88
+ }
89
+ return `${delegation.vcId}>${delegation.id}`;
90
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * StatusList2021 Manager
3
+ *
4
+ * Manages StatusList2021 credentials for efficient delegation revocation.
5
+ *
6
+ * Related Spec: W3C StatusList2021
7
+ */
8
+
9
+ import type {
10
+ StatusList2021Credential,
11
+ CredentialStatus,
12
+ } from '../types/protocol.js';
13
+ import { BitstringManager, type CompressionFunction, type DecompressionFunction } from './bitstring.js';
14
+ import type { VCSigningFunction } from './vc-issuer.js';
15
+ import { canonicalizeJSON } from './utils.js';
16
+
17
+ export interface StatusListStorageProvider {
18
+ getStatusList(statusListId: string): Promise<StatusList2021Credential | null>;
19
+ setStatusList(statusListId: string, credential: StatusList2021Credential): Promise<void>;
20
+ allocateIndex(statusListId: string): Promise<number>;
21
+ }
22
+
23
+ export interface StatusListIdentityProvider {
24
+ getDid(): string;
25
+ getKeyId(): string;
26
+ }
27
+
28
+ export class StatusList2021Manager {
29
+ private statusListBaseUrl: string;
30
+ private defaultListSize: number;
31
+
32
+ constructor(
33
+ private storage: StatusListStorageProvider,
34
+ private identity: StatusListIdentityProvider,
35
+ private signingFunction: VCSigningFunction,
36
+ private compressor: CompressionFunction,
37
+ private decompressor: DecompressionFunction,
38
+ options?: {
39
+ statusListBaseUrl?: string;
40
+ defaultListSize?: number;
41
+ }
42
+ ) {
43
+ this.statusListBaseUrl = options?.statusListBaseUrl || 'https://status.example.com';
44
+ this.defaultListSize = options?.defaultListSize || 131072;
45
+ }
46
+
47
+ async allocateStatusEntry(purpose: 'revocation' | 'suspension'): Promise<CredentialStatus> {
48
+ const statusListId = `${this.statusListBaseUrl}/${purpose}/v1`;
49
+
50
+ const index = await this.storage.allocateIndex(statusListId);
51
+
52
+ await this.ensureStatusListExists(statusListId, purpose);
53
+
54
+ const credentialStatus: CredentialStatus = {
55
+ id: `${statusListId}#${index}`,
56
+ type: 'StatusList2021Entry',
57
+ statusPurpose: purpose,
58
+ statusListIndex: index.toString(),
59
+ statusListCredential: statusListId,
60
+ };
61
+
62
+ return credentialStatus;
63
+ }
64
+
65
+ async updateStatus(credentialStatus: CredentialStatus, revoked: boolean): Promise<void> {
66
+ const { statusListCredential, statusListIndex } = credentialStatus;
67
+
68
+ const statusList = await this.storage.getStatusList(statusListCredential);
69
+ if (!statusList) {
70
+ throw new Error(`Status list not found: ${statusListCredential}`);
71
+ }
72
+
73
+ const manager = await BitstringManager.decode(
74
+ statusList.credentialSubject.encodedList,
75
+ this.compressor,
76
+ this.decompressor
77
+ );
78
+
79
+ const index = parseInt(statusListIndex, 10);
80
+ manager.setBit(index, revoked);
81
+
82
+ const encodedList = await manager.encode();
83
+
84
+ const updatedCredential: StatusList2021Credential = {
85
+ ...statusList,
86
+ credentialSubject: {
87
+ ...statusList.credentialSubject,
88
+ encodedList,
89
+ },
90
+ };
91
+
92
+ const unsignedCredential = { ...updatedCredential };
93
+ delete (unsignedCredential as Record<string, unknown>)['proof'];
94
+
95
+ const canonicalVC = canonicalizeJSON(unsignedCredential);
96
+ const proof = await this.signingFunction(
97
+ canonicalVC,
98
+ this.identity.getDid(),
99
+ this.identity.getKeyId()
100
+ );
101
+
102
+ const signedCredential: StatusList2021Credential = {
103
+ ...updatedCredential,
104
+ proof,
105
+ };
106
+
107
+ await this.storage.setStatusList(statusListCredential, signedCredential);
108
+ }
109
+
110
+ async checkStatus(credentialStatus: CredentialStatus): Promise<boolean> {
111
+ const { statusListCredential, statusListIndex } = credentialStatus;
112
+
113
+ const statusList = await this.storage.getStatusList(statusListCredential);
114
+ if (!statusList) {
115
+ return false;
116
+ }
117
+
118
+ const manager = await BitstringManager.decode(
119
+ statusList.credentialSubject.encodedList,
120
+ this.compressor,
121
+ this.decompressor
122
+ );
123
+
124
+ const index = parseInt(statusListIndex, 10);
125
+ return manager.getBit(index);
126
+ }
127
+
128
+ async getRevokedIndices(statusListId: string): Promise<number[]> {
129
+ const statusList = await this.storage.getStatusList(statusListId);
130
+ if (!statusList) {
131
+ return [];
132
+ }
133
+
134
+ const manager = await BitstringManager.decode(
135
+ statusList.credentialSubject.encodedList,
136
+ this.compressor,
137
+ this.decompressor
138
+ );
139
+
140
+ return manager.getSetBits();
141
+ }
142
+
143
+ private async ensureStatusListExists(
144
+ statusListId: string,
145
+ purpose: 'revocation' | 'suspension'
146
+ ): Promise<void> {
147
+ const existing = await this.storage.getStatusList(statusListId);
148
+ if (existing) {
149
+ return;
150
+ }
151
+
152
+ const manager = new BitstringManager(
153
+ this.defaultListSize,
154
+ this.compressor,
155
+ this.decompressor
156
+ );
157
+ const encodedList = await manager.encode();
158
+
159
+ const unsignedCredential = {
160
+ '@context': [
161
+ 'https://www.w3.org/2018/credentials/v1',
162
+ 'https://w3id.org/vc/status-list/2021/v1',
163
+ ] as [string, string],
164
+ id: statusListId,
165
+ type: ['VerifiableCredential', 'StatusList2021Credential'] as [string, string],
166
+ issuer: this.identity.getDid(),
167
+ issuanceDate: new Date().toISOString(),
168
+ credentialSubject: {
169
+ id: `${statusListId}#list`,
170
+ type: 'StatusList2021' as const,
171
+ statusPurpose: purpose,
172
+ encodedList,
173
+ },
174
+ };
175
+
176
+ const canonicalVC = canonicalizeJSON(unsignedCredential);
177
+ const proof = await this.signingFunction(
178
+ canonicalVC,
179
+ this.identity.getDid(),
180
+ this.identity.getKeyId()
181
+ );
182
+
183
+ const signedCredential: StatusList2021Credential = {
184
+ ...unsignedCredential,
185
+ proof,
186
+ };
187
+
188
+ await this.storage.setStatusList(statusListId, signedCredential);
189
+ }
190
+
191
+ getStatusListBaseUrl(): string {
192
+ return this.statusListBaseUrl;
193
+ }
194
+
195
+ getDefaultListSize(): number {
196
+ return this.defaultListSize;
197
+ }
198
+ }
199
+
200
+ export function createStatusListManager(
201
+ storage: StatusListStorageProvider,
202
+ identity: StatusListIdentityProvider,
203
+ signingFunction: VCSigningFunction,
204
+ compressor: CompressionFunction,
205
+ decompressor: DecompressionFunction,
206
+ options?: {
207
+ statusListBaseUrl?: string;
208
+ defaultListSize?: number;
209
+ }
210
+ ): StatusList2021Manager {
211
+ return new StatusList2021Manager(
212
+ storage,
213
+ identity,
214
+ signingFunction,
215
+ compressor,
216
+ decompressor,
217
+ options
218
+ );
219
+ }
@@ -0,0 +1,366 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MemoryDelegationGraphStorage } from "../memory-graph-storage.js";
3
+ import type { DelegationNode } from "../../delegation-graph.js";
4
+
5
+ describe("MemoryDelegationGraphStorage", () => {
6
+ let storage: MemoryDelegationGraphStorage;
7
+
8
+ const createMockNode = (
9
+ id: string,
10
+ parentId: string | null = null
11
+ ): DelegationNode => ({
12
+ id,
13
+ parentId,
14
+ issuerDid: `did:web:example.com:issuer${id}`,
15
+ subjectDid: `did:web:example.com:subject${id}`,
16
+ children: [],
17
+ credentialStatusId: `https://example.com/status#${id}`,
18
+ });
19
+
20
+ beforeEach(() => {
21
+ storage = new MemoryDelegationGraphStorage();
22
+ });
23
+
24
+ describe("getNode", () => {
25
+ it("should return null for non-existent node", async () => {
26
+ const result = await storage.getNode("non-existent");
27
+ expect(result).toBeNull();
28
+ });
29
+
30
+ it("should return stored node", async () => {
31
+ const node = createMockNode("del-001");
32
+ await storage.setNode(node);
33
+
34
+ const result = await storage.getNode("del-001");
35
+ expect(result).toEqual(node);
36
+ });
37
+ });
38
+
39
+ describe("setNode", () => {
40
+ it("should store node", async () => {
41
+ const node = createMockNode("del-001");
42
+ await storage.setNode(node);
43
+
44
+ const result = await storage.getNode("del-001");
45
+ expect(result).toEqual(node);
46
+ });
47
+
48
+ it("should overwrite existing node", async () => {
49
+ const node1 = createMockNode("del-001");
50
+ await storage.setNode(node1);
51
+
52
+ const node2 = {
53
+ ...node1,
54
+ issuerDid: "did:web:example.com:updated",
55
+ };
56
+ await storage.setNode(node2);
57
+
58
+ const result = await storage.getNode("del-001");
59
+ expect(result?.issuerDid).toBe("did:web:example.com:updated");
60
+ });
61
+ });
62
+
63
+ describe("getChildren", () => {
64
+ it("should return empty array for node with no children", async () => {
65
+ const node = createMockNode("del-001");
66
+ await storage.setNode(node);
67
+
68
+ const children = await storage.getChildren("del-001");
69
+ expect(children).toEqual([]);
70
+ });
71
+
72
+ it("should return direct children only", async () => {
73
+ const parent = createMockNode("del-parent");
74
+ const child1 = createMockNode("del-child1", "del-parent");
75
+ const child2 = createMockNode("del-child2", "del-parent");
76
+ const grandchild = createMockNode("del-grandchild", "del-child1");
77
+
78
+ parent.children = ["del-child1", "del-child2"];
79
+ child1.children = ["del-grandchild"];
80
+
81
+ await storage.setNode(parent);
82
+ await storage.setNode(child1);
83
+ await storage.setNode(child2);
84
+ await storage.setNode(grandchild);
85
+
86
+ const children = await storage.getChildren("del-parent");
87
+ expect(children.length).toBe(2);
88
+ expect(children.map((c) => c.id)).toContain("del-child1");
89
+ expect(children.map((c) => c.id)).toContain("del-child2");
90
+ expect(children.map((c) => c.id)).not.toContain("del-grandchild");
91
+ });
92
+
93
+ it("should return empty array for non-existent node", async () => {
94
+ const children = await storage.getChildren("non-existent");
95
+ expect(children).toEqual([]);
96
+ });
97
+
98
+ it("should filter out missing child nodes", async () => {
99
+ const parent = createMockNode("del-parent");
100
+ parent.children = ["del-child1", "del-missing", "del-child2"];
101
+
102
+ const child1 = createMockNode("del-child1", "del-parent");
103
+ const child2 = createMockNode("del-child2", "del-parent");
104
+
105
+ await storage.setNode(parent);
106
+ await storage.setNode(child1);
107
+ await storage.setNode(child2);
108
+ // del-missing is not stored
109
+
110
+ const children = await storage.getChildren("del-parent");
111
+ expect(children.length).toBe(2);
112
+ expect(children.map((c) => c.id)).toContain("del-child1");
113
+ expect(children.map((c) => c.id)).toContain("del-child2");
114
+ });
115
+ });
116
+
117
+ describe("getChain", () => {
118
+ it("should return single node for root", async () => {
119
+ const root = createMockNode("del-root");
120
+ await storage.setNode(root);
121
+
122
+ const chain = await storage.getChain("del-root");
123
+ expect(chain.length).toBe(1);
124
+ expect(chain[0].id).toBe("del-root");
125
+ });
126
+
127
+ it("should return chain from root to node", async () => {
128
+ const root = createMockNode("del-root");
129
+ const child = createMockNode("del-child", "del-root");
130
+ const grandchild = createMockNode("del-grandchild", "del-child");
131
+
132
+ root.children = ["del-child"];
133
+ child.children = ["del-grandchild"];
134
+
135
+ await storage.setNode(root);
136
+ await storage.setNode(child);
137
+ await storage.setNode(grandchild);
138
+
139
+ const chain = await storage.getChain("del-grandchild");
140
+ expect(chain.length).toBe(3);
141
+ expect(chain[0].id).toBe("del-root");
142
+ expect(chain[1].id).toBe("del-child");
143
+ expect(chain[2].id).toBe("del-grandchild");
144
+ });
145
+
146
+ it("should stop at missing parent", async () => {
147
+ const child = createMockNode("del-child", "del-missing-parent");
148
+ await storage.setNode(child);
149
+
150
+ const chain = await storage.getChain("del-child");
151
+ expect(chain.length).toBe(1);
152
+ expect(chain[0].id).toBe("del-child");
153
+ });
154
+
155
+ it("should return empty array for non-existent node", async () => {
156
+ const chain = await storage.getChain("non-existent");
157
+ expect(chain).toEqual([]);
158
+ });
159
+ });
160
+
161
+ describe("getDescendants", () => {
162
+ it("should return empty array for leaf node", async () => {
163
+ const leaf = createMockNode("del-leaf");
164
+ await storage.setNode(leaf);
165
+
166
+ const descendants = await storage.getDescendants("del-leaf");
167
+ expect(descendants).toEqual([]);
168
+ });
169
+
170
+ it("should return all descendants", async () => {
171
+ const root = createMockNode("del-root");
172
+ const child1 = createMockNode("del-child1", "del-root");
173
+ const child2 = createMockNode("del-child2", "del-root");
174
+ const grandchild = createMockNode("del-grandchild", "del-child1");
175
+
176
+ root.children = ["del-child1", "del-child2"];
177
+ child1.children = ["del-grandchild"];
178
+
179
+ await storage.setNode(root);
180
+ await storage.setNode(child1);
181
+ await storage.setNode(child2);
182
+ await storage.setNode(grandchild);
183
+
184
+ const descendants = await storage.getDescendants("del-root");
185
+ expect(descendants.length).toBe(3);
186
+ expect(descendants.map((d) => d.id)).toContain("del-child1");
187
+ expect(descendants.map((d) => d.id)).toContain("del-child2");
188
+ expect(descendants.map((d) => d.id)).toContain("del-grandchild");
189
+ });
190
+
191
+ it("should handle deep trees", async () => {
192
+ const nodes: DelegationNode[] = [];
193
+ for (let i = 0; i < 5; i++) {
194
+ const parentId = i === 0 ? null : `del-${i - 1}`;
195
+ const node = createMockNode(`del-${i}`, parentId);
196
+ if (i > 0) {
197
+ nodes[i - 1].children.push(`del-${i}`);
198
+ }
199
+ nodes.push(node);
200
+ await storage.setNode(node);
201
+ }
202
+
203
+ const descendants = await storage.getDescendants("del-0");
204
+ expect(descendants.length).toBe(4);
205
+ });
206
+
207
+ it("should prevent infinite loops", async () => {
208
+ // Create a cycle (shouldn't happen in real usage, but test defensive code)
209
+ const node1 = createMockNode("del-1");
210
+ const node2 = createMockNode("del-2", "del-1");
211
+ node1.children = ["del-2"];
212
+ node2.children = ["del-1"]; // Cycle!
213
+
214
+ await storage.setNode(node1);
215
+ await storage.setNode(node2);
216
+
217
+ const descendants = await storage.getDescendants("del-1");
218
+ // Should return node2 only once, not loop infinitely
219
+ expect(descendants.length).toBe(1);
220
+ expect(descendants[0].id).toBe("del-2");
221
+ });
222
+
223
+ it("should return empty array for non-existent node", async () => {
224
+ const descendants = await storage.getDescendants("non-existent");
225
+ expect(descendants).toEqual([]);
226
+ });
227
+ });
228
+
229
+ describe("deleteNode", () => {
230
+ it("should delete node", async () => {
231
+ const node = createMockNode("del-001");
232
+ await storage.setNode(node);
233
+
234
+ await storage.deleteNode("del-001");
235
+
236
+ const result = await storage.getNode("del-001");
237
+ expect(result).toBeNull();
238
+ });
239
+
240
+ it("should not throw when deleting non-existent node", async () => {
241
+ await expect(storage.deleteNode("non-existent")).resolves.not.toThrow();
242
+ });
243
+ });
244
+
245
+ describe("clear", () => {
246
+ it("should remove all nodes", async () => {
247
+ await storage.setNode(createMockNode("del-001"));
248
+ await storage.setNode(createMockNode("del-002"));
249
+
250
+ storage.clear();
251
+
252
+ expect(await storage.getNode("del-001")).toBeNull();
253
+ expect(await storage.getNode("del-002")).toBeNull();
254
+ });
255
+ });
256
+
257
+ describe("getAllNodeIds", () => {
258
+ it("should return empty array when no nodes", () => {
259
+ const ids = storage.getAllNodeIds();
260
+ expect(ids).toEqual([]);
261
+ });
262
+
263
+ it("should return all node IDs", async () => {
264
+ await storage.setNode(createMockNode("del-001"));
265
+ await storage.setNode(createMockNode("del-002"));
266
+ await storage.setNode(createMockNode("del-003"));
267
+
268
+ const ids = storage.getAllNodeIds();
269
+ expect(ids).toContain("del-001");
270
+ expect(ids).toContain("del-002");
271
+ expect(ids).toContain("del-003");
272
+ expect(ids.length).toBe(3);
273
+ });
274
+ });
275
+
276
+ describe("getStats", () => {
277
+ it("should return correct stats for empty storage", () => {
278
+ const stats = storage.getStats();
279
+ expect(stats.totalNodes).toBe(0);
280
+ expect(stats.rootNodes).toBe(0);
281
+ expect(stats.leafNodes).toBe(0);
282
+ expect(stats.maxDepth).toBe(0);
283
+ });
284
+
285
+ it("should count root nodes correctly", async () => {
286
+ await storage.setNode(createMockNode("del-root1"));
287
+ await storage.setNode(createMockNode("del-root2"));
288
+
289
+ const stats = storage.getStats();
290
+ expect(stats.rootNodes).toBe(2);
291
+ });
292
+
293
+ it("should count leaf nodes correctly", async () => {
294
+ const root = createMockNode("del-root");
295
+ const child1 = createMockNode("del-child1", "del-root");
296
+ const child2 = createMockNode("del-child2", "del-root");
297
+
298
+ root.children = ["del-child1", "del-child2"];
299
+
300
+ await storage.setNode(root);
301
+ await storage.setNode(child1);
302
+ await storage.setNode(child2);
303
+
304
+ const stats = storage.getStats();
305
+ expect(stats.leafNodes).toBe(2); // child1 and child2
306
+ });
307
+
308
+ it("should calculate max depth correctly", async () => {
309
+ const root = createMockNode("del-root");
310
+ const child = createMockNode("del-child", "del-root");
311
+ const grandchild = createMockNode("del-grandchild", "del-child");
312
+
313
+ root.children = ["del-child"];
314
+ child.children = ["del-grandchild"];
315
+
316
+ await storage.setNode(root);
317
+ await storage.setNode(child);
318
+ await storage.setNode(grandchild);
319
+
320
+ const stats = storage.getStats();
321
+ expect(stats.maxDepth).toBe(2); // root -> child -> grandchild (depth 2)
322
+ });
323
+
324
+ it("should handle multiple trees", async () => {
325
+ // Tree 1: depth 1
326
+ const root1 = createMockNode("del-root1");
327
+ const child1 = createMockNode("del-child1", "del-root1");
328
+ root1.children = ["del-child1"];
329
+
330
+ // Tree 2: depth 2
331
+ const root2 = createMockNode("del-root2");
332
+ const child2 = createMockNode("del-child2", "del-root2");
333
+ const grandchild2 = createMockNode("del-grandchild2", "del-child2");
334
+ root2.children = ["del-child2"];
335
+ child2.children = ["del-grandchild2"];
336
+
337
+ await storage.setNode(root1);
338
+ await storage.setNode(child1);
339
+ await storage.setNode(root2);
340
+ await storage.setNode(child2);
341
+ await storage.setNode(grandchild2);
342
+
343
+ const stats = storage.getStats();
344
+ expect(stats.totalNodes).toBe(5);
345
+ expect(stats.rootNodes).toBe(2);
346
+ expect(stats.maxDepth).toBe(2); // Max depth across all trees
347
+ });
348
+ });
349
+ });
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+
365
+
366
+