social components inlay.at
atproto components sdui
86
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 133 lines 3.5 kB view raw
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}