@ixo/ucan 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.js +4 -0
  4. package/.turbo/turbo-build.log +4 -0
  5. package/CHANGELOG.md +0 -0
  6. package/README.md +189 -0
  7. package/dist/capabilities/capability.d.ts +33 -0
  8. package/dist/capabilities/capability.d.ts.map +1 -0
  9. package/dist/capabilities/capability.js +53 -0
  10. package/dist/capabilities/capability.js.map +1 -0
  11. package/dist/client/create-client.d.ts +33 -0
  12. package/dist/client/create-client.d.ts.map +1 -0
  13. package/dist/client/create-client.js +104 -0
  14. package/dist/client/create-client.js.map +1 -0
  15. package/dist/did/ixo-resolver.d.ts +8 -0
  16. package/dist/did/ixo-resolver.d.ts.map +1 -0
  17. package/dist/did/ixo-resolver.js +162 -0
  18. package/dist/did/ixo-resolver.js.map +1 -0
  19. package/dist/did/utils.d.ts +4 -0
  20. package/dist/did/utils.d.ts.map +1 -0
  21. package/dist/did/utils.js +85 -0
  22. package/dist/did/utils.js.map +1 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +12 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/store/memory.d.ts +25 -0
  28. package/dist/store/memory.d.ts.map +1 -0
  29. package/dist/store/memory.js +71 -0
  30. package/dist/store/memory.js.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -0
  32. package/dist/types.d.ts +29 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/types.js +2 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/validator/validator.d.ts +29 -0
  37. package/dist/validator/validator.d.ts.map +1 -0
  38. package/dist/validator/validator.js +179 -0
  39. package/dist/validator/validator.js.map +1 -0
  40. package/jest.config.js +3 -0
  41. package/package.json +78 -0
  42. package/scripts/test-ucan.ts +457 -0
  43. package/src/capabilities/capability.ts +244 -0
  44. package/src/client/create-client.ts +329 -0
  45. package/src/did/ixo-resolver.ts +325 -0
  46. package/src/did/utils.ts +141 -0
  47. package/src/index.ts +135 -0
  48. package/src/store/memory.ts +194 -0
  49. package/src/types.ts +108 -0
  50. package/src/validator/validator.ts +399 -0
  51. package/tsconfig.json +18 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @fileoverview Client helpers for creating UCAN invocations and delegations
3
+ *
4
+ * These helpers are used by front-ends to create invocations that can be
5
+ * sent alongside regular API requests (e.g., in the mcpInvocations field).
6
+ *
7
+ * Supports any DID method for audience (did:key, did:ixo, did:web, etc.)
8
+ */
9
+
10
+ import * as Client from '@ucanto/client';
11
+ import { ed25519 } from '@ucanto/principal';
12
+ import type { Signer, Delegation, Capability, Principal } from '@ucanto/interface';
13
+ import type { SupportedDID } from '../types.js';
14
+
15
+ /**
16
+ * Create a principal from any DID string
17
+ *
18
+ * For did:key - parses the key from the DID (full verification support)
19
+ * For other DIDs - creates a simple principal that just holds the DID
20
+ *
21
+ * This allows delegations and invocations to be addressed to any DID method.
22
+ */
23
+ function createPrincipal(did: string): Principal {
24
+ // did:key can be fully parsed (contains the public key)
25
+ if (did.startsWith('did:key:')) {
26
+ return ed25519.Verifier.parse(did);
27
+ }
28
+
29
+ // For other DID methods (did:ixo, did:web, etc.), create a simple principal
30
+ // The audience doesn't need key material - they just need to be identified
31
+ return {
32
+ did: () => did as `did:${string}:${string}`,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Generate a new Ed25519 keypair
38
+ *
39
+ * @returns The generated signer with its DID and private key
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const { signer, did, privateKey } = await generateKeypair();
44
+ * console.log('New DID:', did);
45
+ * // Store privateKey securely for future use
46
+ * ```
47
+ */
48
+ export async function generateKeypair(): Promise<{
49
+ signer: Signer;
50
+ did: string;
51
+ privateKey: string;
52
+ }> {
53
+ const signer = await ed25519.Signer.generate();
54
+ return {
55
+ signer,
56
+ did: signer.did(),
57
+ privateKey: ed25519.Signer.format(signer),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Parse a private key into a signer
63
+ *
64
+ * @param privateKey - The private key (multibase encoded)
65
+ * @param did - The DID to use for the signer (optional) will override the did:key if provided
66
+ * @returns The signer
67
+ */
68
+ export function parseSigner(privateKey: string, did?: SupportedDID): Signer {
69
+ const signer = ed25519.Signer.parse(privateKey);
70
+ if (did) {
71
+ return signer.withDID(did);
72
+ }
73
+ return signer;
74
+ }
75
+
76
+ /**
77
+ * Create a signer from a BIP39 mnemonic
78
+ *
79
+ * Uses the same derivation as IXO verification methods:
80
+ * SHA256(mnemonic) → first 32 bytes as Ed25519 seed
81
+ *
82
+ * This ensures the derived key matches the verification method on-chain.
83
+ *
84
+ * @param mnemonic - BIP39 mnemonic phrase (12-24 words)
85
+ * @param did - The DID to use for the signer (optional) will override the did:key if provided
86
+ * @returns The signer and formatted private key
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const { signer, did, privateKey } = await signerFromMnemonic('word1 word2 ...');
91
+ * console.log('DID:', did);
92
+ * console.log('Private Key (for server config):', privateKey);
93
+ * ```
94
+ */
95
+ export async function signerFromMnemonic(mnemonic: string, did?: SupportedDID): Promise<{
96
+ signer: Signer;
97
+ did: string;
98
+ privateKey: string;
99
+ }> {
100
+ // Use @cosmjs/crypto - same as IXO frontend for verification methods
101
+ // This ensures the derived key matches what's registered on-chain
102
+ const { Ed25519, sha256 } = await import('@cosmjs/crypto');
103
+ const { toUtf8 } = await import('@cosmjs/encoding');
104
+
105
+ // Derive Ed25519 keypair using same method as IXO verification methods:
106
+ // SHA256(mnemonic UTF-8 bytes) → first 32 bytes as seed → Ed25519 keypair
107
+ const seed = sha256(toUtf8(mnemonic.trim())).slice(0, 32);
108
+ const keypair = await Ed25519.makeKeypair(seed);
109
+
110
+ // Note: cosmjs returns keypair.privkey as 64 bytes (seed + pubkey concatenated)
111
+ // but ucanto expects just the 32-byte seed, so we use `seed` directly
112
+
113
+ // Build ucanto's private key format (68 bytes total):
114
+ // M + base64( [0x80, 0x26] + seed(32) + [0xed, 0x01] + pubkey(32) )
115
+ // where:
116
+ // [0x80, 0x26] = varint for 0x1300 (ed25519-priv multicodec)
117
+ // [0xed, 0x01] = varint for 0xed (ed25519-pub multicodec, 237 decimal)
118
+ const ED25519_PRIV_MULTICODEC = new Uint8Array([0x80, 0x26]); // 2 bytes
119
+ const ED25519_PUB_MULTICODEC = new Uint8Array([0xed, 0x01]); // 2 bytes (varint of 237)
120
+
121
+ // Total: 2 + 32 + 2 + 32 = 68 bytes (matches ucanto expectation)
122
+ const keyMaterial = new Uint8Array(
123
+ ED25519_PRIV_MULTICODEC.length + // 2
124
+ seed.length + // 32
125
+ ED25519_PUB_MULTICODEC.length + // 2
126
+ keypair.pubkey.length, // 32
127
+ );
128
+ keyMaterial.set(ED25519_PRIV_MULTICODEC, 0);
129
+ keyMaterial.set(seed, ED25519_PRIV_MULTICODEC.length);
130
+ keyMaterial.set(ED25519_PUB_MULTICODEC, ED25519_PRIV_MULTICODEC.length + seed.length);
131
+ keyMaterial.set(
132
+ keypair.pubkey,
133
+ ED25519_PRIV_MULTICODEC.length + seed.length + ED25519_PUB_MULTICODEC.length,
134
+ );
135
+
136
+ // Encode as base64pad multibase (prefix 'M')
137
+ const base64 = Buffer.from(keyMaterial).toString('base64');
138
+ const multibasePrivateKey = 'M' + base64;
139
+
140
+ // Parse using ucanto's parser to get a proper Signer
141
+ let signer = ed25519.Signer.parse(multibasePrivateKey);
142
+ const finalSigner = did ? signer.withDID(did) : signer;
143
+
144
+ return {
145
+ signer: finalSigner,
146
+ did: finalSigner.did(),
147
+ privateKey: multibasePrivateKey,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Create a delegation (grant capabilities to someone)
153
+ *
154
+ * Supports any DID method for the audience (did:key, did:ixo, did:web, etc.)
155
+ *
156
+ * @param options - Delegation options
157
+ * @returns The delegation object
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * // Delegate to a did:key
162
+ * const delegation = await createDelegation({
163
+ * issuer: mySigner,
164
+ * audience: 'did:key:z6Mk...',
165
+ * capabilities: [{ can: 'employees/read', with: 'myapp:server' }],
166
+ * });
167
+ *
168
+ * // Delegate to a did:ixo
169
+ * const delegation = await createDelegation({
170
+ * issuer: mySigner,
171
+ * audience: 'did:ixo:ixo1abc...',
172
+ * capabilities: [{ can: 'employees/read', with: 'myapp:server' }],
173
+ * });
174
+ * ```
175
+ */
176
+ export async function createDelegation(options: {
177
+ /** The issuer's signer (private key) */
178
+ issuer: Signer;
179
+ /** The audience's DID (who receives the capability) - any DID method supported */
180
+ audience: string;
181
+ /** The capabilities being delegated */
182
+ capabilities: Capability[];
183
+ /** Expiration timestamp (Unix seconds) */
184
+ expiration?: number;
185
+ /** Not before timestamp (Unix seconds) */
186
+ notBefore?: number;
187
+ /** Parent delegations (proof chain) */
188
+ proofs?: Delegation[];
189
+ }): Promise<Delegation> {
190
+ // Create principal from any DID (did:key, did:ixo, did:web, etc.)
191
+ const audiencePrincipal = createPrincipal(options.audience);
192
+
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ return Client.delegate({
195
+ issuer: options.issuer,
196
+ audience: audiencePrincipal,
197
+ capabilities: options.capabilities as any,
198
+ expiration: options.expiration,
199
+ proofs: options.proofs,
200
+ notBefore: options.notBefore,
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Create an invocation (request to use a capability)
206
+ *
207
+ * Supports any DID method for the audience (did:key, did:ixo, did:web, etc.)
208
+ *
209
+ * @param options - Invocation options
210
+ * @returns The invocation object
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Invoke on a did:key server
215
+ * const invocation = await createInvocation({
216
+ * issuer: mySigner,
217
+ * audience: 'did:key:z6Mk...',
218
+ * capability: { can: 'employees/read', with: 'myapp:server' },
219
+ * proofs: [myDelegation],
220
+ * });
221
+ *
222
+ * // Invoke on a did:ixo server
223
+ * const invocation = await createInvocation({
224
+ * issuer: mySigner,
225
+ * audience: 'did:ixo:ixo1oracle...',
226
+ * capability: { can: 'mcp/call', with: 'ixo:oracle:...' },
227
+ * proofs: [myDelegation],
228
+ * });
229
+ * ```
230
+ */
231
+ export async function createInvocation(options: {
232
+ /** The invoker's signer (private key) */
233
+ issuer: Signer;
234
+ /** The service's DID (audience) - any DID method supported */
235
+ audience: string;
236
+ /** The capability being invoked */
237
+ capability: Capability;
238
+ /** Delegation proofs */
239
+ proofs?: Delegation[];
240
+ }) {
241
+ // Create principal from any DID (did:key, did:ixo, did:web, etc.)
242
+ const audiencePrincipal = createPrincipal(options.audience);
243
+
244
+ return Client.invoke({
245
+ issuer: options.issuer,
246
+ audience: audiencePrincipal,
247
+ capability: options.capability,
248
+ proofs: options.proofs ?? [],
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Serialize an invocation to CAR format base64 (for sending in request body)
254
+ *
255
+ * @param invocation - The invocation to serialize
256
+ * @returns Base64-encoded CAR data
257
+ */
258
+ export async function serializeInvocation(
259
+ invocation: Awaited<ReturnType<typeof createInvocation>>,
260
+ ): Promise<string> {
261
+ // Build the invocation into an IPLD view
262
+ const built = await invocation.buildIPLDView();
263
+
264
+ // Archive the invocation
265
+ const archive = await built.archive();
266
+ // Check for error
267
+ if (archive.error) {
268
+ throw new Error(
269
+ `Failed to archive invocation: ${archive.error?.message ?? 'unknown'}`,
270
+ );
271
+ }
272
+ // Get the bytes
273
+ if (!archive.ok) {
274
+ throw new Error('Failed to archive invocation: no data returned');
275
+ }
276
+
277
+ // Convert to base64
278
+ return Buffer.from(archive.ok).toString('base64');
279
+ }
280
+
281
+ /**
282
+ * Serialize a delegation to CAR format base64 (for storage/transport)
283
+ *
284
+ * @param delegation - The delegation to serialize
285
+ * @returns Base64-encoded CAR data
286
+ */
287
+ export async function serializeDelegation(delegation: Delegation): Promise<string> {
288
+ // Archive the delegation (returns Result type)
289
+ const archive = await delegation.archive();
290
+
291
+ // Check for error (archive returns { ok: bytes } or { error: Error })
292
+ if (archive.error) {
293
+ throw new Error(`Failed to archive delegation: ${archive.error.message}`);
294
+ }
295
+ // Get the bytes
296
+ if (!archive.ok) {
297
+ throw new Error('Failed to archive delegation: no data returned');
298
+ }
299
+
300
+ // Convert to base64
301
+ return Buffer.from(archive.ok).toString('base64');
302
+ }
303
+
304
+ /**
305
+ * Parse a serialized delegation from CAR format
306
+ *
307
+ * @param serialized - Base64-encoded CAR data
308
+ * @returns The parsed delegation
309
+ */
310
+ export async function parseDelegation(serialized: string): Promise<Delegation> {
311
+ const { extract } = await import('@ucanto/core/delegation');
312
+ const bytes = new Uint8Array(Buffer.from(serialized, 'base64'));
313
+
314
+ const result = await extract(bytes);
315
+ if (result.error) {
316
+ throw new Error(
317
+ `Failed to parse delegation: ${result.error?.message ?? 'unknown error'}`,
318
+ );
319
+ }
320
+
321
+ if (!result.ok) {
322
+ throw new Error('Failed to parse delegation: no data returned');
323
+ }
324
+
325
+ return result.ok as Delegation;
326
+ }
327
+
328
+ // Re-export useful types
329
+ export type { Signer, Delegation, Capability };
@@ -0,0 +1,325 @@
1
+ /**
2
+ * @fileoverview did:ixo resolver for UCAN validation
3
+ *
4
+ * This module provides a DID resolver that can resolve did:ixo identifiers
5
+ * to their associated did:key identifiers by querying the IXO blockchain
6
+ * indexer for the DID document.
7
+ */
8
+
9
+ import type { DID } from '@ucanto/interface';
10
+ import type { DIDKeyResolver, KeyDID } from '../types.js';
11
+ import { base58Encode, hexDecode, base58Decode } from './utils.js';
12
+
13
+ /**
14
+ * Configuration for the IXO DID resolver
15
+ */
16
+ export interface IxoDIDResolverConfig {
17
+ /**
18
+ * URL of the IXO GraphQL indexer
19
+ * @example 'https://blocksync.ixo.earth/graphql'
20
+ */
21
+ indexerUrl: string;
22
+
23
+ /**
24
+ * Optional fetch implementation (for testing or custom environments)
25
+ */
26
+ fetch?: typeof globalThis.fetch;
27
+ }
28
+
29
+ /**
30
+ * GraphQL query to fetch DID document from IXO indexer
31
+ */
32
+ const DID_DOCUMENT_QUERY = `
33
+ query GetDIDDocument($id: String!) {
34
+ iids(filter: { id: { equalTo: $id } }) {
35
+ nodes {
36
+ id
37
+ verificationMethod
38
+ }
39
+ }
40
+ }
41
+ `;
42
+
43
+ /**
44
+ * Verification method from IXO DID document
45
+ */
46
+ interface VerificationMethod {
47
+ id: string;
48
+ type: string;
49
+ controller: string;
50
+ publicKeyMultibase?: string;
51
+ publicKeyHex?: string;
52
+ publicKeyBase58?: string;
53
+ }
54
+
55
+ /**
56
+ * IXO DID document structure (partial)
57
+ */
58
+ interface IxoDIDDocument {
59
+ id: string;
60
+ verificationMethod: VerificationMethod[];
61
+ }
62
+
63
+
64
+
65
+ // =============================================================================
66
+ // did:key Conversion
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Ed25519 multicodec prefix (0xed)
71
+ * When creating a did:key, we prepend this to the raw public key
72
+ */
73
+ const ED25519_MULTICODEC_PREFIX = new Uint8Array([0xed, 0x01]);
74
+
75
+ /**
76
+ * Convert raw Ed25519 public key bytes to did:key format
77
+ *
78
+ * did:key format for Ed25519:
79
+ * - Prefix with multicodec 0xed01
80
+ * - Encode with base58btc (multibase 'z' prefix)
81
+ * - Result: did:key:z6Mk...
82
+ */
83
+ function rawPublicKeyToDidKey(publicKeyBytes: Uint8Array): KeyDID | null {
84
+ // Ed25519 public keys should be 32 bytes
85
+ if (publicKeyBytes.length !== 32) {
86
+ console.warn(
87
+ `[IxoDIDResolver] Expected 32-byte Ed25519 key, got ${publicKeyBytes.length} bytes`,
88
+ );
89
+ return null;
90
+ }
91
+
92
+ // Prepend the Ed25519 multicodec prefix
93
+ const prefixedKey = new Uint8Array(
94
+ ED25519_MULTICODEC_PREFIX.length + publicKeyBytes.length,
95
+ );
96
+ prefixedKey.set(ED25519_MULTICODEC_PREFIX, 0);
97
+ prefixedKey.set(publicKeyBytes, ED25519_MULTICODEC_PREFIX.length);
98
+
99
+ // Encode with base58btc and add 'z' multibase prefix
100
+ const multibaseEncoded = 'z' + base58Encode(prefixedKey);
101
+
102
+ return `did:key:${multibaseEncoded}` as KeyDID;
103
+ }
104
+
105
+ /**
106
+ * Convert a public key to did:key format
107
+ * Supports Ed25519 keys in multibase, hex, or base58 format
108
+ */
109
+ function publicKeyToDidKey(vm: VerificationMethod): KeyDID | null {
110
+ // console.log('vm', vm);
111
+ // Handle multibase format (preferred)
112
+ if (vm.publicKeyMultibase) {
113
+ // Multibase Ed25519 public keys start with 'z' (base58btc)
114
+ // The did:key format for Ed25519 is did:key:z6Mk...
115
+ if (vm.publicKeyMultibase.startsWith('z')) {
116
+ // Already in the correct format for did:key
117
+ return `did:key:${vm.publicKeyMultibase}` as KeyDID;
118
+ }
119
+
120
+ // Handle other multibase prefixes if needed
121
+ console.warn(
122
+ `[IxoDIDResolver] Unsupported multibase prefix for ${vm.id}: ${vm.publicKeyMultibase.charAt(0)}`,
123
+ );
124
+ return null;
125
+ }
126
+
127
+ // Handle hex format
128
+ if (vm.publicKeyHex) {
129
+ try {
130
+ const publicKeyBytes = hexDecode(vm.publicKeyHex);
131
+ const didKey = rawPublicKeyToDidKey(publicKeyBytes);
132
+ return didKey;
133
+ } catch (error) {
134
+ console.warn(
135
+ `[IxoDIDResolver] Failed to decode publicKeyHex for ${vm.id}: ${error instanceof Error ? error.message : 'Unknown error'}`,
136
+ );
137
+ return null;
138
+ }
139
+ }
140
+
141
+ // Handle base58 format
142
+ if (vm.publicKeyBase58) {
143
+ try {
144
+ const publicKeyBytes = base58Decode(vm.publicKeyBase58);
145
+ const didKey = rawPublicKeyToDidKey(publicKeyBytes);
146
+ return didKey;
147
+ } catch (error) {
148
+ console.warn(
149
+ `[IxoDIDResolver] Failed to decode publicKeyBase58 for ${vm.id}: ${error instanceof Error ? error.message : 'Unknown error'}`,
150
+ );
151
+ return null;
152
+ }
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Creates a DID resolver for did:ixo identifiers
160
+ *
161
+ * This resolver queries the IXO blockchain indexer to fetch DID documents
162
+ * and extracts the verification methods that can be used to verify signatures.
163
+ *
164
+ * @param config - Configuration for the resolver
165
+ * @returns A DIDKeyResolver function compatible with ucanto
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const resolver = createIxoDIDResolver({
170
+ * indexerUrl: 'https://blocksync.ixo.earth/graphql'
171
+ * });
172
+ *
173
+ * const result = await resolver('did:ixo:abc123');
174
+ * if (result.ok) {
175
+ * console.log('Keys:', result.ok); // ['did:key:z6Mk...']
176
+ * }
177
+ * ```
178
+ */
179
+ export function createIxoDIDResolver(
180
+ config: IxoDIDResolverConfig,
181
+ ): DIDKeyResolver {
182
+ const fetchFn = config.fetch ?? globalThis.fetch;
183
+
184
+ return async (
185
+ did: DID,
186
+ ): Promise<
187
+ { ok: KeyDID[] } | { error: { name: string; did: string; message: string } }
188
+ > => {
189
+ // Only handle did:ixo
190
+ if (!did.startsWith('did:ixo:')) {
191
+ return {
192
+ error: {
193
+ name: 'DIDKeyResolutionError',
194
+ did,
195
+ message: `Cannot resolve ${did}: not a did:ixo identifier`,
196
+ },
197
+ };
198
+ }
199
+
200
+ try {
201
+ // Query the IXO indexer
202
+ const response = await fetchFn(config.indexerUrl, {
203
+ method: 'POST',
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ },
207
+ body: JSON.stringify({
208
+ query: DID_DOCUMENT_QUERY,
209
+ variables: { id: did },
210
+ }),
211
+ });
212
+
213
+ if (!response.ok) {
214
+ return {
215
+ error: {
216
+ name: 'DIDKeyResolutionError',
217
+ did,
218
+ message: `Failed to fetch DID document: HTTP ${response.status}`,
219
+ },
220
+ };
221
+ }
222
+
223
+ const data = (await response.json()) as {
224
+ data?: { iids?: { nodes?: IxoDIDDocument[] } };
225
+ errors?: Array<{ message: string }>;
226
+ };
227
+
228
+ if (data.errors && data.errors.length > 0) {
229
+ return {
230
+ error: {
231
+ name: 'DIDKeyResolutionError',
232
+ did,
233
+ message: `GraphQL error: ${data.errors[0]?.message ?? 'Unknown error'}`,
234
+ },
235
+ };
236
+ }
237
+
238
+ const didDoc = data.data?.iids?.nodes?.[0];
239
+ if (!didDoc) {
240
+ return {
241
+ error: {
242
+ name: 'DIDKeyResolutionError',
243
+ did,
244
+ message: `DID document not found for ${did}`,
245
+ },
246
+ };
247
+ }
248
+
249
+ // Extract verification methods and convert to did:key
250
+ const keys: KeyDID[] = [];
251
+
252
+ for (const vm of didDoc.verificationMethod || []) {
253
+ // Look for Ed25519 verification methods
254
+ // Common types: Ed25519VerificationKey2018, Ed25519VerificationKey2020, JsonWebKey2020
255
+ if (
256
+ vm.type.includes('Ed25519') ||
257
+ vm.type === 'JsonWebKey2020' ||
258
+ vm.id.includes('signing')
259
+ ) {
260
+ const keyDid = publicKeyToDidKey(vm);
261
+ if (keyDid) {
262
+ keys.push(keyDid);
263
+ }
264
+ }
265
+ }
266
+
267
+ if (keys.length === 0) {
268
+ return {
269
+ error: {
270
+ name: 'DIDKeyResolutionError',
271
+ did,
272
+ message: `No valid Ed25519 verification methods found in DID document for ${did}`,
273
+ },
274
+ };
275
+ }
276
+
277
+ return { ok: keys };
278
+ } catch (error) {
279
+ return {
280
+ error: {
281
+ name: 'DIDKeyResolutionError',
282
+ did,
283
+ message: `Failed to resolve ${did}: ${error instanceof Error ? error.message : 'Unknown error'}`,
284
+ },
285
+ };
286
+ }
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Creates a composite DID resolver that tries multiple resolvers in order
292
+ *
293
+ * @param resolvers - Array of DID resolvers to try
294
+ * @returns A DIDKeyResolver that tries each resolver until one succeeds
295
+ */
296
+ export function createCompositeDIDResolver(
297
+ resolvers: DIDKeyResolver[],
298
+ ): DIDKeyResolver {
299
+ return async (did: DID) => {
300
+ for (const resolver of resolvers) {
301
+ const result = await resolver(did);
302
+ if ('ok' in result) {
303
+ return result;
304
+ }
305
+ // If this resolver doesn't handle this DID method, try the next one
306
+ if (result.error.message.includes('not a did:')) {
307
+ continue;
308
+ }
309
+ // If it's a different error, return it
310
+ return result;
311
+ }
312
+
313
+ return {
314
+ error: {
315
+ name: 'DIDKeyResolutionError',
316
+ did,
317
+ message: `No resolver could handle ${did}`,
318
+ },
319
+ };
320
+ };
321
+ }
322
+
323
+ // TODO: Add caching layer for resolved DIDs
324
+ // TODO: Add support for resolving from local DID document store
325
+