@kya-os/mcp-i-core 1.2.3-canary.7 → 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.
- package/.claude/settings.local.json +9 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +4514 -0
- package/.turbo/turbo-test.log +2973 -0
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
- package/Composer 3.md +615 -0
- package/GPT-5.md +1169 -0
- package/OPUS-plan.md +352 -0
- package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
- package/PHASE_3_SUMMARY.md +317 -0
- package/PHASE_4.1.3_SUMMARY.md +428 -0
- package/PHASE_4.1_COMPLETE.md +525 -0
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
- package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
- package/TEST_PLAN.md +571 -0
- package/coverage/coverage-final.json +57 -0
- package/dist/__tests__/utils/mock-providers.d.ts +1 -2
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/cache/oauth-config-cache.d.ts +69 -0
- package/dist/cache/oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/oauth-config-cache.js +76 -0
- package/dist/cache/oauth-config-cache.js.map +1 -0
- package/dist/identity/idp-token-resolver.d.ts +53 -0
- package/dist/identity/idp-token-resolver.d.ts.map +1 -0
- package/dist/identity/idp-token-resolver.js +108 -0
- package/dist/identity/idp-token-resolver.js.map +1 -0
- package/dist/identity/idp-token-storage.interface.d.ts +42 -0
- package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
- package/dist/identity/idp-token-storage.interface.js +12 -0
- package/dist/identity/idp-token-storage.interface.js.map +1 -0
- package/dist/identity/user-did-manager.d.ts +39 -1
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +69 -3
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +37 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +9 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/base.d.ts +58 -2
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +266 -11
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +200 -35
- package/dist/services/access-control.service.js.map +1 -1
- package/dist/services/authorization/authorization-registry.d.ts +29 -0
- package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
- package/dist/services/authorization/authorization-registry.js +57 -0
- package/dist/services/authorization/authorization-registry.js.map +1 -0
- package/dist/services/authorization/types.d.ts +53 -0
- package/dist/services/authorization/types.d.ts.map +1 -0
- package/dist/services/authorization/types.js +10 -0
- package/dist/services/authorization/types.js.map +1 -0
- package/dist/services/batch-delegation.service.d.ts +53 -0
- package/dist/services/batch-delegation.service.d.ts.map +1 -0
- package/dist/services/batch-delegation.service.js +95 -0
- package/dist/services/batch-delegation.service.js.map +1 -0
- package/dist/services/oauth-config.service.d.ts +53 -0
- package/dist/services/oauth-config.service.d.ts.map +1 -0
- package/dist/services/oauth-config.service.js +117 -0
- package/dist/services/oauth-config.service.js.map +1 -0
- package/dist/services/oauth-provider-registry.d.ts +77 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -0
- package/dist/services/oauth-provider-registry.js +112 -0
- package/dist/services/oauth-provider-registry.js.map +1 -0
- package/dist/services/oauth-service.d.ts +77 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +348 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
- package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
- package/dist/services/oauth-token-retrieval.service.js +150 -0
- package/dist/services/oauth-token-retrieval.service.js.map +1 -0
- package/dist/services/provider-resolver.d.ts +48 -0
- package/dist/services/provider-resolver.d.ts.map +1 -0
- package/dist/services/provider-resolver.js +120 -0
- package/dist/services/provider-resolver.js.map +1 -0
- package/dist/services/provider-validator.d.ts +55 -0
- package/dist/services/provider-validator.d.ts.map +1 -0
- package/dist/services/provider-validator.js +135 -0
- package/dist/services/provider-validator.js.map +1 -0
- package/dist/services/tool-context-builder.d.ts +57 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +125 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/services/tool-protection.service.d.ts +87 -10
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +282 -112
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/types/oauth-required-error.d.ts +40 -0
- package/dist/types/oauth-required-error.d.ts.map +1 -0
- package/dist/types/oauth-required-error.js +40 -0
- package/dist/types/oauth-required-error.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +33 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +40 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/docs/API_REFERENCE.md +1362 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +24 -50
- package/scripts/audit-compliance.ts +724 -0
- package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
- package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
- package/src/__tests__/delegation-e2e.test.ts +690 -0
- package/src/__tests__/identity/user-did-manager.test.ts +213 -0
- package/src/__tests__/index.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +776 -0
- package/src/__tests__/integration.test.ts +281 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +319 -0
- package/src/__tests__/regression/phase2-regression.test.ts +427 -0
- package/src/__tests__/runtime/audit-logger.test.ts +154 -0
- package/src/__tests__/runtime/base-extensions.test.ts +593 -0
- package/src/__tests__/runtime/base.test.ts +869 -0
- package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
- package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
- package/src/__tests__/runtime/route-interception.test.ts +686 -0
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
- package/src/__tests__/services/agentshield-integration.test.ts +784 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
- package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
- package/src/__tests__/utils/mock-providers.ts +340 -0
- package/src/cache/oauth-config-cache.d.ts +69 -0
- package/src/cache/oauth-config-cache.d.ts.map +1 -0
- package/src/cache/oauth-config-cache.js +71 -0
- package/src/cache/oauth-config-cache.js.map +1 -0
- package/src/cache/oauth-config-cache.ts +123 -0
- package/src/cache/tool-protection-cache.ts +171 -0
- package/src/compliance/EXAMPLE.md +412 -0
- package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
- package/src/compliance/index.ts +8 -0
- package/src/compliance/schema-registry.ts +460 -0
- package/src/compliance/schema-verifier.ts +708 -0
- package/src/config/__tests__/remote-config.spec.ts +268 -0
- package/src/config/remote-config.ts +174 -0
- package/src/config.ts +309 -0
- package/src/delegation/__tests__/audience-validator.test.ts +112 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
- package/src/delegation/__tests__/utils.test.ts +152 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
- package/src/delegation/audience-validator.ts +52 -0
- package/src/delegation/bitstring.ts +278 -0
- package/src/delegation/cascading-revocation.ts +370 -0
- package/src/delegation/delegation-graph.ts +299 -0
- package/src/delegation/index.ts +14 -0
- package/src/delegation/statuslist-manager.ts +353 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/index.ts +9 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
- package/src/delegation/utils.ts +42 -0
- package/src/delegation/vc-issuer.ts +232 -0
- package/src/delegation/vc-verifier.ts +568 -0
- package/src/identity/idp-token-resolver.ts +147 -0
- package/src/identity/idp-token-storage.interface.ts +59 -0
- package/src/identity/user-did-manager.ts +370 -0
- package/src/index.ts +260 -0
- package/src/providers/base.d.ts +91 -0
- package/src/providers/base.d.ts.map +1 -0
- package/src/providers/base.js +38 -0
- package/src/providers/base.js.map +1 -0
- package/src/providers/base.ts +96 -0
- package/src/providers/memory.ts +142 -0
- package/src/runtime/audit-logger.ts +39 -0
- package/src/runtime/base.ts +1329 -0
- package/src/services/__tests__/access-control.integration.test.ts +443 -0
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
- package/src/services/__tests__/access-control.service.test.ts +970 -0
- package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
- package/src/services/__tests__/crypto.service.test.ts +531 -0
- package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
- package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
- package/src/services/__tests__/proof-verifier.test.ts +489 -0
- package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
- package/src/services/__tests__/provider-resolver.test.ts +217 -0
- package/src/services/__tests__/storage.service.test.ts +358 -0
- package/src/services/access-control.service.ts +990 -0
- package/src/services/authorization/authorization-registry.ts +66 -0
- package/src/services/authorization/types.ts +71 -0
- package/src/services/batch-delegation.service.ts +137 -0
- package/src/services/crypto.service.ts +302 -0
- package/src/services/errors.ts +76 -0
- package/src/services/index.ts +9 -0
- package/src/services/oauth-config.service.d.ts +53 -0
- package/src/services/oauth-config.service.d.ts.map +1 -0
- package/src/services/oauth-config.service.js +113 -0
- package/src/services/oauth-config.service.js.map +1 -0
- package/src/services/oauth-config.service.ts +166 -0
- package/src/services/oauth-provider-registry.d.ts +57 -0
- package/src/services/oauth-provider-registry.d.ts.map +1 -0
- package/src/services/oauth-provider-registry.js +73 -0
- package/src/services/oauth-provider-registry.js.map +1 -0
- package/src/services/oauth-provider-registry.ts +123 -0
- package/src/services/oauth-service.ts +510 -0
- package/src/services/oauth-token-retrieval.service.ts +245 -0
- package/src/services/proof-verifier.ts +478 -0
- package/src/services/provider-resolver.d.ts +48 -0
- package/src/services/provider-resolver.d.ts.map +1 -0
- package/src/services/provider-resolver.js +106 -0
- package/src/services/provider-resolver.js.map +1 -0
- package/src/services/provider-resolver.ts +144 -0
- package/src/services/provider-validator.ts +170 -0
- package/src/services/storage.service.ts +566 -0
- package/src/services/tool-context-builder.ts +172 -0
- package/src/services/tool-protection.service.ts +958 -0
- package/src/types/oauth-required-error.ts +63 -0
- package/src/types/tool-protection.ts +155 -0
- package/src/utils/__tests__/did-helpers.test.ts +101 -0
- package/src/utils/base64.ts +148 -0
- package/src/utils/cors.ts +83 -0
- package/src/utils/did-helpers.ts +150 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/storage-keys.ts +278 -0
- package/tsconfig.json +21 -0
- 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
|
+
}
|