open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4const PLC_DIRECTORY = 'https://plc.directory';
5const pdsCache = new Map();
6
7async function fetchDidDocument(did) {
8 let url;
9 if (did.startsWith('did:web:')) {
10 const rest = did.slice('did:web:'.length);
11 const parts = rest.split(':');
12 const domain = decodeURIComponent(parts[0]);
13 if (parts.length === 1) {
14 url = `https://${domain}/.well-known/did.json`;
15 } else {
16 url = `https://${domain}/${parts.slice(1).map(decodeURIComponent).join('/')}/did.json`;
17 }
18 } else {
19 url = `${PLC_DIRECTORY}/${did}`;
20 }
21
22 const res = await fetch(url);
23 if (!res.ok) throw new Error(`failed to resolve DID document for ${did}: ${res.status} ${res.statusText}`);
24 return res.json();
25}
26
27export async function resolvePds(did) {
28 if (pdsCache.has(did)) return pdsCache.get(did);
29 const doc = await fetchDidDocument(did);
30 const pds = doc.service?.find(s => s.type === 'AtprotoPersonalDataServer');
31 if (!pds?.serviceEndpoint) throw new Error(`no PDS found in DID document for ${did}`);
32 pdsCache.set(did, pds.serviceEndpoint);
33 return pds.serviceEndpoint;
34}
35
36export async function listRecordsFromPds(pdsUrl, repo, collection, limit) {
37 const records = [];
38 let cursor;
39 do {
40 const url = new URL('/xrpc/com.atproto.repo.listRecords', pdsUrl);
41 url.searchParams.set('repo', repo);
42 url.searchParams.set('collection', collection);
43 if (limit) url.searchParams.set('limit', String(limit));
44 if (cursor) url.searchParams.set('cursor', cursor);
45 const res = await fetch(url);
46 if (!res.ok) throw new Error(`listRecords failed for ${repo}: ${res.status} ${res.statusText}`);
47 const data = await res.json();
48 records.push(...data.records);
49 cursor = data.cursor;
50 } while (cursor);
51 return { records };
52}
53
54export async function resolveHandleFromDid(did) {
55 try {
56 const doc = await fetchDidDocument(did);
57 const aka = doc.alsoKnownAs?.find(a => a.startsWith('at://'));
58 return aka ? aka.replace('at://', '') : did;
59 } catch {
60 return did;
61 }
62}
63
64export async function resolveHandle(handle) {
65 const url = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`;
66 const res = await fetch(url);
67 if (!res.ok) throw new Error(`could not resolve handle: ${handle}`);
68 const data = await res.json();
69 return data.did;
70}
71
72export async function batchQuery(items, fn, { batchSize = 10, verbose = false } = {}) {
73 if (verbose) console.log(`[verbose] querying ${items.length} accounts in batches of ${batchSize}`);
74 const results = [];
75 for (let i = 0; i < items.length; i += batchSize) {
76 const chunk = items.slice(i, i + batchSize);
77 const settled = await Promise.allSettled(chunk.map(fn));
78 for (let j = 0; j < settled.length; j++) {
79 if (settled[j].status === 'fulfilled' && settled[j].value !== undefined) {
80 results.push(settled[j].value);
81 } else if (settled[j].status === 'rejected' && verbose) {
82 console.log(`[verbose] ${chunk[j]}: error: ${settled[j].reason?.message || settled[j].reason}`);
83 }
84 }
85 }
86 return results;
87}