import { isDid, isHandle } from '@atcute/lexicons/syntax'; import { didDocumentUrl, serviceEndpoint } from '../identity.ts'; import { isPublicHttpsUrl } from '../net.ts'; import { cached, POLICY } from './cache.ts'; import { cacheKey, type Gateway } from './context.ts'; import { CORS, failureResponse, fetchUpstream, MAX_JSON_BYTES, readLimited, UpstreamError, xrpcError } from './proxy.ts'; /** `blue.microcosm.identity.resolveMiniDoc`'s answer. `handle` is `handle.invalid` when it does not * resolve back to the DID. */ export interface MiniDoc { did: string; handle: string; pds: string; } type Found = { found: true; value: T } | { found: false; message: string }; const json = (body: unknown) => Response.json(body, { headers: CORS }); async function readJson(res: Response): Promise { return JSON.parse(new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES))); } /** `com.atproto.identity.resolveDid`, cached for 5 minutes. Straight from PLC or the did:web host. */ export function resolveDid(g: Gateway, did: string, bypass = false): Promise { return cached(g, cacheKey(g, '/xrpc/com.atproto.identity.resolveDid', { did }), POLICY.identity, bypass, async () => { const url = didDocumentUrl(did); if (!url) return xrpcError('DidNotFound', `${did} is not a did:plc or a public did:web`, 400); try { const res = await fetchUpstream(g.fetch, url, { headers: { accept: 'application/json' } }); if (res.status === 404 || res.status === 410) { await res.body?.cancel(); return xrpcError('DidNotFound', `no DID document for ${did}`, 400); } if (!res.ok) { await res.body?.cancel(); throw new UpstreamError('unreachable', `${new URL(url).host} answered ${res.status} for ${did}`); } const doc = await readJson(res); if ((doc as { id?: unknown } | null)?.id !== did) return xrpcError('DidNotFound', `the document at ${url} is not ${did}'s`, 400); return json({ didDoc: doc }); } catch (error) { return error instanceof SyntaxError ? xrpcError('UpstreamFailure', `${did}'s document is not JSON`, 502) : failureResponse(error); } }); } /** A DID document through `resolveDid`'s cache. Throws `UpstreamError` when that is an outage. */ export async function didDocument(g: Gateway, did: string): Promise> { const res = await resolveDid(g, did); if (res.status >= 500) { await res.body?.cancel(); throw new UpstreamError('unreachable', `could not read ${did}'s DID document`); } const body = (await res.json()) as { didDoc?: unknown; message?: string }; return res.ok ? { found: true, value: body.didDoc } : { found: false, message: body.message ?? `no DID document for ${did}` }; } /** `_atproto.` over DNS-over-HTTPS, then `https:///.well-known/atproto-did`. */ async function resolveHandle(g: Gateway, handle: string): Promise { try { const res = await fetchUpstream(g.fetch, `https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`, { headers: { accept: 'application/dns-json' }, }); if (res.ok) { const answers = ((await readJson(res)) as { Answer?: { data?: unknown }[] }).Answer ?? []; const dids = answers.flatMap((a) => { const match = typeof a.data === 'string' ? /^"?did=([^"\s]+)"?$/.exec(a.data) : null; return match && isDid(match[1]) ? [match[1]] : []; }); if (dids.length === 1) return dids[0]; } else { await res.body?.cancel(); } } catch { // DNS unanswered: try the well-known file. } const wellKnown = `https://${handle}/.well-known/atproto-did`; if (!isPublicHttpsUrl(wellKnown)) return undefined; try { const res = await fetchUpstream(g.fetch, wellKnown); if (!res.ok) { await res.body?.cancel(); return undefined; } const did = new TextDecoder().decode(await readLimited(res, 2048)).trim(); return isDid(did) ? did : undefined; } catch { return undefined; } } function handleClaim(doc: unknown): string | undefined { const aka = (doc as { alsoKnownAs?: unknown } | null)?.alsoKnownAs; const first = Array.isArray(aka) ? aka.find((v): v is string => typeof v === 'string' && v.startsWith('at://')) : undefined; const handle = first?.slice('at://'.length).toLowerCase(); return handle && isHandle(handle) ? handle : undefined; } async function viaSlingshot(g: Gateway, identifier: string): Promise { try { const url = new URL('/xrpc/blue.microcosm.identity.resolveMiniDoc', g.slingshot); url.searchParams.set('identifier', identifier); const res = await fetchUpstream(g.fetch, url.toString(), { headers: { accept: 'application/json' } }); if (!res.ok) { await res.body?.cancel(); return undefined; } const body = (await readJson(res)) as Partial; if (typeof body.did !== 'string' || typeof body.handle !== 'string' || typeof body.pds !== 'string') return undefined; if (isDid(identifier) ? body.did !== identifier : body.handle.toLowerCase() !== identifier) return undefined; if (!isPublicHttpsUrl(body.pds)) return undefined; return { did: body.did, handle: body.handle, pds: body.pds }; } catch { return undefined; } } /** The same answer built from PLC or did:web and DNS, checking the handle both ways. */ async function viaDocument(g: Gateway, identifier: string): Promise> { const did = isDid(identifier) ? identifier : await resolveHandle(g, identifier); if (!did) return { found: false, message: `${identifier} does not resolve to a DID` }; const doc = await didDocument(g, did); if (!doc.found) return doc; const pds = serviceEndpoint(doc.value, did, { id: '#atproto_pds' }); if (!pds) return { found: false, message: `${did} names no public https PDS` }; const claimed = handleClaim(doc.value); const verified = isDid(identifier) ? claimed !== undefined && (await resolveHandle(g, claimed)) === did : claimed === identifier; return { found: true, value: { did, handle: verified ? claimed! : 'handle.invalid', pds } }; } /** `blue.microcosm.identity.resolveMiniDoc`, cached for 5 minutes: Slingshot first, then PLC or * did:web and DNS directly. `identifier` is already a parsed DID or lowercase handle. */ export function resolveMiniDoc(g: Gateway, identifier: string, bypass = false): Promise { return cached(g, cacheKey(g, '/xrpc/blue.microcosm.identity.resolveMiniDoc', { identifier }), POLICY.identity, bypass, async () => { const slingshot = await viaSlingshot(g, identifier); if (slingshot) return json(slingshot); try { const direct = await viaDocument(g, identifier); return direct.found ? json(direct.value) : xrpcError('NotFound', direct.message, 400); } catch (error) { return failureResponse(error); } }); } /** Who `identifier` is and where their repo lives, through `resolveMiniDoc`'s cache. */ export async function miniDoc(g: Gateway, identifier: string): Promise> { const res = await resolveMiniDoc(g, identifier); if (res.status >= 500) { await res.body?.cancel(); throw new UpstreamError('unreachable', `could not resolve ${identifier}`); } const body = (await res.json()) as MiniDoc & { message?: string }; return res.ok ? { found: true, value: body } : { found: false, message: body.message ?? `could not resolve ${identifier}` }; }