← Files CloudflareARCHIVED FILE

skills/turnstile-spin/scripts/widget-create.sh

4.05 KB · Oct 1, 2026 · 00:02 UTC

↓ Download file

#!/usr/bin/env bash
# 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) 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}"
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

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_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")"

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

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

SHA-256: ebc3ef13ef99f4f6f93400ce66ffc3848c1913aac0cb0876470c6378cf1d9fed