forked from pdsls.dev/pdsls
atproto explorer
at main 5.0 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 } from "@atcute/lexicon-resolver"; 17import { Did, Handle } from "@atcute/lexicons"; 18import { isHandle, Nsid } from "@atcute/lexicons/syntax"; 19import { createStore } from "solid-js/store"; 20import { setPDS } from "../components/navbar"; 21 22const didDocumentResolver = new CompositeDidDocumentResolver({ 23 methods: { 24 plc: new PlcDidDocumentResolver({ 25 apiUrl: localStorage.plcDirectory ?? "https://plc.directory", 26 }), 27 web: new AtprotoWebDidDocumentResolver(), 28 }, 29}); 30 31const handleResolver = new CompositeHandleResolver({ 32 strategy: "dns-first", 33 methods: { 34 dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }), 35 http: new WellKnownHandleResolver(), 36 }, 37}); 38 39const authorityResolver = new DohJsonLexiconAuthorityResolver({ 40 dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", 41}); 42 43const didPDSCache: Record<string, string> = {}; 44const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({}); 45const didDocCache: Record<string, DidDocument> = {}; 46const getPDS = async (did: string) => { 47 if (did in didPDSCache) return didPDSCache[did]; 48 49 if (!isAtprotoDid(did)) { 50 throw new Error("Not a valid DID identifier"); 51 } 52 53 const doc = await didDocumentResolver.resolve(did); 54 didDocCache[did] = doc; 55 56 const pds = getPdsEndpoint(doc); 57 const labeler = getLabelerEndpoint(doc); 58 59 if (labeler) { 60 setLabelerCache(did, labeler); 61 } 62 63 if (!pds) { 64 throw new Error("No PDS found"); 65 } 66 67 return (didPDSCache[did] = pds); 68}; 69 70const resolveHandle = async (handle: Handle) => { 71 if (!isHandle(handle)) { 72 throw new Error("Not a valid handle"); 73 } 74 75 return await handleResolver.resolve(handle); 76}; 77 78const resolveDidDoc = async (did: Did) => { 79 if (!isAtprotoDid(did)) { 80 throw new Error("Not a valid DID identifier"); 81 } 82 return await didDocumentResolver.resolve(did); 83}; 84 85const validateHandle = async (handle: Handle, did: Did) => { 86 if (!isHandle(handle)) return false; 87 88 let resolvedDid: string; 89 try { 90 resolvedDid = await handleResolver.resolve(handle); 91 } catch (err) { 92 console.error(err); 93 return false; 94 } 95 if (resolvedDid !== did) return false; 96 return true; 97}; 98 99const resolvePDS = async (did: string) => { 100 setPDS(undefined); 101 const pds = await getPDS(did); 102 if (!pds) throw new Error("No PDS found"); 103 setPDS(pds.replace("https://", "").replace("http://", "")); 104 return pds; 105}; 106 107const resolveLexiconAuthority = async (nsid: Nsid) => { 108 return await authorityResolver.resolve(nsid); 109}; 110 111interface LinkData { 112 links: { 113 [key: string]: { 114 [key: string]: { 115 records: number; 116 distinct_dids: number; 117 }; 118 }; 119 }; 120} 121 122type LinksWithRecords = { 123 cursor: string; 124 total: number; 125 linking_records: Array<{ did: string; collection: string; rkey: string }>; 126}; 127 128type LinksWithDids = { 129 cursor: string; 130 total: number; 131 linking_dids: Array<string>; 132}; 133 134const getConstellation = async ( 135 endpoint: string, 136 target: string, 137 collection?: string, 138 path?: string, 139 cursor?: string, 140 limit?: number, 141) => { 142 const url = new URL("https://constellation.microcosm.blue"); 143 url.pathname = endpoint; 144 url.searchParams.set("target", target); 145 if (collection) { 146 if (!path) throw new Error("collection and path must either both be set or neither"); 147 url.searchParams.set("collection", collection); 148 url.searchParams.set("path", path); 149 } else { 150 if (path) throw new Error("collection and path must either both be set or neither"); 151 } 152 if (limit) url.searchParams.set("limit", `${limit}`); 153 if (cursor) url.searchParams.set("cursor", `${cursor}`); 154 const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); 155 if (!res.ok) throw new Error("failed to fetch from constellation"); 156 return await res.json(); 157}; 158 159const getAllBacklinks = (target: string) => getConstellation("/links/all", target); 160 161const getRecordBacklinks = ( 162 target: string, 163 collection: string, 164 path: string, 165 cursor?: string, 166 limit?: number, 167): Promise<LinksWithRecords> => 168 getConstellation("/links", target, collection, path, cursor, limit || 100); 169 170const getDidBacklinks = ( 171 target: string, 172 collection: string, 173 path: string, 174 cursor?: string, 175 limit?: number, 176): Promise<LinksWithDids> => 177 getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100); 178 179export { 180 didDocCache, 181 getAllBacklinks, 182 getDidBacklinks, 183 getPDS, 184 getRecordBacklinks, 185 labelerCache, 186 resolveDidDoc, 187 resolveHandle, 188 resolveLexiconAuthority, 189 resolvePDS, 190 validateHandle, 191 type LinkData, 192 type LinksWithDids, 193 type LinksWithRecords, 194};