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 { Did, Handle } from "@atcute/lexicons";
17import { isHandle } from "@atcute/lexicons/syntax";
18import { createStore } from "solid-js/store";
19import { setPDS } from "../components/navbar";
20
21const didDocumentResolver = new CompositeDidDocumentResolver({
22 methods: {
23 plc: new PlcDidDocumentResolver({
24 apiUrl: localStorage.plcDirectory ?? "https://plc.directory",
25 }),
26 web: new AtprotoWebDidDocumentResolver(),
27 },
28});
29
30const handleResolver = new CompositeHandleResolver({
31 strategy: "dns-first",
32 methods: {
33 dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }),
34 http: new WellKnownHandleResolver(),
35 },
36});
37
38const didPDSCache: Record<string, string> = {};
39const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({});
40const didDocCache: Record<string, DidDocument> = {};
41const getPDS = async (did: string) => {
42 if (did in didPDSCache) return didPDSCache[did];
43
44 if (!isAtprotoDid(did)) {
45 throw new Error("Not a valid DID identifier");
46 }
47
48 const doc = await didDocumentResolver.resolve(did);
49 didDocCache[did] = doc;
50
51 const pds = getPdsEndpoint(doc);
52 const labeler = getLabelerEndpoint(doc);
53
54 if (labeler) {
55 setLabelerCache(did, labeler);
56 }
57
58 if (!pds) {
59 throw new Error("No PDS found");
60 }
61
62 return (didPDSCache[did] = pds);
63};
64
65const resolveHandle = async (handle: Handle) => {
66 if (!isHandle(handle)) {
67 throw new Error("Not a valid handle");
68 }
69
70 return await handleResolver.resolve(handle);
71};
72
73const resolveDidDoc = async (did: Did) => {
74 if (!isAtprotoDid(did)) {
75 throw new Error("Not a valid DID identifier");
76 }
77 return await didDocumentResolver.resolve(did);
78};
79
80const validateHandle = async (handle: Handle, did: Did) => {
81 if (!isHandle(handle)) return false;
82
83 let resolvedDid: string;
84 try {
85 resolvedDid = await handleResolver.resolve(handle);
86 } catch (err) {
87 console.error(err);
88 return false;
89 }
90 if (resolvedDid !== did) return false;
91 return true;
92};
93
94const resolvePDS = async (did: string) => {
95 setPDS(undefined);
96 const pds = await getPDS(did);
97 if (!pds) throw new Error("No PDS found");
98 setPDS(pds.replace("https://", "").replace("http://", ""));
99 return pds;
100};
101
102interface LinkData {
103 links: {
104 [key: string]: {
105 [key: string]: {
106 records: number;
107 distinct_dids: number;
108 };
109 };
110 };
111}
112
113const getConstellation = async (
114 endpoint: string,
115 target: string,
116 collection?: string,
117 path?: string,
118 cursor?: string,
119 limit?: number,
120) => {
121 const url = new URL("https://constellation.microcosm.blue");
122 url.pathname = endpoint;
123 url.searchParams.set("target", target);
124 if (collection) {
125 if (!path) throw new Error("collection and path must either both be set or neither");
126 url.searchParams.set("collection", collection);
127 url.searchParams.set("path", path);
128 } else {
129 if (path) throw new Error("collection and path must either both be set or neither");
130 }
131 if (limit) url.searchParams.set("limit", `${limit}`);
132 if (cursor) url.searchParams.set("cursor", `${cursor}`);
133 const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
134 if (!res.ok) throw new Error("failed to fetch from constellation");
135 return await res.json();
136};
137
138const getAllBacklinks = (target: string) => getConstellation("/links/all", target);
139
140const getRecordBacklinks = (
141 target: string,
142 collection: string,
143 path: string,
144 cursor?: string,
145 limit?: number,
146) => getConstellation("/links", target, collection, path, cursor, limit || 100);
147
148const getDidBacklinks = (
149 target: string,
150 collection: string,
151 path: string,
152 cursor?: string,
153 limit?: number,
154) => getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100);
155
156export {
157 didDocCache,
158 getAllBacklinks,
159 getDidBacklinks,
160 getPDS,
161 getRecordBacklinks,
162 labelerCache,
163 resolveDidDoc,
164 resolveHandle,
165 resolvePDS,
166 validateHandle,
167 type LinkData,
168};