this repo has no description
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}