mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 11:37:36 +00:00
refactor(turnstile-spin): tighten auth handling, expand documentation, and add tldraw-offline matcher
This commit is contained in:
1 parent
20dc5f4489
commit
03ef5c94ea
14 files changed
+1559
-499
No files matched your search
@@ -1,57 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Creates a Turnstile widget via the Cloudflare API.
|
||||
#
|
||||
# Reads:
|
||||
# $CLOUDFLARE_API_TOKEN (required)
|
||||
#
|
||||
# Args:
|
||||
# --account-id <id> Cloudflare account ID
|
||||
# --name <name> Widget name (e.g. "myproject (Spin)")
|
||||
# --domains <a,b,c> Comma-separated domain list (include localhost,127.0.0.1)
|
||||
# --mode <managed|invisible|non-interactive> Default: managed
|
||||
#
|
||||
# Outputs JSON to stdout. Exit 0 on success, 1 on failure. Diagnostics on stderr.
|
||||
# ok: {"status":"ok","sitekey":"<key>","secret":"<secret>"}
|
||||
# error: {"status":"error","code":<code>,"message":"<msg>"}
|
||||
# code 10000 → token lacks Account.Turnstile:Edit
|
||||
# Creates a Turnstile widget without writing credentials or the response to disk.
|
||||
|
||||
set +x
|
||||
set -uo pipefail
|
||||
|
||||
need_arg() {
|
||||
if [[ -z "${2-}" || "$2" == --* ]]; then
|
||||
echo "widget-create: missing value for $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
MODE="managed"
|
||||
ACCOUNT_ID=""
|
||||
NAME=""
|
||||
DOMAINS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--account-id) ACCOUNT_ID="$2"; shift 2 ;;
|
||||
--name) NAME="$2"; shift 2 ;;
|
||||
--domains) DOMAINS="$2"; shift 2 ;;
|
||||
--mode) MODE="$2"; shift 2 ;;
|
||||
case "$1" in
|
||||
--account-id) need_arg "$1" "${2-}"; ACCOUNT_ID="$2"; shift 2 ;;
|
||||
--name) need_arg "$1" "${2-}"; NAME="$2"; shift 2 ;;
|
||||
--domains) need_arg "$1" "${2-}"; DOMAINS="$2"; shift 2 ;;
|
||||
--mode) need_arg "$1" "${2-}"; MODE="$2"; shift 2 ;;
|
||||
*) echo "widget-create: unknown arg $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN must be set}"
|
||||
: "${ACCOUNT_ID:?--account-id required}"
|
||||
: "${NAME:?--name required}"
|
||||
: "${DOMAINS:?--domains required}"
|
||||
API_TOKEN="$CLOUDFLARE_API_TOKEN"
|
||||
unset CLOUDFLARE_API_TOKEN
|
||||
[[ -n "$ACCOUNT_ID" ]] || { echo "widget-create: --account-id required" >&2; exit 2; }
|
||||
[[ -n "$NAME" ]] || { echo "widget-create: --name required" >&2; exit 2; }
|
||||
[[ -n "$DOMAINS" ]] || { echo "widget-create: --domains required" >&2; exit 2; }
|
||||
[[ "$API_TOKEN" =~ ^[A-Za-z0-9_-]+$ ]] || {
|
||||
echo "widget-create: CLOUDFLARE_API_TOKEN has an invalid format" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$MODE" in
|
||||
managed|invisible|non-interactive) ;;
|
||||
*) echo "widget-create: unsupported mode" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
domains_json=$(python3 -c "import sys; print(__import__('json').dumps(sys.argv[1].split(',')))" "$DOMAINS")
|
||||
for command_name in curl python3; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || {
|
||||
echo "widget-create: $command_name is required" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
body=$(curl -sS -X POST \
|
||||
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/challenges/widgets" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$NAME\",\"domains\":$domains_json,\"mode\":\"$MODE\"}" 2>/dev/null)
|
||||
BODY_JSON="$(python3 -I -c '
|
||||
import json, sys
|
||||
name, domains_csv, mode = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
domains = [domain.strip() for domain in domains_csv.split(",") if domain.strip()]
|
||||
if not domains:
|
||||
raise SystemExit(2)
|
||||
print(json.dumps({"name": name, "domains": domains, "mode": mode}))
|
||||
' "$NAME" "$DOMAINS" "$MODE")" || {
|
||||
echo "widget-create: --domains must include at least one domain" >&2
|
||||
exit 2
|
||||
}
|
||||
ACCOUNT_ENCODED="$(python3 -I -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$ACCOUNT_ID")"
|
||||
|
||||
success=$(echo "$body" | (jq -r '.success' 2>/dev/null || python3 -c "import sys,json; print(str(json.load(sys.stdin).get('success',False)).lower())"))
|
||||
|
||||
if [ "$success" = "true" ]; then
|
||||
sitekey=$(echo "$body" | (jq -r '.result.sitekey' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['sitekey'])"))
|
||||
secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])"))
|
||||
echo "{\"status\":\"ok\",\"sitekey\":\"$sitekey\",\"secret\":\"$secret\"}"
|
||||
exit 0
|
||||
if ! API_RESPONSE="$(
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$API_TOKEN" |
|
||||
curl --disable --config - --silent --show-error --write-out $'\n%{http_code}' -X POST \
|
||||
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ENCODED/challenges/widgets" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$BODY_JSON"
|
||||
)"; then
|
||||
echo "widget-create: Cloudflare API request failed" >&2
|
||||
echo '{"status":"error","code":0,"message":"Cloudflare API request failed"}'
|
||||
exit 1
|
||||
fi
|
||||
unset BODY_JSON
|
||||
unset API_TOKEN
|
||||
|
||||
code=$(echo "$body" | (jq -r '.errors[0].code // 0' 2>/dev/null || echo "0"))
|
||||
message=$(echo "$body" | (jq -r '.errors[0].message // "unknown"' 2>/dev/null || echo "unknown") | tr -d '"')
|
||||
echo "widget-create: failed (code=$code): $message" >&2
|
||||
echo "{\"status\":\"error\",\"code\":$code,\"message\":\"$message\"}"
|
||||
exit 1
|
||||
HTTP_CODE="${API_RESPONSE##*$'\n'}"
|
||||
RESPONSE_BODY="${API_RESPONSE%$'\n'*}"
|
||||
unset API_RESPONSE
|
||||
|
||||
if ! printf '%s' "$RESPONSE_BODY" | python3 -I -c '
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
http_code = sys.argv[1]
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print(f"widget-create: non-JSON response (HTTP {http_code})", file=sys.stderr)
|
||||
print(json.dumps({"status":"error","code":0,"message":"Cloudflare API returned an invalid response"}))
|
||||
raise SystemExit(1)
|
||||
|
||||
errors = data.get("errors") if isinstance(data, dict) else []
|
||||
first = errors[0] if isinstance(errors, list) and errors and isinstance(errors[0], dict) else {}
|
||||
code = first.get("code", 0)
|
||||
if not isinstance(data, dict) or data.get("success") is not True:
|
||||
print(f"widget-create: request failed (HTTP {http_code}, code={code})", file=sys.stderr)
|
||||
print(json.dumps({"status":"error","code":code,"message":"Cloudflare API request failed"}))
|
||||
raise SystemExit(1)
|
||||
|
||||
result = data.get("result")
|
||||
sitekey = result.get("sitekey") if isinstance(result, dict) else None
|
||||
secret = result.get("secret") if isinstance(result, dict) else None
|
||||
if not (
|
||||
isinstance(sitekey, str)
|
||||
and re.fullmatch(r"\S{1,256}", sitekey)
|
||||
and isinstance(secret, str)
|
||||
and re.fullmatch(r"\S{1,1024}", secret)
|
||||
):
|
||||
print("widget-create: API returned invalid widget credentials", file=sys.stderr)
|
||||
print(json.dumps({"status":"error","code":0,"message":"Cloudflare API returned invalid widget credentials"}))
|
||||
raise SystemExit(1)
|
||||
|
||||
print(json.dumps({"status":"ok","sitekey":sitekey,"secret":secret}))
|
||||
' "$HTTP_CODE"; then
|
||||
unset RESPONSE_BODY
|
||||
exit 1
|
||||
fi
|
||||
unset RESPONSE_BODY
|
||||
Reference in new issue
Block a user