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