run

tcmts.dev.devenv.run · #r227g4na · sh file

#!/bin/bash
# tcm's AT Protocol flows against Bluesky's own software, run locally and offline: dev-env's PLC directory and PDS
# (its OAuth server, consent pages and repositories), with bridge.mjs, fed by the PDS's firehose, standing in for
# Jetstream and a relay's listReposByCollection (dev-env has neither). `run.sh <tcm.js>`, in a directory holding
# these files: it installs dev-env there once (not as tcm's dependency), starts the network, and checks
#   1. tcm release --announce (loopback OAuth, PKCE, DPoP, granular repo: scopes) writes the record;
#   2. tcm lib add @handle/pkg and tcm lib upgrade, through the PLC and handle resolution;
#   3. tcm lib attest (review, yank, a yank refused from someone else), shown by tcm lib;
#   4. a kept sign-in refreshed, and its scope enforced by the PDS;
#   5. tcm registry: catch-up, getRecord proofs checked, a visitor signing in (browser OAuth) and starring, Libraries I
#      use, live events, and tcm lib search;
#   6. publishing tcm's lexicons (tcmts.dev.publishLexicons, from tcm's codebase: $TCM_TRIAL_CODEBASE, on
#      $TCM_TRIAL_BRANCH), as alice standing in for tcm.codes: the PDS takes the lexicon scope and the schemas.
# Needs Node, npm, curl and Chrome (or $CHROME). What's said along the way is in *.log and shots/.
set -euo pipefail
TCM_JS=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
DIR=$(cd "$(dirname "$0")" && pwd)
cd "$DIR"
fail() { printf 'FAILED: %s\n' "$*" >&2; exit 1; }
expect() { printf '%s' "$3" | grep -Eq -- "$2" || fail "$(printf '%s: expected /%s/ in:\n%s' "$1" "$2" "$3")"; echo "ok: $1"; }
waitfor() { for _ in $(seq 1 "${3:-120}"); do eval "$2" >/dev/null 2>&1 && return 0; sleep 1; done; fail "timed out waiting for $1"; }
port() { node -e 'const s=require("net").createServer().listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})'; }
json() { node -p "JSON.parse(require('fs').readFileSync('env.json','utf8'))$1"; }
trap 'kill $(jobs -p) 2>/dev/null || true' EXIT

if [ ! -d node_modules/@atproto/dev-env ]; then
  echo "installing @atproto/dev-env here (once)"
  # (@atproto/lex-schema 0.2.10 exports a walk that clashes with @atproto/lex's own: dev-env doesn't load with it)
  printf '{"name":"tcm-devenv","private":true,"type":"module","overrides":{"@atproto/lex-schema":"0.2.9"}}\n' > package.json
  npm install @atproto/dev-env@0.7.1 puppeteer-core@24 --no-audit --no-fund --loglevel=error
fi
rm -rf env.json config geo app registrydir shots ./*.log
node net.mjs > net.log 2>&1 &
waitfor "the network" "[ -s env.json ]"
node bridge.mjs > bridge.log 2>&1 &
waitfor "the bridge" "grep -q bridge env.json"
PDS=$(json .pds); BRIDGE=$(json .bridge); ALICE=$(json '.accounts["alice.test"].did'); MAYA=$(json '.accounts["maya.test"].did')
# (dev-env's servers are on this machine, over http: allowed only on a local network)
export TCM_ATPROTO_DIRECTORY=$(json .directory) TCM_ATPROTO_LOCAL=1 XDG_CONFIG_HOME=$DIR/config TCM_AUTHOR=ada BROWSER="node $DIR/yes.mjs"
unset TCM_CODEBASE TCM_BRANCH JETSTREAM_API_KEY
tcm() { node "$TCM_JS" "$@" 2>&1; }

echo "== 1. announcing a release"
mkdir geo app
(cd geo && tcm init >/dev/null && printf '// tcm: namespace geo\n/** Plane geometry. */\nexport const manifest = { name: "geo", description: "Shapes and their areas.", license: "MIT" };\n/** A square'"'"'s area. */\nexport const area = (side: number) => side * side;\n' > scratch.ts && tcm add >/dev/null)
GEO=$(port)
(cd geo && exec node "$TCM_JS" serve --public --port "$GEO" > ../serve-geo.log 2>&1) &
waitfor "geo's server" "curl -s -o /dev/null http://127.0.0.1:$GEO/"
out=$(cd geo && tcm release geo@1.0.0 --announce --as alice.test --source "http://127.0.0.1:$GEO")
expect "the PDS took the loopback client and its repo:codes.tcm.* scopes, and the release was announced" "announced geo@1.0.0 as @alice.test" "$out"
expect "its consent page asked to publish to the repository" "Publish changes" "$(cat shots/cli-*-consent.txt)"
expect "the record is in alice's repository" '"version":"1.0.0"' "$(curl -s "$PDS/xrpc/com.atproto.repo.getRecord?repo=$ALICE&collection=codes.tcm.release&rkey=geo:1.0.0")"
(cd geo && printf '// tcm: namespace geo\n/** A cube'"'"'s volume. */\nexport const cube = (side: number) => side ** 3;\n' > scratch.ts && tcm add >/dev/null)
expect "a second release, on the kept sign-in" "announced geo@1.1.0" "$(cd geo && BROWSER=false tcm release --announce)"

echo "== 2. adding and upgrading a library by its announcement"
(cd app && tcm init >/dev/null && printf '// tcm: namespace app\nexport const total = () => 1;\n' > scratch.ts && tcm add >/dev/null)
expect "tcm lib add, through the PLC and handle resolution" "added app.lib.geo: geo@1.0.0 by @alice.test" "$(cd app && tcm lib add @alice.test/geo@1.0.0)"
expect "tcm lib upgrade, by alice's DID" "upgraded app.lib.geo to geo@1.1.0" "$(cd app && tcm lib upgrade geo)"

echo "== 3. reviews and yanks"
expect "maya reviews it" "reviewed geo@1.1.0 .* as @maya.test" "$(cd app && tcm lib attest @alice.test/geo@1.1.0 review -m 'Read all of cube.' --as maya.test)"
expect "alice yanks 1.0.0" "yanked geo@1.0.0 .* as @alice.test" "$(cd geo && tcm lib attest geo@1.0.0 yank -m 'use 1.1.0' --as alice.test)"
expect "maya can't yank alice's release" "only its author can yank" "$(cd app && tcm lib attest @alice.test/geo@1.0.0 yank -m 'not mine' --as maya.test || true)"
(cd app && tcm lib trust --add @maya.test >/dev/null)
expect "tcm lib shows maya's review, once trusted" "reviewed by @maya.test: Read all of cube." "$(cd app && tcm lib)"
expect "adding 1.0.0 says it's yanked" "yanked by its author @alice.test: use 1.1.0" "$(cd app && tcm lib add @alice.test/geo@1.0.0 --at app.lib.geo0)"

echo "== 4. a kept sign-in, refreshed, and its scope"
node -e 'const f="config/tcm/atproto.json",fs=require("fs"),s=JSON.parse(fs.readFileSync(f));Object.values(s.sessions).find(x=>x.handle==="alice.test").expiresAt=Date.now()-1000;fs.writeFileSync(f,JSON.stringify(s,null,2))'
expect "an expired access token is refreshed, with no browser" "reviewed geo@1.1.0 .* as @alice.test" "$(cd geo && BROWSER=false tcm lib attest geo@1.1.0 review -m 'Checked it myself.' --as alice.test)"
probe=$(node scope-probe.mjs alice.test)
expect "the PDS refuses a post on tcm's token" "app.bsky.feed.post: 403 .*ScopeMissingError" "$probe"
expect "and a star, which announcing doesn't ask for" "codes.tcm.star .*: 403" "$probe"
expect "and allows a review" "codes.tcm.review .*: 200" "$probe"

echo "== 5. the registry"
REGISTRY=$(port); APP=$(port)
(cd app && exec node "$TCM_JS" serve --public --port "$APP" > ../serve-app.log 2>&1) &
TCM_JETSTREAM=$BRIDGE TCM_RELAY=$BRIDGE TCM_REGISTRY_EVERY=5s node "$TCM_JS" registry --port "$REGISTRY" --dir "$DIR/registrydir" > registry.log 2>&1 &
waitfor "the registry to start" "grep -qE 'jobs +recurring' registry.log" 600
waitfor "both releases mirrored and checked" "node page.mjs http://127.0.0.1:$REGISTRY/@alice.test/geo/1.0.0 | grep -q 'signed by @alice.test'" 180
lib=$(node page.mjs "http://127.0.0.1:$REGISTRY/@alice.test/geo/1.0.0")
expect "getRecord proofs from the real PDS check out" "✓ signed by @alice.test" "$lib"
expect "alice's yank shows" "Yanked by @alice.test: use 1.1.0" "$lib"
waitfor "the reviews" "node page.mjs 'http://127.0.0.1:$REGISTRY/?show=reviews' | grep -q 'Read all of cube'" 60
expect "a record that isn't a review is left out" "Reviews 2" "$(node page.mjs "http://127.0.0.1:$REGISTRY/")"
visit=$(node visitor.mjs "http://127.0.0.1:$REGISTRY" "http://127.0.0.1:$APP")
echo "$visit" | sed 's/^/  /'
expect "a visitor signs in with browser OAuth" "signed in, back at .*@maya.test" "$visit"
expect "starring writes to her repository, and the registry counts it" "after starring: ★ Starred 1; the PDS's getRecord: 200" "$visit"
expect "taking it back deletes it" "after taking it back: ☆ Star 0; the PDS's getRecord: 400" "$visit"
expect "Libraries I use, from her codebase" "Libraries I use, with http://127.0.0.1:$APP: [^|]+(\|[^|]+)+" "$visit"
expect "the page said nothing wrong (but the PDS asking for a DPoP nonce)" "page errors: (none|400 POST [^ ]+/oauth/par: \{\"error\":\"use_dpop_nonce\"[^|]*)$" "$visit"
(cd app && tcm lib attest @alice.test/geo@1.0.0 advisory --severity high --fixed-in 1.1.0 -m 'area is wrong for negatives' --as maya.test >/dev/null)
waitfor "the advisory, live" "node page.mjs 'http://127.0.0.1:$REGISTRY/?show=advisories' | grep -q 'area is wrong for negatives'" 60
echo "ok: a new advisory arrives live"
expect "tcm lib search asks the registry" "geo\.area +\(side: number\) => number" "$(cd app && tcm lib search area --registry "http://127.0.0.1:$REGISTRY")"
echo "== 6. publishing tcm's lexicons"
# (as its maintainers run it, from tcm's codebase; alice stands in for tcm.codes, whose _lexicon DNS isn't this network's)
publish() { (cd "$TCM_TRIAL_CODEBASE" && TCM_BRANCH="$TCM_TRIAL_BRANCH" TCM_LEXICON_DID=$ALICE node "$TCM_JS" run tcmts.dev.publishLexicons -- "$@" 2>&1); }
expect "signing in as anyone but the authority's DID is refused" "maya.test is .*not publishing as them" "$(publish --as maya.test || true)"
out=$(publish --as alice.test)
expect "the PDS took the repo:com.atproto.lexicon.schema scope and the five schemas" "published at://$ALICE/com.atproto.lexicon.schema/codes.tcm.star" "$out"
expect "its consent page asked to publish to the repository" "Publish changes" "$(cat "$(ls -t shots/cli-*-consent.txt | head -1)")"
expect "a schema is in alice's repository" '"id":"codes.tcm.release"' "$(curl -s "$PDS/xrpc/com.atproto.repo.getRecord?repo=$ALICE&collection=com.atproto.lexicon.schema&rkey=codes.tcm.release")"
expect "publishing again writes nothing" "nothing to do" "$(BROWSER=false publish)"
jwt=$(curl -s -X POST -H 'content-type: application/json' -d "{\"identifier\":\"alice.test\",\"password\":\"$(json '.accounts["alice.test"].password')\"}" "$PDS/xrpc/com.atproto.server.createSession" | node -pe 'JSON.parse(require("fs").readFileSync(0)).accessJwt')
curl -sf -X POST -H "authorization: Bearer $jwt" -H 'content-type: application/json' -d "{\"repo\":\"$ALICE\",\"collection\":\"com.atproto.lexicon.schema\",\"rkey\":\"codes.tcm.star\",\"record\":{\"\$type\":\"com.atproto.lexicon.schema\",\"lexicon\":1,\"id\":\"codes.tcm.star\",\"defs\":{\"main\":{\"type\":\"token\"}}}}" "$PDS/xrpc/com.atproto.repo.putRecord" >/dev/null
out=$(BROWSER=false publish)
expect "a changed schema is replaced, on the kept sign-in" "updated at://$ALICE/com.atproto.lexicon.schema/codes.tcm.star" "$out"
expect "and the others left as they are" "unchanged +codes.tcm.release" "$out"
echo "all passed"

used by