atmosphere explorer
0
fork

Configure Feed

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

at main 275 lines 7.7 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, string> = {}; 82const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({}); 83const didDocCache: Record<string, DidDocument> = {}; 84const getPDS = async (did: string) => { 85 if (did in didPDSCache) return didPDSCache[did]; 86 87 if (!isAtprotoDid(did)) { 88 throw new Error("Not a valid DID identifier"); 89 } 90 91 let doc: DidDocument; 92 try { 93 doc = await didDocumentResolver().resolve(did); 94 didDocCache[did] = doc; 95 } catch (e) { 96 console.error(e); 97 throw new Error("Error during did document resolution"); 98 } 99 100 const pds = getPdsEndpoint(doc); 101 const labeler = getLabelerEndpoint(doc); 102 103 if (labeler) { 104 setLabelerCache(did, labeler); 105 } 106 107 if (!pds) { 108 throw new Error("No PDS found"); 109 } 110 111 return (didPDSCache[did] = pds); 112}; 113 114const resolveHandle = async (handle: Handle) => { 115 if (!isHandle(handle)) { 116 throw new Error("Not a valid handle"); 117 } 118 119 return await handleResolver.resolve(handle); 120}; 121 122const resolveDidDoc = async (did: Did) => { 123 if (!isAtprotoDid(did)) { 124 throw new Error("Not a valid DID identifier"); 125 } 126 return await didDocumentResolver().resolve(did); 127}; 128 129const validateHandle = async (handle: Handle, did: Did) => { 130 if (!isHandle(handle)) return false; 131 132 let resolvedDid: string; 133 try { 134 resolvedDid = await handleResolver.resolve(handle); 135 } catch (err) { 136 console.error(err); 137 return false; 138 } 139 if (resolvedDid !== did) return false; 140 return true; 141}; 142 143const resolveLexiconAuthority = async (nsid: Nsid) => { 144 return await authorityResolver.resolve(nsid); 145}; 146 147const resolveLexiconAuthorityDirect = async (authority: string) => { 148 const dohUrl = "https://dns.google/resolve?"; 149 const reversedAuthority = authority.split(".").reverse().join("."); 150 const domain = `_lexicon.${reversedAuthority}`; 151 const url = new URL(dohUrl); 152 url.searchParams.set("name", domain); 153 url.searchParams.set("type", "TXT"); 154 155 const response = await fetch(url.toString()); 156 if (!response.ok) { 157 throw new Error(`Failed to resolve lexicon authority for ${authority}`); 158 } 159 160 const data = await response.json(); 161 if (!data.Answer || data.Answer.length === 0) { 162 throw new Error(`No lexicon authority found for ${authority}`); 163 } 164 165 const txtRecord = data.Answer[0].data.replace(/"/g, ""); 166 167 if (!txtRecord.startsWith("did=")) { 168 throw new Error(`Invalid lexicon authority record for ${authority}`); 169 } 170 171 return txtRecord.replace("did=", ""); 172}; 173 174const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => { 175 return await schemaResolver().resolve(authority, nsid); 176}; 177 178interface LinkData { 179 links: { 180 [key: string]: { 181 [key: string]: { 182 records: number; 183 distinct_dids: number; 184 }; 185 }; 186 }; 187} 188 189type LinksWithRecords = { 190 cursor: string; 191 total: number; 192 linking_records: Array<{ did: string; collection: string; rkey: string }>; 193}; 194 195const getConstellation = async ( 196 endpoint: string, 197 target: string, 198 collection?: string, 199 path?: string, 200 cursor?: string, 201 limit?: number, 202) => { 203 const url = new URL("https://constellation.microcosm.blue"); 204 url.pathname = endpoint; 205 url.searchParams.set("target", target); 206 if (collection) { 207 if (!path) throw new Error("collection and path must either both be set or neither"); 208 url.searchParams.set("collection", collection); 209 url.searchParams.set("path", path); 210 } else { 211 if (path) throw new Error("collection and path must either both be set or neither"); 212 } 213 if (limit) url.searchParams.set("limit", `${limit}`); 214 if (cursor) url.searchParams.set("cursor", `${cursor}`); 215 const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); 216 if (!res.ok) throw new Error("failed to fetch from constellation"); 217 return await res.json(); 218}; 219 220const getAllBacklinks = (target: string) => getConstellation("/links/all", target); 221 222const getRecordBacklinks = ( 223 target: string, 224 collection: string, 225 path: string, 226 cursor?: string, 227 limit?: number, 228): Promise<LinksWithRecords> => 229 getConstellation("/links", target, collection, path, cursor, limit || 100); 230 231export interface HandleResolveResult { 232 success: boolean; 233 did?: string; 234 error?: string; 235} 236 237export const resolveHandleDetailed = async (handle: Handle) => { 238 const dnsResolver = new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }); 239 const httpResolver = new WellKnownHandleResolver({ fetch: handleHttpProxyFetch }); 240 241 const tryResolve = async ( 242 resolver: DohJsonHandleResolver | WellKnownHandleResolver, 243 timeoutMs: number = 5000, 244 ): Promise<HandleResolveResult> => { 245 try { 246 const timeoutPromise = new Promise<never>((_, reject) => 247 setTimeout(() => reject(new Error("Request timed out")), timeoutMs), 248 ); 249 const did = await Promise.race([resolver.resolve(handle), timeoutPromise]); 250 return { success: true, did }; 251 } catch (err: any) { 252 return { success: false, error: err.message ?? String(err) }; 253 } 254 }; 255 256 const [dns, http] = await Promise.all([tryResolve(dnsResolver), tryResolve(httpResolver)]); 257 258 return { dns, http }; 259}; 260 261export { 262 didDocCache, 263 getAllBacklinks, 264 getPDS, 265 getRecordBacklinks, 266 labelerCache, 267 resolveDidDoc, 268 resolveHandle, 269 resolveLexiconAuthority, 270 resolveLexiconAuthorityDirect, 271 resolveLexiconSchema, 272 validateHandle, 273 type LinkData, 274 type LinksWithRecords, 275};