Bluesky PDS Change
In case you also put your PDS on your base domain and greatly regretted it >.>
update_pds.sh
#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# Bluesky PDS DID Endpoint Update Script
# Updates the atproto_pds endpoint in your DID document via PLC directory
# =============================================================================
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Error codes
readonly ERR_GENERAL=1
readonly ERR_MISSING_DEPS=2
readonly ERR_INVALID_ARGS=3
readonly ERR_AUTH_FAILED=4
readonly ERR_OPERATION_FAILED=5
readonly ERR_USER_CANCELLED=6
readonly ERR_NETWORK_ERROR=7
# Centralized error reporting function
report_error() {
local code="$1"
local message="$2"
local suggestion="${3:-}"
echo -e "${RED}Error: $message${NC}" >&2
[[ -n "$suggestion" ]] && echo -e "${YELLOW}Suggestion: $suggestion${NC}" >&2
return "$code"
}
# Centralized success reporting function
report_success() {
local message="$1"
echo -e "${GREEN}✓ $message${NC}"
}
# Centralized warning reporting function
report_warning() {
local message="$1"
echo -e "${YELLOW}⚠ $message${NC}"
}
# Centralized info reporting function
report_info() {
local message="$1"
echo -e "${BLUE}$message${NC}"
}
# Check required dependencies
check_prerequisites() {
local missing=()
command -v curl >/dev/null 2>&1 || missing+=("curl")
command -v jq >/dev/null 2>&1 || missing+=("jq")
if [[ ${#missing[@]} -gt 0 ]]; then
report_error $ERR_MISSING_DEPS "Missing required tools: ${missing[*]}" \
"Install missing dependencies before running this script"
return $ERR_MISSING_DEPS
fi
return 0
}
# Normalize URL by removing trailing slashes
normalize_base() {
local u="${1:-}"
while [[ "$u" == */ ]]; do u="${u%/}"; done
echo "$u"
}
# Validate URL format
validate_url() {
local url="$1"
local name="$2"
if [[ -z "$url" ]]; then
report_error $ERR_INVALID_ARGS "$name cannot be empty"
return $ERR_INVALID_ARGS
fi
if [[ ! "$url" =~ ^https?:// ]]; then
report_error $ERR_INVALID_ARGS "$name must start with http:// or https://" \
"Example: https://pds.example.com"
return $ERR_INVALID_ARGS
fi
return 0
}
# Prompt for user input
prompt() {
local var="$1" msg="$2" secret="${3:-false}" val
if [[ "$secret" == "true" ]]; then
read -r -s -p "$msg" val </dev/tty >&2; echo >&2
else
read -r -p "$msg" val </dev/tty >&2
fi
printf -v "$var" '%s' "$val"
}
# Make HTTP request with error handling
http_request() {
local method="$1"
local url="$2"
local auth_header="${3:-}"
local content_type="${4:-}"
local data="${5:-}"
local body
local http_code
local curl_args=(-sS -X "$method")
[[ -n "$auth_header" ]] && curl_args+=(-H "Authorization: $auth_header")
[[ -n "$content_type" ]] && curl_args+=(-H "Content-Type: $content_type")
[[ -n "$data" ]] && curl_args+=(-d "$data")
# Write HTTP status code to fd 3 via a temp file descriptor approach
local tmpfile
tmpfile=$(mktemp)
body=$(curl "${curl_args[@]}" -o - -w "%{http_code}" -- "$url" 2>"$tmpfile") || {
local curl_err
curl_err=$(<"$tmpfile")
rm -f "$tmpfile"
report_error $ERR_NETWORK_ERROR "Network request failed: $curl_err" \
"Check your internet connection and that the PDS URL is correct"
return $ERR_NETWORK_ERROR
}
rm -f "$tmpfile"
# The status code is appended to the body by -w; extract it
http_code="${body: -3}"
body="${body:0:${#body}-3}"
# Guard against non-numeric status codes
if ! [[ "$http_code" =~ ^[0-9]{3}$ ]]; then
report_error $ERR_NETWORK_ERROR "Unexpected response (no valid HTTP status code)"
return $ERR_NETWORK_ERROR
fi
# Check for HTTP errors
if [[ "$http_code" -ge 400 ]]; then
local error_msg
error_msg=$(jq -r '.message // .error // "Unknown error"' 2>/dev/null <<< "$body" || echo "$body")
report_error $ERR_OPERATION_FAILED "HTTP $http_code: $error_msg"
return $ERR_OPERATION_FAILED
fi
echo "$body"
return 0
}
# Cleanup function for sensitive data
cleanup() {
unset PASSWORD JWT TOKEN 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# =============================================================================
# Main Script
# =============================================================================
main() {
report_info "=== Bluesky PDS DID Endpoint Update ==="
report_info "Updates the atproto_pds endpoint in your DID document"
echo
# Check prerequisites
if ! check_prerequisites; then
return $?
fi
# Gather input
prompt PDS "Current PDS base URL (e.g. https://example.com): "
PDS="$(normalize_base "$PDS")"
if ! validate_url "$PDS" "Current PDS URL"; then
return $?
fi
prompt NEW_PDS "New PDS base URL to write into DID doc (e.g. https://pds.example.com): "
NEW_PDS="$(normalize_base "$NEW_PDS")"
if ! validate_url "$NEW_PDS" "New PDS URL"; then
return $?
fi
prompt IDENTIFIER "Account identifier (handle or email): "
if [[ -z "$IDENTIFIER" ]]; then
report_error $ERR_INVALID_ARGS "Account identifier cannot be empty"
return $ERR_INVALID_ARGS
fi
prompt PASSWORD "Password (or app password): " true
if [[ -z "$PASSWORD" ]]; then
report_error $ERR_INVALID_ARGS "Password cannot be empty"
return $ERR_INVALID_ARGS
fi
# Step 1: Create session
echo
report_info "Creating session on: $PDS"
local session_payload
session_payload=$(jq -n --arg id "$IDENTIFIER" --arg pw "$PASSWORD" '{identifier:$id,password:$pw}')
# Password no longer needed
unset PASSWORD
local SESSION_JSON
SESSION_JSON=$(http_request "POST" "$PDS/xrpc/com.atproto.server.createSession" "" "application/json" "$session_payload") || {
report_error $ERR_AUTH_FAILED "Failed to create session" \
"Check your credentials and PDS URL"
return $ERR_AUTH_FAILED
}
local JWT DID HANDLE
JWT="$(jq -r '.accessJwt // empty' <<< "$SESSION_JSON")"
DID="$(jq -r '.did // empty' <<< "$SESSION_JSON")"
HANDLE="$(jq -r '.handle // empty' <<< "$SESSION_JSON")"
if [[ -z "$JWT" ]]; then
report_error $ERR_AUTH_FAILED "Failed to create session - no access token received" \
"Check your credentials"
jq . >&2 <<< "$SESSION_JSON"
return $ERR_AUTH_FAILED
fi
report_success "Session created for: $HANDLE ($DID)"
# Step 2: Request PLC operation signature
echo
report_info "Requesting PLC operation signature email..."
if ! http_request "POST" "$PDS/xrpc/com.atproto.identity.requestPlcOperationSignature" "Bearer $JWT" >/dev/null; then
report_error $ERR_OPERATION_FAILED "Failed to request PLC operation signature"
return $ERR_OPERATION_FAILED
fi
report_success "Verification email sent"
report_warning "Check your email for the verification code"
prompt TOKEN "Enter the emailed code (token): "
if [[ -z "$TOKEN" ]]; then
report_error $ERR_INVALID_ARGS "Token cannot be empty"
return $ERR_INVALID_ARGS
fi
# Step 3: Sign PLC operation
echo
report_info "Signing PLC operation (service endpoint -> $NEW_PDS)..."
local sign_payload
sign_payload=$(jq -n --arg token "$TOKEN" --arg endpoint "$NEW_PDS" '{
token: $token,
services: {
atproto_pds: { type: "AtprotoPersonalDataServer", endpoint: $endpoint }
}
}')
# Token no longer needed
unset TOKEN
local SIGNED_JSON
SIGNED_JSON=$(http_request "POST" "$PDS/xrpc/com.atproto.identity.signPlcOperation" "Bearer $JWT" "application/json" "$sign_payload") || {
report_error $ERR_OPERATION_FAILED "Failed to sign PLC operation" \
"The verification code may be invalid or expired"
return $ERR_OPERATION_FAILED
}
local OP
OP="$(jq -c '.operation // empty' <<< "$SIGNED_JSON")"
if [[ -z "$OP" ]]; then
report_error $ERR_OPERATION_FAILED "Failed to sign PLC operation - no operation returned"
jq . >&2 <<< "$SIGNED_JSON"
return $ERR_OPERATION_FAILED
fi
report_success "PLC operation signed"
# Step 4: Submit PLC operation
echo
report_info "Submitting PLC operation to PLC directory..."
local submit_payload
submit_payload=$(jq -n --argjson op "$OP" '{operation:$op}')
local SUBMIT_JSON
SUBMIT_JSON=$(http_request "POST" "$PDS/xrpc/com.atproto.identity.submitPlcOperation" "Bearer $JWT" "application/json" "$submit_payload") || {
report_error $ERR_OPERATION_FAILED "Failed to submit PLC operation"
return $ERR_OPERATION_FAILED
}
report_success "PLC operation submitted"
# Step 5: Verify the update
echo
report_info "Verifying DID document via plc.directory..."
sleep 2 # Brief delay for propagation
# JWT no longer needed
unset JWT
local DID_DOC
DID_DOC=$(http_request "GET" "https://plc.directory/$DID") || {
report_warning "Could not verify DID document - check manually at: https://plc.directory/$DID"
return 0
}
# Extract and display service information
local SERVICE_INFO
SERVICE_INFO=$(jq '.service' <<< "$DID_DOC")
if [[ "$SERVICE_INFO" != "null" && -n "$SERVICE_INFO" ]]; then
report_success "DID document updated successfully"
echo
report_info "Current service configuration:"
jq . <<< "$SERVICE_INFO"
# Verify the endpoint was updated correctly
local CURRENT_ENDPOINT
CURRENT_ENDPOINT=$(jq -r '.service[]? | select(.id == "#atproto_pds") | .serviceEndpoint // empty' <<< "$DID_DOC")
if [[ "$CURRENT_ENDPOINT" == "$NEW_PDS" ]]; then
report_success "Endpoint successfully updated to: $NEW_PDS"
else
report_warning "Endpoint may not have updated yet. Current value: $CURRENT_ENDPOINT"
report_info "It may take a few moments for changes to propagate"
fi
else
report_warning "Could not parse service information from DID document"
jq . 2>/dev/null <<< "$DID_DOC" || echo "$DID_DOC"
fi
echo
report_success "Done!"
report_info "If you have multiple accounts on this PDS, run this script once per account."
}
# Run main function
main "$@"