// Turning what a visitor types at sign-in into the server this page will // talk to. The repository browser reads one account's repositories; the // visitor signing in to review them is anybody, on any PDS, so their handle // or DID resolves here rather than against this site's own account. // // Everything runs in the browser: a DNS-over-HTTPS resolver for a handle's // TXT record, and the DID directory for the document. Both are build-time // settings, because a local stack runs a PLC of its own holding DIDs the // public directory has never seen. const PLC_URL = String( import.meta.env?.VITE_PLC_URL || 'https://plc.directory', ).replace(/\/$/, ''); const DOH_URL = String( import.meta.env?.VITE_DOH_URL || 'https://cloudflare-dns.com/dns-query', ); /** * @typedef {Object} Identity * @property {string} did * @property {string} handle - the handle the DID document claims, or the DID * @property {string} pds - origin of the account's PDS, no trailing slash */ /** * @param {string} did * @returns {string} */ function didDocumentUrl(did) { if (did.startsWith('did:plc:')) return `${PLC_URL}/${did}`; if (did.startsWith('did:web:')) { const host = decodeURIComponent(did.slice('did:web:'.length)); return `https://${host}/.well-known/did.json`; } throw new Error(`Unsupported DID method: ${did}`); } /** * @param {string} did * @returns {Promise<{id: string, alsoKnownAs?: string[], service?: Array<{id: string, type: string, serviceEndpoint: string}>}>} */ async function resolveDid(did) { const response = await fetch(didDocumentUrl(did)); if (!response.ok) throw new Error(`Could not resolve ${did}`); return response.json(); } /** * The PDS a DID document names. * @param {Awaited>} doc * @returns {string} */ function pdsEndpoint(doc) { const service = (doc.service || []).find((entry) => entry.id.endsWith('#atproto_pds'), ); if (!service) throw new Error('That account names no PDS.'); return String(service.serviceEndpoint).replace(/\/$/, ''); } /** * The handle a DID document claims. A DID with no `at://` alias reads as * itself, which is what an account mid-handle-change looks like. * @param {Awaited>} doc * @returns {string} */ function documentHandle(doc) { const aka = (doc.alsoKnownAs || []).find((name) => name.startsWith('at://')); return aka ? aka.slice('at://'.length) : doc.id; } /** * A handle's DID from its `_atproto` TXT record, or null. * @param {string} handle * @returns {Promise} */ async function didFromDns(handle) { const url = new URL(DOH_URL); url.searchParams.set('name', `_atproto.${handle}`); url.searchParams.set('type', 'TXT'); try { const response = await fetch(url, { headers: { accept: 'application/dns-json' }, }); if (!response.ok) return null; const body = await response.json(); for (const answer of body?.Answer || []) { // A TXT answer arrives quoted, and a long one arrives as several quoted // strings that have to be joined before the prefix is readable. const text = String(answer.data || '').replace(/"\s*"/g, ''); const match = text.match(/^"?did=(did:[^"\s]+)"?$/); if (match) return match[1]; } } catch { // A resolver that is down or blocked leaves the well-known lookup. } return null; } /** * A handle's DID from the well-known document it serves, or null. Reachable * only for a handle whose host allows this origin to read it. * @param {string} handle * @returns {Promise} */ async function didFromWellKnown(handle) { try { const response = await fetch(`https://${handle}/.well-known/atproto-did`); if (!response.ok) return null; const did = (await response.text()).trim(); return did.startsWith('did:') ? did : null; } catch { return null; } } /** * Resolve a handle or DID to the account it names. * * A handle resolved through DNS is checked against the document's own * `alsoKnownAs`, so a TXT record naming somebody else's DID resolves to * nothing rather than to them. * * @param {string} input - a handle, a DID, or either with an `@` or `at://` * @returns {Promise} */ export async function resolveIdentity(input) { const cleaned = input .trim() .replace(/^at:\/\//, '') .replace(/^@/, '') .replace(/\/$/, '') .toLowerCase(); if (!cleaned) throw new Error('Enter a handle or a DID.'); if (cleaned.startsWith('did:')) { const doc = await resolveDid(cleaned); return { did: cleaned, handle: documentHandle(doc), pds: pdsEndpoint(doc) }; } const did = (await didFromDns(cleaned)) ?? (await didFromWellKnown(cleaned)); if (!did) throw new Error(`Could not resolve the handle ${cleaned}.`); const doc = await resolveDid(did); const claimed = (doc.alsoKnownAs || []).map((name) => name.replace(/^at:\/\//, '').toLowerCase(), ); if (!claimed.includes(cleaned)) { throw new Error(`${did} does not claim the handle ${cleaned}.`); } return { did, handle: cleaned, pds: pdsEndpoint(doc) }; }