const DNS_ENDPOINT = "https://cloudflare-dns.com/dns-query"; const logResolve = createLogger("resolve"); /** * Resolves an AT Protocol handle to its DID, DID document, and PDS URL. * Uses DNS-over-HTTPS first, falls back to `.well-known/atproto-did.json`. * @param {string} handle - The user's AT Protocol handle (e.g. "user.bsky.social") * @returns {Promise<{did: string, pdsUrl: string, doc: Object}>} */ async function resolvePDS(handle) { const done = logResolve.timer("resolvePDS"); const did = await resolveDID(handle); const doc = await resolveDIDDocument(did); const pdsUrl = extractPdsUrl(doc, did); done(`resolved ${handle} -> ${did}`); return { did, pdsUrl, doc }; } /** * Resolves a handle to a DID string via DNS or .well-known. * @param {string} handle - The handle to resolve * @returns {Promise} The resolved DID */ async function resolveDID(handle) { const cleaned = handle.replace(/^https?:\/\//, "").replace(/\/$/, ""); const fromDns = await resolveDIDviaDNS(cleaned); if (fromDns) { logResolve.debug("resolveDID via DNS", { handle: cleaned }); return fromDns; } const fromWellKnown = await resolveDIDviaWellKnown(cleaned); if (fromWellKnown) { logResolve.debug("resolveDID via well-known", { handle: cleaned }); return fromWellKnown; } logResolve.error("resolveDID failed", { handle }); throw new Error(`Could not resolve handle: ${handle}`); } /** * Looks up a DID via DNS TXT record at _atproto. using Cloudflare DoH. * @param {string} handle - The handle to look up * @returns {Promise} The DID if found, or null */ async function resolveDIDviaDNS(handle) { try { const url = `${DNS_ENDPOINT}?name=_atproto.${encodeURIComponent(handle)}&type=TXT`; logResolve.debug("resolveDIDviaDNS", { handle }); const resp = await fetch(url, { headers: { Accept: "application/dns-json" }, }); if (!resp.ok) { logResolve.debug("DNS query not ok", { status: resp.status }); return null; } const data = await resp.json(); if (!data.Answer) { logResolve.debug("No DNS answer records"); return null; } for (const record of data.Answer) { if (record.type === 16 && record.data) { const txt = record.data.replace(/^"|"$/g, "").trim(); const match = txt.match(/^did=(did:.+)$/); if (match) return match[1]; } } logResolve.debug("No DID record in DNS answer"); return null; } catch (err) { logResolve.debug("DNS resolution failed", { error: err.message }); return null; } } /** * Fetches a DID from the handle's .well-known/atproto-did.json endpoint. * @param {string} handle - The handle to check * @returns {Promise} The DID if found, or null */ async function resolveDIDviaWellKnown(handle) { try { const url = `https://${handle}/.well-known/atproto-did.json`; logResolve.debug("resolveDIDviaWellKnown", { handle }); const resp = await fetch(url); if (!resp.ok) { logResolve.debug("Well-known not ok", { status: resp.status }); return null; } const data = await resp.json(); if (data.did) return data.did; logResolve.debug("No DID in well-known response"); return null; } catch (err) { logResolve.debug("Well-known fetch failed", { error: err.message }); return null; } } /** * Fetches a DID document for a given DID (supports did:plc and did:web). * @param {string} did - The DID to resolve (e.g. "did:plc:abc123" or "did:web:example.com") * @returns {Promise} The DID document */ async function resolveDIDDocument(did) { logResolve.debug("resolveDIDDocument", { did }); if (did.startsWith("did:plc:")) { return resolvePLCDID(did); } if (did.startsWith("did:web:")) { return resolveWebDID(did); } logResolve.error("Unsupported DID method", { did }); throw new Error(`Unsupported DID method: ${did}`); } /** * Fetches a DID document from plc.directory for a did:plc identifier. * @param {string} did - The did:plc identifier * @returns {Promise} The PLC DID document */ async function resolvePLCDID(did) { const url = `https://plc.directory/${encodeURIComponent(did)}`; logResolve.debug("resolvePLCDID", { did }); const done = logResolve.timer("PLC directory fetch"); const resp = await fetch(url); done(`PLC directory responded ${resp.status}`); if (!resp.ok) { throw new Error(`PLC directory returned HTTP ${resp.status} for ${did}`); } return resp.json(); } /** * Fetches a DID document from the domain's .well-known/did.json for a did:web identifier. * @param {string} did - The did:web identifier (e.g. "did:web:example.com") * @returns {Promise} The Web DID document */ async function resolveWebDID(did) { const domain = did.replace(/^did:web:/, "").replace(/:/g, "/"); const url = `https://${domain}/.well-known/did.json`; logResolve.debug("resolveWebDID", { did, domain }); const resp = await fetch(url); if (!resp.ok) { throw new Error(`Web DID document not found at ${url}`); } return resp.json(); } /** * Extracts the PDS URL from a DID document's service entries. * Looks for a service with type "AtprotoPersonalDataServer". * @param {Object} doc - The DID document * @param {string} did - The DID (used for error messages) * @returns {string} The PDS base URL */ function extractPdsUrl(doc, did) { if (!doc || !doc.service) { logResolve.error("No services in DID document", { did }); throw new Error(`No services in DID document for ${did}`); } for (const svc of doc.service) { if (svc.type === "AtprotoPersonalDataServer" && svc.serviceEndpoint) { return svc.serviceEndpoint.replace(/\/+$/, ""); } } logResolve.error("No AtprotoPersonalDataServer service", { did }); throw new Error(`No AtprotoPersonalDataServer service in DID document for ${did}`); }