this repo has no description
at design-docs 136 lines 3.4 kB view raw
1import type { Document, Publication } from "./types.ts"; 2 3const DOCUMENT_COLLECTION = "site.standard.document"; 4const PUBLICATION_COLLECTION = "site.standard.publication"; 5 6interface AtUri { 7 did: string; 8 collection: string; 9 rkey: string; 10} 11 12/** 13 * Parse an AT URI into its components 14 * Format: at://did:plc:xyz/collection/rkey 15 */ 16export function parseAtUri(uri: string): AtUri { 17 const match = uri.match(/^at:\/\/(did:[^/]+)\/([^/]+)\/([^/]+)$/); 18 if (!match) { 19 throw new Error(`Invalid AT URI: ${uri}`); 20 } 21 return { 22 did: match[1] as string, 23 collection: match[2] as string, 24 rkey: match[3] as string, 25 }; 26} 27 28/** 29 * Get the PDS endpoint for a DID by resolving through plc.directory 30 */ 31export async function getPdsEndpoint(did: string): Promise<string> { 32 const response = await fetch(`https://plc.directory/${did}`); 33 if (!response.ok) { 34 throw new Error(`Failed to resolve DID ${did}: ${response.status}`); 35 } 36 37 const doc = (await response.json()) as { 38 service?: Array<{ id: string; serviceEndpoint: string }>; 39 }; 40 const pdsService = doc.service?.find((s) => s.id === "#atproto_pds"); 41 42 if (!pdsService) { 43 throw new Error(`No PDS service found for DID ${did}`); 44 } 45 46 return pdsService.serviceEndpoint; 47} 48 49/** 50 * Fetch a publication record from a PDS 51 */ 52export async function fetchPublication( 53 pdsEndpoint: string, 54 did: string, 55 rkey: string, 56): Promise<Publication> { 57 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${PUBLICATION_COLLECTION}&rkey=${encodeURIComponent(rkey)}`; 58 59 const response = await fetch(url); 60 if (!response.ok) { 61 throw new Error(`Failed to fetch publication: ${response.status}`); 62 } 63 64 const data = (await response.json()) as { value: Publication }; 65 return data.value; 66} 67 68/** 69 * Fetch all documents for a publication from a PDS 70 */ 71export async function fetchDocuments( 72 pdsEndpoint: string, 73 did: string, 74 publicationUri: string, 75): Promise<Document[]> { 76 const documents: Document[] = []; 77 let cursor: string | undefined; 78 79 do { 80 const params = new URLSearchParams({ 81 repo: did, 82 collection: DOCUMENT_COLLECTION, 83 limit: "100", 84 }); 85 if (cursor) { 86 params.set("cursor", cursor); 87 } 88 89 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?${params}`; 90 const response = await fetch(url); 91 92 if (!response.ok) { 93 throw new Error(`Failed to fetch documents: ${response.status}`); 94 } 95 96 const data = (await response.json()) as { 97 records: Array<{ value: Document }>; 98 cursor?: string; 99 }; 100 101 // Filter to only documents belonging to this publication 102 for (const record of data.records) { 103 if (record.value.site === publicationUri) { 104 documents.push(record.value); 105 } 106 } 107 108 cursor = data.cursor; 109 } while (cursor); 110 111 return documents; 112} 113 114/** 115 * Fetch a publication and its documents given a publication AT URI 116 */ 117export async function fetchPublicationWithDocuments( 118 publicationUri: string, 119): Promise<{ 120 publication: Publication; 121 documents: Document[]; 122}> { 123 const { did, collection, rkey } = parseAtUri(publicationUri); 124 125 if (collection !== PUBLICATION_COLLECTION) { 126 throw new Error( 127 `Expected ${PUBLICATION_COLLECTION} collection, got ${collection}`, 128 ); 129 } 130 131 const pdsEndpoint = await getPdsEndpoint(did); 132 const publication = await fetchPublication(pdsEndpoint, did, rkey); 133 const documents = await fetchDocuments(pdsEndpoint, did, publicationUri); 134 135 return { publication, documents }; 136}