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 { setPDS } from "../components/navbar";
22import { plcDirectory } from "../views/settings";
23
24export const didDocumentResolver = createMemo(
25 () =>
26 new CompositeDidDocumentResolver({
27 methods: {
28 plc: new PlcDidDocumentResolver({
29 apiUrl: plcDirectory(),
30 }),
31 web: new AtprotoWebDidDocumentResolver(),
32 },
33 }),
34);
35
36export const handleResolver = new CompositeHandleResolver({
37 strategy: "dns-first",
38 methods: {
39 dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }),
40 http: new WellKnownHandleResolver(),
41 },
42});
43
44const authorityResolver = new DohJsonLexiconAuthorityResolver({
45 dohUrl: "https://dns.google/resolve?",
46});
47
48const schemaResolver = createMemo(
49 () =>
50 new LexiconSchemaResolver({
51 didDocumentResolver: didDocumentResolver(),
52 }),
53);
54
55const didPDSCache: Record<string, string> = {};
56const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({});
57const didDocCache: Record<string, DidDocument> = {};
58const getPDS = async (did: string) => {
59 if (did in didPDSCache) return didPDSCache[did];
60
61 if (!isAtprotoDid(did)) {
62 throw new Error("Not a valid DID identifier");
63 }
64
65 let doc: DidDocument;
66 try {
67 doc = await didDocumentResolver().resolve(did);
68 didDocCache[did] = doc;
69 } catch (e) {
70 console.error(e);
71 throw new Error("Error during did document resolution");
72 }
73
74 const pds = getPdsEndpoint(doc);
75 const labeler = getLabelerEndpoint(doc);
76
77 if (labeler) {
78 setLabelerCache(did, labeler);
79 }
80
81 if (!pds) {
82 throw new Error("No PDS found");
83 }
84
85 return (didPDSCache[did] = pds);
86};
87
88const resolveHandle = async (handle: Handle) => {
89 if (!isHandle(handle)) {
90 throw new Error("Not a valid handle");
91 }
92
93 return await handleResolver.resolve(handle);
94};
95
96const resolveDidDoc = async (did: Did) => {
97 if (!isAtprotoDid(did)) {
98 throw new Error("Not a valid DID identifier");
99 }
100 return await didDocumentResolver().resolve(did);
101};
102
103const validateHandle = async (handle: Handle, did: Did) => {
104 if (!isHandle(handle)) return false;
105
106 let resolvedDid: string;
107 try {
108 resolvedDid = await handleResolver.resolve(handle);
109 } catch (err) {
110 console.error(err);
111 return false;
112 }
113 if (resolvedDid !== did) return false;
114 return true;
115};
116
117const resolvePDS = async (did: string) => {
118 try {
119 setPDS(undefined);
120 const pds = await getPDS(did);
121 if (!pds) throw new Error("No PDS found");
122 setPDS(pds.replace("https://", "").replace("http://", ""));
123 return pds;
124 } catch (err) {
125 setPDS("Missing PDS");
126 throw err;
127 }
128};
129
130const resolveLexiconAuthority = async (nsid: Nsid) => {
131 return await authorityResolver.resolve(nsid);
132};
133
134const resolveLexiconAuthorityDirect = async (authority: string) => {
135 const dohUrl = "https://dns.google/resolve?";
136 const reversedAuthority = authority.split(".").reverse().join(".");
137 const domain = `_lexicon.${reversedAuthority}`;
138 const url = new URL(dohUrl);
139 url.searchParams.set("name", domain);
140 url.searchParams.set("type", "TXT");
141
142 const response = await fetch(url.toString());
143 if (!response.ok) {
144 throw new Error(`Failed to resolve lexicon authority for ${authority}`);
145 }
146
147 const data = await response.json();
148 if (!data.Answer || data.Answer.length === 0) {
149 throw new Error(`No lexicon authority found for ${authority}`);
150 }
151
152 const txtRecord = data.Answer[0].data.replace(/"/g, "");
153
154 if (!txtRecord.startsWith("did=")) {
155 throw new Error(`Invalid lexicon authority record for ${authority}`);
156 }
157
158 return txtRecord.replace("did=", "");
159};
160
161const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => {
162 return await schemaResolver().resolve(authority, nsid);
163};
164
165interface LinkData {
166 links: {
167 [key: string]: {
168 [key: string]: {
169 records: number;
170 distinct_dids: number;
171 };
172 };
173 };
174}
175
176type LinksWithRecords = {
177 cursor: string;
178 total: number;
179 linking_records: Array<{ did: string; collection: string; rkey: string }>;
180};
181
182const getConstellation = async (
183 endpoint: string,
184 target: string,
185 collection?: string,
186 path?: string,
187 cursor?: string,
188 limit?: number,
189) => {
190 const url = new URL("https://constellation.microcosm.blue");
191 url.pathname = endpoint;
192 url.searchParams.set("target", target);
193 if (collection) {
194 if (!path) throw new Error("collection and path must either both be set or neither");
195 url.searchParams.set("collection", collection);
196 url.searchParams.set("path", path);
197 } else {
198 if (path) throw new Error("collection and path must either both be set or neither");
199 }
200 if (limit) url.searchParams.set("limit", `${limit}`);
201 if (cursor) url.searchParams.set("cursor", `${cursor}`);
202 const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
203 if (!res.ok) throw new Error("failed to fetch from constellation");
204 return await res.json();
205};
206
207const getAllBacklinks = (target: string) => getConstellation("/links/all", target);
208
209const getRecordBacklinks = (
210 target: string,
211 collection: string,
212 path: string,
213 cursor?: string,
214 limit?: number,
215): Promise<LinksWithRecords> =>
216 getConstellation("/links", target, collection, path, cursor, limit || 100);
217
218export {
219 didDocCache,
220 getAllBacklinks,
221 getPDS,
222 getRecordBacklinks,
223 labelerCache,
224 resolveDidDoc,
225 resolveHandle,
226 resolveLexiconAuthority,
227 resolveLexiconAuthorityDirect,
228 resolveLexiconSchema,
229 resolvePDS,
230 validateHandle,
231 type LinkData,
232 type LinksWithRecords,
233};