/** * Reading the one or two public records a preview describes. * * This is `@radial/atproto` used exactly as the browser uses it — DID → PDS → `getRecord`, on bare * `fetch`, unauthenticated, with the lexicon validation `FetchRepoTransport` already does. The * worker holds no credential of any kind and could not write if it wanted to; everything it reads * is public data that anyone can read the same way. * * The transport is built around an injected `fetch` so the whole path is drivable from a test with * canned DID documents and records, which is the same shape `@radial/atproto`'s own tests use. */ import { FetchRepoTransport, pdsEndpoint, resolveDidDocument, type FetchLike, type ResolvedRecord, } from '@radial/atproto' import type { ArtifactRecord, ArtifactRequestRecord, GoalRecord } from '@radial/core' import { COLLECTIONS } from '@radial/core' import type { PreviewTarget } from './target.js' /** * A transport that will only talk to an `https:` PDS. * * A DID document names its own host, so the set of origins this can be pointed at is open by * design. There is no internal network behind a Cloudflare worker to reach into, so this is not * plugging an SSRF hole so much as refusing to do something there is no reason to do: read a * member's records in the clear on the strength of a document anyone could have published. */ export function previewTransport(fetcher: FetchLike): FetchRepoTransport { return new FetchRepoTransport(async (did, signal) => { const document = await resolveDidDocument(did, (input, init) => fetcher(input, signal ? { ...init, signal } : init), ) const endpoint = pdsEndpoint(document, did) if (endpoint.protocol !== 'https:') { throw new Error(`Refusing a non-https PDS for ${did}: ${endpoint.protocol}`) } return endpoint }, fetcher) } export interface PreviewRecords { goal: GoalRecord unit?: ArtifactRecord | ArtifactRequestRecord } /** * The records behind a goal deep link. Both reads are in flight at once and share one deadline; a * unit that fails to arrive degrades to the goal on its own rather than sinking the whole preview, * and a goal that fails to arrive means there is nothing to say and the caller keeps the defaults. */ export async function readGoalPreview( transport: Pick, target: Extract, signal?: AbortSignal, ): Promise { const [goal, unit] = await Promise.all([ settled(transport.getRecord(target.goal, signal)), target.unit ? settled(transport.getRecord(target.unit, signal)) : Promise.resolve(undefined), ]) if (goal?.value.$type !== COLLECTIONS.goal) return undefined const value = unit?.value const usable = (value?.$type === COLLECTIONS.artifact || value?.$type === COLLECTIONS.artifactRequest) && value.goal?.uri === target.goal ? value : undefined return { goal: goal.value, ...(usable ? { unit: usable } : {}) } } const settled = (pending: Promise): Promise => pending.catch(() => undefined)