publishLexicons
uses tcmts.atproto.readSessions, which uses node:fs
Publish the lexicons of tcm's records (tcmts.records.lexicons) as com.atproto.lexicon.schema records, for tcm's
maintainers: pnpm tcm run tcmts.dev.publishLexicons [-- --as <handle>] [--dry-run]. Each document is checked
against the Lexicon spec first; the account is the one _lexicon.tcm.codes names in DNS (signing in as anyone else
is refused), with a loopback OAuth sign-in asking only to write lexicon schemas. Idempotent: a schema published as
it is now isn't written again, a changed one is replaced, a new one is created. --dry-run says what it would do,
without signing in.
import { atprotoSession, keepSession, pdsFetch, readSessions, resolveIdentity } from "#codebase/tcmts/atproto"; import { authorityOf, checkedLexicon, lexiconDid, PUBLISH_SCOPE, publishedSchema, sameSchema, SCHEMA_COLLECTION, } from "#codebase/tcmts/dev/lexicons"; import { lexicons } from "#codebase/tcmts/records"; export async function publishLexicons(...args: string[]): Promise<void> { let dry = false; let as: string | undefined; for (let i = 0; i < args.length; i++) if (args[i] === "--dry-run") dry = true; else if (args[i] === "--as" && args[i + 1]) as = args[++i]; else throw new Error(`usage: publishLexicons [--as <handle>] [--dry-run] (not ${args[i]})`); const docs = lexicons.map(checkedLexicon); const authorities = [...new Set(docs.map((d) => authorityOf(String(d.id))))]; const named = await Promise.all(authorities.map(lexiconDid)); const missing = authorities.filter((_, i) => !named[i]); if (missing.length) throw new Error( `no DID to publish as: ${missing.map((a) => `_lexicon.${a}`).join(", ")} names none in DNS (a TXT record "did=<did>")`, ); if (new Set(named).size > 1) throw new Error( `the lexicons' authorities name different DIDs (${authorities.map((a, i) => `${a}: ${named[i]}`).join(", ")}): publish them one authority at a time`, ); const did = named[0]!; const id = await resolveIdentity(did); const where = authorities.map((a) => `_lexicon.${a}`).join(", "); console.log(`${where} names ${id.handle ? `@${id.handle} (${did})` : did}, at ${id.pds}`); if (as) { const them = await resolveIdentity(as); if (them.did !== did) throw new Error( `--as ${as} is ${them.did}, but ${where} names ${did}: not publishing as them`, ); } const plan = await Promise.all( docs.map(async (doc) => { const old = await publishedSchema(id.pds, did, String(doc.id)); const was = !old ? "new" : sameSchema(old.value, doc) ? "unchanged" : "changed"; return { doc, old, was }; }), ); for (const p of plan) console.log(` ${p.was.padEnd(10)}${String(p.doc.id)}`); const writes = plan.filter((p) => p.was !== "unchanged"); if (!writes.length) return console.log("every schema is published as it is: nothing to do"); if (dry) return console.log(`would write ${writes.length} (--dry-run: nothing written)`); const before = readSessions().last; try { const s = await atprotoSession({ who: as ?? did, scope: PUBLISH_SCOPE, why: "publishing lexicons writes them to the authority's repository", }); if (s.did !== did) throw new Error(`signed in as ${s.did}, but ${where} names ${did}: not publishing as them`); for (const { doc, old } of writes) { const nsid = String(doc.id); const res = await pdsFetch( s, `/xrpc/com.atproto.repo.${old ? "putRecord" : "createRecord"}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ repo: did, collection: SCHEMA_COLLECTION, rkey: nsid, record: { $type: SCHEMA_COLLECTION, ...doc }, ...(old ? { swapRecord: old.cid } : {}), }), }, ); const got = (await res.json().catch(() => ({}))) as { uri?: string; error?: string; message?: string; }; if (!res.ok) throw new Error( `publishing ${nsid}: ${res.status} ${got.message ?? got.error ?? ""}`.trim(), ); console.log( `${old ? "updated" : "published"} ${got.uri ?? `at://${did}/${SCHEMA_COLLECTION}/${nsid}`}`, ); } } finally { // (the maintainer's own account stays the one tcm signs in as, when it isn't told) const kept = readSessions(); if (before && kept.last !== before && kept.sessions[before]) keepSession(before, kept.sessions[before]); } }
uses
- tcmts.atproto.atprotoSession
- tcmts.atproto.keepSession
- tcmts.atproto.pdsFetch
- tcmts.atproto.readSessions
- tcmts.atproto.resolveIdentity
- tcmts.dev.lexicons.PUBLISH_SCOPE
- tcmts.dev.lexicons.SCHEMA_COLLECTION
- tcmts.dev.lexicons.authorityOf
- tcmts.dev.lexicons.checkedLexicon
- tcmts.dev.lexicons.lexiconDid
- tcmts.dev.lexicons.publishedSchema
- tcmts.dev.lexicons.sameSchema
- tcmts.records.lexicons