@kya-os/mcp-i-core 1.2.3-canary.6 → 1.3.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 (231) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.turbo/turbo-test$colon$coverage.log +4514 -0
  4. package/.turbo/turbo-test.log +2973 -0
  5. package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
  6. package/Composer 3.md +615 -0
  7. package/GPT-5.md +1169 -0
  8. package/OPUS-plan.md +352 -0
  9. package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
  10. package/PHASE_3_SUMMARY.md +317 -0
  11. package/PHASE_4.1.3_SUMMARY.md +428 -0
  12. package/PHASE_4.1_COMPLETE.md +525 -0
  13. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
  14. package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
  15. package/TEST_PLAN.md +571 -0
  16. package/coverage/coverage-final.json +57 -0
  17. package/dist/__tests__/utils/mock-providers.d.ts +1 -2
  18. package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
  19. package/dist/__tests__/utils/mock-providers.js.map +1 -1
  20. package/dist/cache/oauth-config-cache.d.ts +69 -0
  21. package/dist/cache/oauth-config-cache.d.ts.map +1 -0
  22. package/dist/cache/oauth-config-cache.js +76 -0
  23. package/dist/cache/oauth-config-cache.js.map +1 -0
  24. package/dist/identity/idp-token-resolver.d.ts +53 -0
  25. package/dist/identity/idp-token-resolver.d.ts.map +1 -0
  26. package/dist/identity/idp-token-resolver.js +108 -0
  27. package/dist/identity/idp-token-resolver.js.map +1 -0
  28. package/dist/identity/idp-token-storage.interface.d.ts +42 -0
  29. package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
  30. package/dist/identity/idp-token-storage.interface.js +12 -0
  31. package/dist/identity/idp-token-storage.interface.js.map +1 -0
  32. package/dist/identity/user-did-manager.d.ts +39 -1
  33. package/dist/identity/user-did-manager.d.ts.map +1 -1
  34. package/dist/identity/user-did-manager.js +69 -3
  35. package/dist/identity/user-did-manager.js.map +1 -1
  36. package/dist/index.d.ts +22 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +39 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/runtime/audit-logger.d.ts +37 -0
  41. package/dist/runtime/audit-logger.d.ts.map +1 -0
  42. package/dist/runtime/audit-logger.js +9 -0
  43. package/dist/runtime/audit-logger.js.map +1 -0
  44. package/dist/runtime/base.d.ts +58 -2
  45. package/dist/runtime/base.d.ts.map +1 -1
  46. package/dist/runtime/base.js +266 -11
  47. package/dist/runtime/base.js.map +1 -1
  48. package/dist/services/access-control.service.d.ts.map +1 -1
  49. package/dist/services/access-control.service.js +200 -35
  50. package/dist/services/access-control.service.js.map +1 -1
  51. package/dist/services/authorization/authorization-registry.d.ts +29 -0
  52. package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
  53. package/dist/services/authorization/authorization-registry.js +57 -0
  54. package/dist/services/authorization/authorization-registry.js.map +1 -0
  55. package/dist/services/authorization/types.d.ts +53 -0
  56. package/dist/services/authorization/types.d.ts.map +1 -0
  57. package/dist/services/authorization/types.js +10 -0
  58. package/dist/services/authorization/types.js.map +1 -0
  59. package/dist/services/batch-delegation.service.d.ts +53 -0
  60. package/dist/services/batch-delegation.service.d.ts.map +1 -0
  61. package/dist/services/batch-delegation.service.js +95 -0
  62. package/dist/services/batch-delegation.service.js.map +1 -0
  63. package/dist/services/oauth-config.service.d.ts +53 -0
  64. package/dist/services/oauth-config.service.d.ts.map +1 -0
  65. package/dist/services/oauth-config.service.js +117 -0
  66. package/dist/services/oauth-config.service.js.map +1 -0
  67. package/dist/services/oauth-provider-registry.d.ts +77 -0
  68. package/dist/services/oauth-provider-registry.d.ts.map +1 -0
  69. package/dist/services/oauth-provider-registry.js +112 -0
  70. package/dist/services/oauth-provider-registry.js.map +1 -0
  71. package/dist/services/oauth-service.d.ts +77 -0
  72. package/dist/services/oauth-service.d.ts.map +1 -0
  73. package/dist/services/oauth-service.js +348 -0
  74. package/dist/services/oauth-service.js.map +1 -0
  75. package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
  76. package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
  77. package/dist/services/oauth-token-retrieval.service.js +150 -0
  78. package/dist/services/oauth-token-retrieval.service.js.map +1 -0
  79. package/dist/services/provider-resolver.d.ts +48 -0
  80. package/dist/services/provider-resolver.d.ts.map +1 -0
  81. package/dist/services/provider-resolver.js +120 -0
  82. package/dist/services/provider-resolver.js.map +1 -0
  83. package/dist/services/provider-validator.d.ts +55 -0
  84. package/dist/services/provider-validator.d.ts.map +1 -0
  85. package/dist/services/provider-validator.js +135 -0
  86. package/dist/services/provider-validator.js.map +1 -0
  87. package/dist/services/tool-context-builder.d.ts +57 -0
  88. package/dist/services/tool-context-builder.d.ts.map +1 -0
  89. package/dist/services/tool-context-builder.js +125 -0
  90. package/dist/services/tool-context-builder.js.map +1 -0
  91. package/dist/services/tool-protection.service.d.ts +87 -10
  92. package/dist/services/tool-protection.service.d.ts.map +1 -1
  93. package/dist/services/tool-protection.service.js +282 -112
  94. package/dist/services/tool-protection.service.js.map +1 -1
  95. package/dist/types/oauth-required-error.d.ts +40 -0
  96. package/dist/types/oauth-required-error.d.ts.map +1 -0
  97. package/dist/types/oauth-required-error.js +40 -0
  98. package/dist/types/oauth-required-error.js.map +1 -0
  99. package/dist/utils/did-helpers.d.ts +33 -0
  100. package/dist/utils/did-helpers.d.ts.map +1 -1
  101. package/dist/utils/did-helpers.js +40 -0
  102. package/dist/utils/did-helpers.js.map +1 -1
  103. package/dist/utils/index.d.ts +1 -0
  104. package/dist/utils/index.d.ts.map +1 -1
  105. package/dist/utils/index.js +1 -0
  106. package/dist/utils/index.js.map +1 -1
  107. package/docs/API_REFERENCE.md +1362 -0
  108. package/docs/COMPLIANCE_MATRIX.md +691 -0
  109. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  110. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  111. package/package.json +24 -50
  112. package/scripts/audit-compliance.ts +724 -0
  113. package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
  114. package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
  115. package/src/__tests__/delegation-e2e.test.ts +690 -0
  116. package/src/__tests__/identity/user-did-manager.test.ts +213 -0
  117. package/src/__tests__/index.test.ts +56 -0
  118. package/src/__tests__/integration/full-flow.test.ts +776 -0
  119. package/src/__tests__/integration.test.ts +281 -0
  120. package/src/__tests__/providers/base.test.ts +173 -0
  121. package/src/__tests__/providers/memory.test.ts +319 -0
  122. package/src/__tests__/regression/phase2-regression.test.ts +427 -0
  123. package/src/__tests__/runtime/audit-logger.test.ts +154 -0
  124. package/src/__tests__/runtime/base-extensions.test.ts +593 -0
  125. package/src/__tests__/runtime/base.test.ts +869 -0
  126. package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
  127. package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
  128. package/src/__tests__/runtime/route-interception.test.ts +686 -0
  129. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
  130. package/src/__tests__/services/agentshield-integration.test.ts +784 -0
  131. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
  132. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
  133. package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
  134. package/src/__tests__/utils/mock-providers.ts +340 -0
  135. package/src/cache/oauth-config-cache.d.ts +69 -0
  136. package/src/cache/oauth-config-cache.d.ts.map +1 -0
  137. package/src/cache/oauth-config-cache.js +71 -0
  138. package/src/cache/oauth-config-cache.js.map +1 -0
  139. package/src/cache/oauth-config-cache.ts +123 -0
  140. package/src/cache/tool-protection-cache.ts +171 -0
  141. package/src/compliance/EXAMPLE.md +412 -0
  142. package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
  143. package/src/compliance/index.ts +8 -0
  144. package/src/compliance/schema-registry.ts +460 -0
  145. package/src/compliance/schema-verifier.ts +708 -0
  146. package/src/config/__tests__/remote-config.spec.ts +268 -0
  147. package/src/config/remote-config.ts +174 -0
  148. package/src/config.ts +309 -0
  149. package/src/delegation/__tests__/audience-validator.test.ts +112 -0
  150. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  151. package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
  152. package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
  153. package/src/delegation/__tests__/utils.test.ts +152 -0
  154. package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
  155. package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
  156. package/src/delegation/audience-validator.ts +52 -0
  157. package/src/delegation/bitstring.ts +278 -0
  158. package/src/delegation/cascading-revocation.ts +370 -0
  159. package/src/delegation/delegation-graph.ts +299 -0
  160. package/src/delegation/index.ts +14 -0
  161. package/src/delegation/statuslist-manager.ts +353 -0
  162. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  163. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  164. package/src/delegation/storage/index.ts +9 -0
  165. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  166. package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
  167. package/src/delegation/utils.ts +42 -0
  168. package/src/delegation/vc-issuer.ts +232 -0
  169. package/src/delegation/vc-verifier.ts +568 -0
  170. package/src/identity/idp-token-resolver.ts +147 -0
  171. package/src/identity/idp-token-storage.interface.ts +59 -0
  172. package/src/identity/user-did-manager.ts +370 -0
  173. package/src/index.ts +260 -0
  174. package/src/providers/base.d.ts +91 -0
  175. package/src/providers/base.d.ts.map +1 -0
  176. package/src/providers/base.js +38 -0
  177. package/src/providers/base.js.map +1 -0
  178. package/src/providers/base.ts +96 -0
  179. package/src/providers/memory.ts +142 -0
  180. package/src/runtime/audit-logger.ts +39 -0
  181. package/src/runtime/base.ts +1329 -0
  182. package/src/services/__tests__/access-control.integration.test.ts +443 -0
  183. package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
  184. package/src/services/__tests__/access-control.service.test.ts +970 -0
  185. package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
  186. package/src/services/__tests__/crypto.service.test.ts +531 -0
  187. package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
  188. package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
  189. package/src/services/__tests__/proof-verifier.test.ts +489 -0
  190. package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
  191. package/src/services/__tests__/provider-resolver.test.ts +217 -0
  192. package/src/services/__tests__/storage.service.test.ts +358 -0
  193. package/src/services/access-control.service.ts +990 -0
  194. package/src/services/authorization/authorization-registry.ts +66 -0
  195. package/src/services/authorization/types.ts +71 -0
  196. package/src/services/batch-delegation.service.ts +137 -0
  197. package/src/services/crypto.service.ts +302 -0
  198. package/src/services/errors.ts +76 -0
  199. package/src/services/index.ts +9 -0
  200. package/src/services/oauth-config.service.d.ts +53 -0
  201. package/src/services/oauth-config.service.d.ts.map +1 -0
  202. package/src/services/oauth-config.service.js +113 -0
  203. package/src/services/oauth-config.service.js.map +1 -0
  204. package/src/services/oauth-config.service.ts +166 -0
  205. package/src/services/oauth-provider-registry.d.ts +57 -0
  206. package/src/services/oauth-provider-registry.d.ts.map +1 -0
  207. package/src/services/oauth-provider-registry.js +73 -0
  208. package/src/services/oauth-provider-registry.js.map +1 -0
  209. package/src/services/oauth-provider-registry.ts +123 -0
  210. package/src/services/oauth-service.ts +510 -0
  211. package/src/services/oauth-token-retrieval.service.ts +245 -0
  212. package/src/services/proof-verifier.ts +478 -0
  213. package/src/services/provider-resolver.d.ts +48 -0
  214. package/src/services/provider-resolver.d.ts.map +1 -0
  215. package/src/services/provider-resolver.js +106 -0
  216. package/src/services/provider-resolver.js.map +1 -0
  217. package/src/services/provider-resolver.ts +144 -0
  218. package/src/services/provider-validator.ts +170 -0
  219. package/src/services/storage.service.ts +566 -0
  220. package/src/services/tool-context-builder.ts +172 -0
  221. package/src/services/tool-protection.service.ts +958 -0
  222. package/src/types/oauth-required-error.ts +63 -0
  223. package/src/types/tool-protection.ts +155 -0
  224. package/src/utils/__tests__/did-helpers.test.ts +101 -0
  225. package/src/utils/base64.ts +148 -0
  226. package/src/utils/cors.ts +83 -0
  227. package/src/utils/did-helpers.ts +150 -0
  228. package/src/utils/index.ts +8 -0
  229. package/src/utils/storage-keys.ts +278 -0
  230. package/tsconfig.json +21 -0
  231. package/vitest.config.ts +56 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Delegation Audience Validation
3
+ *
4
+ * Validates if a delegation's audience matches the server DID.
5
+ * Supports both single server DID and multiple server DIDs.
6
+ *
7
+ * @package @kya-os/mcp-i-core/delegation
8
+ */
9
+
10
+ import type { DelegationRecord } from "@kya-os/contracts/delegation";
11
+
12
+ /**
13
+ * Verify if a delegation's audience matches the server DID
14
+ *
15
+ * @param delegation - Delegation record to check
16
+ * @param serverDid - Server DID to verify against
17
+ * @returns true if delegation is valid for this server
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Delegation without audience - valid on any server
22
+ * verifyDelegationAudience(delegation, "did:web:server1.com") // true
23
+ *
24
+ * // Delegation with matching audience
25
+ * verifyDelegationAudience(delegation, "did:web:server1.com") // true
26
+ *
27
+ * // Delegation with non-matching audience
28
+ * verifyDelegationAudience(delegation, "did:web:server2.com") // false
29
+ *
30
+ * // Delegation with array audience containing server
31
+ * verifyDelegationAudience(delegation, "did:web:server1.com") // true
32
+ * ```
33
+ */
34
+ export function verifyDelegationAudience(
35
+ delegation: DelegationRecord,
36
+ serverDid: string
37
+ ): boolean {
38
+ // If no audience specified, delegation is valid for any server
39
+ if (!delegation.constraints.audience) {
40
+ return true;
41
+ }
42
+
43
+ // Check if server DID matches audience
44
+ const audience = delegation.constraints.audience;
45
+ if (typeof audience === "string") {
46
+ return audience === serverDid;
47
+ }
48
+
49
+ // Array of server DIDs
50
+ return audience.includes(serverDid);
51
+ }
52
+
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Bitstring Utilities for StatusList2021
3
+ *
4
+ * Implements GZIP compression + base64url encoding for efficient status lists.
5
+ * Per W3C StatusList2021 spec, each bit represents credential status:
6
+ * - 0: Not revoked/suspended
7
+ * - 1: Revoked/suspended
8
+ *
9
+ * Related Spec: W3C StatusList2021
10
+ * Python Reference: Delegation-Revocation.md
11
+ */
12
+
13
+ /**
14
+ * Platform-agnostic bitstring operations
15
+ *
16
+ * Implementations must provide compression/decompression functions
17
+ * since these are platform-specific (Node.js uses zlib, Cloudflare uses CompressionStream)
18
+ */
19
+
20
+ /**
21
+ * Compression function interface
22
+ */
23
+ export interface CompressionFunction {
24
+ /**
25
+ * Compress data using GZIP
26
+ * @param data - Data to compress
27
+ * @returns Compressed data
28
+ */
29
+ compress(data: Uint8Array): Promise<Uint8Array>;
30
+ }
31
+
32
+ /**
33
+ * Decompression function interface
34
+ */
35
+ export interface DecompressionFunction {
36
+ /**
37
+ * Decompress GZIP data
38
+ * @param data - Compressed data
39
+ * @returns Decompressed data
40
+ */
41
+ decompress(data: Uint8Array): Promise<Uint8Array>;
42
+ }
43
+
44
+ /**
45
+ * Bitstring encoder/decoder
46
+ *
47
+ * Manages a bitstring for credential status tracking.
48
+ * Platform-agnostic - requires compression functions to be injected.
49
+ */
50
+ export class BitstringManager {
51
+ private bits: Uint8Array;
52
+ private size: number;
53
+
54
+ constructor(
55
+ size: number,
56
+ private compressor: CompressionFunction,
57
+ private decompressor: DecompressionFunction
58
+ ) {
59
+ this.size = size;
60
+ // Allocate bytes (8 bits per byte)
61
+ const byteCount = Math.ceil(size / 8);
62
+ this.bits = new Uint8Array(byteCount);
63
+ }
64
+
65
+ /**
66
+ * Set a bit at a specific index
67
+ *
68
+ * @param index - The bit index (0-based)
69
+ * @param value - true to set (revoked), false to clear (active)
70
+ */
71
+ setBit(index: number, value: boolean): void {
72
+ if (index < 0 || index >= this.size) {
73
+ throw new Error(`Bit index ${index} out of range (0-${this.size - 1})`);
74
+ }
75
+
76
+ const byteIndex = Math.floor(index / 8);
77
+ const bitIndex = index % 8;
78
+
79
+ if (value) {
80
+ // Set bit to 1
81
+ this.bits[byteIndex] |= 1 << bitIndex;
82
+ } else {
83
+ // Clear bit to 0
84
+ this.bits[byteIndex] &= ~(1 << bitIndex);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get a bit at a specific index
90
+ *
91
+ * @param index - The bit index (0-based)
92
+ * @returns true if set (revoked), false if clear (active)
93
+ */
94
+ getBit(index: number): boolean {
95
+ if (index < 0 || index >= this.size) {
96
+ throw new Error(`Bit index ${index} out of range (0-${this.size - 1})`);
97
+ }
98
+
99
+ const byteIndex = Math.floor(index / 8);
100
+ const bitIndex = index % 8;
101
+
102
+ return (this.bits[byteIndex] & (1 << bitIndex)) !== 0;
103
+ }
104
+
105
+ /**
106
+ * Get all set bit indices
107
+ *
108
+ * @returns Array of indices where bits are set to 1
109
+ */
110
+ getSetBits(): number[] {
111
+ const setBits: number[] = [];
112
+ for (let i = 0; i < this.size; i++) {
113
+ if (this.getBit(i)) {
114
+ setBits.push(i);
115
+ }
116
+ }
117
+ return setBits;
118
+ }
119
+
120
+ /**
121
+ * Encode bitstring to base64url (GZIP compressed)
122
+ *
123
+ * Per StatusList2021 spec:
124
+ * 1. GZIP compress the bitstring
125
+ * 2. Base64url encode the compressed data
126
+ *
127
+ * @returns Base64url-encoded compressed bitstring
128
+ */
129
+ async encode(): Promise<string> {
130
+ // Step 1: GZIP compress
131
+ const compressed = await this.compressor.compress(this.bits);
132
+
133
+ // Step 2: Base64url encode
134
+ return this.base64urlEncode(compressed);
135
+ }
136
+
137
+ /**
138
+ * Decode base64url bitstring (GZIP compressed)
139
+ *
140
+ * @param encodedList - Base64url-encoded compressed bitstring
141
+ * @returns BitstringManager instance
142
+ */
143
+ static async decode(
144
+ encodedList: string,
145
+ compressor: CompressionFunction,
146
+ decompressor: DecompressionFunction
147
+ ): Promise<BitstringManager> {
148
+ // Step 1: Base64url decode
149
+ const compressed = BitstringManager.base64urlDecode(encodedList);
150
+
151
+ // Step 2: GZIP decompress
152
+ const decompressed = await decompressor.decompress(compressed);
153
+
154
+ // Step 3: Create manager with decoded bits
155
+ const size = decompressed.length * 8;
156
+ const manager = new BitstringManager(size, compressor, decompressor);
157
+ manager.bits = decompressed;
158
+ return manager;
159
+ }
160
+
161
+ /**
162
+ * Get the raw bitstring
163
+ */
164
+ getRawBits(): Uint8Array {
165
+ return this.bits;
166
+ }
167
+
168
+ /**
169
+ * Get the size (number of bits)
170
+ */
171
+ getSize(): number {
172
+ return this.size;
173
+ }
174
+
175
+ /**
176
+ * Base64url encode (RFC 4648)
177
+ *
178
+ * Platform-agnostic implementation.
179
+ */
180
+ private base64urlEncode(data: Uint8Array): string {
181
+ // Convert to base64
182
+ const base64 = this.bytesToBase64(data);
183
+
184
+ // Convert base64 to base64url
185
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
186
+ }
187
+
188
+ /**
189
+ * Base64url decode (RFC 4648)
190
+ */
191
+ private static base64urlDecode(encoded: string): Uint8Array {
192
+ // Convert base64url to base64
193
+ let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
194
+
195
+ // Add padding if needed
196
+ while (base64.length % 4 !== 0) {
197
+ base64 += '=';
198
+ }
199
+
200
+ // Convert base64 to bytes
201
+ return BitstringManager.base64ToBytes(base64);
202
+ }
203
+
204
+ /**
205
+ * Convert bytes to base64
206
+ *
207
+ * Platform-agnostic implementation (works in Node and browsers/Cloudflare)
208
+ */
209
+ private bytesToBase64(bytes: Uint8Array): string {
210
+ const binary = Array.from(bytes)
211
+ .map((byte) => String.fromCharCode(byte))
212
+ .join('');
213
+ return btoa(binary);
214
+ }
215
+
216
+ /**
217
+ * Convert base64 to bytes
218
+ */
219
+ private static base64ToBytes(base64: string): Uint8Array {
220
+ const binary = atob(base64);
221
+ const bytes = new Uint8Array(binary.length);
222
+ for (let i = 0; i < binary.length; i++) {
223
+ bytes[i] = binary.charCodeAt(i);
224
+ }
225
+ return bytes;
226
+ }
227
+
228
+ /**
229
+ * Create a bitstring from a list of indices to set
230
+ *
231
+ * @param size - Total size of the bitstring
232
+ * @param setBits - Indices to set to 1
233
+ * @param compressor - Compression function
234
+ * @param decompressor - Decompression function
235
+ * @returns BitstringManager instance
236
+ */
237
+ static fromSetBits(
238
+ size: number,
239
+ setBits: number[],
240
+ compressor: CompressionFunction,
241
+ decompressor: DecompressionFunction
242
+ ): BitstringManager {
243
+ const manager = new BitstringManager(size, compressor, decompressor);
244
+ for (const index of setBits) {
245
+ manager.setBit(index, true);
246
+ }
247
+ return manager;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Helper to check if an index is set in an encoded status list
253
+ *
254
+ * Convenience function for quick status checks without creating a full manager.
255
+ *
256
+ * @param encodedList - Base64url-encoded compressed bitstring
257
+ * @param index - The bit index to check
258
+ * @param decompressor - Decompression function
259
+ * @returns true if bit is set (revoked), false otherwise
260
+ */
261
+ export async function isIndexSet(
262
+ encodedList: string,
263
+ index: number,
264
+ decompressor: DecompressionFunction
265
+ ): Promise<boolean> {
266
+ // Decode without needing compressor (we're only reading)
267
+ const compressed = BitstringManager['base64urlDecode'](encodedList);
268
+ const decompressed = await decompressor.decompress(compressed);
269
+
270
+ const byteIndex = Math.floor(index / 8);
271
+ const bitIndex = index % 8;
272
+
273
+ if (byteIndex >= decompressed.length) {
274
+ return false; // Out of range = not set
275
+ }
276
+
277
+ return (decompressed[byteIndex] & (1 << bitIndex)) !== 0;
278
+ }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Cascading Revocation Manager
3
+ *
4
+ * Implements cascading revocation per Python POC design.
5
+ * When a parent delegation is revoked, all children are automatically revoked.
6
+ *
7
+ * SOLID Principles:
8
+ * - Single Responsibility: Only handles cascading revocation logic
9
+ * - Open/Closed: Extensible via hooks/callbacks
10
+ * - Liskov Substitution: Works with any graph/statuslist implementations
11
+ * - Interface Segregation: Minimal interface
12
+ * - Dependency Inversion: Depends on abstractions (graph, statuslist)
13
+ *
14
+ * Related Spec: MCP-I §4.4, Delegation Chains
15
+ * Python Reference: Delegation-Revocation.md:45-67
16
+ */
17
+
18
+ import type { DelegationCredential, CredentialStatus } from '@kya-os/contracts';
19
+ import { DelegationGraphManager, DelegationNode } from './delegation-graph';
20
+ import { StatusList2021Manager } from './statuslist-manager';
21
+
22
+ /**
23
+ * Revocation event for auditing/logging
24
+ */
25
+ export interface RevocationEvent {
26
+ /** Delegation ID that was revoked */
27
+ delegationId: string;
28
+
29
+ /** Whether this was the root of the cascade or a child */
30
+ isRoot: boolean;
31
+
32
+ /** Parent delegation ID (if cascaded) */
33
+ parentId?: string;
34
+
35
+ /** Timestamp */
36
+ timestamp: number;
37
+
38
+ /** Reason for revocation */
39
+ reason?: string;
40
+ }
41
+
42
+ /**
43
+ * Revocation hook function
44
+ *
45
+ * Called for each delegation during cascading revocation.
46
+ * Useful for auditing, logging, or custom logic.
47
+ */
48
+ export type RevocationHook = (event: RevocationEvent) => Promise<void> | void;
49
+
50
+ /**
51
+ * Options for cascading revocation
52
+ */
53
+ export interface CascadingRevocationOptions {
54
+ /** Reason for revocation (for audit trail) */
55
+ reason?: string;
56
+
57
+ /** Optional hook called for each revocation */
58
+ onRevoke?: RevocationHook;
59
+
60
+ /** Maximum depth to cascade (prevents infinite loops) */
61
+ maxDepth?: number;
62
+
63
+ /** Dry run - don't actually revoke, just return what would be revoked */
64
+ dryRun?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Cascading Revocation Manager
69
+ *
70
+ * Coordinates revocation across the delegation graph.
71
+ * Per Delegation-Revocation.md:45-67:
72
+ * - When parent revoked → all descendants revoked
73
+ * - Uses StatusList2021 for efficient updates
74
+ * - Maintains audit trail
75
+ */
76
+ export class CascadingRevocationManager {
77
+ constructor(
78
+ private graph: DelegationGraphManager,
79
+ private statusList: StatusList2021Manager
80
+ ) {}
81
+
82
+ /**
83
+ * Revoke a delegation and all its descendants
84
+ *
85
+ * Per Delegation-Revocation.md:56-67:
86
+ * 1. Revoke the target delegation
87
+ * 2. Find all descendants
88
+ * 3. Revoke each descendant
89
+ * 4. Trigger hooks for auditing
90
+ *
91
+ * @param delegationId - The delegation ID to revoke
92
+ * @param options - Revocation options
93
+ * @returns Array of revoked delegation IDs
94
+ */
95
+ async revokeDelegation(
96
+ delegationId: string,
97
+ options: CascadingRevocationOptions = {}
98
+ ): Promise<RevocationEvent[]> {
99
+ const maxDepth = options.maxDepth || 100; // Safety limit
100
+ const events: RevocationEvent[] = [];
101
+
102
+ // Get the target delegation
103
+ const targetNode = await this.graph.getNode(delegationId);
104
+ if (!targetNode) {
105
+ throw new Error(`Delegation not found: ${delegationId}`);
106
+ }
107
+
108
+ // Check depth to prevent infinite loops
109
+ const depth = await this.graph.getDepth(delegationId);
110
+ if (depth > maxDepth) {
111
+ throw new Error(
112
+ `Delegation depth ${depth} exceeds maximum ${maxDepth}`
113
+ );
114
+ }
115
+
116
+ // Revoke the target delegation
117
+ const rootEvent = await this.revokeNode(
118
+ targetNode,
119
+ true,
120
+ options.reason,
121
+ options.dryRun
122
+ );
123
+ events.push(rootEvent);
124
+
125
+ if (options.onRevoke) {
126
+ await options.onRevoke(rootEvent);
127
+ }
128
+
129
+ // Get all descendants
130
+ const descendants = await this.graph.getDescendants(delegationId);
131
+
132
+ // Revoke each descendant
133
+ for (const descendant of descendants) {
134
+ const event = await this.revokeNode(
135
+ descendant,
136
+ false,
137
+ `Cascaded from ${delegationId}`,
138
+ options.dryRun,
139
+ delegationId
140
+ );
141
+ events.push(event);
142
+
143
+ if (options.onRevoke) {
144
+ await options.onRevoke(event);
145
+ }
146
+ }
147
+
148
+ return events;
149
+ }
150
+
151
+ /**
152
+ * Revoke a single node
153
+ *
154
+ * @param node - The delegation node
155
+ * @param isRoot - Whether this is the root of the cascade
156
+ * @param reason - Reason for revocation
157
+ * @param dryRun - If true, don't actually revoke
158
+ * @param parentId - Parent ID if cascaded
159
+ * @returns Revocation event
160
+ */
161
+ private async revokeNode(
162
+ node: DelegationNode,
163
+ isRoot: boolean,
164
+ reason?: string,
165
+ dryRun?: boolean,
166
+ parentId?: string
167
+ ): Promise<RevocationEvent> {
168
+ const event: RevocationEvent = {
169
+ delegationId: node.id,
170
+ isRoot,
171
+ parentId,
172
+ timestamp: Date.now(),
173
+ reason,
174
+ };
175
+
176
+ if (dryRun) {
177
+ return event;
178
+ }
179
+
180
+ // Parse the credential status from the node
181
+ if (node.credentialStatusId) {
182
+ const credentialStatus = this.parseCredentialStatus(node.credentialStatusId);
183
+ if (credentialStatus) {
184
+ await this.statusList.updateStatus(credentialStatus, true);
185
+ }
186
+ }
187
+
188
+ return event;
189
+ }
190
+
191
+ /**
192
+ * Restore (un-revoke) a delegation
193
+ *
194
+ * Note: This does NOT cascade to children.
195
+ * Only the specific delegation is restored.
196
+ *
197
+ * @param delegationId - The delegation ID to restore
198
+ * @returns Revocation event
199
+ */
200
+ async restoreDelegation(delegationId: string): Promise<RevocationEvent> {
201
+ const node = await this.graph.getNode(delegationId);
202
+ if (!node) {
203
+ throw new Error(`Delegation not found: ${delegationId}`);
204
+ }
205
+
206
+ const event: RevocationEvent = {
207
+ delegationId: node.id,
208
+ isRoot: true,
209
+ timestamp: Date.now(),
210
+ reason: 'Restored',
211
+ };
212
+
213
+ if (node.credentialStatusId) {
214
+ const credentialStatus = this.parseCredentialStatus(node.credentialStatusId);
215
+ if (credentialStatus) {
216
+ await this.statusList.updateStatus(credentialStatus, false);
217
+ }
218
+ }
219
+
220
+ return event;
221
+ }
222
+
223
+ /**
224
+ * Check if a delegation is revoked
225
+ *
226
+ * Checks both:
227
+ * 1. The delegation itself
228
+ * 2. Any of its ancestors (cascading check)
229
+ *
230
+ * Per Delegation-Revocation.md:56: If any ancestor is revoked, this is revoked.
231
+ *
232
+ * @param delegationId - The delegation ID
233
+ * @returns true if revoked (directly or via cascade)
234
+ */
235
+ async isRevoked(delegationId: string): Promise<{
236
+ revoked: boolean;
237
+ reason?: string;
238
+ revokedAncestor?: string;
239
+ }> {
240
+ // Get the chain from root to this delegation
241
+ const chain = await this.graph.getChain(delegationId);
242
+
243
+ // Check each node in the chain (bottom-up, most specific first)
244
+ for (const node of chain.reverse()) {
245
+ if (node.credentialStatusId) {
246
+ const credentialStatus = this.parseCredentialStatus(node.credentialStatusId);
247
+ if (credentialStatus) {
248
+ const isRevoked = await this.statusList.checkStatus(credentialStatus);
249
+ if (isRevoked) {
250
+ return {
251
+ revoked: true,
252
+ reason:
253
+ node.id === delegationId
254
+ ? 'Directly revoked'
255
+ : 'Ancestor revoked',
256
+ revokedAncestor: node.id === delegationId ? undefined : node.id,
257
+ };
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ return { revoked: false };
264
+ }
265
+
266
+ /**
267
+ * Get all revoked delegations in a subtree
268
+ *
269
+ * @param rootId - The root delegation ID
270
+ * @returns Array of revoked delegation IDs
271
+ */
272
+ async getRevokedInSubtree(rootId: string): Promise<string[]> {
273
+ const descendants = await this.graph.getDescendants(rootId);
274
+ const revoked: string[] = [];
275
+
276
+ // Check root
277
+ const rootRevoked = await this.isRevoked(rootId);
278
+ if (rootRevoked.revoked) {
279
+ revoked.push(rootId);
280
+ }
281
+
282
+ // Check each descendant
283
+ for (const node of descendants) {
284
+ const isRevoked = await this.isRevoked(node.id);
285
+ if (isRevoked.revoked) {
286
+ revoked.push(node.id);
287
+ }
288
+ }
289
+
290
+ return revoked;
291
+ }
292
+
293
+ /**
294
+ * Parse credential status from stored ID
295
+ *
296
+ * The credentialStatusId is stored as a composite:
297
+ * "statusListUrl#index"
298
+ *
299
+ * @param credentialStatusId - The stored credential status ID
300
+ * @returns Parsed CredentialStatus, or null if invalid
301
+ */
302
+ private parseCredentialStatus(
303
+ credentialStatusId: string
304
+ ): CredentialStatus | null {
305
+ const match = credentialStatusId.match(/^(.+)#(\d+)$/);
306
+ if (!match) return null;
307
+
308
+ const [, statusListCredential, indexStr] = match;
309
+ const index = parseInt(indexStr, 10);
310
+
311
+ return {
312
+ id: credentialStatusId,
313
+ type: 'StatusList2021Entry',
314
+ statusPurpose: 'revocation', // Assume revocation (could be enhanced)
315
+ statusListIndex: index.toString(),
316
+ statusListCredential,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Validate that a delegation can be used
322
+ *
323
+ * Checks:
324
+ * 1. The delegation itself is not revoked
325
+ * 2. No ancestors are revoked
326
+ * 3. The chain is valid
327
+ *
328
+ * @param delegationId - The delegation ID
329
+ * @returns Validation result
330
+ */
331
+ async validateDelegation(delegationId: string): Promise<{
332
+ valid: boolean;
333
+ reason?: string;
334
+ }> {
335
+ // Check if revoked
336
+ const revokedCheck = await this.isRevoked(delegationId);
337
+ if (revokedCheck.revoked) {
338
+ return {
339
+ valid: false,
340
+ reason: revokedCheck.revokedAncestor
341
+ ? `Ancestor ${revokedCheck.revokedAncestor} is revoked`
342
+ : 'Delegation is revoked',
343
+ };
344
+ }
345
+
346
+ // Validate chain structure
347
+ const chainValidation = await this.graph.validateChain(delegationId);
348
+ if (!chainValidation.valid) {
349
+ return chainValidation;
350
+ }
351
+
352
+ return { valid: true };
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Create a cascading revocation manager
358
+ *
359
+ * Convenience factory function.
360
+ *
361
+ * @param graph - Delegation graph manager
362
+ * @param statusList - StatusList2021 manager
363
+ * @returns CascadingRevocationManager instance
364
+ */
365
+ export function createCascadingRevocationManager(
366
+ graph: DelegationGraphManager,
367
+ statusList: StatusList2021Manager
368
+ ): CascadingRevocationManager {
369
+ return new CascadingRevocationManager(graph, statusList);
370
+ }