social components
inlay.at
atproto
components
sdui
1"use cache";
2
3import { cacheLife } from "next/cache";
4
5// --- DID Document resolution ---
6
7type DidDocument = {
8 id: string;
9 alsoKnownAs?: string[];
10 service?: { id: string; type: string; serviceEndpoint: string }[];
11};
12
13async function resolveDidDocument(did: string): Promise<DidDocument> {
14 if (did.startsWith("did:plc:")) {
15 const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
16 if (!res.ok)
17 throw new Error(`PLC directory error for ${did}: ${res.status}`);
18 return (await res.json()) as DidDocument;
19 }
20 if (did.startsWith("did:web:")) {
21 const domain = did.slice("did:web:".length).replaceAll(":", "/");
22 const res = await fetch(`https://${domain}/.well-known/did.json`);
23 if (!res.ok)
24 throw new Error(`did:web resolution failed for ${did}: ${res.status}`);
25 return (await res.json()) as DidDocument;
26 }
27 throw new Error(`Unsupported DID method: ${did}`);
28}
29
30// --- Public API ---
31
32/**
33 * Resolve a DID to its service endpoint URL
34 */
35export async function resolveDidToService(
36 did: string,
37 serviceId = "#atproto_pds"
38): Promise<string> {
39 cacheLife("hours");
40
41 const doc = await resolveDidDocument(did);
42 const service = doc.service?.find((s) => s.id === serviceId);
43 if (!service) {
44 throw new Error(`No ${serviceId} service found for DID: ${did}`);
45 }
46 return service.serviceEndpoint;
47}
48
49/**
50 * Resolve a DID to its handle from the DID document
51 */
52export async function resolveDidToHandle(did: string): Promise<string> {
53 cacheLife("hours");
54
55 if (!did.startsWith("did:")) return did;
56
57 try {
58 const doc = await resolveDidDocument(did);
59 const aka = doc.alsoKnownAs?.[0];
60 if (aka?.startsWith("at://")) {
61 const handle = aka.slice("at://".length);
62 if (handle && handle !== "handle.invalid") return handle;
63 }
64 } catch {
65 // Fall through to return DID
66 }
67 return did;
68}
69
70/**
71 * Resolve a handle to its DID via the handle's server
72 */
73export async function resolveHandleToDid(
74 handle: string
75): Promise<string | null> {
76 cacheLife("hours");
77
78 if (handle.startsWith("did:")) return handle;
79
80 try {
81 // Try DNS TXT first via DoH
82 const dnsRes = await fetch(
83 `https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`,
84 { headers: { Accept: "application/dns-json" } }
85 );
86 if (dnsRes.ok) {
87 const dns = (await dnsRes.json()) as {
88 Answer?: { data: string }[];
89 };
90 const txt = dns.Answer?.find((a) => a.data?.includes("did="));
91 if (txt) {
92 const did = txt.data
93 .replace(/^"/, "")
94 .replace(/"$/, "")
95 .replace("did=", "");
96 if (did.startsWith("did:")) return did;
97 }
98 }
99 } catch {
100 // Fall through to HTTP well-known
101 }
102
103 try {
104 const res = await fetch(`https://${handle}/.well-known/atproto-did`);
105 if (res.ok) {
106 const did = (await res.text()).trim();
107 if (did.startsWith("did:")) return did;
108 }
109 } catch {
110 // Fall through
111 }
112
113 return null;
114}
115
116/**
117 * Resolve a handle or DID to { did, handle, pds }
118 */
119export async function resolveIdentity(
120 identity: string
121): Promise<{ did: string; handle: string; pds: string }> {
122 cacheLife("minutes");
123
124 const did = identity.startsWith("did:")
125 ? identity
126 : await resolveHandleToDid(identity);
127 if (!did) throw new Error(`Failed to resolve: ${identity}`);
128 const [handle, pds] = await Promise.all([
129 resolveDidToHandle(did),
130 resolveDidToService(did),
131 ]);
132 return { did, handle, pds };
133}