open source is social v-it.org
at main 87 lines 3.2 kB view raw
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}