Reference file: tokens.md
Tokens
This page covers operational token handling for Tempest deployment and account
migration work. Keep raw tokens out of Git, logs, tickets, screenshots, and chat.
Use .sandbox/ or a local password manager for temporary working files.
Token Types
Common tokens in deployment and migration:
ADMIN_TOKEN: raw operator token for Tempest admin routes. Railway stores onlyTEMPEST_ADMIN_TOKEN_HASH.- account
accessJwt: short-lived bearer token returned bycom.atproto.server.createSession. - account
refreshJwt: refresh token returned by session creation and refresh. serviceAuth: scoped proof from one PDS to another, returned bycom.atproto.server.getServiceAuth.- PLC operation token/code: short-lived one-time authorization for
com.atproto.identity.signPlcOperation, requested from the current PDS duringdid:plcidentity migration. - app password: Bluesky-compatible account password substitute for client and bot login. Prefer this over the main account password for migration commands when the source PDS accepts it.
Source Account Access Token
To migrate an account from its current PDS, first create a normal session on the
current authoritative PDS. For the current tempestpds.bsky.social migration
example:
export OLD_PDS="https://jellybaby.us-east.host.bsky.network"
export OLD_AUTH_PDS="$OLD_PDS"
export OLD_LOGIN_PDS="$OLD_AUTH_PDS"
export HANDLE="tempestpds.bsky.social"
export OLD_IDENTIFIER="$HANDLE"
export TEMPEST="https://tempest.desertthunder.dev"
export TEMPEST_SERVICE_DID="did:web:tempest.desertthunder.dev"
read -rs OLD_PASSWORD
export OLD_PASSWORD
Use the main account password for PLC operation signing. App passwords can be useful for ordinary source-PDS access, but they may produce a session that cannot request a PLC operation signature.
Do not assign passwords with unquoted shell syntax. Characters like $, !,
backticks, and backslashes can be expanded or interpreted by the shell. Prefer
silent input:
unset OLD_PASSWORD
read -rs OLD_PASSWORD
export OLD_PASSWORD
If assigning directly, use single quotes:
export OLD_PASSWORD='literal-password-with-$N'
If the source PDS asks for an auth-factor token/code during login, pass it to
the CLI as OLD_AUTH_FACTOR_TOKEN:
read -rs OLD_AUTH_FACTOR_TOKEN
export OLD_AUTH_FACTOR_TOKEN
The migration CLI reads the same environment variables as the curl examples and writes the same artifacts:
uv run --project scripts tempest login-source
Use the account password or a Bluesky app password:
curl -fsS -X POST "$OLD_PDS/xrpc/com.atproto.server.createSession" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg identifier "$HANDLE" \
--arg password "$OLD_PASSWORD" \
'{identifier: $identifier, password: $password}')" \
> .sandbox/old_session.json
Extract the source access token:
export OLD_ACCESS="$(jq -r .accessJwt .sandbox/old_session.json)"
Check that extraction worked without printing the token:
jq '{did, handle, has_access: (.accessJwt != null), has_refresh: (.refreshJwt != null)}' \
.sandbox/old_session.json
Service Auth for Migration
Ask the old PDS for service auth scoped to account creation on Tempest:
uv run --project scripts tempest service-auth
The equivalent curl call is:
curl -fsS -G "$OLD_PDS/xrpc/com.atproto.server.getServiceAuth" \
-H "Authorization: Bearer $OLD_ACCESS" \
--data-urlencode "aud=$TEMPEST_SERVICE_DID" \
--data-urlencode "lxm=com.atproto.server.createAccount" \
> .sandbox/service_auth_create_account.json
aud must be a DID. Do not use https://tempest.desertthunder.dev as the
getServiceAuth audience; current PDS implementations reject URL audiences with
InvalidRequest.
Check that a token exists without printing it:
jq '{has_token: (.token != null)}' .sandbox/service_auth_create_account.json
Export the token for the next Tempest request:
export SERVICE_AUTH="$(jq -r .token .sandbox/service_auth_create_account.json)"
Use that value as serviceAuth when calling Tempest
com.atproto.server.createAccount with an existing DID. The service-auth token
must have:
- issuer and subject equal to the account DID;
- audience equal to the target service DID, such as
did:web:tempest.desertthunder.dev; - method (
lxm) equal tocom.atproto.server.createAccount.
Admin Token Hash
Generate TEMPEST_ADMIN_TOKEN_HASH through the same uv project:
uv run --project scripts tempest argon
The ar and arg2 aliases run the same helper:
uv run --project scripts tempest ar --only-hash
PLC Operation Token
After repo import and blob upload, the did:plc document must be updated so
#atproto_pds points at Tempest. The source PDS signs that PLC operation after
issuing a short-lived token or emailing a one-time code:
uv run --project scripts tempest plc-recommended
uv run --project scripts tempest plc-request-token
If .sandbox/plc_token.json contains a token, the CLI will read it. If the
source PDS emails a code instead, keep it out of shell history when possible and
export it only for the signing step:
read -rs PLC_TOKEN
export PLC_TOKEN
uv run --project scripts tempest plc-sign
The signed operation is written to .sandbox/plc_signed_operation.json.
Inspect its service endpoint before submitting it:
jq '.operation.services.atproto_pds.endpoint' .sandbox/plc_signed_operation.json
For this deployment the value must be https://tempest.desertthunder.dev.
If plc-request-token returns Bad token scope, the source session is not
authorized for PLC signing. Re-run login-source with the main account password
and any required OLD_AUTH_FACTOR_TOKEN, then retry plc-request-token. If
handle login returns Invalid identifier or password, set OLD_IDENTIFIER to
the source account email address and retry login-source.
After a successful login-source, verify whether the same token works for
ordinary old-PDS auth:
uv run --project scripts tempest source-session-status
If that succeeds while plc-request-token fails, the saved source session is
valid and the source PDS is specifically refusing PLC signing for that token
scope.
Local lexicon references:
priv/lexicons/official/com/atproto/identity/requestPlcOperationSignature.jsondefines this as an authenticated no-input procedure that requests an emailed code.priv/lexicons/official/com/atproto/identity/signPlcOperation.jsondefines thetokenfield consumed by signing.priv/lexicons/official/com/atproto/server/createSession.jsondefines the optionalauthFactorTokenused during session creation.
If the repository host rejects main-password login, try a separate source login
host while leaving OLD_PDS unchanged for repo and blob export and
OLD_AUTH_PDS unchanged for authenticated old-PDS operations:
export OLD_PDS="https://jellybaby.us-east.host.bsky.network"
export OLD_AUTH_PDS="$OLD_PDS"
export OLD_LOGIN_PDS="https://bsky.social"
uv run --project scripts tempest login-source
uv run --project scripts tempest plc-request-token
Safety Notes
-
Do not commit
.sandbox/old_session.json,.sandbox/service_auth_create_account.json,.sandbox/plc_token.json,.sandbox/plc_signed_operation.json, or shell history containing raw tokens. -
Prefer app passwords over the main account password for source-PDS session creation.
-
Revoke or rotate the app password after migration.
-
Treat
serviceAuthas short-lived migration material. Regenerate it if the migration attempt is delayed. -
Treat PLC operation tokens/codes as single-use, short-lived migration material. Regenerate the token/code if signing fails or the token expires.
-
Treat Tempest
accessJwtas short-lived. If migration commands returnBearer token is invalidorBearer token is expired, refresh the saved Tempest session:uv run --project scripts tempest refresh-session -
Keep the old PDS account active until Tempest passes repo, blob, firehose, crawler, DID, and real-client checks.