-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdid.ts
More file actions
77 lines (69 loc) · 2.68 KB
/
Copy pathdid.ts
File metadata and controls
77 lines (69 loc) · 2.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* DID resolution → Ed25519 public key (SPEC §7).
*
* - `did:key`: fully offline (multibase base58btc + multicodec ed25519-pub).
* - `did:web`: resolved over HTTPS (TLS REQUIRED); `fetch` is injectable for tests.
*
* Returns the 32-byte Ed25519 public key, or `null` if it cannot be resolved.
*/
import { base58 } from "@scure/base";
const ED25519_PUB_MULTICODEC = [0xed, 0x01] as const;
/** Decode an Ed25519 `did:key` to its 32-byte public key. */
export function resolveDidKey(did: string): Uint8Array | null {
const mb = did.slice("did:key:".length);
if (!mb.startsWith("z")) return null;
let bytes: Uint8Array;
try {
bytes = base58.decode(mb.slice(1));
} catch {
return null;
}
if (bytes[0] !== ED25519_PUB_MULTICODEC[0] || bytes[1] !== ED25519_PUB_MULTICODEC[1]) return null;
const pub = bytes.slice(2);
return pub.length === 32 ? pub : null;
}
/** did:web → HTTPS URL of the DID Document. */
export function didWebToUrl(did: string): string | null {
const rest = did.slice("did:web:".length);
if (rest === "") return null;
const parts = rest.split(":").map((p) => decodeURIComponent(p));
const host = parts[0]!;
if (parts.length === 1) return `https://${host}/.well-known/did.json`;
return `https://${host}/${parts.slice(1).join("/")}/did.json`;
}
type FetchLike = (url: string) => Promise<{ ok: boolean; json: () => Promise<unknown> }>;
function pubKeyFromDidDocument(doc: unknown): Uint8Array | null {
if (typeof doc !== "object" || doc === null) return null;
const vms = (doc as { verificationMethod?: unknown }).verificationMethod;
if (!Array.isArray(vms)) return null;
for (const vm of vms) {
const mb = (vm as { publicKeyMultibase?: unknown }).publicKeyMultibase;
if (typeof mb === "string" && mb.startsWith("z")) {
const key = resolveDidKey("did:key:" + mb);
if (key) return key;
}
}
return null;
}
/** Resolve a did:web over HTTPS. `fetchImpl` defaults to global fetch. */
export async function resolveDidWeb(
did: string,
fetchImpl: FetchLike = fetch as unknown as FetchLike,
): Promise<Uint8Array | null> {
const url = didWebToUrl(did);
if (!url || !url.startsWith("https://")) return null; // TLS required
try {
const res = await fetchImpl(url);
if (!res.ok) return null;
return pubKeyFromDidDocument(await res.json());
} catch {
return null;
}
}
export type DidResolver = (did: string) => Promise<Uint8Array | null>;
/** Default resolver: did:key offline, did:web over HTTPS, others unresolved. */
export const defaultResolveDid: DidResolver = async (did) => {
if (did.startsWith("did:key:")) return resolveDidKey(did);
if (did.startsWith("did:web:")) return resolveDidWeb(did);
return null;
};