#!/usr/bin/env node /** * `npm run dev` — the whole resolution chain running locally, and the commands to poke at it. * * Builds the fixture lexicons, serves them, and stands up a DNS server that answers the * `_lexicon.*` TXT records the namespace needs. Then it prints, rather than describes, the exact * `dig` and `curl` invocations that walk the chain, and stays running until Ctrl-C. * * Nothing here needs root, a registrar, or a certificate. Why that is possible, and which link of * the chain is genuinely real, is in [LOCAL_E2E](../docs/LOCAL_E2E.md). */ import { existsSync } from 'node:fs' import { rm } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { buildFixtures, FIXTURE_AUTHORITY_DOMAINS, missingPrerequisite } from '../test/helpers/build.js' import { startDnsServer } from '../test/helpers/dns.js' import { startAuthority } from '../test/helpers/server.js' const DIST = fileURLToPath(new URL('../test/fixtures/dist', import.meta.url)) const LEXICONS = fileURLToPath(new URL('../test/fixtures/lexicons', import.meta.url)) const HTTP_PORT = Number(process.env.CORPORA_DEV_PORT ?? 2583) const DNS_PORT = Number(process.env.CORPORA_DEV_DNS_PORT ?? 5333) const blocked = missingPrerequisite() if (blocked) { process.stderr.write(`npm run dev: ${blocked}\n`) process.exit(1) } /** * The DID carries the port, so a fixed port is what keeps the printed commands and the built * artifacts stable across restarts. Say so plainly when it is taken rather than throwing a listen * stack trace at someone who just wanted to see the thing work. * @param {() => Promise} start * @param {string} what * @param {string} envVar * @param {number} port */ async function bind(start, what, envVar, port) { try { return await start() } catch (err) { if (/** @type {{ code?: string }} */ (err)?.code !== 'EADDRINUSE') throw err process.stderr.write(`npm run dev: ${what} port ${port} is already in use. Set ${envVar} to another.\n`) return process.exit(1) } } const server = await bind( () => startAuthority({ port: HTTP_PORT, prepare: async ({ did, service }) => { await rm(DIST, { recursive: true, force: true }) return buildFixtures({ out: DIST, did, service, lexicons: LEXICONS }) }, }), 'the authority', 'CORPORA_DEV_PORT', HTTP_PORT, ) const dns = await bind( () => startDnsServer({ port: DNS_PORT, records: Object.fromEntries(FIXTURE_AUTHORITY_DOMAINS.map((d) => [d, `did=${server.did}`])), }), 'the DNS server', 'CORPORA_DEV_DNS_PORT', DNS_PORT, ) const nsids = Object.keys(server.data.records).sort() const { origin, did } = server const q = encodeURIComponent const bold = (/** @type {string} */ s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s) const dim = (/** @type {string} */ s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s) process.stdout.write(` ${bold('corpora dev')} — a lexicon authority on localhost authority ${origin} ${dim(`(${server.mode})`)} DID ${did} DNS 127.0.0.1:${dns.port} ${dim('UDP, TXT only')} artifacts ${DIST} lexicons ${nsids.join('\n ')} ${bold('1. NSID to authority domain, over DNS')} ${dim(' The resolver queries _lexicon., where the authority is the NSID minus its')} ${dim(' final segment, reversed. There is no fallback to a shallower name.')} ${FIXTURE_AUTHORITY_DOMAINS.map((d) => ` dig @127.0.0.1 -p ${dns.port} +short TXT ${d}`).join('\n')} ${bold('2. DID document')} ${dim(" did:web:localhost%3A resolves over plain http — @atproto/identity downgrades the")} ${dim(' scheme for the literal hostname `localhost`, which is what makes this work without TLS.')} curl -s ${origin}/.well-known/did.json | jq ${bold('3. Signed inclusion proof — this is what an OAuth Auth Server reads')} ${nsids .slice(0, 2) .map( (n) => ` curl -s "${origin}/xrpc/com.atproto.sync.getRecord?did=${q(did)}&collection=com.atproto.lexicon.schema&rkey=${n}" | wc -c`, ) .join('\n')} ${bold('4. Plain record JSON, for eyeballing')} curl -s "${origin}/xrpc/com.atproto.repo.getRecord?repo=${q(did)}&collection=com.atproto.lexicon.schema&rkey=${nsids[0]}" | jq .value curl -s "${origin}/xrpc/com.atproto.repo.listRecords?repo=${q(did)}&collection=com.atproto.lexicon.schema" | jq '.records[].uri' ${bold('5. Verify it the way you would verify a deployment')} CORPORA_DNS_SERVERS=127.0.0.1:${dns.port} node bin/corpora.js verify \\ --origin ${origin} --did '${did}' --source test/fixtures/lexicons ${bold('6. Point a real resolver at it')} ${dim(' @atproto/lexicon-resolver reads DNS from the process resolver, so a client needs the')} ${dim(' local DNS server and a fetch that will talk to loopback. See docs/LOCAL_E2E.md.')} ${devSnippet()} ${dim('Ctrl-C to stop.')} `) /** * The official resolver, run against this dev authority, copy-pasteable. * * Two lines of it are the whole local-development trick: `setServers` aims Node at the local DNS * server, and `syncBuiltinESMExports` makes that visible to a library that took a namespace import * of `node:dns/promises` before we changed anything. Injecting `fetch` replaces the resolver's * SSRF-hardened default, which refuses loopback on principle. */ function devSnippet() { return [ ` node --input-type=module -e "`, ` import dns from 'node:dns'`, ` import { syncBuiltinESMExports } from 'node:module'`, ` import { buildLexiconResolver } from '@atproto/lexicon-resolver'`, ` dns.setServers(['127.0.0.1:${dns.port}']); syncBuiltinESMExports()`, ` const r = await buildLexiconResolver({ rpc: { fetch } })('${nsids[0]}')`, ` console.log(r.uri.toString(), r.cid.toString())`, ` "`, ].join('\n') } let stopping = false const stop = async () => { if (stopping) return stopping = true process.stdout.write('\nstopping\n') await dns.close() await server.close() process.exit(0) } process.on('SIGINT', stop) process.on('SIGTERM', stop) // Keep the process alive even though both servers already hold handles; this makes the intent // explicit and survives a future refactor that unrefs one of them. setInterval(() => {}, 1 << 30) if (!existsSync(DIST)) process.stderr.write(`warning: ${DIST} disappeared after the build\n`)