#!/bin/sh
# shared helpers for the dev bootstrap scripts. source, don't run.
: "${PDS_URL:?PDS_URL must be set}"
PASSWORD="${PASSWORD:-password}"
CREATED_AT="${CREATED_AT:-2025-09-22T11:14:35+01:00}"
# fail LINE... -> every line on stderr, then abort
fail() {
printf '%s\n' "$@" >&2
exit 1
}
# resolve_handle HANDLE → DID on stdout
resolve_handle() {
rh_resp=$(curl -sS -w '\n%{http_code}' \
"${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=$1")
rh_body=$(printf '%s\n' "$rh_resp" | sed '$d')
rh_status=$(printf '%s\n' "$rh_resp" | tail -n1)
case "$rh_status" in
200) printf '%s\n' "$rh_body" | jq -er '.did' ;;
400) : ;; # not found — expected
*) fail "resolveHandle $1: HTTP $rh_status: $rh_body" ;;
esac
}
require_handle() {
rq_did=$(resolve_handle "$1")
[ -n "$rq_did" ] || fail "resolveHandle $1: the pds doesn't know that handle." \
' init-data.sh creates the seed accounts, so it either never ran or failed.'
printf '%s\n' "$rq_did"
}
# login DID/Handle → access JWT on stdout
# each login costs ~0.4s of password hashing on the pds, so LOGIN_CACHE skips it
login() {
if [ -n "${LOGIN_CACHE:-}" ] && [ -s "$LOGIN_CACHE/$1" ]; then
cat "$LOGIN_CACHE/$1"
return 0
fi
lg_jwt=$(curl -fsS -H "Content-Type: application/json" \
-d "{\"identifier\":\"$1\",\"password\":\"${PASSWORD}\"}" \
"${PDS_URL}/xrpc/com.atproto.server.createSession" \
| jq -er '.accessJwt') || return 1
[ -z "${LOGIN_CACHE:-}" ] || printf '%s\n' "$lg_jwt" > "$LOGIN_CACHE/$1"
printf '%s\n' "$lg_jwt"
}
# service_token JWT LXM [AUD] → service auth JWT for the knot, on stdout.
# AUD defaults to the did:web of $KNOT_HOSTNAME.
service_token() {
st_aud=${3:-"did:web:$(printf '%s' "${KNOT_HOSTNAME:?KNOT_HOSTNAME must be set}" | sed 's/:/%3A/g')"}
curl -fsS -H "Authorization: Bearer $1" \
"${PDS_URL}/xrpc/com.atproto.server.getServiceAuth?aud=${st_aud}&lxm=$2&exp=$(( $(date +%s) + 240 ))" \
| jq -er '.token'
}
# put_record at://DID/COLLECTION/RKEY CID
# → puts /fixtures/records/DID/COLLECTION/RKEY/CID.json, fails unless the pds returns CID.
put_record() {
case "$1" in
at://did:*/*/?*) ;;
*) fail "put_record: expected at://DID/COLLECTION/RKEY, got $1" ;;
esac
pr_rest=${1#at://}
pr_did=${pr_rest%%/*}
pr_rest=${pr_rest#*/}
pr_collection=${pr_rest%%/*}
pr_rkey=${pr_rest#*/}
pr_record=$(cat "${FIXTURES:-/fixtures}/records/$pr_did/$pr_collection/$pr_rkey/$2.json")
pr_jwt=$(login "$pr_did")
pr_payload=$(jq -nc \
--arg repo "$pr_did" \
--arg collection "$pr_collection" \
--arg rkey "$pr_rkey" \
--argjson record "$pr_record" \
'{repo:$repo, collection:$collection, rkey:$rkey, record:$record}')
pr_resp=$(curl -fsS \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${pr_jwt}" \
-d "$pr_payload" \
"${PDS_URL}/xrpc/com.atproto.repo.putRecord")
printf '[record] %s\n' "$1" >&2
pr_cid=$(printf '%s\n' "$pr_resp" | jq -er '.cid') # -e: fail loudly if the cid is absent
[ "$pr_cid" = "$2" ] || fail "put_record $1: cid is $pr_cid, expected $2"
}
# upload_blob DID CID MIME_TYPE
# → uploads /fixtures/blobs/DID/CID, fails unless the pds returns CID.
upload_blob() {
ub_cid=$(curl -fsS --data-binary "@${FIXTURES:-/fixtures}/blobs/$1/$2" \
-H "Content-Type: $3" \
-H "Authorization: Bearer $(login "$1")" \
"${PDS_URL}/xrpc/com.atproto.repo.uploadBlob" \
| jq -er '.blob.ref."$link"')
[ "$ub_cid" = "$2" ] || fail "upload_blob $1/$2: cid is $ub_cid, expected $2"
printf '[blob] %s/%s\n' "$1" "$2" >&2
}