decentralised message store
at main 5.8 kB view raw
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};