chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

main
106 lines · 3.5 KB Raw History
// 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]
//
// 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'),
  },
})

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)