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