@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.
- package/.eslintrc.js +9 -0
- package/.prettierignore +3 -0
- package/.prettierrc.js +4 -0
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +0 -0
- package/README.md +189 -0
- package/dist/capabilities/capability.d.ts +33 -0
- package/dist/capabilities/capability.d.ts.map +1 -0
- package/dist/capabilities/capability.js +53 -0
- package/dist/capabilities/capability.js.map +1 -0
- package/dist/client/create-client.d.ts +33 -0
- package/dist/client/create-client.d.ts.map +1 -0
- package/dist/client/create-client.js +104 -0
- package/dist/client/create-client.js.map +1 -0
- package/dist/did/ixo-resolver.d.ts +8 -0
- package/dist/did/ixo-resolver.d.ts.map +1 -0
- package/dist/did/ixo-resolver.js +162 -0
- package/dist/did/ixo-resolver.js.map +1 -0
- package/dist/did/utils.d.ts +4 -0
- package/dist/did/utils.d.ts.map +1 -0
- package/dist/did/utils.js +85 -0
- package/dist/did/utils.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/store/memory.d.ts +25 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +71 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validator/validator.d.ts +29 -0
- package/dist/validator/validator.d.ts.map +1 -0
- package/dist/validator/validator.js +179 -0
- package/dist/validator/validator.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +78 -0
- package/scripts/test-ucan.ts +457 -0
- package/src/capabilities/capability.ts +244 -0
- package/src/client/create-client.ts +329 -0
- package/src/did/ixo-resolver.ts +325 -0
- package/src/did/utils.ts +141 -0
- package/src/index.ts +135 -0
- package/src/store/memory.ts +194 -0
- package/src/types.ts +108 -0
- package/src/validator/validator.ts +399 -0
- 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
|
+
|