@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,179 @@
1
+ /**
2
+ * Outbound Delegation Proof Tests
3
+ *
4
+ * Tests for buildDelegationProofJWT and buildChainString helpers.
5
+ * All 8 test cases from spec Section 5.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { generateKeyPair, exportJWK, decodeJwt, decodeProtectedHeader } from "jose";
10
+ import {
11
+ buildDelegationProofJWT,
12
+ buildChainString,
13
+ type DelegationProofOptions,
14
+ type Ed25519PrivateJWK,
15
+ } from "../outbound-proof.js";
16
+ import type { DelegationRecord } from "../../types/protocol.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Test key helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ async function generateTestKeyPair(): Promise<{
23
+ privateKeyJwk: Ed25519PrivateJWK;
24
+ kid: string;
25
+ }> {
26
+ const { privateKey } = await generateKeyPair("EdDSA", { crv: "Ed25519" });
27
+ const jwk = await exportJWK(privateKey);
28
+ const privateKeyJwk: Ed25519PrivateJWK = {
29
+ kty: "OKP",
30
+ crv: "Ed25519",
31
+ x: jwk.x as string,
32
+ d: jwk.d as string,
33
+ };
34
+ return { privateKeyJwk, kid: "did:web:agent.example.com#key-1" };
35
+ }
36
+
37
+ const AGENT_DID = "did:web:agent.example.com";
38
+ const USER_DID = "did:web:alice.example.com";
39
+ const DELEGATION_ID = "del-abc-123";
40
+ const DELEGATION_CHAIN = "vc-abc>del-abc-123";
41
+ const SCOPES = ["read_files", "write_calendar"];
42
+ const TARGET_HOSTNAME = "api.example.com";
43
+
44
+ function baseOptions(overrides: Partial<DelegationProofOptions> = {}): DelegationProofOptions {
45
+ return {
46
+ agentDid: AGENT_DID,
47
+ userDid: USER_DID,
48
+ delegationId: DELEGATION_ID,
49
+ delegationChain: DELEGATION_CHAIN,
50
+ scopes: SCOPES,
51
+ privateKeyJwk: {} as Ed25519PrivateJWK, // overridden in test setup
52
+ kid: "did:web:agent.example.com#key-1",
53
+ targetHostname: TARGET_HOSTNAME,
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // buildDelegationProofJWT tests
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("buildDelegationProofJWT", () => {
63
+ it("happy path — produces a valid EdDSA compact JWT", async () => {
64
+ const { privateKeyJwk, kid } = await generateTestKeyPair();
65
+ const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
66
+
67
+ expect(typeof jwt).toBe("string");
68
+ // Compact JWT has exactly 3 parts
69
+ expect(jwt.split(".")).toHaveLength(3);
70
+
71
+ const header = decodeProtectedHeader(jwt);
72
+ expect(header.alg).toBe("EdDSA");
73
+ expect(header.kid).toBe(kid);
74
+ });
75
+
76
+ it("JWT payload contains all required claims", async () => {
77
+ const { privateKeyJwk, kid } = await generateTestKeyPair();
78
+ const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
79
+
80
+ const payload = decodeJwt(jwt);
81
+ expect(payload.iss).toBe(AGENT_DID);
82
+ expect(payload.sub).toBe(USER_DID);
83
+ expect(payload.aud).toBe(TARGET_HOSTNAME);
84
+ expect(typeof payload.jti).toBe("string");
85
+ expect(payload.delegation_id).toBe(DELEGATION_ID);
86
+ expect(payload.delegation_chain).toBe(DELEGATION_CHAIN);
87
+ expect(payload.scope).toBe(SCOPES.join(","));
88
+ expect(typeof payload.iat).toBe("number");
89
+ expect(typeof payload.exp).toBe("number");
90
+ });
91
+
92
+ it("exp is exactly 60 seconds from iat", async () => {
93
+ const { privateKeyJwk, kid } = await generateTestKeyPair();
94
+ const jwt = await buildDelegationProofJWT(baseOptions({ privateKeyJwk, kid }));
95
+
96
+ const payload = decodeJwt(jwt);
97
+ expect((payload.exp as number) - (payload.iat as number)).toBe(60);
98
+ });
99
+
100
+ it("jti is unique across multiple calls", async () => {
101
+ const { privateKeyJwk, kid } = await generateTestKeyPair();
102
+ const opts = baseOptions({ privateKeyJwk, kid });
103
+
104
+ const jwt1 = await buildDelegationProofJWT(opts);
105
+ const jwt2 = await buildDelegationProofJWT(opts);
106
+
107
+ const payload1 = decodeJwt(jwt1);
108
+ const payload2 = decodeJwt(jwt2);
109
+ expect(payload1.jti).not.toBe(payload2.jti);
110
+ });
111
+
112
+ it("throws when private key is invalid", async () => {
113
+ const badKeyJwk: Ed25519PrivateJWK = {
114
+ kty: "OKP",
115
+ crv: "Ed25519",
116
+ x: "invalid",
117
+ d: "invalid",
118
+ };
119
+
120
+ await expect(
121
+ buildDelegationProofJWT(baseOptions({ privateKeyJwk: badKeyJwk }))
122
+ ).rejects.toThrow();
123
+ });
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // buildChainString tests
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe("buildChainString", () => {
131
+ const baseDelegation: DelegationRecord = {
132
+ id: "del-123",
133
+ issuerDid: "did:web:issuer.example.com",
134
+ subjectDid: "did:web:agent.example.com",
135
+ vcId: "vc-abc",
136
+ constraints: { scopes: ["read:files"] },
137
+ signature: "sig",
138
+ status: "active",
139
+ };
140
+
141
+ it("single-hop delegation encodes correctly", () => {
142
+ const result = buildChainString(baseDelegation);
143
+ expect(result).toBe("vc-abc>del-123");
144
+ });
145
+
146
+ it("multi-hop (3 levels) encodes correctly when concatenated", () => {
147
+ const del1: DelegationRecord = { ...baseDelegation, id: "del-1", vcId: "vc-1" };
148
+ const del2: DelegationRecord = { ...baseDelegation, id: "del-2", vcId: "vc-2", parentId: "del-1" };
149
+ const del3: DelegationRecord = { ...baseDelegation, id: "del-3", vcId: "vc-3", parentId: "del-2" };
150
+
151
+ // Build the chain by concatenating each hop (caller responsibility for multi-hop)
152
+ const chain = [del1, del2, del3].map(buildChainString).join(">");
153
+ expect(chain).toBe("vc-1>del-1>vc-2>del-2>vc-3>del-3");
154
+ });
155
+
156
+ it("returns delegation.id when vcId is absent", () => {
157
+ const delegation: DelegationRecord = {
158
+ ...baseDelegation,
159
+ vcId: undefined as unknown as string,
160
+ };
161
+ const result = buildChainString(delegation);
162
+ expect(result).toBe("del-123");
163
+ });
164
+
165
+ it("empty chain returns empty string gracefully", () => {
166
+ const emptyDelegation = {
167
+ id: "",
168
+ issuerDid: "did:web:issuer.example.com",
169
+ subjectDid: "did:web:agent.example.com",
170
+ vcId: undefined as unknown as string,
171
+ constraints: { scopes: [] },
172
+ signature: "sig",
173
+ status: "active" as const,
174
+ } satisfies DelegationRecord;
175
+
176
+ const result = buildChainString(emptyDelegation);
177
+ expect(result).toBe("");
178
+ });
179
+ });
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Tests for StatusList2021Manager
3
+ *
4
+ * Covers:
5
+ * - Status entry allocation
6
+ * - Status updates (revoke/restore)
7
+ * - Status checking
8
+ * - Status list creation
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from "vitest";
12
+ import {
13
+ StatusList2021Manager,
14
+ createStatusListManager,
15
+ type StatusListStorageProvider,
16
+ type StatusListIdentityProvider,
17
+ } from "../statuslist-manager.js";
18
+ import type { VCSigningFunction } from "../vc-issuer.js";
19
+ import type { CompressionFunction, DecompressionFunction } from "../bitstring.js";
20
+ import type {
21
+ StatusList2021Credential,
22
+ CredentialStatus,
23
+ } from "../../types/protocol.js";
24
+
25
+ describe("StatusList2021Manager", () => {
26
+ let mockStorage: StatusListStorageProvider;
27
+ let mockIdentity: StatusListIdentityProvider;
28
+ let mockSigningFunction: VCSigningFunction;
29
+ let mockCompressor: CompressionFunction;
30
+ let mockDecompressor: DecompressionFunction;
31
+
32
+ // Create a simple bitstring representation for testing
33
+ const createEncodedList = (size: number = 16): string => {
34
+ // Create a simple base64url encoded "bitstring" for testing
35
+ // This represents an empty list (all zeros)
36
+ const bytes = new Uint8Array(Math.ceil(size / 8));
37
+ return Buffer.from(bytes).toString("base64url");
38
+ };
39
+
40
+ const createStatusListCredential = (
41
+ id: string,
42
+ purpose: "revocation" | "suspension",
43
+ encodedList?: string
44
+ ): StatusList2021Credential => ({
45
+ "@context": [
46
+ "https://www.w3.org/2018/credentials/v1",
47
+ "https://w3id.org/vc/status-list/2021/v1",
48
+ ],
49
+ id,
50
+ type: ["VerifiableCredential", "StatusList2021Credential"],
51
+ issuer: "did:key:z6MkIssuer",
52
+ issuanceDate: new Date().toISOString(),
53
+ credentialSubject: {
54
+ id: `${id}#list`,
55
+ type: "StatusList2021",
56
+ statusPurpose: purpose,
57
+ encodedList: encodedList || createEncodedList(),
58
+ },
59
+ proof: {
60
+ type: "Ed25519Signature2020",
61
+ created: new Date().toISOString(),
62
+ verificationMethod: "did:key:z6MkIssuer#keys-1",
63
+ proofPurpose: "assertionMethod",
64
+ proofValue: "mock-proof-value",
65
+ },
66
+ });
67
+
68
+ beforeEach(() => {
69
+ mockStorage = {
70
+ getStatusList: vi.fn().mockResolvedValue(null),
71
+ setStatusList: vi.fn().mockResolvedValue(undefined),
72
+ allocateIndex: vi.fn().mockResolvedValue(0),
73
+ };
74
+
75
+ mockIdentity = {
76
+ getDid: vi.fn().mockReturnValue("did:key:z6MkTestIssuer"),
77
+ getKeyId: vi.fn().mockReturnValue("did:key:z6MkTestIssuer#keys-1"),
78
+ };
79
+
80
+ mockSigningFunction = vi.fn().mockResolvedValue({
81
+ type: "Ed25519Signature2020",
82
+ created: new Date().toISOString(),
83
+ verificationMethod: "did:key:z6MkTestIssuer#keys-1",
84
+ proofPurpose: "assertionMethod",
85
+ proofValue: "mock-signature",
86
+ });
87
+
88
+ // Simple mock compressor/decompressor (just passes through for testing)
89
+ mockCompressor = {
90
+ compress: vi.fn().mockImplementation(async (data: Uint8Array) => data),
91
+ };
92
+
93
+ mockDecompressor = {
94
+ decompress: vi.fn().mockImplementation(async (data: Uint8Array) => data),
95
+ };
96
+ });
97
+
98
+ describe("constructor and configuration", () => {
99
+ it("should use default configuration values", () => {
100
+ const manager = new StatusList2021Manager(
101
+ mockStorage,
102
+ mockIdentity,
103
+ mockSigningFunction,
104
+ mockCompressor,
105
+ mockDecompressor
106
+ );
107
+
108
+ expect(manager.getStatusListBaseUrl()).toBe("https://status.example.com");
109
+ expect(manager.getDefaultListSize()).toBe(131072); // 128K
110
+ });
111
+
112
+ it("should accept custom configuration values", () => {
113
+ const manager = new StatusList2021Manager(
114
+ mockStorage,
115
+ mockIdentity,
116
+ mockSigningFunction,
117
+ mockCompressor,
118
+ mockDecompressor,
119
+ {
120
+ statusListBaseUrl: "https://custom.example.com/status",
121
+ defaultListSize: 65536,
122
+ }
123
+ );
124
+
125
+ expect(manager.getStatusListBaseUrl()).toBe(
126
+ "https://custom.example.com/status"
127
+ );
128
+ expect(manager.getDefaultListSize()).toBe(65536);
129
+ });
130
+ });
131
+
132
+ describe("allocateStatusEntry", () => {
133
+ it("should allocate a revocation status entry", async () => {
134
+ const manager = new StatusList2021Manager(
135
+ mockStorage,
136
+ mockIdentity,
137
+ mockSigningFunction,
138
+ mockCompressor,
139
+ mockDecompressor
140
+ );
141
+
142
+ vi.mocked(mockStorage.allocateIndex).mockResolvedValue(42);
143
+
144
+ const entry = await manager.allocateStatusEntry("revocation");
145
+
146
+ expect(entry).toEqual({
147
+ id: "https://status.example.com/revocation/v1#42",
148
+ type: "StatusList2021Entry",
149
+ statusPurpose: "revocation",
150
+ statusListIndex: "42",
151
+ statusListCredential: "https://status.example.com/revocation/v1",
152
+ });
153
+
154
+ expect(mockStorage.allocateIndex).toHaveBeenCalledWith(
155
+ "https://status.example.com/revocation/v1"
156
+ );
157
+ });
158
+
159
+ it("should allocate a suspension status entry", async () => {
160
+ const manager = new StatusList2021Manager(
161
+ mockStorage,
162
+ mockIdentity,
163
+ mockSigningFunction,
164
+ mockCompressor,
165
+ mockDecompressor
166
+ );
167
+
168
+ vi.mocked(mockStorage.allocateIndex).mockResolvedValue(100);
169
+
170
+ const entry = await manager.allocateStatusEntry("suspension");
171
+
172
+ expect(entry).toEqual({
173
+ id: "https://status.example.com/suspension/v1#100",
174
+ type: "StatusList2021Entry",
175
+ statusPurpose: "suspension",
176
+ statusListIndex: "100",
177
+ statusListCredential: "https://status.example.com/suspension/v1",
178
+ });
179
+ });
180
+
181
+ it("should create status list if it doesn't exist", async () => {
182
+ const manager = new StatusList2021Manager(
183
+ mockStorage,
184
+ mockIdentity,
185
+ mockSigningFunction,
186
+ mockCompressor,
187
+ mockDecompressor
188
+ );
189
+
190
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
191
+
192
+ await manager.allocateStatusEntry("revocation");
193
+
194
+ // Should check if status list exists
195
+ expect(mockStorage.getStatusList).toHaveBeenCalled();
196
+ // Should create new status list
197
+ expect(mockStorage.setStatusList).toHaveBeenCalledWith(
198
+ "https://status.example.com/revocation/v1",
199
+ expect.objectContaining({
200
+ type: ["VerifiableCredential", "StatusList2021Credential"],
201
+ credentialSubject: expect.objectContaining({
202
+ type: "StatusList2021",
203
+ statusPurpose: "revocation",
204
+ }),
205
+ })
206
+ );
207
+ });
208
+
209
+ it("should not create status list if it already exists", async () => {
210
+ const manager = new StatusList2021Manager(
211
+ mockStorage,
212
+ mockIdentity,
213
+ mockSigningFunction,
214
+ mockCompressor,
215
+ mockDecompressor
216
+ );
217
+
218
+ const existingCredential = createStatusListCredential(
219
+ "https://status.example.com/revocation/v1",
220
+ "revocation"
221
+ );
222
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
223
+ existingCredential
224
+ );
225
+
226
+ await manager.allocateStatusEntry("revocation");
227
+
228
+ // Should check if exists
229
+ expect(mockStorage.getStatusList).toHaveBeenCalled();
230
+ // Should NOT create new one
231
+ expect(mockStorage.setStatusList).not.toHaveBeenCalled();
232
+ });
233
+ });
234
+
235
+ describe("updateStatus", () => {
236
+ it("should revoke a credential by setting its status bit", async () => {
237
+ const manager = new StatusList2021Manager(
238
+ mockStorage,
239
+ mockIdentity,
240
+ mockSigningFunction,
241
+ mockCompressor,
242
+ mockDecompressor
243
+ );
244
+
245
+ const statusListId = "https://status.example.com/revocation/v1";
246
+ const existingCredential = createStatusListCredential(
247
+ statusListId,
248
+ "revocation"
249
+ );
250
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
251
+ existingCredential
252
+ );
253
+
254
+ const credentialStatus: CredentialStatus = {
255
+ id: `${statusListId}#5`,
256
+ type: "StatusList2021Entry",
257
+ statusPurpose: "revocation",
258
+ statusListIndex: "5",
259
+ statusListCredential: statusListId,
260
+ };
261
+
262
+ await manager.updateStatus(credentialStatus, true);
263
+
264
+ // Should save updated status list
265
+ expect(mockStorage.setStatusList).toHaveBeenCalledWith(
266
+ statusListId,
267
+ expect.objectContaining({
268
+ credentialSubject: expect.objectContaining({
269
+ encodedList: expect.any(String),
270
+ }),
271
+ proof: expect.any(Object),
272
+ })
273
+ );
274
+ });
275
+
276
+ it("should restore a credential by clearing its status bit", async () => {
277
+ const manager = new StatusList2021Manager(
278
+ mockStorage,
279
+ mockIdentity,
280
+ mockSigningFunction,
281
+ mockCompressor,
282
+ mockDecompressor
283
+ );
284
+
285
+ const statusListId = "https://status.example.com/revocation/v1";
286
+ // Create a credential where bit 5 is already set
287
+ const bytes = new Uint8Array(2);
288
+ bytes[0] = 0b00100000; // Bit 5 is set
289
+ const encodedList = Buffer.from(bytes).toString("base64url");
290
+ const existingCredential = createStatusListCredential(
291
+ statusListId,
292
+ "revocation",
293
+ encodedList
294
+ );
295
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
296
+ existingCredential
297
+ );
298
+
299
+ const credentialStatus: CredentialStatus = {
300
+ id: `${statusListId}#5`,
301
+ type: "StatusList2021Entry",
302
+ statusPurpose: "revocation",
303
+ statusListIndex: "5",
304
+ statusListCredential: statusListId,
305
+ };
306
+
307
+ await manager.updateStatus(credentialStatus, false);
308
+
309
+ // Should re-sign and save
310
+ expect(mockSigningFunction).toHaveBeenCalled();
311
+ expect(mockStorage.setStatusList).toHaveBeenCalled();
312
+ });
313
+
314
+ it("should throw error if status list not found", async () => {
315
+ const manager = new StatusList2021Manager(
316
+ mockStorage,
317
+ mockIdentity,
318
+ mockSigningFunction,
319
+ mockCompressor,
320
+ mockDecompressor
321
+ );
322
+
323
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
324
+
325
+ const credentialStatus: CredentialStatus = {
326
+ id: "https://status.example.com/revocation/v1#5",
327
+ type: "StatusList2021Entry",
328
+ statusPurpose: "revocation",
329
+ statusListIndex: "5",
330
+ statusListCredential: "https://status.example.com/revocation/v1",
331
+ };
332
+
333
+ await expect(
334
+ manager.updateStatus(credentialStatus, true)
335
+ ).rejects.toThrow("Status list not found");
336
+ });
337
+ });
338
+
339
+ describe("checkStatus", () => {
340
+ it("should return false for non-revoked credential", async () => {
341
+ const manager = new StatusList2021Manager(
342
+ mockStorage,
343
+ mockIdentity,
344
+ mockSigningFunction,
345
+ mockCompressor,
346
+ mockDecompressor
347
+ );
348
+
349
+ const statusListId = "https://status.example.com/revocation/v1";
350
+ // Empty bitstring (all zeros)
351
+ const existingCredential = createStatusListCredential(
352
+ statusListId,
353
+ "revocation"
354
+ );
355
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
356
+ existingCredential
357
+ );
358
+
359
+ const credentialStatus: CredentialStatus = {
360
+ id: `${statusListId}#5`,
361
+ type: "StatusList2021Entry",
362
+ statusPurpose: "revocation",
363
+ statusListIndex: "5",
364
+ statusListCredential: statusListId,
365
+ };
366
+
367
+ const isRevoked = await manager.checkStatus(credentialStatus);
368
+
369
+ expect(isRevoked).toBe(false);
370
+ });
371
+
372
+ it("should return true for revoked credential", async () => {
373
+ const manager = new StatusList2021Manager(
374
+ mockStorage,
375
+ mockIdentity,
376
+ mockSigningFunction,
377
+ mockCompressor,
378
+ mockDecompressor
379
+ );
380
+
381
+ const statusListId = "https://status.example.com/revocation/v1";
382
+ // Create bitstring with bit 5 set
383
+ const bytes = new Uint8Array(2);
384
+ bytes[0] = 0b00100000; // Bit 5 is set
385
+ const encodedList = Buffer.from(bytes).toString("base64url");
386
+ const existingCredential = createStatusListCredential(
387
+ statusListId,
388
+ "revocation",
389
+ encodedList
390
+ );
391
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
392
+ existingCredential
393
+ );
394
+
395
+ const credentialStatus: CredentialStatus = {
396
+ id: `${statusListId}#5`,
397
+ type: "StatusList2021Entry",
398
+ statusPurpose: "revocation",
399
+ statusListIndex: "5",
400
+ statusListCredential: statusListId,
401
+ };
402
+
403
+ const isRevoked = await manager.checkStatus(credentialStatus);
404
+
405
+ expect(isRevoked).toBe(true);
406
+ });
407
+
408
+ it("should return false if status list doesn't exist", async () => {
409
+ const manager = new StatusList2021Manager(
410
+ mockStorage,
411
+ mockIdentity,
412
+ mockSigningFunction,
413
+ mockCompressor,
414
+ mockDecompressor
415
+ );
416
+
417
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
418
+
419
+ const credentialStatus: CredentialStatus = {
420
+ id: "https://status.example.com/revocation/v1#5",
421
+ type: "StatusList2021Entry",
422
+ statusPurpose: "revocation",
423
+ statusListIndex: "5",
424
+ statusListCredential: "https://status.example.com/revocation/v1",
425
+ };
426
+
427
+ const isRevoked = await manager.checkStatus(credentialStatus);
428
+
429
+ expect(isRevoked).toBe(false);
430
+ });
431
+ });
432
+
433
+ describe("getRevokedIndices", () => {
434
+ it("should return empty array if status list doesn't exist", async () => {
435
+ const manager = new StatusList2021Manager(
436
+ mockStorage,
437
+ mockIdentity,
438
+ mockSigningFunction,
439
+ mockCompressor,
440
+ mockDecompressor
441
+ );
442
+
443
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(null);
444
+
445
+ const indices = await manager.getRevokedIndices(
446
+ "https://status.example.com/revocation/v1"
447
+ );
448
+
449
+ expect(indices).toEqual([]);
450
+ });
451
+
452
+ it("should return indices of set bits", async () => {
453
+ const manager = new StatusList2021Manager(
454
+ mockStorage,
455
+ mockIdentity,
456
+ mockSigningFunction,
457
+ mockCompressor,
458
+ mockDecompressor
459
+ );
460
+
461
+ const statusListId = "https://status.example.com/revocation/v1";
462
+ // Create bitstring with bits 0, 2, and 5 set
463
+ const bytes = new Uint8Array(2);
464
+ bytes[0] = 0b00100101; // Bits 0, 2, 5 are set
465
+ const encodedList = Buffer.from(bytes).toString("base64url");
466
+ const existingCredential = createStatusListCredential(
467
+ statusListId,
468
+ "revocation",
469
+ encodedList
470
+ );
471
+ vi.mocked(mockStorage.getStatusList).mockResolvedValue(
472
+ existingCredential
473
+ );
474
+
475
+ const indices = await manager.getRevokedIndices(statusListId);
476
+
477
+ expect(indices).toContain(0);
478
+ expect(indices).toContain(2);
479
+ expect(indices).toContain(5);
480
+ });
481
+ });
482
+
483
+ describe("createStatusListManager factory", () => {
484
+ it("should create a StatusList2021Manager instance", () => {
485
+ const manager = createStatusListManager(
486
+ mockStorage,
487
+ mockIdentity,
488
+ mockSigningFunction,
489
+ mockCompressor,
490
+ mockDecompressor,
491
+ {
492
+ statusListBaseUrl: "https://factory.example.com/status",
493
+ }
494
+ );
495
+
496
+ expect(manager).toBeInstanceOf(StatusList2021Manager);
497
+ expect(manager.getStatusListBaseUrl()).toBe(
498
+ "https://factory.example.com/status"
499
+ );
500
+ });
501
+
502
+ it("should work without options", () => {
503
+ const manager = createStatusListManager(
504
+ mockStorage,
505
+ mockIdentity,
506
+ mockSigningFunction,
507
+ mockCompressor,
508
+ mockDecompressor
509
+ );
510
+
511
+ expect(manager).toBeInstanceOf(StatusList2021Manager);
512
+ expect(manager.getStatusListBaseUrl()).toBe("https://status.example.com");
513
+ });
514
+ });
515
+ });