export const DEFAULT_PDS_URL = "https://bsky.social"; interface DidDocument { service?: Array<{ id: string; type: string; serviceEndpoint: string }>; } /** * Normalize a user supplied identifier (strips a leading "@", trims, lowercases) */ function normalizeIdentifier(identifier: string): string { return identifier.trim().replace(/^@/, "").toLowerCase(); } /** * Check whether an identifier can be resolved through the identity system. * App passwords also accept an email address, which has no DID document. */ export function isResolvableIdentifier(identifier: string): boolean { const normalized = normalizeIdentifier(identifier); if (normalized.startsWith("did:")) return true; // Handles are domain names, emails are not return normalized.includes(".") && !normalized.includes("@"); } /** * Find the PDS service endpoint in a DID document */ function getPdsEndpoint(didDoc: DidDocument): string | undefined { const pdsService = didDoc.service?.find( (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer", ); return pdsService?.serviceEndpoint; } /** * Resolve a handle to a DID */ export async function resolveHandleToDid(handle: string): Promise { const normalized = normalizeIdentifier(handle); if (normalized.startsWith("did:")) { return normalized; } // Try to resolve handle via Bluesky API const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(normalized)}`; try { const resolveResponse = await fetch(resolveUrl); if (resolveResponse.ok) { const resolveData = (await resolveResponse.json()) as { did: string }; if (resolveData.did) { return resolveData.did; } } } catch { // Fall through to the well-known lookup below } // Fall back to the handle's own domain, which self-hosted handles may serve // even when they are unknown to the Bluesky appview try { const wellKnownResponse = await fetch( `https://${normalized}/.well-known/atproto-did`, ); if (wellKnownResponse.ok) { const did = (await wellKnownResponse.text()).trim(); if (did.startsWith("did:")) { return did; } } } catch { // Fall through to the error below } throw new Error("Could not resolve handle"); } /** * Resolve a DID to the PDS URL listed in its DID document */ export async function resolveDidToPDS(did: string): Promise { let pdsUrl: string | undefined; if (did.startsWith("did:plc:")) { // Fetch DID document from plc.directory const didDocUrl = `https://plc.directory/${did}`; const didDocResponse = await fetch(didDocUrl); if (!didDocResponse.ok) { throw new Error("Could not fetch DID document"); } pdsUrl = getPdsEndpoint((await didDocResponse.json()) as DidDocument); } else if (did.startsWith("did:web:")) { // For did:web, fetch the DID document from the domain const domain = did.replace("did:web:", ""); const didDocUrl = `https://${domain}/.well-known/did.json`; const didDocResponse = await fetch(didDocUrl); if (!didDocResponse.ok) { throw new Error("Could not fetch DID document"); } pdsUrl = getPdsEndpoint((await didDocResponse.json()) as DidDocument); } if (!pdsUrl) { throw new Error("Could not find PDS URL for user"); } return pdsUrl; } export async function resolveHandleToPDS(handle: string): Promise { // First, resolve the handle to a DID const did = await resolveHandleToDid(handle); // Now resolve the DID to get the PDS URL from the DID document return resolveDidToPDS(did); } /** * Resolve the PDS for an identifier, falling back to the default PDS when the * identifier can't be resolved (e.g. an email address, or a network failure). */ export async function resolvePdsUrlOrDefault( identifier: string, ): Promise { if (!isResolvableIdentifier(identifier)) { console.warn( `Could not resolve a PDS from "${identifier}", falling back to ${DEFAULT_PDS_URL}. Set PDS_URL if you use another PDS.`, ); return DEFAULT_PDS_URL; } try { return await resolveHandleToPDS(identifier); } catch (error) { console.warn( `Could not resolve a PDS for "${identifier}" (${error instanceof Error ? error.message : error}), falling back to ${DEFAULT_PDS_URL}. Set PDS_URL if you use another PDS.`, ); return DEFAULT_PDS_URL; } }