publishLexicons

tcmts.dev.publishLexicons · #ndxyu3xf · term · runs on the server

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