mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 03:37:36 +00:00
96 lines
5.0 KiB
Bash
96 lines
5.0 KiB
Bash
#!/usr/bin/env bash
|
|||
|
|
# Probes Cloudflare API auth state for the Turnstile Spin agent.
|
||
|
|
#
|
||
|
|
# Reads:
|
||
|
|
# $CLOUDFLARE_API_TOKEN (required)
|
||
|
|
# $CLOUDFLARE_ACCOUNT_ID (optional; if set, must be one of the token's accounts)
|
||
|
|
#
|
||
|
|
# Outputs JSON to stdout, always exits 0. The agent reads `status`:
|
||
|
|
# "ok" ; selected account passed both Turnstile and Workers scope probes
|
||
|
|
# "missing_token" ; no token set, or wrangler whoami failed
|
||
|
|
# "missing_scope" ; token lacks Account.Turnstile:Edit on the selected account
|
||
|
|
# "missing_workers_scope"; token has Turnstile scope but lacks Workers Scripts on the selected account
|
||
|
|
# "multiple_accounts" ; token covers >1 accounts and $CLOUDFLARE_ACCOUNT_ID is unset; agent must ask user to pick, set it, and re-run
|
||
|
|
# "account_mismatch" ; $CLOUDFLARE_ACCOUNT_ID is set but is not in the token's accounts list
|
||
|
|
#
|
||
|
|
# Human-readable diagnostics go to stderr. The agent surfaces them to the user.
|
||
|
|
|
||
|
|
set -uo pipefail
|
||
|
|
|
||
|
|
emit() {
|
||
|
|
echo "$1"
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
|
||
|
|
token="${CLOUDFLARE_API_TOKEN:-}"
|
||
|
|
declared_account="${CLOUDFLARE_ACCOUNT_ID:-}"
|
||
|
|
|
||
|
|
if [ -z "$token" ]; then
|
||
|
|
echo "auth-probe: \$CLOUDFLARE_API_TOKEN is not set." >&2
|
||
|
|
emit '{"status":"missing_token","reason":"no_env_var"}'
|
||
|
|
fi
|
||
|
|
|
||
|
|
whoami_json=$(npx wrangler whoami --json 2>/dev/null || true)
|
||
|
|
if [ -z "$whoami_json" ] || [ "$(echo "$whoami_json" | head -c 1)" != "{" ]; then
|
||
|
|
echo "auth-probe: wrangler whoami returned no JSON. Token may be invalid or expired." >&2
|
||
|
|
emit '{"status":"missing_token","reason":"whoami_failed"}'
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Extract the accounts array. Fall back to python3 if jq is missing.
|
||
|
|
accounts_json=$(echo "$whoami_json" | (jq -c '.accounts' 2>/dev/null || python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)['accounts']))"))
|
||
|
|
account_count=$(echo "$accounts_json" | (jq 'length' 2>/dev/null || python3 -c "import sys,json; print(len(json.load(sys.stdin)))"))
|
||
|
|
|
||
|
|
if [ -z "$account_count" ] || [ "$account_count" = "0" ] || [ "$account_count" = "null" ]; then
|
||
|
|
echo "auth-probe: wrangler whoami succeeded but no accounts found on the token." >&2
|
||
|
|
emit '{"status":"missing_token","reason":"no_accounts"}'
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Pick the account to probe:
|
||
|
|
# - $CLOUDFLARE_ACCOUNT_ID set: must be in the token's accounts list, else account_mismatch
|
||
|
|
# - unset, exactly 1 account: use it silently
|
||
|
|
# - unset, >1 accounts: emit multiple_accounts; agent picks and re-runs
|
||
|
|
if [ -n "$declared_account" ]; then
|
||
|
|
in_list=$(echo "$accounts_json" | (jq --arg id "$declared_account" 'map(.id) | index($id) != null' 2>/dev/null || python3 -c "import sys,json; print('true' if any(a['id']==sys.argv[1] for a in json.load(sys.stdin)) else 'false')" "$declared_account"))
|
||
|
|
if [ "$in_list" != "true" ]; then
|
||
|
|
echo "auth-probe: \$CLOUDFLARE_ACCOUNT_ID ($declared_account) is not one of the token's accounts." >&2
|
||
|
|
echo "auth-probe: Either unset \$CLOUDFLARE_ACCOUNT_ID, or set it to an account included in the token's Account Resources." >&2
|
||
|
|
emit "{\"status\":\"account_mismatch\",\"declared\":\"$declared_account\",\"accounts\":$accounts_json}"
|
||
|
|
fi
|
||
|
|
account_id="$declared_account"
|
||
|
|
elif [ "$account_count" = "1" ]; then
|
||
|
|
account_id=$(echo "$accounts_json" | (jq -r '.[0].id' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])"))
|
||
|
|
else
|
||
|
|
echo "auth-probe: token covers $account_count accounts; ask the user to pick one, then export \$CLOUDFLARE_ACCOUNT_ID and re-run." >&2
|
||
|
|
emit "{\"status\":\"multiple_accounts\",\"accounts\":$accounts_json}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Probe Turnstile scope on the selected account
|
||
|
|
tmp=$(mktemp)
|
||
|
|
http_code=$(curl -sS -w "%{http_code}" -o "$tmp" \
|
||
|
|
"https://api.cloudflare.com/client/v4/accounts/$account_id/challenges/widgets" \
|
||
|
|
-H "Authorization: Bearer $token" 2>/dev/null || echo "000")
|
||
|
|
body=$(cat "$tmp"); rm -f "$tmp"
|
||
|
|
success=$(echo "$body" | (jq -r '.success' 2>/dev/null || echo "false"))
|
||
|
|
|
||
|
|
if [ "$success" != "true" ]; then
|
||
|
|
echo "auth-probe: token cannot read /challenges/widgets on account $account_id (HTTP $http_code). Missing Account.Turnstile:Edit." >&2
|
||
|
|
emit "{\"status\":\"missing_scope\",\"account_id\":\"$account_id\",\"http_code\":$http_code}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Probe Workers scope on the selected account. GET /workers/scripts requires
|
||
|
|
# Account.Workers Scripts:Read, which is a best-effort proxy for Edit. Tokens
|
||
|
|
# granted Edit-only (without Read) will fail this probe and emit a confusing
|
||
|
|
# missing_workers_scope; the agent should suggest adding Read alongside Edit.
|
||
|
|
tmp=$(mktemp)
|
||
|
|
workers_code=$(curl -sS -w "%{http_code}" -o "$tmp" \
|
||
|
|
"https://api.cloudflare.com/client/v4/accounts/$account_id/workers/scripts" \
|
||
|
|
-H "Authorization: Bearer $token" 2>/dev/null || echo "000")
|
||
|
|
workers_body=$(cat "$tmp"); rm -f "$tmp"
|
||
|
|
workers_success=$(echo "$workers_body" | (jq -r '.success' 2>/dev/null || echo "false"))
|
||
|
|
|
||
|
|
if [ "$workers_success" != "true" ]; then
|
||
|
|
echo "auth-probe: token cannot read /workers/scripts on account $account_id (HTTP $workers_code). Missing Account.Workers Scripts:Edit." >&2
|
||
|
|
emit "{\"status\":\"missing_workers_scope\",\"account_id\":\"$account_id\",\"http_code\":$workers_code}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
emit "{\"status\":\"ok\",\"account_id\":\"$account_id\",\"accounts\":$accounts_json}"
|