import { AtpAgent } from '@atproto/api'; import { resolveHandleToDid, resolvePdsFromIdentifier } from './pds-resolver.js'; export { resolveHandleToDid }; /** * Parse an AT-URI into its components * @param uri - AT-URI string (e.g., "at://did:plc:abc/collection/rkey") * @returns Parsed components or null if invalid */ export function parseAtUri(uri: string): { did: string; collection: string; rkey?: string; } | null { // AT-URI format: at://did:method:identifier/collection[/rkey] const match = uri.match( /^at:\/\/(did:[a-z]+:[a-zA-Z0-9._:%-]+)\/([a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*)(?:\/([a-zA-Z0-9._-]+))?$/ ); if (!match) { return null; } const [, did, collection, rkey] = match; return { did, collection, ...(rkey && { rkey }), }; } /** * Resolve a repository identifier to its repo DID (the identifier used in issue records). * Resolves the owner's PDS independently of the caller's authenticated session, * so cross-PDS lookups (e.g., a tngl.sh user looking up a bsky.social repo) work. * * Issue records store `repoDid` in their `.repo` field, not the AT-URI of the * `sh.tangled.repo` record. Falls back to the record's AT-URI if `repoDid` is absent. * * @param ownerDidOrHandle - DID (e.g., "did:plc:abc") or handle (e.g., "mark.bsky.social") * @param repoName - Repository name * @returns repoDid string, or record AT-URI as fallback * @throws Error if repository not found */ export async function resolveRepoId(ownerDidOrHandle: string, repoName: string): Promise { // Resolve owner to DID const isDid = ownerDidOrHandle.startsWith('did:'); const did = isDid ? ownerDidOrHandle : await resolveHandleToDid(ownerDidOrHandle); try { // Resolve the owner's PDS and query their records directly, // bypassing the caller's authenticated agent entirely const ownerPds = await resolvePdsFromIdentifier(did); const ownerAgent = new AtpAgent({ service: ownerPds }); // Query for sh.tangled.repo records // TODO: does not paginate; will silently fail to find a repo for users with >100 repos. const response = await ownerAgent.com.atproto.repo.listRecords({ repo: did, collection: 'sh.tangled.repo', limit: 100, }); // Find the record matching the repo name. // Newer records use the repo name as the rkey; older records store the name // in value.name with an opaque tid as the rkey. Check both. const repoRecord = response.data.records.find((record) => { const rkey = record.uri.split('/').pop(); const recordName = (record.value as { name?: string }).name; return rkey === repoName || recordName === repoName; }); if (!repoRecord) { throw new Error(`Repository '${repoName}' not found for ${ownerDidOrHandle}`); } // Return repoDid if present; fall back to record URI const repoDid = (repoRecord.value as { repoDid?: string }).repoDid; return repoDid ?? repoRecord.uri; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to resolve repository AT-URI: ${error.message}`); } throw new Error('Failed to resolve repository AT-URI: Unknown error'); } }