Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172#!/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<any>} 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 } = serverconst 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.<authority>, 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<port> 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 = falseconst 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`)