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 { createStore } from "solid-js/store";
20import { setPDS } from "../components/navbar";
21
22export const didDocumentResolver = new CompositeDidDocumentResolver({
23 methods: {
24 plc: new PlcDidDocumentResolver({
25 apiUrl: localStorage.getItem("plcDirectory") ?? "https://plc.directory",
26 }),
27 web: new AtprotoWebDidDocumentResolver(),
28 },
29});
30
31export const 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 schemaResolver = new LexiconSchemaResolver({
44 didDocumentResolver: didDocumentResolver,
45});
46
47const didPDSCache: Record<string, string> = {};
48const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({});
49const didDocCache: Record<string, DidDocument> = {};
50const getPDS = async (did: string) => {
51 if (did in didPDSCache) return didPDSCache[did];
52
53 if (!isAtprotoDid(did)) {
54 throw new Error("Not a valid DID identifier");
55 }
56
57 const doc = await didDocumentResolver.resolve(did);
58 didDocCache[did] = doc;
59
60 const pds = getPdsEndpoint(doc);
61 const labeler = getLabelerEndpoint(doc);
62
63 if (labeler) {
64 setLabelerCache(did, labeler);
65 }
66
67 if (!pds) {
68 throw new Error("No PDS found");
69 }
70
71 return (didPDSCache[did] = pds);
72};
73
74const resolveHandle = async (handle: Handle) => {
75 if (!isHandle(handle)) {
76 throw new Error("Not a valid handle");
77 }
78
79 return await handleResolver.resolve(handle);
80};
81
82const resolveDidDoc = async (did: Did) => {
83 if (!isAtprotoDid(did)) {
84 throw new Error("Not a valid DID identifier");
85 }
86 return await didDocumentResolver.resolve(did);
87};
88
89const validateHandle = async (handle: Handle, did: Did) => {
90 if (!isHandle(handle)) return false;
91
92 let resolvedDid: string;
93 try {
94 resolvedDid = await handleResolver.resolve(handle);
95 } catch (err) {
96 console.error(err);
97 return false;
98 }
99 if (resolvedDid !== did) return false;
100 return true;
101};
102
103const resolvePDS = async (did: string) => {
104 setPDS(undefined);
105 const pds = await getPDS(did);
106 if (!pds) throw new Error("No PDS found");
107 setPDS(pds.replace("https://", "").replace("http://", ""));
108 return pds;
109};
110
111const resolveLexiconAuthority = async (nsid: Nsid) => {
112 return await authorityResolver.resolve(nsid);
113};
114
115const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => {
116 return await schemaResolver.resolve(authority, nsid);
117};
118
119interface LinkData {
120 links: {
121 [key: string]: {
122 [key: string]: {
123 records: number;
124 distinct_dids: number;
125 };
126 };
127 };
128}
129
130type LinksWithRecords = {
131 cursor: string;
132 total: number;
133 linking_records: Array<{ did: string; collection: string; rkey: string }>;
134};
135
136type LinksWithDids = {
137 cursor: string;
138 total: number;
139 linking_dids: Array<string>;
140};
141
142const getConstellation = async (
143 endpoint: string,
144 target: string,
145 collection?: string,
146 path?: string,
147 cursor?: string,
148 limit?: number,
149) => {
150 const url = new URL("https://constellation.microcosm.blue");
151 url.pathname = endpoint;
152 url.searchParams.set("target", target);
153 if (collection) {
154 if (!path) throw new Error("collection and path must either both be set or neither");
155 url.searchParams.set("collection", collection);
156 url.searchParams.set("path", path);
157 } else {
158 if (path) throw new Error("collection and path must either both be set or neither");
159 }
160 if (limit) url.searchParams.set("limit", `${limit}`);
161 if (cursor) url.searchParams.set("cursor", `${cursor}`);
162 const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
163 if (!res.ok) throw new Error("failed to fetch from constellation");
164 return await res.json();
165};
166
167const getAllBacklinks = (target: string) => getConstellation("/links/all", target);
168
169const getRecordBacklinks = (
170 target: string,
171 collection: string,
172 path: string,
173 cursor?: string,
174 limit?: number,
175): Promise<LinksWithRecords> =>
176 getConstellation("/links", target, collection, path, cursor, limit || 100);
177
178const getDidBacklinks = (
179 target: string,
180 collection: string,
181 path: string,
182 cursor?: string,
183 limit?: number,
184): Promise<LinksWithDids> =>
185 getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100);
186
187export {
188 didDocCache,
189 getAllBacklinks,
190 getDidBacklinks,
191 getPDS,
192 getRecordBacklinks,
193 labelerCache,
194 resolveDidDoc,
195 resolveHandle,
196 resolveLexiconAuthority,
197 resolveLexiconSchema,
198 resolvePDS,
199 validateHandle,
200 type LinkData,
201 type LinksWithDids,
202 type LinksWithRecords,
203};