atproto pds in zig pds.zat.dev
pds atproto
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491#!/usr/bin/env shset -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)work=$(mktemp -d "${TMPDIR:-/tmp}/zds-smoke.XXXXXX")port="${ZDS_SMOKE_PORT:-2585}"db="${work}/zds-smoke.sqlite3"blob_root="${work}/zds-smoke-blobs"log="${work}/zds-smoke.log"base="http://127.0.0.1:${port}"jwt_secret="smoke-jwt-secret"
cleanup() { status=$? if [ -n "${server_pid:-}" ]; then kill "$server_pid" 2>/dev/null || true wait "$server_pid" 2>/dev/null || true fi if [ -n "${plc_stub_pid:-}" ]; then kill "$plc_stub_pid" 2>/dev/null || true wait "$plc_stub_pid" 2>/dev/null || true fi if [ "$status" -eq 0 ]; then rm -rf "$work" else echo "Smoke failed; diagnostic files: $work" >&2 fi}trap cleanup EXITtrap 'exit 130' INTtrap 'exit 143' TERM
cd "$root"rm -f "$db" "$db-wal" "$db-shm" "$log"rm -rf "$blob_root"mkdir -p "$blob_root"
if [ "${ZDS_SMOKE_PREBUILT:-0}" != 1 ]; then zig build smoke-buildfi
# stand-in PLC directory so createAccount genesis never reaches plc.directory;# it also hosts a public OAuth client's metadata so PAR can be exercised, and# stands in for the default appview so proxying never reaches api.bsky.app.plc_port="${ZDS_SMOKE_PLC_PORT:-2586}"client_id="http://127.0.0.1:${plc_port}/oauth-client-metadata.json"python3 -c "import jsonfrom http.server import BaseHTTPRequestHandler, HTTPServermetadata = json.dumps({ 'client_id': '$client_id', 'client_name': 'smoke', 'redirect_uris': ['http://127.0.0.1/callback'], 'scope': 'atproto repo:*', 'grant_types': ['authorization_code', 'refresh_token'], 'response_types': ['code'], 'token_endpoint_auth_method': 'none', 'application_type': 'native', 'dpop_bound_access_tokens': True,}).encode()feed = json.dumps({'feed': [], 'cursor': 'smoke-appview'}).encode()class Stub(BaseHTTPRequestHandler): def do_GET(self): if self.path.startswith('/xrpc/app.bsky.feed.getAuthorFeed') and self.headers.get('authorization', '').startswith('Bearer '): body = feed elif self.path == '/oauth-client-metadata.json': body = metadata else: self.send_response(404) self.end_headers() return self.send_response(200) self.send_header('content-type', 'application/json') self.send_header('content-length', str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self): self.rfile.read(int(self.headers.get('content-length', 0))) self.send_response(200) self.end_headers() def log_message(self, *args): passHTTPServer(('127.0.0.1', $plc_port), Stub).serve_forever()" &plc_stub_pid=$!
# run the installed binary directly so $! is the server itself; killing the# `zig build run` wrapper orphans zds and leaves the port taken for the next runZDS_PLC_ROTATION_KEY=1111111111111111111111111111111111111111111111111111111111111111 \ZDS_PROXY_SERVICE_URL="http://127.0.0.1:${plc_port}" \./zig-out/bin/zds \ --host 127.0.0.1 \ --jwt-secret "$jwt_secret" \ --port "$port" \ --db "$db" \ --blobstore-path "$blob_root" \ --public-url "$base" \ --server-did did:web:localhost \ --handle-domains .test \ --invite-required \ --admin-token smoke-admin-token \ --operator-handle smoke-operator.test \ --plc-directory "http://127.0.0.1:${plc_port}" \ >"$log" 2>&1 &server_pid=$!
i=0while [ "$i" -lt 80 ]; do if curl -fsS "$base/xrpc/_health" >/dev/null 2>&1; then break fi i=$((i + 1)) sleep 0.1done
curl -fsS "$base/xrpc/_health" >/dev/null# A normal build must never pick up cached operator drawings implicitly.account_page=$(curl -fsS "$base/account/manage")if printf '%s' "$account_page" | grep -q '/ui/icon?cid='; then echo 'unconfigured build unexpectedly includes custom drawings' >&2 exit 1fitest "$(curl -sS -o /dev/null -w '%{http_code}' "$base/ui/icon?cid=bafkreidhkabyea5vkonzvfdjvgbinqt76kovuli7hrwqqffx5cx2nf4chi")" = 404stats_page=$(curl -fsS "$base/stats")printf '%s' "$stats_page" | grep -q '<h1>pds health</h1>'printf '%s' "$stats_page" | grep -q '<h2>writes</h2>'curl -fsS "$base/xrpc/com.atproto.server.describeServer" >/dev/nullsqlite3 "$db" "insert into accounts (did, handle, email, password_hash, activated_at, email_confirmed_at) values ('did:plc:smoketest', 'smoke.test', 'smoke@test.com', 'password', unixepoch(), unixepoch())"sqlite3 "$db" "insert into oauth_requests (request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, login_hint, expires_at) values ('smoke-oauth', 'https://client.example/oauth-client.json', 'https://client.example/callback', 'repo:*?action=create blob:*/*', 'state', 'challenge', 'S256', 'smoke.test', unixepoch() + 600)"
resolved=$(curl -fsS "$base/xrpc/com.atproto.identity.resolveHandle?handle=smoke.test")printf '%s' "$resolved" | grep -q '"did":"did:plc:smoketest"'well_known=$(curl -fsS -H 'host: smoke.test' "$base/.well-known/atproto-did")test "$well_known" = "did:plc:smoketest"unsupported_well_known_status=$(curl -sS -o "$work/zds-unsupported-handle.txt" -w '%{http_code}' \ -H 'host: smoke.example.com' "$base/.well-known/atproto-did")test "$unsupported_well_known_status" = "404"
session=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createSession" \ -H 'content-type: application/json' \ --data '{"identifier":"smoke.test","password":"password"}')token=$(printf '%s' "$session" | sed -n 's/.*"accessJwt":"\([^"]*\)".*/\1/p')test -n "$token"printf '%s' "$session" | grep -q '"didDoc":'! printf '%s' "$session" | grep -q '"status":'printf '%s' "$session" | grep -q '"serviceEndpoint":"http://127.0.0.1:'session_did=$(printf '%s' "$session" | jq -r .did)# Grouped account access uses the same password-session boundary and validates paging.access_page=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/dev.zat.account.listAccess?kind=password&limit=1")printf '%s' "$access_page" | jq -e '.authorized >= 1 and (.groups | length) == 1 and (.connections | length) == 0' >/dev/nullaccess_group=$(printf '%s' "$access_page" | jq -r '.groups[0].key')curl -fsS -G -H "authorization: Bearer $token" --data-urlencode "kind=password" --data-urlencode "group=$access_group" "$base/xrpc/dev.zat.account.listAccess" | jq -e --arg did "$session_did" '.connections | length > 0 and all(.[]; .did == $did)' >/dev/nullfor query in 'kind=unknown' 'state=unknown' 'cursor=-1' 'cursor=nope' 'limit=0' 'limit=101'; do test "$(curl -sS -o /dev/null -w '%{http_code}' -H "authorization: Bearer $token" "$base/xrpc/dev.zat.account.listAccess?$query")" = 400donetest "$(curl -sS -o /dev/null -w '%{http_code}' "$base/xrpc/dev.zat.account.listAccess")" = 401# an access token this server signed whose exp has passed must answer 400# ExpiredToken (the reference PDS's answer, which clients key on to refresh),# while a token that never verified stays 401 InvalidToken.expired_token=$(python3 -c "import base64, hashlib, hmac, json, timedef b64(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()header = b64(json.dumps({'alg': 'HS256', 'typ': 'JWT'}).encode())now = int(time.time())payload = b64(json.dumps({'sub': '$session_did', 'scope': 'com.atproto.access', 'iat': now - 7200, 'exp': now - 3600, 'jti': 'zds-smoke-expired'}).encode())sig = hmac.new(b'$jwt_secret', f'{header}.{payload}'.encode(), hashlib.sha256).digest()print(f'{header}.{payload}.{b64(sig)}')")expired_body="${work}/zds-smoke-expired.json"expired_status=$(curl -sS -o "$expired_body" -w '%{http_code}' \ -H "authorization: Bearer $expired_token" "$base/xrpc/com.atproto.server.getSession")test "$expired_status" = "400"grep -q '"error":"ExpiredToken"' "$expired_body"garbage_status=$(curl -sS -o "$expired_body" -w '%{http_code}' \ -H "authorization: Bearer ${expired_token}x" "$base/xrpc/com.atproto.server.getSession")test "$garbage_status" = "401"grep -q '"error":"InvalidToken"' "$expired_body"
invalid_auth_headers="${work}/zds-smoke-invalid-auth.headers"invalid_auth_body="${work}/zds-smoke-invalid-auth.json"invalid_auth_status=$(curl -sS -D "$invalid_auth_headers" -o "$invalid_auth_body" -w '%{http_code}' \ -H 'authorization: Bearer invalid-token' \ "$base/xrpc/com.atproto.server.getSession")test "$invalid_auth_status" = "401"grep -qi '^www-authenticate: Bearer error="invalid_token", error_description="Token is invalid"' "$invalid_auth_headers"
invalid_dpop_status=$(curl -sS -D "$invalid_auth_headers" -o "$invalid_auth_body" -w '%{http_code}' \ -H 'authorization: DPoP invalid-token' \ "$base/xrpc/com.atproto.server.getSession")test "$invalid_dpop_status" = "401"grep -qi '^www-authenticate: DPoP error="invalid_token", error_description="Token is invalid"' "$invalid_auth_headers"grep -qi '^dpop-nonce:' "$invalid_auth_headers"
# PAR scope validation must terminate the request: an unregistered scope is a# 400 invalid_scope, never a 201 (a helper once wrote the error and returned# success, and the handler overwrote it with a request_uri).par_body="${work}/zds-smoke-par.json"# the client is native and registers a port-less loopback redirect, so PAR# sending the port it actually bound is the RFC 8252 exception at work; the# path still has to match exactly.par() { par_redirect="${par_redirect:-http://127.0.0.1:${plc_port}/callback}" curl -sS -o "$par_body" -w '%{http_code}' -X POST "$base/oauth/par" \ --data-urlencode "client_id=${par_client:-$client_id}" \ --data-urlencode "redirect_uri=$par_redirect" \ --data-urlencode 'response_type=code' \ --data-urlencode 'code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' \ --data-urlencode 'code_challenge_method=S256' \ --data-urlencode 'state=x' \ --data-urlencode "scope=$1"}test "$(par_redirect="http://127.0.0.1:${plc_port}/elsewhere" par 'atproto repo:*')" = "400"grep -q 'redirect_uri not registered' "$par_body"test "$(par 'atproto garbage:thing')" = "400"grep -q '"error":"invalid_scope"' "$par_body"test "$(par 'atproto include:com.example.nope')" = "400"grep -q '"error":"invalid_scope"' "$par_body"test "$(par 'atproto transition:generic repo:*')" = "400"grep -q '"error":"invalid_scope"' "$par_body"# a valid scope gets past validation to the DPoP check (no proof was sent)test "$(par 'atproto repo:*')" = "400"grep -q '"error":"use_dpop_nonce"' "$par_body"
# the spec's localhost client development exception: an `http://localhost`# client_id (no port) is virtual, so nothing is fetched and its metadata comes# from the query string. Reaching the DPoP check proves metadata, redirect, and# scope all resolved.test "$(par_client='http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A4444%2Fcb&scope=atproto%20repo%3A*' par_redirect='http://127.0.0.1:4444/cb' par 'atproto repo:*')" = "400"grep -q '"error":"use_dpop_nonce"' "$par_body"test "$(par_client='http://localhost' par_redirect='http://127.0.0.1:5199/' par 'atproto')" = "400"grep -q '"error":"use_dpop_nonce"' "$par_body"test "$(par_client='http://localhost?scope=atproto' par_redirect='http://[::1]:5199/' par 'atproto repo:*')" = "400"grep -q '"error":"invalid_scope"' "$par_body"test "$(par_client='http://localhost?redirect_uri=https%3A%2F%2Fevil.example%2F' par_redirect='https://evil.example/' par 'atproto')" = "400"grep -q 'Invalid loopback client_id' "$par_body"test "$(par_client='http://localhost?redirect_uri=http%3A%2F%2Flocalhost%3A4444%2Fcb' par_redirect='http://localhost:4444/cb' par 'atproto')" = "400"grep -q 'Invalid loopback client_id' "$par_body"# a port makes it an ordinary URL client, which has to be fetchedtest "$(par_client='http://localhost:1' par_redirect='http://127.0.0.1:5199/' par 'atproto')" = "400"grep -q 'Could not fetch client metadata' "$par_body"
describe=$(curl -fsS "$base/xrpc/com.atproto.server.describeServer")printf '%s' "$describe" | grep -q '"inviteCodeRequired":true'missing_invite_status=$(curl -sS -o "$work/zds-missing-invite.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.server.createAccount" \ -H 'content-type: application/json' \ --data '{"handle":"missing-invite.test","email":"missing-invite@test.com","password":"password"}')test "$missing_invite_status" = "400"grep -q '"error":"InvalidInviteCode"' "$work/zds-missing-invite.json"invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \ -H "authorization: Bearer smoke-admin-token" \ -H 'content-type: application/json' \ --data '{"useCount":1,"forAccount":"did:plc:smoketest"}')printf '%s' "$invite" | grep -q '"code":"127-0-0-1-'invites=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.server.getAccountInviteCodes")printf '%s' "$invites" | grep -q '"available":1'
signup_page=$(curl -fsS "$base/signup")printf '%s' "$signup_page" | grep -q '<title>join zds</title>'printf '%s' "$signup_page" | grep -q 'com.atproto.server.describeServer'printf '%s' "$signup_page" | grep -q 'com.atproto.server.createAccount'printf '%s' "$signup_page" | grep -q 'DM <a href="https://bsky.app/profile/smoke-operator.test"'signup_invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \ -H "authorization: Bearer smoke-admin-token" \ -H 'content-type: application/json' \ --data '{"useCount":1}' | jq -r .code)signup=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createAccount" \ -H 'content-type: application/json' \ --data "$(printf '{"handle":"signup-smoke.test","email":"signup-smoke@test.com","password":"signup-password","inviteCode":"%s"}' "$signup_invite")")printf '%s' "$signup" | grep -q '"handle":"signup-smoke.test"'printf '%s' "$signup" | grep -q '"accessJwt":'printf '%s' "$signup" | grep -q '"refreshJwt":'signup_did=$(printf '%s' "$signup" | jq -r .did)test -n "$signup_did"jwt_jti() { printf '%s' "$1" | cut -d. -f2 | python3 -c 'import base64, json, sys; p = sys.stdin.read().strip(); print(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["jti"])'}refresh_session() { curl -sS -o "$2" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.server.refreshSession" -H "authorization: Bearer $1"}# refresh rotates the pair; the retired refresh token replayed inside the grace# window gets the same successor (a client racing itself must not be logged out),# and once the successor itself rotates the retired token is refused as expired.signup_refresh=$(printf '%s' "$signup" | jq -r .refreshJwt)refresh_body="${work}/zds-smoke-refresh.json"test "$(refresh_session "$signup_refresh" "$refresh_body")" = "200"rotated_access=$(jq -r .accessJwt "$refresh_body")rotated_refresh=$(jq -r .refreshJwt "$refresh_body")test "$(refresh_session "$signup_refresh" "$refresh_body")" = "200"test "$(jwt_jti "$(jq -r .refreshJwt "$refresh_body")")" = "$(jwt_jti "$rotated_refresh")"test "$(jwt_jti "$(jq -r .accessJwt "$refresh_body")")" = "$(jwt_jti "$rotated_access")"curl -fsS -H "authorization: Bearer $(jq -r .accessJwt "$refresh_body")" "$base/xrpc/com.atproto.server.getSession" | grep -q "\"did\":\"$signup_did\""test "$(refresh_session "$rotated_refresh" "$refresh_body")" = "200"signup_access=$(jq -r .accessJwt "$refresh_body")test "$(refresh_session "$signup_refresh" "$refresh_body")" = "400"grep -q '"error":"ExpiredToken"' "$refresh_body"# a session revoked underneath its holder answers ExpiredToken on refresh, the# reference's answer for a refresh token it no longer holds, so the client logs out cleanlyapp_password=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createAppPassword" \ -H "authorization: Bearer $signup_access" -H 'content-type: application/json' \ --data '{"name":"smoke-app"}' | jq -r .password)app_session=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createSession" \ -H 'content-type: application/json' \ --data "$(jq -nc --arg p "$app_password" '{identifier:"signup-smoke.test",password:$p}')")app_refresh=$(printf '%s' "$app_session" | jq -r .refreshJwt)curl -fsS -X POST "$base/xrpc/com.atproto.server.revokeAppPassword" \ -H "authorization: Bearer $signup_access" -H 'content-type: application/json' \ --data '{"name":"smoke-app"}' >/dev/nulltest "$(refresh_session "$app_refresh" "$refresh_body")" = "400"grep -q '"error":"ExpiredToken"' "$refresh_body"# a new account must be announced on the firehose immediately: identity,# account, genesis commit, and sync events plus a servable empty repogenesis=$(curl -fsS "$base/xrpc/com.atproto.sync.getLatestCommit?did=$signup_did")printf '%s' "$genesis" | grep -q '"cid":'printf '%s' "$genesis" | grep -q '"rev":'curl -fsS "$base/xrpc/com.atproto.sync.getRepo?did=$signup_did" -o "$work/zds-signup-genesis.car"test -s "$work/zds-signup-genesis.car"signup_events=$(sqlite3 "$db" "select count(*) from seq_events where did = '$signup_did'")test "$signup_events" = "4"signup_taken_invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \ -H "authorization: Bearer smoke-admin-token" \ -H 'content-type: application/json' \ --data '{"useCount":1}' | jq -r .code)signup_taken_status=$(curl -sS -o "$work/zds-signup-taken.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.server.createAccount" \ -H 'content-type: application/json' \ --data "$(printf '{"handle":"signup-smoke.test","email":"signup-smoke-2@test.com","password":"signup-password","inviteCode":"%s"}' "$signup_taken_invite")")test "$signup_taken_status" = "400"! grep -q '"error":"InvalidInviteCode"' "$work/zds-signup-taken.json"grep -q '"error":' "$work/zds-signup-taken.json"
curl -fsS -X POST "$base/xrpc/app.bsky.actor.putPreferences" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data '{"preferences":[{"$type":"app.bsky.actor.defs#contentLabelPref","label":"dogs","visibility":"show"},{"$type":"app.bsky.actor.defs#contentLabelPref","label":"cats","visibility":"warn"},{"$type":"app.bsky.actor.defs#personalDetailsPref","birthDate":"1970-01-01"},{"$type":"app.bsky.actor.defs#declaredAgePref","isOverAge13":false,"isOverAge16":false,"isOverAge18":false}]}' >/dev/nullprefs=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/app.bsky.actor.getPreferences")printf '%s' "$prefs" | grep -q '"label":"dogs"'printf '%s' "$prefs" | grep -q '"label":"cats"'printf '%s' "$prefs" | grep -q '"birthDate":"1970-01-01"'printf '%s' "$prefs" | grep -q '"isOverAge13":true'printf '%s' "$prefs" | grep -q '"isOverAge16":true'printf '%s' "$prefs" | grep -q '"isOverAge18":true'test "$(printf '%s' "$prefs" | grep -o 'declaredAgePref' | wc -l | tr -d ' ')" = "1"
create=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"smoke","createdAt":"2026-05-22T00:00:00.000Z"}}')printf '%s' "$create" | grep -q '"uri":"at://did:plc:smoketest/app.bsky.feed.post/'printf '%s' "$create" | grep -q '"validationStatus":"valid"'post_uri=$(printf '%s' "$create" | jq -r .uri)post_cid=$(printf '%s' "$create" | sed -n 's/.*"cid":"\([^"]*\)".*/\1/p')test -n "$post_cid"
# sync.getRecord serves a proof CAR (commit + mst path + record); strict OAuth# lexicon resolvers depend on it and previously got 404 UnknownMethodpost_rkey=$(printf '%s' "$create" | jq -r '.uri | split("/")[-1]')proof_car="$work/zds-record-proof.car"proof_status=$(curl -sS -o "$proof_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")test "$proof_status" = "200"test -s "$proof_car"proof_type=$(curl -sS -o /dev/null -w '%{content_type}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")test "$proof_type" = "application/vnd.ipld.car"proof_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")test "$proof_head" = "200"# exclusion proof for an absent rkey is still a 200 CARexclusion_status=$(curl -sS -o "$proof_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=3zdsnosuchrkey")test "$exclusion_status" = "200"test -s "$proof_car"proof_missing_repo=$(curl -sS -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:nobody&collection=app.bsky.feed.post&rkey=$post_rkey")test "$proof_missing_repo" = "404"
like=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data "$(printf '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.like","record":{"$type":"app.bsky.feed.like","subject":{"uri":"%s","cid":"%s"},"createdAt":"2026-05-22T00:00:01.000Z"}}' "$post_uri" "$post_cid")")printf '%s' "$like" | grep -q '"uri":"at://did:plc:smoketest/app.bsky.feed.like/'printf '%s' "$like" | grep -q '"validationStatus":"valid"'like_rkey=$(printf '%s' "$like" | jq -r '.uri | split("/")[-1]')test "$(printf '%s' "$like_rkey" | wc -c | tr -d ' ')" = "13"case "$like_rkey" in 3zds*) echo "generated app.bsky.feed.like rkey is not a TID: $like_rkey" >&2 exit 1 ;;esac
invalid_like_status=$(curl -sS -o "$work/zds-invalid-like.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data "$(printf '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.like","rkey":"3zdsbad","record":{"$type":"app.bsky.feed.like","subject":{"uri":"%s","cid":"%s"},"createdAt":"2026-05-22T00:00:02.000Z"}}' "$post_uri" "$post_cid")")test "$invalid_like_status" = "400"grep -q '"error":"InvalidRequest"' "$work/zds-invalid-like.json"
# a guard that writes an xrpc error must end the request. requireRepoMatches# once wrote 400 and returned success, and createRecord went on to write the# record and answer 200 over it.wrong_repo_body="${work}/zds-smoke-wrong-repo.json"wrong_repo_status=$(curl -sS -o "$wrong_repo_body" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data '{"repo":"did:plc:someoneelse","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"must not land","createdAt":"2026-05-22T00:00:03.000Z"}}')test "$wrong_repo_status" = "400"grep -q '"error":"InvalidRepo"' "$wrong_repo_body"! curl -fsS "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:smoketest&collection=app.bsky.feed.post&limit=10" | grep -q 'must not land'# and a 404 from requirePublicRepoAvailable stays a 404 rather than a 500missing_repo_status=$(curl -sS -o "$wrong_repo_body" -w '%{http_code}' "$base/xrpc/com.atproto.repo.getRecord?repo=did:plc:nobody&collection=app.bsky.feed.post&rkey=3zds")test "$missing_repo_status" = "404"grep -q '"error":"RepoNotFound"' "$wrong_repo_body"
# the xrpc proxy path bypasses dispatch, so its HandledResponse must be caught# there too: a bad token on a proxied method is a 401 InvalidToken json body,# never a plain-text 500 (the top-level handler once overwrote it).proxy_body="${work}/zds-smoke-proxy.json"proxy_status=$(curl -sS -o "$proxy_body" -w '%{http_code}' -X POST "$base/xrpc/app.bsky.notification.updateSeen" \ -H "authorization: Bearer not-a-real-token" \ -H 'atproto-proxy: did:web:api.bsky.app#bsky_appview' \ -H 'content-type: application/json' \ --data '{"seenAt":"2026-05-22T00:00:03.000Z"}')test "$proxy_status" = "401"grep -q '"error":"InvalidToken"' "$proxy_body"
# clients such as Bridgy call app.bsky.* on the PDS without an atproto-proxy# header; like the reference PDS, those go to the configured appview instead# of 404 UnknownMethod, and still require the caller's own session.default_proxy_status=$(curl -sS -o "$proxy_body" -w '%{http_code}' "$base/xrpc/app.bsky.feed.getAuthorFeed?actor=did:plc:smoketest&limit=30" \ -H "authorization: Bearer $token")test "$default_proxy_status" = "200"test "$(jq -r .cursor "$proxy_body")" = "smoke-appview"default_proxy_status=$(curl -sS -o "$proxy_body" -w '%{http_code}' "$base/xrpc/app.bsky.feed.getAuthorFeed?actor=did:plc:smoketest&limit=30")test "$default_proxy_status" = "401"grep -q '"error":"AuthenticationRequired"' "$proxy_body"default_proxy_status=$(curl -sS -o "$proxy_body" -w '%{http_code}' "$base/xrpc/com.example.noSuchMethod" \ -H "authorization: Bearer $token")test "$default_proxy_status" = "404"grep -q '"error":"UnknownMethod"' "$proxy_body"
records=$(curl -fsS "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:smoketest&collection=app.bsky.feed.post&limit=10")printf '%s' "$records" | grep -q '"text":"smoke"'
latest=$(curl -fsS "$base/xrpc/com.atproto.sync.getLatestCommit?did=did:plc:smoketest")printf '%s' "$latest" | grep -q '"cid":"'printf '%s' "$latest" | grep -q '"rev":"'
repo_car="${work}/zds-smoke.car"code=$(curl -sS -o "$repo_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest")test "$code" = "200"test "$(wc -c < "$repo_car")" -gt 100repo_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest")test "$repo_head" = "200"
blob_payload="${work}/zds-smoke-blob.jpg"printf '\377\330\377\340zds-smoke' > "$blob_payload"blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \ -H "authorization: Bearer $token" \ -H 'content-type: image/jpeg' \ --data-binary "@$blob_payload")printf '%s' "$blob" | grep -q '"blob":'printf '%s' "$blob" | grep -q '"mimeType":"image/jpeg"'blob_cid=$(printf '%s' "$blob" | sed -n 's/.*"\$link":"\([^"]*\)".*/\1/p')test -n "$blob_cid"blob_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:smoketest&cid=$blob_cid")test "$blob_head" = "404"blob_ref=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data "$(jq -nc --arg cid "$blob_cid" '{repo:"did:plc:smoketest",collection:"dev.zds.smoke.blob",record:{"$type":"dev.zds.smoke.blob",blob:{"$type":"blob",ref:{"$link":$cid},mimeType:"image/jpeg",size:13}}}')")printf '%s' "$blob_ref" | grep -q '"uri":"at://did:plc:smoketest/dev.zds.smoke.blob/'blob_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:smoketest&cid=$blob_cid")test "$blob_head" = "200"
large_blob_payload="${work}/zds-smoke-large-blob.jpg"dd if=/dev/zero bs=1048576 count=17 of="$large_blob_payload" 2>/dev/nulllarge_blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \ -H "authorization: Bearer $token" \ -H 'content-type: image/jpeg' \ --data-binary "@$large_blob_payload")printf '%s' "$large_blob" | grep -q '"blob":'printf '%s' "$large_blob" | grep -q '"size":17825792'
oauth_info=$(curl -fsS -H 'accept: application/json' "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-oauth")printf '%s' "$oauth_info" | grep -q '"login_hint":"smoke.test"'oauth_page="${work}/zds-smoke-oauth.html"curl -fsS "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-oauth" > "$oauth_page"grep -q 'value="smoke.test"' "$oauth_page"grep -Fq 'repo:*?action=create' "$oauth_page"grep -Fq 'blob:*/*' "$oauth_page"sqlite3 "$db" "insert into oauth_requests (request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, login_hint, expires_at) values ('smoke-loopback', 'http://localhost?scope=atproto', 'http://127.0.0.1:5199/', 'atproto', 'state', 'challenge', 'S256', 'smoke.test', unixepoch() + 600)"curl -fsS "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-loopback" | grep -q 'development client'python3 tools/smoke-browser-login.py "$base" "$db"
echo "zds smoke ok"