// Runs the reference PDS (@atproto/pds via @atproto/dev-env) and a PLC // directory on free ports, creates test accounts, prints one JSON line // describing them, and shuts down when stdin closes. // // node harness/pds.mjs [--accounts alice,bob,carol] [--rate-limits] [--blob-limit ] // // KNOTLESS_DEV_ENV names a built dev-env package to use instead of the npm // one, such as an atproto checkout's packages/dev-env on the spaces alpha. // // A gateway is an OAuth client, which the PDS reaches only over https at a // hostname. So the harness makes a certificate for GATEWAY_HOST, trusts // it, and resolves that name to 127.0.0.1; tests put harness/tls-proxy.mjs // in front of a gateway with it. import { fileURLToPath, pathToFileURL } from 'node:url' import { dirname, join, resolve } from 'node:path' import { execFileSync } from 'node:child_process' import { mkdtempSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import dns from 'node:dns' import tls from 'node:tls' // Relative to the repository root. const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const devEnv = process.env.KNOTLESS_DEV_ENV const { TestNetworkNoAppView } = await import( devEnv ? pathToFileURL(resolve(root, devEnv, 'dist/index.js')).href : '@atproto/dev-env' ) const GATEWAY_HOST = 'gateway.knotless.internal' const certs = mkdtempSync(join(tmpdir(), 'knotless-tls-')) const cert = join(certs, 'cert.pem') const key = join(certs, 'key.pem') execFileSync('openssl', [ 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes', '-keyout', key, '-out', cert, '-days', '7', '-subj', `/CN=${GATEWAY_HOST}`, '-addext', `subjectAltName=DNS:${GATEWAY_HOST}`, ], { stdio: 'ignore' }) tls.setDefaultCACertificates([...tls.getCACertificates('default'), readFileSync(cert, 'utf8')]) const lookup = dns.lookup dns.lookup = (hostname, options, callback) => { if (typeof options === 'function') [callback, options] = [options, {}] if (hostname !== GATEWAY_HOST) return lookup(hostname, options, callback) return options?.all ? callback(null, [{ address: '127.0.0.1', family: 4 }]) : callback(null, '127.0.0.1', 4) } const arg = (name) => { const i = process.argv.indexOf(name) return i === -1 ? undefined : process.argv[i + 1] } const names = (arg('--accounts') ?? 'alice,bob,carol').split(',') const net = await TestNetworkNoAppView.create({ pds: { hostname: 'localhost', rateLimitsEnabled: process.argv.includes('--rate-limits'), ...(arg('--blob-limit') && { blobUploadLimit: Number(arg('--blob-limit')) }), }, }) const accounts = [] for (const name of names) { const password = `${name}-password` const res = await fetch( `${net.pds.url}/xrpc/com.atproto.server.createAccount`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: `${name}@test.com`, handle: `${name}.test`, password, }), }, ) if (!res.ok) throw new Error(`createAccount ${name}: ${await res.text()}`) const { did, handle } = await res.json() accounts.push({ handle, did, password }) } const described = await fetch( `${net.pds.url}/xrpc/com.atproto.server.describeServer`, ).then((res) => res.json()) console.log( JSON.stringify({ pds: net.pds.url, plc: net.plc.url, spaces: described.spacesAlpha === true, gateway: { host: GATEWAY_HOST, cert, key }, accounts, }), ) const shutdown = async () => { await net.close() process.exit(0) } process.stdin.on('end', shutdown) process.stdin.on('close', shutdown) process.stdin.resume() process.on('SIGTERM', shutdown) process.on('SIGINT', shutdown)