decentralised sync engine
at main 6.9 kB view raw
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};