@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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/__tests__/utils/mock-providers.d.ts +2 -1
  3. package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
  4. package/dist/__tests__/utils/mock-providers.js.map +1 -1
  5. package/dist/config/remote-config.d.ts +51 -0
  6. package/dist/config/remote-config.d.ts.map +1 -1
  7. package/dist/config/remote-config.js +74 -0
  8. package/dist/config/remote-config.js.map +1 -1
  9. package/dist/config.d.ts +1 -1
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +4 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/delegation/did-key-resolver.d.ts +64 -0
  14. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  15. package/dist/delegation/did-key-resolver.js +159 -0
  16. package/dist/delegation/did-key-resolver.js.map +1 -0
  17. package/dist/delegation/utils.d.ts +76 -0
  18. package/dist/delegation/utils.d.ts.map +1 -1
  19. package/dist/delegation/utils.js +117 -0
  20. package/dist/delegation/utils.js.map +1 -1
  21. package/dist/identity/user-did-manager.d.ts +95 -12
  22. package/dist/identity/user-did-manager.d.ts.map +1 -1
  23. package/dist/identity/user-did-manager.js +107 -25
  24. package/dist/identity/user-did-manager.js.map +1 -1
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +23 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/runtime/base.d.ts +25 -8
  30. package/dist/runtime/base.d.ts.map +1 -1
  31. package/dist/runtime/base.js +74 -21
  32. package/dist/runtime/base.js.map +1 -1
  33. package/dist/services/session-registration.service.d.ts.map +1 -1
  34. package/dist/services/session-registration.service.js +10 -90
  35. package/dist/services/session-registration.service.js.map +1 -1
  36. package/dist/services/tool-protection.service.d.ts +5 -2
  37. package/dist/services/tool-protection.service.d.ts.map +1 -1
  38. package/dist/services/tool-protection.service.js +72 -24
  39. package/dist/services/tool-protection.service.js.map +1 -1
  40. package/dist/utils/base58.d.ts +31 -0
  41. package/dist/utils/base58.d.ts.map +1 -0
  42. package/dist/utils/base58.js +103 -0
  43. package/dist/utils/base58.js.map +1 -0
  44. package/package.json +3 -3
  45. package/src/__tests__/identity/user-did-manager.test.ts +64 -45
  46. package/src/__tests__/integration/full-flow.test.ts +23 -10
  47. package/src/__tests__/runtime/base-extensions.test.ts +23 -21
  48. package/src/__tests__/runtime/proof-client-did.test.ts +19 -18
  49. package/src/__tests__/services/agentshield-integration.test.ts +10 -3
  50. package/src/__tests__/services/tool-protection-merged-config.test.ts +485 -0
  51. package/src/__tests__/services/tool-protection.service.test.ts +18 -11
  52. package/src/config/__tests__/merged-config.spec.ts +445 -0
  53. package/src/config/remote-config.ts +90 -0
  54. package/src/config.ts +3 -0
  55. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  56. package/src/delegation/did-key-resolver.ts +179 -0
  57. package/src/delegation/utils.ts +179 -0
  58. package/src/identity/user-did-manager.ts +185 -29
  59. package/src/index.ts +36 -1
  60. package/src/runtime/base.ts +84 -21
  61. package/src/services/session-registration.service.ts +26 -121
  62. package/src/services/tool-protection.service.ts +125 -56
  63. package/src/utils/base58.ts +109 -0
  64. 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
+ }
@@ -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
+ }