atmosphere explorer pds.ls
tool typescript atproto
428
fork

Configure Feed

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

at main 281 lines 7.9 kB view raw
1import "@atcute/atproto"; 2import { 3 type DidDocument, 4 getLabelerEndpoint, 5 getPdsEndpoint, 6 isAtprotoDid, 7} from "@atcute/identity"; 8import { 9 AtprotoWebDidDocumentResolver, 10 CompositeDidDocumentResolver, 11 CompositeHandleResolver, 12 DohJsonHandleResolver, 13 PlcDidDocumentResolver, 14 WellKnownHandleResolver, 15} from "@atcute/identity-resolver"; 16import { DohJsonLexiconAuthorityResolver, LexiconSchemaResolver } from "@atcute/lexicon-resolver"; 17import { Did, Handle } from "@atcute/lexicons"; 18import { AtprotoDid, isHandle, Nsid } from "@atcute/lexicons/syntax"; 19import { createMemo } from "solid-js"; 20import { createStore } from "solid-js/store"; 21import { plcDirectory } from "../views/settings"; 22 23const proxyFetch = (rewrite: (url: URL) => string): typeof fetch => { 24 return async (input, init) => { 25 try { 26 return await fetch(input, init); 27 } catch (err) { 28 if (init?.signal?.aborted) throw err; 29 const url = new URL( 30 typeof input === "string" ? input 31 : input instanceof URL ? input.href 32 : input.url, 33 ); 34 return fetch(rewrite(url)); 35 } 36 }; 37}; 38 39const didWebProxyFetch = proxyFetch( 40 (url) => `/resolve-did-web?host=${encodeURIComponent(url.host)}`, 41); 42const dnsProxyFetch = proxyFetch( 43 (url) => 44 `/resolve-handle-dns?handle=${encodeURIComponent(url.searchParams.get("name")?.replace("_atproto.", "") ?? "")}`, 45); 46const handleHttpProxyFetch = proxyFetch( 47 (url) => `/resolve-handle-http?handle=${encodeURIComponent(url.host)}`, 48); 49 50export const didDocumentResolver = createMemo( 51 () => 52 new CompositeDidDocumentResolver({ 53 methods: { 54 plc: new PlcDidDocumentResolver({ 55 apiUrl: plcDirectory(), 56 }), 57 web: new AtprotoWebDidDocumentResolver({ fetch: didWebProxyFetch }), 58 }, 59 }), 60); 61 62export const handleResolver = new CompositeHandleResolver({ 63 strategy: "dns-first", 64 methods: { 65 dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?", fetch: dnsProxyFetch }), 66 http: new WellKnownHandleResolver({ fetch: handleHttpProxyFetch }), 67 }, 68}); 69 70const authorityResolver = new DohJsonLexiconAuthorityResolver({ 71 dohUrl: "https://dns.google/resolve?", 72}); 73 74const schemaResolver = createMemo( 75 () => 76 new LexiconSchemaResolver({ 77 didDocumentResolver: didDocumentResolver(), 78 }), 79); 80 81const didPDSCache: Record<string, Promise<string>> = {}; 82const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({}); 83const didDocCache: Record<string, DidDocument> = {}; 84const getPDS = (did: string): Promise<string> => { 85 if (did in didPDSCache) return didPDSCache[did]; 86 87 if (!isAtprotoDid(did)) { 88 return Promise.reject(new Error("Not a valid DID identifier")); 89 } 90 91 didPDSCache[did] = (async () => { 92 let doc: DidDocument; 93 try { 94 doc = await didDocumentResolver().resolve(did); 95 didDocCache[did] = doc; 96 } catch (e) { 97 console.error(e); 98 delete didPDSCache[did]; 99 throw new Error("Error during did document resolution"); 100 } 101 102 const pds = getPdsEndpoint(doc); 103 const labeler = getLabelerEndpoint(doc); 104 105 if (labeler) { 106 setLabelerCache(did, labeler); 107 } 108 109 if (!pds) { 110 delete didPDSCache[did]; 111 throw new Error("No PDS found"); 112 } 113 114 return pds; 115 })(); 116 117 return didPDSCache[did]; 118}; 119 120const resolveHandle = async (handle: Handle) => { 121 if (!isHandle(handle)) { 122 throw new Error("Not a valid handle"); 123 } 124 125 return await handleResolver.resolve(handle); 126}; 127 128const resolveDidDoc = async (did: Did) => { 129 if (!isAtprotoDid(did)) { 130 throw new Error("Not a valid DID identifier"); 131 } 132 return await didDocumentResolver().resolve(did); 133}; 134 135const validateHandle = async (handle: Handle, did: Did) => { 136 if (!isHandle(handle)) return false; 137 138 let resolvedDid: string; 139 try { 140 resolvedDid = await handleResolver.resolve(handle); 141 } catch (err) { 142 console.error(err); 143 return false; 144 } 145 if (resolvedDid !== did) return false; 146 return true; 147}; 148 149const resolveLexiconAuthority = async (nsid: Nsid) => { 150 return await authorityResolver.resolve(nsid); 151}; 152 153const resolveLexiconAuthorityDirect = async (authority: string) => { 154 const dohUrl = "https://dns.google/resolve?"; 155 const reversedAuthority = authority.split(".").reverse().join("."); 156 const domain = `_lexicon.${reversedAuthority}`; 157 const url = new URL(dohUrl); 158 url.searchParams.set("name", domain); 159 url.searchParams.set("type", "TXT"); 160 161 const response = await fetch(url.toString()); 162 if (!response.ok) { 163 throw new Error(`Failed to resolve lexicon authority for ${authority}`); 164 } 165 166 const data = await response.json(); 167 if (!data.Answer || data.Answer.length === 0) { 168 throw new Error(`No lexicon authority found for ${authority}`); 169 } 170 171 const txtRecord = data.Answer[0].data.replace(/"/g, ""); 172 173 if (!txtRecord.startsWith("did=")) { 174 throw new Error(`Invalid lexicon authority record for ${authority}`); 175 } 176 177 return txtRecord.replace("did=", ""); 178}; 179 180const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => { 181 return await schemaResolver().resolve(authority, nsid); 182}; 183 184interface LinkData { 185 links: { 186 [key: string]: { 187 [key: string]: { 188 records: number; 189 distinct_dids: number; 190 }; 191 }; 192 }; 193} 194 195type LinksWithRecords = { 196 cursor: string; 197 total: number; 198 linking_records: Array<{ did: string; collection: string; rkey: string }>; 199}; 200 201const getConstellation = async ( 202 endpoint: string, 203 target: string, 204 collection?: string, 205 path?: string, 206 cursor?: string, 207 limit?: number, 208) => { 209 const url = new URL("https://constellation.microcosm.blue"); 210 url.pathname = endpoint; 211 url.searchParams.set("target", target); 212 if (collection) { 213 if (!path) throw new Error("collection and path must either both be set or neither"); 214 url.searchParams.set("collection", collection); 215 url.searchParams.set("path", path); 216 } else { 217 if (path) throw new Error("collection and path must either both be set or neither"); 218 } 219 if (limit) url.searchParams.set("limit", `${limit}`); 220 if (cursor) url.searchParams.set("cursor", `${cursor}`); 221 const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); 222 if (!res.ok) throw new Error("failed to fetch from constellation"); 223 return await res.json(); 224}; 225 226const getAllBacklinks = (target: string) => getConstellation("/links/all", target); 227 228const getRecordBacklinks = ( 229 target: string, 230 collection: string, 231 path: string, 232 cursor?: string, 233 limit?: number, 234): Promise<LinksWithRecords> => 235 getConstellation("/links", target, collection, path, cursor, limit || 100); 236 237export interface HandleResolveResult { 238 success: boolean; 239 did?: string; 240 error?: string; 241} 242 243export const resolveHandleDetailed = async (handle: Handle) => { 244 const dnsResolver = new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }); 245 const httpResolver = new WellKnownHandleResolver({ fetch: handleHttpProxyFetch }); 246 247 const tryResolve = async ( 248 resolver: DohJsonHandleResolver | WellKnownHandleResolver, 249 timeoutMs: number = 5000, 250 ): Promise<HandleResolveResult> => { 251 try { 252 const timeoutPromise = new Promise<never>((_, reject) => 253 setTimeout(() => reject(new Error("Request timed out")), timeoutMs), 254 ); 255 const did = await Promise.race([resolver.resolve(handle), timeoutPromise]); 256 return { success: true, did }; 257 } catch (err: any) { 258 return { success: false, error: err.message ?? String(err) }; 259 } 260 }; 261 262 const [dns, http] = await Promise.all([tryResolve(dnsResolver), tryResolve(httpResolver)]); 263 264 return { dns, http }; 265}; 266 267export { 268 didDocCache, 269 getAllBacklinks, 270 getPDS, 271 getRecordBacklinks, 272 labelerCache, 273 resolveDidDoc, 274 resolveHandle, 275 resolveLexiconAuthority, 276 resolveLexiconAuthorityDirect, 277 resolveLexiconSchema, 278 validateHandle, 279 type LinkData, 280 type LinksWithRecords, 281};