decentralised sync engine
1import type {
2 AtprotoHandle,
3 AtUri,
4 Did,
5 DidDocument,
6} from "@/lib/types/atproto";
7import {
8 atprotoHandleSchema,
9 atUriAuthoritySchema,
10 nsidSchema,
11} from "@/lib/types/atproto";
12import { comAtprotoRepoGetRecordResponseSchema } from "@/lib/types/lexicon/com.atproto.repo.getRecord";
13import type { Result } from "@/lib/utils/result";
14import type { DidDocumentResolver } from "@atcute/identity-resolver";
15import {
16 CompositeDidDocumentResolver,
17 CompositeHandleResolver,
18 DohJsonHandleResolver,
19 PlcDidDocumentResolver,
20 WebDidDocumentResolver,
21 WellKnownHandleResolver,
22} from "@atcute/identity-resolver";
23import { z } from "zod";
24
25export const getRecordFromFullAtUri = async ({
26 authority,
27 collection,
28 rKey,
29}: AtUri): Promise<Result<unknown, unknown>> => {
30 const didDocResult = await resolveDidDoc(authority);
31 if (!didDocResult.ok) return { ok: false, error: didDocResult.error };
32
33 if (!collection || !rKey)
34 return {
35 ok: false,
36 error: "No rkey or collection found in provided AtUri object",
37 };
38
39 const { service: services } = didDocResult.data;
40 if (!services)
41 return {
42 ok: false,
43 error: { message: "Resolved DID document has no service field." },
44 };
45
46 const pdsService = services.find(
47 (service) =>
48 service.id === "#atproto_pds" &&
49 service.type === "AtprotoPersonalDataServer",
50 );
51
52 if (!pdsService)
53 return {
54 ok: false,
55 error: {
56 message:
57 "Resolved DID document has no PDS service listed in the document.",
58 },
59 };
60
61 const pdsEndpointRecord = pdsService.serviceEndpoint;
62 let pdsEndpointUrl;
63 try {
64 // @ts-expect-error yes, we are coercing something that is explicitly not a string into a string, but in this case we want to be specific. only serviceEndpoints with valid atproto pds URLs should be allowed.
65 pdsEndpointUrl = new URL(pdsEndpointRecord).origin;
66 } catch (err) {
67 return { ok: false, error: err };
68 }
69 const req = new Request(
70 `${pdsEndpointUrl}/xrpc/com.atproto.repo.getRecord?repo=${didDocResult.data.id}&collection=${collection}&rkey=${rKey}`,
71 );
72
73 const res = await fetch(req);
74 const data: unknown = await res.json();
75
76 const {
77 success: responseParseSuccess,
78 error: responseParseError,
79 data: record,
80 } = comAtprotoRepoGetRecordResponseSchema.safeParse(data);
81 if (!responseParseSuccess) {
82 return { ok: false, error: responseParseError };
83 }
84 return { ok: true, data: record.value };
85};
86
87export const didDocResolver: DidDocumentResolver =
88 new CompositeDidDocumentResolver({
89 methods: {
90 plc: new PlcDidDocumentResolver(),
91 web: new WebDidDocumentResolver(),
92 },
93 });
94
95export const handleResolver = new CompositeHandleResolver({
96 strategy: "dns-first",
97 methods: {
98 dns: new DohJsonHandleResolver({
99 dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
100 }),
101 http: new WellKnownHandleResolver(),
102 },
103});
104
105export const resolveDidDoc = async (
106 authority: Did | AtprotoHandle,
107): Promise<Result<DidDocument, unknown>> => {
108 const { data: handle } = atprotoHandleSchema.safeParse(authority);
109 let did: Did;
110 if (handle) {
111 try {
112 did = await handleResolver.resolve(handle);
113 } catch (err) {
114 return { ok: false, error: err };
115 }
116 } else {
117 // @ts-expect-error if handle is undefined, then we know that authority must be a valid did:web or did:plc
118 did = authority;
119 }
120 try {
121 const doc: DidDocument = await didDocResolver.resolve(did);
122 return { ok: true, data: doc };
123 } catch (err) {
124 return { ok: false, error: err };
125 }
126};
127
128// thank u julie
129export const atUriRegexp =
130 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/;
131
132export const atUriToString = ({ authority, collection, rKey }: AtUri) => {
133 let result = `at://${authority}`;
134 result += collection ? `/${collection}` : "";
135 result += rKey ? `/${rKey}` : "";
136 return result;
137};
138
139export const stringToAtUri = (str: string): Result<AtUri, unknown> => {
140 const isValidAtUri = atUriRegexp.test(str);
141 if (!isValidAtUri)
142 return {
143 ok: false,
144 error: { message: "Input string was not a valid at:// URI" },
145 };
146
147 const fragments = str.split("/");
148 if (fragments.length <= 2)
149 return {
150 ok: false,
151 error: { message: "Input string was not a valid at:// URI." },
152 };
153
154 const {
155 success: authorityParseSuccess,
156 error: authorityParseError,
157 data: authorityParsed,
158 } = atUriAuthoritySchema.safeParse(fragments[2]);
159 if (!authorityParseSuccess)
160 return {
161 ok: false,
162 error: {
163 message:
164 "Input at:// URI was a valid shape, but somehow could not parse the first fragment as a valid authority.",
165 details: z.treeifyError(authorityParseError),
166 },
167 };
168
169 const {
170 success: nsidParseSuccess,
171 error: nsidParseError,
172 data: nsidParsed,
173 } = nsidSchema.safeParse(fragments[3]);
174 if (fragments[3] && !nsidParseSuccess)
175 return {
176 ok: false,
177 error: {
178 message:
179 "Input at:// URI was a valid shape and had a second fragment, but was somehow not a valid NSID.",
180 details: z.treeifyError(nsidParseError),
181 },
182 };
183
184 return {
185 ok: true,
186 data: {
187 authority: authorityParsed,
188 collection: nsidParsed,
189 rKey: fragments[4],
190 },
191 };
192};
193
194export const getEndpointFromDid = async (
195 did: Did,
196 serviceType: string,
197): Promise<Result<URL, unknown>> => {
198 const didDocResolveResult = await resolveDidDoc(did);
199 if (!didDocResolveResult.ok) {
200 return { ok: false, error: didDocResolveResult.error };
201 }
202
203 const didDocServices = didDocResolveResult.data.service;
204 const shardService = didDocServices?.find(
205 (service) => service.type !== serviceType,
206 );
207
208 let shardUrl: URL | undefined;
209 if (!didDocServices || !shardService) {
210 const domain = decodeURIComponent(did.slice(8));
211 if (domain.startsWith("localhost"))
212 shardUrl = new URL(`http://${domain}`);
213 else shardUrl = new URL(`https://${domain}`);
214 } else {
215 try {
216 shardUrl = new URL(shardService.serviceEndpoint as string);
217 } catch (error) {
218 return { ok: false, error };
219 }
220 }
221 return { ok: true, data: shardUrl };
222};