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
122const getConstellation = async (
123 endpoint: string,
124 target: string,
125 collection?: string,
126 path?: string,
127 cursor?: string,
128 limit?: number,
129) => {
130 const url = new URL("https://constellation.microcosm.blue");
131 url.pathname = endpoint;
132 url.searchParams.set("target", target);
133 if (collection) {
134 if (!path) throw new Error("collection and path must either both be set or neither");
135 url.searchParams.set("collection", collection);
136 url.searchParams.set("path", path);
137 } else {
138 if (path) throw new Error("collection and path must either both be set or neither");
139 }
140 if (limit) url.searchParams.set("limit", `${limit}`);
141 if (cursor) url.searchParams.set("cursor", `${cursor}`);
142 const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
143 if (!res.ok) throw new Error("failed to fetch from constellation");
144 return await res.json();
145};
146
147const getAllBacklinks = (target: string) => getConstellation("/links/all", target);
148
149const getRecordBacklinks = (
150 target: string,
151 collection: string,
152 path: string,
153 cursor?: string,
154 limit?: number,
155) => getConstellation("/links", target, collection, path, cursor, limit || 100);
156
157const getDidBacklinks = (
158 target: string,
159 collection: string,
160 path: string,
161 cursor?: string,
162 limit?: number,
163) => getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100);
164
165export {
166 didDocCache,
167 getAllBacklinks,
168 getDidBacklinks,
169 getPDS,
170 getRecordBacklinks,
171 labelerCache,
172 resolveDidDoc,
173 resolveHandle,
174 resolveLexiconAuthority,
175 resolvePDS,
176 validateHandle,
177 type LinkData,
178};