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