import { Client, simpleFetchHandler } from "@atcute/client"; import { DidDocument, getPdsEndpoint } from "@atcute/identity"; import { lexiconDoc } from "@atcute/lexicon-doc"; import { RecordValidator } from "@atcute/lexicon-doc/validations"; import { FailedLexiconResolutionError, ResolvedSchema } from "@atcute/lexicon-resolver"; import { ActorIdentifier, is, Nsid } from "@atcute/lexicons"; import { AtprotoDid, Did, isNsid } from "@atcute/lexicons/syntax"; import { verifyRecord } from "@atcute/repo"; import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js"; import { hasUserScope } from "../auth/scope-utils"; import { agent } from "../auth/state"; import { Backlinks } from "../components/backlinks.jsx"; import { Button } from "../components/button.jsx"; import { RecordEditor, setPlaceholder } from "../components/create"; import { CopyMenu, DropdownMenu, MenuProvider, MenuSeparator, NavMenu, } from "../components/dropdown.jsx"; import { JSONValue } from "../components/json.jsx"; import { LexiconSchemaView } from "../components/lexicon-schema.jsx"; import { Modal } from "../components/modal.jsx"; import { pds } from "../components/navbar.jsx"; import { addNotification, removeNotification } from "../components/notification.jsx"; import Tooltip from "../components/tooltip.jsx"; import { didDocumentResolver, resolveLexiconAuthority, resolveLexiconSchema, resolvePDS, } from "../utils/api.js"; import { AtUri, uriTemplates } from "../utils/templates.js"; import { lexicons } from "../utils/types/lexicons.js"; const authorityCache = new Map>(); const documentCache = new Map>(); const schemaCache = new Map>(); const getAuthoritySegment = (nsid: string): string => { const segments = nsid.split("."); return segments.slice(0, -1).join("."); }; const resolveSchema = async (authority: AtprotoDid, nsid: Nsid): Promise => { const cacheKey = `${authority}:${nsid}`; let cachedSchema = schemaCache.get(cacheKey); if (cachedSchema) { return cachedSchema; } const schemaPromise = (async () => { let didDocPromise = documentCache.get(authority); if (!didDocPromise) { didDocPromise = didDocumentResolver.resolve(authority); documentCache.set(authority, didDocPromise); } const didDocument = await didDocPromise; const pdsEndpoint = getPdsEndpoint(didDocument); if (!pdsEndpoint) { throw new FailedLexiconResolutionError(nsid, { cause: new TypeError(`no pds service in did document; did=${authority}`), }); } const rpc = new Client({ handler: simpleFetchHandler({ service: pdsEndpoint }) }); const response = await rpc.get("com.atproto.repo.getRecord", { params: { repo: authority, collection: "com.atproto.lexicon.schema", rkey: nsid, }, }); if (!response.ok) { throw new Error(`got http ${response.status}`); } return response.data.value; })(); schemaCache.set(cacheKey, schemaPromise); try { return await schemaPromise; } catch (err) { schemaCache.delete(cacheKey); throw err; } }; const extractRefs = (obj: any): Nsid[] => { const refs: Set = new Set(); const traverse = (value: any) => { if (!value || typeof value !== "object") return; if (value.type === "ref" && value.ref) { const ref = value.ref; if (!ref.startsWith("#")) { const nsid = ref.split("#")[0]; if (isNsid(nsid)) refs.add(nsid); } } if (value.type === "union" && Array.isArray(value.refs)) { for (const ref of value.refs) { if (!ref.startsWith("#")) { const nsid = ref.split("#")[0]; if (isNsid(nsid)) refs.add(nsid); } } } if (Array.isArray(value)) value.forEach(traverse); else Object.values(value).forEach(traverse); }; traverse(obj); return Array.from(refs) as Nsid[]; }; const resolveAllLexicons = async ( nsid: Nsid, depth: number = 0, resolved: Map = new Map(), failed: Set = new Set(), inFlight: Map> = new Map(), ): Promise<{ resolved: Map; failed: Set }> => { if (depth >= 10) { console.warn(`Maximum recursion depth reached for ${nsid}`); return { resolved, failed }; } if (resolved.has(nsid) || failed.has(nsid)) return { resolved, failed }; if (inFlight.has(nsid)) { await inFlight.get(nsid); return { resolved, failed }; } const fetchPromise = (async () => { let authority: AtprotoDid | undefined; const authoritySegment = getAuthoritySegment(nsid); try { let authorityPromise = authorityCache.get(authoritySegment); if (!authorityPromise) { authorityPromise = resolveLexiconAuthority(nsid); authorityCache.set(authoritySegment, authorityPromise); } authority = await authorityPromise; const schema = await resolveSchema(authority, nsid); resolved.set(nsid, schema); const refs = extractRefs(schema); if (refs.length > 0) { await Promise.all( refs.map((ref) => resolveAllLexicons(ref, depth + 1, resolved, failed, inFlight)), ); } } catch (err) { console.error(`Failed to resolve lexicon ${nsid}:`, err); failed.add(nsid); authorityCache.delete(authoritySegment); if (authority) { documentCache.delete(authority); } } finally { inFlight.delete(nsid); } })(); inFlight.set(nsid, fetchPromise); await fetchPromise; return { resolved, failed }; }; export const RecordView = () => { const location = useLocation(); const navigate = useNavigate(); const params = useParams(); const [openDelete, setOpenDelete] = createSignal(false); const [verifyError, setVerifyError] = createSignal(""); const [validationError, setValidationError] = createSignal(""); const [externalLink, setExternalLink] = createSignal< { label: string; link: string; icon?: string } | undefined >(); const [lexiconUri, setLexiconUri] = createSignal(); const [validRecord, setValidRecord] = createSignal(undefined); const [validSchema, setValidSchema] = createSignal(undefined); const [schema, setSchema] = createSignal(); const [lexiconNotFound, setLexiconNotFound] = createSignal(); const [remoteValidation, setRemoteValidation] = createSignal(); const did = params.repo; let rpc: Client; const fetchRecord = async () => { setValidRecord(undefined); setValidSchema(undefined); setLexiconUri(undefined); const pds = await resolvePDS(did!); rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); const res = await rpc.get("com.atproto.repo.getRecord", { params: { repo: did as ActorIdentifier, collection: params.collection as `${string}.${string}.${string}`, rkey: params.rkey!, }, }); if (!res.ok) { setValidRecord(false); setVerifyError(res.data.error); throw new Error(res.data.error); } setPlaceholder(res.data.value); setExternalLink(checkUri(res.data.uri, res.data.value)); resolveLexicon(params.collection as Nsid); verifyRecordIntegrity(); validateLocalSchema(res.data.value); return res.data; }; const [record, { refetch }] = createResource(fetchRecord); const validateLocalSchema = async (record: Record) => { try { if (params.collection === "com.atproto.lexicon.schema") { setLexiconNotFound(false); lexiconDoc.parse(record, { mode: "passthrough" }); setValidSchema(true); } else if (params.collection && params.collection in lexicons) { if (is(lexicons[params.collection], record)) setValidSchema(true); else setValidSchema(false); } } catch (err: any) { console.error("Schema validation error:", err); setValidSchema(false); setValidationError(err.message || String(err)); } }; const validateRemoteSchema = async (record: Record) => { try { setRemoteValidation(true); const { resolved, failed } = await resolveAllLexicons(params.collection as Nsid); if (failed.size > 0) { console.error(`Failed to resolve ${failed.size} documents:`, Array.from(failed)); setValidSchema(false); setValidationError(`Unable to resolve lexicon documents: ${Array.from(failed).join(", ")}`); return; } const lexiconDocs = Object.fromEntries(resolved); console.log(lexiconDocs); const validator = new RecordValidator(lexiconDocs, params.collection as Nsid); validator.parse({ key: params.rkey ?? null, object: record, }); setValidSchema(true); } catch (err: any) { console.error("Schema validation error:", err); setValidSchema(false); setValidationError(err.message || String(err)); } setRemoteValidation(false); }; const verifyRecordIntegrity = async () => { try { const { ok, data } = await rpc.get("com.atproto.sync.getRecord", { params: { did: did as Did, collection: params.collection as Nsid, rkey: params.rkey!, }, as: "bytes", }); if (!ok) throw data.error; await verifyRecord({ did: did as AtprotoDid, collection: params.collection!, rkey: params.rkey!, carBytes: data as Uint8Array, }); setValidRecord(true); } catch (err: any) { console.error("Record verification error:", err); setVerifyError(err.message); setValidRecord(false); } }; const resolveLexicon = async (nsid: Nsid) => { try { const authority = await resolveLexiconAuthority(nsid); setLexiconUri(`at://${authority}/com.atproto.lexicon.schema/${nsid}`); if (params.collection !== "com.atproto.lexicon.schema") { const schema = await resolveLexiconSchema(authority, nsid); setSchema(schema); setLexiconNotFound(false); } } catch { setLexiconNotFound(true); } }; const deleteRecord = async () => { rpc = new Client({ handler: agent()! }); await rpc.post("com.atproto.repo.deleteRecord", { input: { repo: params.repo as ActorIdentifier, collection: params.collection as `${string}.${string}.${string}`, rkey: params.rkey!, }, }); const id = addNotification({ message: "Record deleted", type: "success", }); setTimeout(() => removeNotification(id), 3000); navigate(`/at://${params.repo}/${params.collection}`); }; const checkUri = (uri: string, record: any) => { const uriParts = uri.split("/"); // expected: ["at:", "", "repo", "collection", "rkey"] if (uriParts.length != 5) return undefined; if (uriParts[0] !== "at:" || uriParts[1] !== "") return undefined; const parsedUri: AtUri = { repo: uriParts[2], collection: uriParts[3], rkey: uriParts[4] }; const template = uriTemplates[parsedUri.collection]; if (!template) return undefined; return template(parsedUri, record); }; const RecordTab = (props: { tab: "record" | "backlinks" | "info" | "schema"; label: string; error?: boolean; }) => { const isActive = () => { if (!location.hash && props.tab === "record") return true; if (location.hash === `#${props.tab}`) return true; if (props.tab === "schema" && location.hash.startsWith("#schema:")) return true; return false; }; return ( ); }; return (
setOpenDelete(false)}>

Delete this record?

{(cid) => } {(externalLink) => ( )}
Lexicon schema could not be resolved. Resolving lexicon schema...
Error: {err.message}
}>
Error: {err.message}
} > } >

AT URI

{record()?.uri}

CID

{record()?.cid}

Record verification

{verifyError()}

Schema validation

{validationError()}

Lexicon schema

); };