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