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