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};