diff --git a/src/lib/microcosm/hydrate.ts b/src/lib/microcosm/hydrate.ts index 98dce9a..53e0512 100644 --- a/src/lib/microcosm/hydrate.ts +++ b/src/lib/microcosm/hydrate.ts @@ -30,6 +30,40 @@ import {constellation} from '#/lib/microcosm' import {getRecordByUri, resolveMiniDoc} from '#/lib/microcosm/slingshot' import {IMG_CDN_URL} from '#/env' +/** + * The root of trust for verifications. Bluesky designates "trusted verifiers" + * by issuing them a verification record from this DID; those verifiers can in + * turn verify regular users. A verification of X by issuer I therefore only + * counts if Bluesky verified I (i.e. I is a trusted verifier). This mirrors the + * AppView's trusted-verifier filter, reconstructed entirely from records rather + * than a private allowlist. + */ +const VERIFICATION_ROOT_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur' + +/** + * Whether `did` is a trusted verifier: Bluesky's root DID issued a verification + * record for it. Cached because the same issuer recurs across many profiles. + */ +const trustedVerifierCache = new Map>() +function isTrustedVerifier(did: string, signal?: AbortSignal): Promise { + const hit = trustedVerifierCache.get(did) + if (hit) return hit + const p = constellation + .getBacklinks( + { + subject: did, + source: constellation.Sources.verifications, + did: [VERIFICATION_ROOT_DID], + limit: 1, + }, + signal, + ) + .then(r => r.total > 0) + .catch(() => false) + trustedVerifierCache.set(did, p) + return p +} + /** * Image CDN base. Avatars/banners/feed images are blob refs in the raw record; * the AppView rewrites them to CDN URLs and we reconstruct the same scheme @@ -201,33 +235,52 @@ async function hydrateEmbed( * `app.bsky.graph.verification` records whose `subject` points at the verified * DID (a Constellation backlink); each record's repo is the issuer. * - * NOTE on "trusted verifiers": the AppView only surfaces verifications from a - * curated trusted-verifier set, which is Bluesky policy not present in the - * records, so we can't reproduce that filter. We include every issuer and let - * the UI decide. `isValid` is computed structurally: the handle captured in the - * record must still match the subject's current handle (per the lexicon). + * Trust is reconstructed, not assumed: a verification only counts if its issuer + * is a trusted verifier, i.e. Bluesky's root DID verified that issuer (see + * `isTrustedVerifier`). Verifications from untrusted issuers are dropped, matching + * the AppView. `isValid` additionally requires that the handle captured in the + * record still matches the subject's current handle (per the lexicon). + * + * The subject's own `trustedVerifierStatus` is `valid` iff Bluesky verified the + * subject directly (which is how a trusted verifier is designated). */ async function hydrateVerificationState( did: string, currentHandle: string, signal?: AbortSignal, ): Promise { - const page = await constellation - .getBacklinks( - { - subject: did, - source: constellation.Sources.verifications, - limit: 20, - }, - signal, - ) - .catch(() => undefined) - if (!page || page.records.length === 0) return undefined + const [page, subjectIsVerifier] = await Promise.all([ + constellation + .getBacklinks( + { + subject: did, + source: constellation.Sources.verifications, + limit: 20, + }, + signal, + ) + .catch(() => undefined), + isTrustedVerifier(did, signal), + ]) + + const trustedVerifierStatus = subjectIsVerifier ? 'valid' : 'none' + + if (!page || page.records.length === 0) { + if (!subjectIsVerifier) return undefined + return { + $type: 'app.bsky.actor.defs#verificationState', + verifications: [], + verifiedStatus: 'none', + trustedVerifierStatus, + } + } const verifications = ( await Promise.all( page.records.map( async (ref): Promise => { + // Only verifications from trusted verifiers count. + if (!(await isTrustedVerifier(ref.did, signal))) return undefined const uri = `at://${ref.did}/${ref.collection}/${ref.rkey}` const rec = await getRecordByUri(uri, undefined, signal).catch( () => undefined, @@ -259,15 +312,14 @@ async function hydrateVerificationState( ) ).filter(Boolean) as AppBskyActorDefs.VerificationView[] - if (verifications.length === 0) return undefined + if (verifications.length === 0 && !subjectIsVerifier) return undefined const verifiedStatus = verifications.some(v => v.isValid) ? 'valid' : 'none' return { $type: 'app.bsky.actor.defs#verificationState', verifications, verifiedStatus, - // We are not a trusted verifier ourselves; report none. - trustedVerifierStatus: 'none', + trustedVerifierStatus, } }