chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# Local dev stack for pds.js.
#
# `just dev` brings up the docker infra (local PLC, relay, Caddy), runs the
# example PDS on the host, registers a mock user in the local PLC, and seeds a
# few records. Then sign in at http://localhost:2471/account with the printed
# credentials. `just dev-down` tears it all back down.
#
# `just dev-git` is the repository browser instead: a PDS on localhost with a
# repository, a package and an image already linked, and Vite serving
# @pdsjs/git-ui against it at http://localhost:5173.
#
# `just dev-spaces` is the space browser: two PDSes writing to one space, with
# @pdsjs/spaces-ui at http://localhost:5174.
#
# That account answers on localhost because the page resolves the DID document
# and then reads the endpoint it names. A localhost endpoint is out of the
# docker relay's reach, and this stack has no use for the relay, so its infra
# is the local PLC alone. `VITE_PLC_URL` is what sends the page to that PLC
# rather than the public directory.
#
# `just git-host-for` is the other direction: no local PDS at all. It points
# git-host at some real account's PDS over the public XRPC surface and opens
# the repository browser for that account.
set shell := ["bash", "-uc"]
# Host port the PDS listens on. Caddy proxies :3443 -> :2471; keep in sync with
# test/helpers/test-port.js and docker/Caddyfile.
port := "2471"
data := ".dev-pds"
pidfile := ".dev-pds/pds.pid"
log := ".dev-pds/pds.log"
base := "http://localhost:2471"
handle := "alice"
bob_port := "2472"
carol_port := "2473"
# 2474 to 2481 belong to the test suite, which binds them on the same machine.
nova_port := "2482"
dan_port := "2483"
plc := "http://localhost:2582"
ui_port := "5173"
spaces_ui_port := "5174"
gh_port := "8057"
gh_dir := ".dev-git-host"
# List available recipes.
default:
@just --list
# One-shot: infra + PDS + mock user, ready to sign in.
dev: build-ui stack-up pds-start seed
@echo ""
@echo "Ready. Open {{base}}/account and sign in with the credentials above."
@echo "This serves a prebuilt UI (no hot reload). For live UI work: just dev-live"
# Live-reload dev: infra + PDS (backend --watch) + Vite HMR for the account UI.
dev-live: stack-up pds-start seed
@echo ""
@echo "PDS on {{base}} (backend hot-restarts). Starting Vite HMR for the UI…"
@echo "Open http://localhost:5173/account and sign in with the credentials above."
npm run dev --workspace=@pdsjs/account-ui
# Two-account dev for permissioned data and multi-writer git: alice's stack
# plus bob on his own PDS (:{{bob_port}}), both registered in the local PLC
# with host-reachable endpoints so the two servers can notify each other. The
# tradeoff: the docker relay cannot crawl endpoints on localhost, so firehose
# consumers in the compose stack see nothing from these accounts.
dev-multi: stack-up pds-start
PDS_DEV_PDS_URL={{base}} node scripts/dev-seed.mjs {{handle}}
just bob-start
@echo ""
@echo "alice on {{base}}, bob on http://localhost:{{bob_port}} (DID in {{data}}/bob/credentials-bob.json)."
@echo "Both use the password test-password. Starting Vite HMR for the UI…"
npm run dev --workspace=@pdsjs/account-ui
# The repository browser with a repository, a package, an image and a history
# of check runs to read.
dev-git: plc-up (pds-start "localhost:2471") (seed base) git-demo git-checks
@echo ""
@echo "Open http://localhost:{{ui_port}} — the repository is demo-app."
@echo "The account page is {{base}}/account (handle {{handle}}.localhost, password test-password)."
PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/git-ui -- --port {{ui_port}} --strictPort
# The space browser with a space to read: alice hosts it, bob writes into it
# from his own PDS, and the page reaches bob's repo the way the proposal says
# to — a delegation token from alice's server, a credential from the authority,
# then a read addressed straight at bob's.
dev-spaces: plc-up (pds-start "localhost:2471") (seed base) bob-start spaces-demo
@echo ""
@echo "Open http://localhost:{{spaces_ui_port}} and sign in with alice's DID above."
@echo "A .localhost handle resolves nowhere, so the DID is what this stack signs in with."
PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/spaces-ui -- --port {{spaces_ui_port}} --strictPort
# The repository browser with one repository worked on from five accounts, so
# its pull request list has open branches, a rebase with two versions, a
# conflict, reviews and a fork to read. Five servers, because a PDS answers for
# a single DID.
dev-git-collab: plc-up (pds-start "localhost:2471") (seed base) (member-start "bob" bob_port) (member-start "carol" carol_port) (member-start "nova" nova_port) (member-start "dan" dan_port) git-collab-demo
@echo ""
@echo "Open http://localhost:{{ui_port}}/demo-app/pulls"
PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/git-ui -- --port {{ui_port}} --strictPort
# Serve one real account's atproto git repositories through git-host: resolve
# the handle's PDS from the directory, build the browser app, and open the
# forge. `git-host-stop` tears it down. The forge reads the account's PDS over
# the public XRPC surface, so any account with dev.pdsjs.git.* records works.
git-host-for handle="grain.social":
#!/usr/bin/env bash
set -euo pipefail
npm run build --workspace=@pdsjs/git-ui
resolved="$(curl -sS -w '\n%{http_code}' "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle={{handle}}")"
code="${resolved##*$'\n'}"
body="${resolved%$'\n'*}"
if [[ "$code" != "200" ]] || [[ -z "$body" ]]; then
echo "could not resolve {{handle}}: $body"; exit 1
fi
did="$(node -pe "JSON.parse(process.argv[1]).did" "$body")"
if [[ "$did" == did:plc:* ]]; then
doc="$(curl -fsS "https://plc.directory/$did/data")"
pds="$(node -pe "JSON.parse(process.argv[1]).services.atproto_pds.endpoint" "$doc")"
else
host="${did#did:web:}"; host="${host//:/\/}"
doc="$(curl -fsS "https://$host/.well-known/did.json")"
pds="$(node -pe "JSON.parse(process.argv[1]).service.find(s => s.id === 'atproto_pds').serviceEndpoint" "$doc")"
fi
echo "{{handle}} -> $did @ $pds"
mkdir -p {{gh_dir}}
lsof -ti tcp:{{gh_port}} | xargs kill 2>/dev/null || true
rm -f {{gh_dir}}/forge.sqlite
nohup node packages/git-host/src/cli.js \
--pds "$pds" \
--account "$did" \
--db {{gh_dir}}/forge.sqlite \
--port {{gh_port}} \
--ui packages/git-ui/dist \
--no-watch \
> {{gh_dir}}/forge.log 2>&1 &
echo $! > {{gh_dir}}/forge.pid
disown || true
echo -n "Waiting for {{handle}}'s forge on :{{gh_port}}"
for _ in $(seq 1 30); do
if curl -fsS "http://localhost:{{gh_port}}/xrpc/dev.pdsjs.git.listRepos" >/dev/null 2>&1; then echo " ready"; break; fi
echo -n "."; sleep 1
done
open "http://localhost:{{gh_port}}/"
# Stop the forge started by git-host-for.
git-host-stop:
[ -f {{gh_dir}}/forge.pid ] && kill "$(cat {{gh_dir}}/forge.pid)" 2>/dev/null || true
lsof -ti tcp:{{gh_port}} | xargs kill 2>/dev/null || true
rm -f {{gh_dir}}/forge.pid
# One repository, four accounts, and the records the pull request list reads.
git-collab-demo:
PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-collab-demo.mjs
# The same four accounts, working on this repository rather than a toy one:
# changes of tens of files over a dozen commits, which is the size an agent
# writes. Run it after dev-git-collab, against the servers that are already up.
git-scale-demo:
PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-scale-demo.mjs
# A space, a second member on another PDS, and records from both.
spaces-demo:
node scripts/dev-spaces-demo.mjs
# A repository, an npm package, an image, and the record that links them.
git-demo:
PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-demo.mjs
# A few more commits, and the check records a runner would publish about them.
git-checks:
PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-checks.mjs
# Start the local PLC alone, for a stack that needs no relay.
plc-up:
#!/usr/bin/env bash
set -euo pipefail
docker compose up -d plc
echo -n "Waiting for local PLC"
for _ in $(seq 1 30); do
if curl -fsS {{plc}}/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
echo -n "."; sleep 1
done
echo " timed out"; exit 1
# Start bob's PDS in the background and register him in the local PLC.
bob-start: (member-start "bob" bob_port)
# Start one more account's PDS in the background, on its own port, and
# register it in the local PLC. Each account is one server: pds.js answers for
# a single DID, so a second contributor means a second process.
member-start name port:
#!/usr/bin/env bash
set -euo pipefail
lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
rm -rf {{data}}/{{name}}
mkdir -p {{data}}/{{name}}/blobs
PORT={{port}} \
HOSTNAME=localhost:{{port}} \
JWT_SECRET=dev-secret-{{name}} \
PDS_PASSWORD=test-password \
PDS_ENABLE_SPACES=true \
PDS_EXPERIMENTAL_GIT_HTTP=true \
PLC_URL=http://localhost:2582 \
RELAY_URL=http://localhost:2470 \
APPVIEW_URL=https://api.bsky.app \
APPVIEW_DID=did:web:api.bsky.app \
PDS_DB_PATH={{data}}/{{name}}/pds.db \
PDS_BLOBS_DIR={{data}}/{{name}}/blobs \
nohup node --watch examples/node/index.js > {{data}}/{{name}}/pds.log 2>&1 &
echo $! > {{data}}/{{name}}/pds.pid
disown || true
echo -n "Waiting for {{name}}'s PDS on :{{port}}"
for _ in $(seq 1 30); do
if curl -fsS http://localhost:{{port}}/xrpc/_health >/dev/null 2>&1; then echo " ready"; break; fi
echo -n "."; sleep 1
done
(cd {{data}}/{{name}} && PDS_PASSWORD=test-password node ../../scripts/setup.js --pds http://localhost:{{port}} --plc-url http://localhost:2582 --handle {{name}} >/dev/null)
node -e "console.log('{{name}}:', JSON.parse(require('fs').readFileSync('{{data}}/{{name}}/credentials-{{name}}.json')).did)"
# Build the account UI (the PDS serves its prebuilt assets).
build-ui:
npm run build:ui
# Start the docker infra with clean volumes and wait for the local PLC.
stack-up:
#!/usr/bin/env bash
set -euo pipefail
docker compose down -v >/dev/null 2>&1 || true
docker compose up -d
echo -n "Waiting for local PLC"
for _ in $(seq 1 30); do
if curl -fsS http://localhost:2582/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
echo -n "."; sleep 1
done
echo " timed out"; exit 1
# Start the example PDS in the background (host port {{port}}), on the hostname the account answers to.
pds-start hostname="host.docker.internal:3443":
#!/usr/bin/env bash
set -euo pipefail
lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
rm -rf {{data}}
mkdir -p {{data}}/blobs
PORT={{port}} \
HOSTNAME={{hostname}} \
JWT_SECRET=dev-secret \
PDS_PASSWORD=test-password \
PDS_ENABLE_SPACES=true \
PDS_EXPERIMENTAL_GIT_HTTP=true \
PDS_EXPERIMENTAL_OCI=true \
PDS_EXPERIMENTAL_NPM=true \
PLC_URL={{plc}} \
RELAY_URL=http://localhost:2470 \
APPVIEW_URL=https://api.bsky.app \
APPVIEW_DID=did:web:api.bsky.app \
PDS_DB_PATH={{data}}/pds.db \
PDS_BLOBS_DIR={{data}}/blobs \
nohup node --watch examples/node/index.js > {{log}} 2>&1 &
echo $! > {{pidfile}}
disown || true
echo -n "Waiting for PDS on :{{port}}"
for _ in $(seq 1 30); do
if curl -fsS {{base}}/xrpc/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
echo -n "."; sleep 1
done
echo " timed out — see {{log}}"; exit 1
# Register a mock user in the local PLC and seed records, at the PDS URL the PLC records.
seed pds_url="https://host.docker.internal:3443":
PDS_DEV_PDS_URL={{pds_url}} node scripts/dev-seed.mjs {{handle}}
# Tail the PDS log.
logs:
tail -f {{log}}
# Stop the PDS and every member started beside it (leaves docker infra up).
#
# Each server runs under `node --watch`, so killing the child alone leaves a
# supervisor that restarts it on the next file change, on a port the test
# suite wants. The pid files name the supervisors.
pds-stop:
#!/usr/bin/env bash
[ -f {{pidfile}} ] && kill "$(cat {{pidfile}})" 2>/dev/null || true
lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
rm -f {{pidfile}}
for member in {{data}}/*/pds.pid; do
[ -f "$member" ] || continue
pid="$(cat "$member")"
pkill -P "$pid" 2>/dev/null || true
kill "$pid" 2>/dev/null || true
rm -f "$member"
done
# Stop the PDS and tear down the docker infra (with volumes).
dev-down: pds-stop
docker compose down -v