@kya-os/mcp-i-core 1.3.10-canary.clientinfo.20251126124133 → 1.3.10
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/.turbo/turbo-build.log +1 -1
- package/dist/__tests__/utils/mock-providers.d.ts +2 -1
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/config/remote-config.d.ts +51 -0
- package/dist/config/remote-config.d.ts.map +1 -1
- package/dist/config/remote-config.js +74 -0
- package/dist/config/remote-config.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -1
- package/dist/config.js.map +1 -1
- package/dist/delegation/did-key-resolver.d.ts +64 -0
- package/dist/delegation/did-key-resolver.d.ts.map +1 -0
- package/dist/delegation/did-key-resolver.js +159 -0
- package/dist/delegation/did-key-resolver.js.map +1 -0
- package/dist/delegation/utils.d.ts +76 -0
- package/dist/delegation/utils.d.ts.map +1 -1
- package/dist/delegation/utils.js +117 -0
- package/dist/delegation/utils.js.map +1 -1
- package/dist/identity/user-did-manager.d.ts +95 -12
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +107 -25
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +25 -8
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +74 -21
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/session-registration.service.d.ts.map +1 -1
- package/dist/services/session-registration.service.js +10 -90
- package/dist/services/session-registration.service.js.map +1 -1
- package/dist/services/tool-protection.service.d.ts +5 -2
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +72 -24
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/utils/base58.d.ts +31 -0
- package/dist/utils/base58.d.ts.map +1 -0
- package/dist/utils/base58.js +103 -0
- package/dist/utils/base58.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/identity/user-did-manager.test.ts +64 -45
- package/src/__tests__/integration/full-flow.test.ts +23 -10
- package/src/__tests__/runtime/base-extensions.test.ts +23 -21
- package/src/__tests__/runtime/proof-client-did.test.ts +19 -18
- package/src/__tests__/services/agentshield-integration.test.ts +10 -3
- package/src/__tests__/services/tool-protection-merged-config.test.ts +485 -0
- package/src/__tests__/services/tool-protection.service.test.ts +18 -11
- package/src/config/__tests__/merged-config.spec.ts +445 -0
- package/src/config/remote-config.ts +90 -0
- package/src/config.ts +3 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
- package/src/delegation/did-key-resolver.ts +179 -0
- package/src/delegation/utils.ts +179 -0
- package/src/identity/user-did-manager.ts +185 -29
- package/src/index.ts +36 -1
- package/src/runtime/base.ts +84 -21
- package/src/services/session-registration.service.ts +26 -121
- package/src/services/tool-protection.service.ts +125 -56
- package/src/utils/base58.ts +109 -0
- package/coverage/coverage-final.json +0 -57
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createDidKeyResolver,
|
|
4
|
+
isEd25519DidKey,
|
|
5
|
+
extractPublicKeyFromDidKey,
|
|
6
|
+
publicKeyToJwk,
|
|
7
|
+
resolveDidKeySync,
|
|
8
|
+
} from "../did-key-resolver";
|
|
9
|
+
import { base58Encode, base58Decode, isValidBase58 } from "../../utils/base58";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tests for did:key resolver and base58 utilities
|
|
13
|
+
*
|
|
14
|
+
* These tests verify the Phase 3 VC verification infrastructure:
|
|
15
|
+
* - Base58 encoding/decoding for multibase keys
|
|
16
|
+
* - did:key resolution to DID Documents
|
|
17
|
+
* - Ed25519 public key extraction
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
describe("Base58 Utilities", () => {
|
|
21
|
+
describe("base58Encode", () => {
|
|
22
|
+
it("should encode empty bytes", () => {
|
|
23
|
+
expect(base58Encode(new Uint8Array([]))).toBe("");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should encode single byte", () => {
|
|
27
|
+
expect(base58Encode(new Uint8Array([0]))).toBe("1");
|
|
28
|
+
expect(base58Encode(new Uint8Array([1])).length).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should encode known values", () => {
|
|
32
|
+
// 'Hello' in bytes
|
|
33
|
+
const helloBytes = new TextEncoder().encode("Hello");
|
|
34
|
+
const encoded = base58Encode(helloBytes);
|
|
35
|
+
expect(encoded.length).toBeGreaterThan(0);
|
|
36
|
+
expect(isValidBase58(encoded)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle leading zeros", () => {
|
|
40
|
+
const withLeadingZeros = new Uint8Array([0, 0, 1, 2, 3]);
|
|
41
|
+
const encoded = base58Encode(withLeadingZeros);
|
|
42
|
+
// Leading zeros become '1' in base58
|
|
43
|
+
expect(encoded.startsWith("11")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("base58Decode", () => {
|
|
48
|
+
it("should decode empty string", () => {
|
|
49
|
+
expect(base58Decode("")).toEqual(new Uint8Array([]));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should decode leading '1' as zero bytes", () => {
|
|
53
|
+
const result = base58Decode("111");
|
|
54
|
+
expect(result).toEqual(new Uint8Array([0, 0, 0]));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should throw on invalid characters", () => {
|
|
58
|
+
// '0', 'O', 'I', 'l' are not in base58 alphabet
|
|
59
|
+
expect(() => base58Decode("0invalid")).toThrow("Invalid base58 character");
|
|
60
|
+
expect(() => base58Decode("testO")).toThrow("Invalid base58 character");
|
|
61
|
+
expect(() => base58Decode("testI")).toThrow("Invalid base58 character");
|
|
62
|
+
expect(() => base58Decode("testl")).toThrow("Invalid base58 character");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should roundtrip with base58Encode", () => {
|
|
66
|
+
const originalBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
67
|
+
const encoded = base58Encode(originalBytes);
|
|
68
|
+
const decoded = base58Decode(encoded);
|
|
69
|
+
expect(decoded).toEqual(originalBytes);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should roundtrip Ed25519 key bytes", () => {
|
|
73
|
+
// Simulate a 32-byte Ed25519 public key with multicodec prefix
|
|
74
|
+
const ed25519Prefix = new Uint8Array([0xed, 0x01]);
|
|
75
|
+
const mockPublicKey = new Uint8Array(32).fill(42);
|
|
76
|
+
const fullBytes = new Uint8Array([...ed25519Prefix, ...mockPublicKey]);
|
|
77
|
+
|
|
78
|
+
const encoded = base58Encode(fullBytes);
|
|
79
|
+
const decoded = base58Decode(encoded);
|
|
80
|
+
expect(decoded).toEqual(fullBytes);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("isValidBase58", () => {
|
|
85
|
+
it("should return true for empty string", () => {
|
|
86
|
+
expect(isValidBase58("")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return true for valid base58 strings", () => {
|
|
90
|
+
expect(isValidBase58("123456789")).toBe(true);
|
|
91
|
+
expect(isValidBase58("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return false for invalid characters", () => {
|
|
95
|
+
expect(isValidBase58("0")).toBe(false);
|
|
96
|
+
expect(isValidBase58("O")).toBe(false);
|
|
97
|
+
expect(isValidBase58("I")).toBe(false);
|
|
98
|
+
expect(isValidBase58("l")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("did:key Resolver", () => {
|
|
104
|
+
// Known test vector for Ed25519 did:key
|
|
105
|
+
// This creates a deterministic did:key from known public key bytes
|
|
106
|
+
const createTestDidKey = (publicKeyBytes: Uint8Array): string => {
|
|
107
|
+
const prefix = new Uint8Array([0xed, 0x01]); // Ed25519 multicodec
|
|
108
|
+
const fullBytes = new Uint8Array([...prefix, ...publicKeyBytes]);
|
|
109
|
+
return `did:key:z${base58Encode(fullBytes)}`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
describe("isEd25519DidKey", () => {
|
|
113
|
+
it("should return true for Ed25519 did:key", () => {
|
|
114
|
+
// Ed25519 keys start with z6Mk
|
|
115
|
+
expect(isEd25519DidKey("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should return false for non-did:key", () => {
|
|
119
|
+
expect(isEd25519DidKey("did:web:example.com")).toBe(false);
|
|
120
|
+
expect(isEd25519DidKey("did:example:123")).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should return false for non-Ed25519 did:key", () => {
|
|
124
|
+
// Secp256k1 keys start with z6Ls or other prefixes
|
|
125
|
+
expect(isEd25519DidKey("did:key:z7r8os")).toBe(false);
|
|
126
|
+
expect(isEd25519DidKey("did:key:zQ3s")).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return false for invalid did:key format", () => {
|
|
130
|
+
expect(isEd25519DidKey("did:key:")).toBe(false);
|
|
131
|
+
expect(isEd25519DidKey("did:key:invalid")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("extractPublicKeyFromDidKey", () => {
|
|
136
|
+
it("should extract public key bytes from valid did:key", () => {
|
|
137
|
+
const mockPublicKey = new Uint8Array(32).map((_, i) => i);
|
|
138
|
+
const didKey = createTestDidKey(mockPublicKey);
|
|
139
|
+
|
|
140
|
+
const extractedKey = extractPublicKeyFromDidKey(didKey);
|
|
141
|
+
expect(extractedKey).not.toBeNull();
|
|
142
|
+
expect(extractedKey).toEqual(mockPublicKey);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should return null for non-did:key", () => {
|
|
146
|
+
expect(extractPublicKeyFromDidKey("did:web:example.com")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should return null for invalid multicodec prefix", () => {
|
|
150
|
+
// Create a did:key with wrong prefix (not Ed25519)
|
|
151
|
+
const wrongPrefix = new Uint8Array([0x00, 0x00]); // Not Ed25519
|
|
152
|
+
const mockPublicKey = new Uint8Array(32).fill(1);
|
|
153
|
+
const fullBytes = new Uint8Array([...wrongPrefix, ...mockPublicKey]);
|
|
154
|
+
const invalidDid = `did:key:z${base58Encode(fullBytes)}`;
|
|
155
|
+
|
|
156
|
+
expect(extractPublicKeyFromDidKey(invalidDid)).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return null for too short key", () => {
|
|
160
|
+
const shortBytes = new Uint8Array([0xed, 0x01, 1, 2, 3]); // Only 3 bytes of key
|
|
161
|
+
const shortDid = `did:key:z${base58Encode(shortBytes)}`;
|
|
162
|
+
|
|
163
|
+
expect(extractPublicKeyFromDidKey(shortDid)).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("publicKeyToJwk", () => {
|
|
168
|
+
it("should convert public key bytes to JWK format", () => {
|
|
169
|
+
const publicKeyBytes = new Uint8Array(32).map((_, i) => i);
|
|
170
|
+
const jwk = publicKeyToJwk(publicKeyBytes);
|
|
171
|
+
|
|
172
|
+
expect(jwk.kty).toBe("OKP");
|
|
173
|
+
expect(jwk.crv).toBe("Ed25519");
|
|
174
|
+
expect(jwk.x).toBeDefined();
|
|
175
|
+
expect(typeof jwk.x).toBe("string");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should produce base64url-encoded x value", () => {
|
|
179
|
+
const publicKeyBytes = new Uint8Array(32).fill(0);
|
|
180
|
+
const jwk = publicKeyToJwk(publicKeyBytes);
|
|
181
|
+
|
|
182
|
+
// Base64url should not contain +, /, or =
|
|
183
|
+
expect(jwk.x).not.toMatch(/[+/=]/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("createDidKeyResolver", () => {
|
|
188
|
+
it("should resolve Ed25519 did:key to DID Document", async () => {
|
|
189
|
+
const mockPublicKey = new Uint8Array(32).map((_, i) => i);
|
|
190
|
+
const didKey = createTestDidKey(mockPublicKey);
|
|
191
|
+
|
|
192
|
+
const resolver = createDidKeyResolver();
|
|
193
|
+
const didDoc = await resolver.resolve(didKey);
|
|
194
|
+
|
|
195
|
+
expect(didDoc).not.toBeNull();
|
|
196
|
+
expect(didDoc?.id).toBe(didKey);
|
|
197
|
+
expect(didDoc?.verificationMethod).toHaveLength(1);
|
|
198
|
+
expect(didDoc?.verificationMethod?.[0].type).toBe("Ed25519VerificationKey2020");
|
|
199
|
+
expect(didDoc?.verificationMethod?.[0].controller).toBe(didKey);
|
|
200
|
+
expect(didDoc?.verificationMethod?.[0].publicKeyJwk).toBeDefined();
|
|
201
|
+
expect(didDoc?.authentication).toContain(`${didKey}#keys-1`);
|
|
202
|
+
expect(didDoc?.assertionMethod).toContain(`${didKey}#keys-1`);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should return null for non-Ed25519 did:key", async () => {
|
|
206
|
+
const resolver = createDidKeyResolver();
|
|
207
|
+
const result = await resolver.resolve("did:key:z7r8os");
|
|
208
|
+
|
|
209
|
+
expect(result).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should return null for non-did:key DIDs", async () => {
|
|
213
|
+
const resolver = createDidKeyResolver();
|
|
214
|
+
const result = await resolver.resolve("did:web:example.com");
|
|
215
|
+
|
|
216
|
+
expect(result).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("resolveDidKeySync", () => {
|
|
221
|
+
it("should synchronously resolve Ed25519 did:key", () => {
|
|
222
|
+
const mockPublicKey = new Uint8Array(32).map((_, i) => i);
|
|
223
|
+
const didKey = createTestDidKey(mockPublicKey);
|
|
224
|
+
|
|
225
|
+
const didDoc = resolveDidKeySync(didKey);
|
|
226
|
+
|
|
227
|
+
expect(didDoc).not.toBeNull();
|
|
228
|
+
expect(didDoc?.id).toBe(didKey);
|
|
229
|
+
expect(didDoc?.verificationMethod).toHaveLength(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should return null for invalid DIDs", () => {
|
|
233
|
+
expect(resolveDidKeySync("did:web:example.com")).toBeNull();
|
|
234
|
+
expect(resolveDidKeySync("did:key:invalid")).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("VC-JWT Roundtrip Integration", () => {
|
|
240
|
+
it("should correctly resolve did:key generated by UserDidManager pattern", async () => {
|
|
241
|
+
// This test simulates the pattern used in UserDidManager.generateKeyPair()
|
|
242
|
+
// which creates did:key DIDs for users
|
|
243
|
+
|
|
244
|
+
// Simulate generating a random Ed25519 key (32 bytes)
|
|
245
|
+
const mockPublicKey = crypto.getRandomValues(new Uint8Array(32));
|
|
246
|
+
|
|
247
|
+
// Encode as did:key (same pattern as UserDidManager)
|
|
248
|
+
const multicodecPrefix = new Uint8Array([0xed, 0x01]);
|
|
249
|
+
const multicodecBytes = new Uint8Array([...multicodecPrefix, ...mockPublicKey]);
|
|
250
|
+
const multibaseEncoded = base58Encode(multicodecBytes);
|
|
251
|
+
const didKey = `did:key:z${multibaseEncoded}`;
|
|
252
|
+
|
|
253
|
+
// Verify we can resolve this did:key back to get the public key
|
|
254
|
+
const resolver = createDidKeyResolver();
|
|
255
|
+
const didDoc = await resolver.resolve(didKey);
|
|
256
|
+
|
|
257
|
+
expect(didDoc).not.toBeNull();
|
|
258
|
+
expect(didDoc?.id).toBe(didKey);
|
|
259
|
+
expect(didDoc?.verificationMethod?.[0]?.publicKeyJwk).toBeDefined();
|
|
260
|
+
|
|
261
|
+
// Extract public key and verify it matches
|
|
262
|
+
const extractedKey = extractPublicKeyFromDidKey(didKey);
|
|
263
|
+
expect(extractedKey).toEqual(mockPublicKey);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID:key Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves did:key DIDs to DID Documents with verification methods.
|
|
5
|
+
* Supports Ed25519 keys (multicodec prefix 0xed01).
|
|
6
|
+
*
|
|
7
|
+
* did:key format: did:key:z<multibase-base58btc(<multicodec-prefix><public-key>)>
|
|
8
|
+
*
|
|
9
|
+
* For Ed25519:
|
|
10
|
+
* - Multicodec prefix: 0xed 0x01
|
|
11
|
+
* - Public key: 32 bytes
|
|
12
|
+
* - Multibase prefix: 'z' (base58btc)
|
|
13
|
+
*
|
|
14
|
+
* @see https://w3c-ccg.github.io/did-method-key/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { base58Decode } from '../utils/base58';
|
|
18
|
+
import { base64urlEncodeFromBytes } from '../utils/base64';
|
|
19
|
+
import type { DIDResolver, DIDDocument, VerificationMethod } from './vc-verifier';
|
|
20
|
+
|
|
21
|
+
/** Ed25519 multicodec prefix (0xed 0x01) */
|
|
22
|
+
const ED25519_MULTICODEC_PREFIX = new Uint8Array([0xed, 0x01]);
|
|
23
|
+
|
|
24
|
+
/** Ed25519 public key length */
|
|
25
|
+
const ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a DID is a valid did:key with Ed25519 key
|
|
29
|
+
*
|
|
30
|
+
* Ed25519 keys in did:key start with 'z6Mk' after the method prefix.
|
|
31
|
+
* The 'z' is the multibase prefix for base58btc, and '6Mk' is the
|
|
32
|
+
* base58-encoded prefix for Ed25519 (0xed 0x01).
|
|
33
|
+
*
|
|
34
|
+
* @param did - The DID to check
|
|
35
|
+
* @returns true if it's a valid did:key with Ed25519 key
|
|
36
|
+
*/
|
|
37
|
+
export function isEd25519DidKey(did: string): boolean {
|
|
38
|
+
return did.startsWith('did:key:z6Mk');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the public key bytes from a did:key DID
|
|
43
|
+
*
|
|
44
|
+
* @param did - The did:key DID
|
|
45
|
+
* @returns Public key bytes or null if invalid
|
|
46
|
+
*/
|
|
47
|
+
export function extractPublicKeyFromDidKey(did: string): Uint8Array | null {
|
|
48
|
+
if (!did.startsWith('did:key:z')) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Extract the multibase-encoded part (after 'did:key:')
|
|
54
|
+
const multibaseKey = did.replace('did:key:', '');
|
|
55
|
+
|
|
56
|
+
// Remove the 'z' multibase prefix (base58btc)
|
|
57
|
+
const base58Encoded = multibaseKey.slice(1);
|
|
58
|
+
|
|
59
|
+
// Decode from base58
|
|
60
|
+
const multicodecBytes = base58Decode(base58Encoded);
|
|
61
|
+
|
|
62
|
+
// Check for Ed25519 multicodec prefix (0xed 0x01)
|
|
63
|
+
if (
|
|
64
|
+
multicodecBytes.length < ED25519_MULTICODEC_PREFIX.length + ED25519_PUBLIC_KEY_LENGTH ||
|
|
65
|
+
multicodecBytes[0] !== ED25519_MULTICODEC_PREFIX[0] ||
|
|
66
|
+
multicodecBytes[1] !== ED25519_MULTICODEC_PREFIX[1]
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract the public key (bytes after the prefix)
|
|
72
|
+
return multicodecBytes.slice(ED25519_MULTICODEC_PREFIX.length);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert Ed25519 public key bytes to JWK format
|
|
80
|
+
*
|
|
81
|
+
* @param publicKeyBytes - 32-byte Ed25519 public key
|
|
82
|
+
* @returns JWK object
|
|
83
|
+
*/
|
|
84
|
+
export function publicKeyToJwk(publicKeyBytes: Uint8Array): {
|
|
85
|
+
kty: string;
|
|
86
|
+
crv: string;
|
|
87
|
+
x: string;
|
|
88
|
+
} {
|
|
89
|
+
return {
|
|
90
|
+
kty: 'OKP',
|
|
91
|
+
crv: 'Ed25519',
|
|
92
|
+
x: base64urlEncodeFromBytes(publicKeyBytes),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a DID:key resolver
|
|
98
|
+
*
|
|
99
|
+
* Returns a DIDResolver that can resolve did:key DIDs to DID Documents.
|
|
100
|
+
* Currently supports only Ed25519 keys.
|
|
101
|
+
*
|
|
102
|
+
* @returns DIDResolver implementation for did:key
|
|
103
|
+
*/
|
|
104
|
+
export function createDidKeyResolver(): DIDResolver {
|
|
105
|
+
return {
|
|
106
|
+
resolve: async (did: string): Promise<DIDDocument | null> => {
|
|
107
|
+
// Check if it's a did:key with Ed25519
|
|
108
|
+
if (!isEd25519DidKey(did)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract the public key
|
|
113
|
+
const publicKeyBytes = extractPublicKeyFromDidKey(did);
|
|
114
|
+
if (!publicKeyBytes) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Convert to JWK
|
|
119
|
+
const publicKeyJwk = publicKeyToJwk(publicKeyBytes);
|
|
120
|
+
|
|
121
|
+
// Get the multibase-encoded key for publicKeyMultibase
|
|
122
|
+
const multibaseKey = did.replace('did:key:', '');
|
|
123
|
+
|
|
124
|
+
// Construct the verification method
|
|
125
|
+
const verificationMethod: VerificationMethod = {
|
|
126
|
+
id: `${did}#keys-1`,
|
|
127
|
+
type: 'Ed25519VerificationKey2020',
|
|
128
|
+
controller: did,
|
|
129
|
+
publicKeyJwk,
|
|
130
|
+
publicKeyMultibase: multibaseKey,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Construct and return the DID Document
|
|
134
|
+
return {
|
|
135
|
+
id: did,
|
|
136
|
+
verificationMethod: [verificationMethod],
|
|
137
|
+
authentication: [`${did}#keys-1`],
|
|
138
|
+
assertionMethod: [`${did}#keys-1`],
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve a did:key DID synchronously
|
|
146
|
+
*
|
|
147
|
+
* Convenience function for cases where async is not needed.
|
|
148
|
+
*
|
|
149
|
+
* @param did - The did:key DID to resolve
|
|
150
|
+
* @returns DID Document or null if invalid
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDidKeySync(did: string): DIDDocument | null {
|
|
153
|
+
if (!isEd25519DidKey(did)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const publicKeyBytes = extractPublicKeyFromDidKey(did);
|
|
158
|
+
if (!publicKeyBytes) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const publicKeyJwk = publicKeyToJwk(publicKeyBytes);
|
|
163
|
+
const multibaseKey = did.replace('did:key:', '');
|
|
164
|
+
|
|
165
|
+
const verificationMethod: VerificationMethod = {
|
|
166
|
+
id: `${did}#keys-1`,
|
|
167
|
+
type: 'Ed25519VerificationKey2020',
|
|
168
|
+
controller: did,
|
|
169
|
+
publicKeyJwk,
|
|
170
|
+
publicKeyMultibase: multibaseKey,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
id: did,
|
|
175
|
+
verificationMethod: [verificationMethod],
|
|
176
|
+
authentication: [`${did}#keys-1`],
|
|
177
|
+
assertionMethod: [`${did}#keys-1`],
|
|
178
|
+
};
|
|
179
|
+
}
|
package/src/delegation/utils.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Following DRY (Don't Repeat Yourself) principle.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { base64urlEncodeFromString } from '../utils/base64';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* JSON canonicalization (RFC 8785)
|
|
10
12
|
*
|
|
@@ -40,3 +42,180 @@ export function canonicalizeJSON(obj: any): string {
|
|
|
40
42
|
}
|
|
41
43
|
throw new Error(`Cannot canonicalize type: ${typeof obj}`);
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* JWT Header for EdDSA (Ed25519) signed credentials
|
|
48
|
+
*/
|
|
49
|
+
export interface VCJWTHeader {
|
|
50
|
+
alg: 'EdDSA';
|
|
51
|
+
typ: 'JWT';
|
|
52
|
+
kid?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* VC-JWT Payload structure
|
|
57
|
+
*
|
|
58
|
+
* Per W3C VC-JWT spec, the VC is embedded in the JWT claims.
|
|
59
|
+
* Standard claims (iss, sub, exp, iat, jti) are derived from the VC.
|
|
60
|
+
*/
|
|
61
|
+
export interface VCJWTPayload {
|
|
62
|
+
/** Issuer DID (from vc.issuer) */
|
|
63
|
+
iss: string;
|
|
64
|
+
/** Subject DID (from vc.credentialSubject.id) */
|
|
65
|
+
sub?: string;
|
|
66
|
+
/** Expiration time (from vc.expirationDate) */
|
|
67
|
+
exp?: number;
|
|
68
|
+
/** Issued at time (from vc.issuanceDate) */
|
|
69
|
+
iat?: number;
|
|
70
|
+
/** JWT ID (from vc.id) */
|
|
71
|
+
jti?: string;
|
|
72
|
+
/** The complete VC (without proof) */
|
|
73
|
+
vc: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Options for encoding a VC as JWT
|
|
78
|
+
*/
|
|
79
|
+
export interface EncodeVCAsJWTOptions {
|
|
80
|
+
/** Key ID for the JWT header */
|
|
81
|
+
keyId?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create unsigned JWT parts (header + payload) for a VC
|
|
86
|
+
*
|
|
87
|
+
* Prepares the VC for signing by extracting standard claims and
|
|
88
|
+
* encoding the header and payload as base64url strings.
|
|
89
|
+
*
|
|
90
|
+
* @param vc - The Verifiable Credential (without proof)
|
|
91
|
+
* @param options - Encoding options
|
|
92
|
+
* @returns Object with encoded parts and signing input
|
|
93
|
+
*/
|
|
94
|
+
export function createUnsignedVCJWT(
|
|
95
|
+
vc: Record<string, unknown>,
|
|
96
|
+
options: EncodeVCAsJWTOptions = {}
|
|
97
|
+
): {
|
|
98
|
+
header: VCJWTHeader;
|
|
99
|
+
payload: VCJWTPayload;
|
|
100
|
+
encodedHeader: string;
|
|
101
|
+
encodedPayload: string;
|
|
102
|
+
signingInput: string;
|
|
103
|
+
} {
|
|
104
|
+
// Create JWT header
|
|
105
|
+
const header: VCJWTHeader = {
|
|
106
|
+
alg: 'EdDSA',
|
|
107
|
+
typ: 'JWT',
|
|
108
|
+
};
|
|
109
|
+
if (options.keyId) {
|
|
110
|
+
header.kid = options.keyId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract standard claims from VC
|
|
114
|
+
const issuer = typeof vc.issuer === 'string' ? vc.issuer : (vc.issuer as Record<string, unknown>)?.id as string;
|
|
115
|
+
const subject = (vc.credentialSubject as Record<string, unknown>)?.id as string | undefined;
|
|
116
|
+
|
|
117
|
+
// Parse dates to Unix timestamps
|
|
118
|
+
let exp: number | undefined;
|
|
119
|
+
let iat: number | undefined;
|
|
120
|
+
|
|
121
|
+
if (vc.expirationDate && typeof vc.expirationDate === 'string') {
|
|
122
|
+
exp = Math.floor(new Date(vc.expirationDate).getTime() / 1000);
|
|
123
|
+
}
|
|
124
|
+
if (vc.issuanceDate && typeof vc.issuanceDate === 'string') {
|
|
125
|
+
iat = Math.floor(new Date(vc.issuanceDate).getTime() / 1000);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Remove proof from VC for JWT payload (signature is in JWT itself)
|
|
129
|
+
const vcWithoutProof = { ...vc };
|
|
130
|
+
delete vcWithoutProof.proof;
|
|
131
|
+
|
|
132
|
+
// Build JWT payload
|
|
133
|
+
const payload: VCJWTPayload = {
|
|
134
|
+
iss: issuer,
|
|
135
|
+
vc: vcWithoutProof,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (subject) payload.sub = subject;
|
|
139
|
+
if (exp) payload.exp = exp;
|
|
140
|
+
if (iat) payload.iat = iat;
|
|
141
|
+
if (vc.id && typeof vc.id === 'string') payload.jti = vc.id;
|
|
142
|
+
|
|
143
|
+
// Encode header and payload
|
|
144
|
+
const encodedHeader = base64urlEncodeFromString(JSON.stringify(header));
|
|
145
|
+
const encodedPayload = base64urlEncodeFromString(JSON.stringify(payload));
|
|
146
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
header,
|
|
150
|
+
payload,
|
|
151
|
+
encodedHeader,
|
|
152
|
+
encodedPayload,
|
|
153
|
+
signingInput,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Complete a JWT with a signature
|
|
159
|
+
*
|
|
160
|
+
* Takes the signing input and a base64url-encoded signature to create the final JWT.
|
|
161
|
+
*
|
|
162
|
+
* @param signingInput - The header.payload string that was signed
|
|
163
|
+
* @param signature - Base64url-encoded signature
|
|
164
|
+
* @returns Complete JWT string (header.payload.signature)
|
|
165
|
+
*/
|
|
166
|
+
export function completeVCJWT(signingInput: string, signature: string): string {
|
|
167
|
+
return `${signingInput}.${signature}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse a VC-JWT and extract the VC
|
|
172
|
+
*
|
|
173
|
+
* Does NOT verify the signature - use with a verification function.
|
|
174
|
+
*
|
|
175
|
+
* @param jwt - The JWT string
|
|
176
|
+
* @returns Parsed JWT parts
|
|
177
|
+
*/
|
|
178
|
+
export function parseVCJWT(jwt: string): {
|
|
179
|
+
header: VCJWTHeader;
|
|
180
|
+
payload: VCJWTPayload;
|
|
181
|
+
signature: string;
|
|
182
|
+
signingInput: string;
|
|
183
|
+
} | null {
|
|
184
|
+
const parts = jwt.split('.');
|
|
185
|
+
if (parts.length !== 3) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Decode header and payload
|
|
191
|
+
const headerJson = base64urlDecodeToString(parts[0]);
|
|
192
|
+
const payloadJson = base64urlDecodeToString(parts[1]);
|
|
193
|
+
|
|
194
|
+
const header = JSON.parse(headerJson) as VCJWTHeader;
|
|
195
|
+
const payload = JSON.parse(payloadJson) as VCJWTPayload;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
header,
|
|
199
|
+
payload,
|
|
200
|
+
signature: parts[2],
|
|
201
|
+
signingInput: `${parts[0]}.${parts[1]}`,
|
|
202
|
+
};
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Decode base64url string to string (internal helper)
|
|
210
|
+
*/
|
|
211
|
+
function base64urlDecodeToString(input: string): string {
|
|
212
|
+
// Add padding if needed
|
|
213
|
+
const padded = input + '='.repeat((4 - input.length % 4) % 4);
|
|
214
|
+
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
215
|
+
|
|
216
|
+
if (typeof atob !== 'undefined') {
|
|
217
|
+
return atob(base64);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Buffer.from(base64, 'base64').toString('utf-8');
|
|
221
|
+
}
|