/** * `corpora verify` — the command you run while nervous about a cutover. * * The output is built to be read under stress: one line per check, aligned, grouped the way the * resolution chain is ordered, failures repeated at the end with the detail that tells you what to * do. `--json` for machines. Non-zero exit on any failure. * * @see [INTERFACES](../../docs/INTERFACES.md) */ import { parseArgs } from 'node:util' import { verifyAuthority } from './index.js' const USAGE = `corpora verify — check a built or deployed authority over HTTP Usage: corpora verify --origin [--did ] [--source ] [--json] Options: --origin Base URL of the authority. Required. --did The DID the authority must claim. Read from did.json otherwise. --source Lexicon source directory; served records are diffed against it. --json Machine-readable report on stdout. Also accepted: --expect-did-doc Expected sha256 of did.json bytes, as published alongside the source. --no-dns Skip the _lexicon TXT checks. Use before DNS is flipped to a new DID. --dns-server Ask this resolver instead of the system one. Repeatable. Also read from CORPORA_DNS_SERVERS, comma separated. Exits non-zero if any check fails. ` /** * @param {string[]} argv * @returns {Promise} */ export async function run(argv) { if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(USAGE) return 0 } const { values } = parseArgs({ args: argv, options: { origin: { type: 'string' }, did: { type: 'string' }, source: { type: 'string' }, json: { type: 'boolean' }, 'expect-did-doc': { type: 'string' }, 'no-dns': { type: 'boolean' }, 'dns-server': { type: 'string', multiple: true }, }, }) if (!values.origin) { process.stderr.write(`corpora verify: --origin is required\n\n${USAGE}`) return 1 } const envServers = (process.env.CORPORA_DNS_SERVERS ?? '').split(',').map((s) => s.trim()).filter(Boolean) const dnsServers = values['dns-server'] ?? (envServers.length ? envServers : undefined) const report = await verifyAuthority({ origin: values.origin, did: values.did, sourceDir: values.source, expectDidDoc: values['expect-did-doc'], skipDns: values['no-dns'], dnsServers, }) if (values.json) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`) } else { process.stdout.write(format(report)) } return report.ok ? 0 : 1 } /** * @param {import('./index.js').VerifyReport} report * @returns {string} */ export function format(report) { const colour = process.stdout.isTTY && !process.env.NO_COLOR const dim = (/** @type {string} */ s) => (colour ? `\x1b[2m${s}\x1b[0m` : s) const red = (/** @type {string} */ s) => (colour ? `\x1b[31m${s}\x1b[0m` : s) const green = (/** @type {string} */ s) => (colour ? `\x1b[32m${s}\x1b[0m` : s) const bold = (/** @type {string} */ s) => (colour ? `\x1b[1m${s}\x1b[0m` : s) const width = Math.max(0, ...report.checks.map((c) => c.name.length)) const lines = [``, `${bold('corpora verify')} ${report.origin ?? ''}`] /** @type {string | undefined} */ let group for (const check of report.checks) { if (check.group !== group) { group = check.group lines.push(``, ` ${dim(group ?? '')}`) } const mark = check.ok ? green('ok ') : red('FAIL') lines.push(` ${mark} ${check.name.padEnd(width)} ${dim(check.detail)}`) } const failed = report.checks.filter((c) => !c.ok) const count = `${report.checks.length} check${report.checks.length === 1 ? '' : 's'}` const scope = report.nsids.length ? `${report.nsids.length} lexicon${report.nsids.length === 1 ? '' : 's'} under ${report.did}` : (report.did ?? '') lines.push(``) if (failed.length === 0) { lines.push(` ${green('passed')} ${count}${scope ? `, ${scope}` : ''}`) } else { lines.push(` ${red(`${failed.length} of ${count} failed`)}${scope ? `, ${scope}` : ''}`) for (const check of failed) lines.push(` ${red('·')} ${check.name}: ${check.detail}`) } lines.push(``) return lines.join('\n') }