#!/usr/bin/env bash # gitea-token — manage Gitea npm tokens (rotate, validate, print). # # Usage: # bash scripts/gitea/token.sh status # show env vs file status # bash scripts/gitea/token.sh print # print current token to stdout # bash scripts/gitea/token.sh validate # HTTP 200 probe against registry # bash scripts/gitea/token.sh rotate # mint new token, write to file # (requires GITEA_ADMIN_USER/PASS in env # or macOS Keychain entry 'gitea-admin') # # Token file resolution (matches switch-network.sh): # ~/.gitea_npm_token_${NETWORK} (per-network, preferred) # ~/.gitea_npm_token (fallback) # # Why this exists: # - Stops manual file editing → stale shell envs. # - One command rotates the token, writes the file, exports to current shell. # - In any shell, `eval "$(bash gitea/token.sh print --export)"` refreshes # GITEA_NPM_TOKEN without re-sourcing ~/.zshrc. set -uo pipefail cmd="${1:-status}" shift 2>/dev/null || true NETWORK="${NETWORK:-home}" GITEA_NPM_HOST="${GITEA_NPM_HOST:-localhost}" GITEA_NPM_OWNER="${GITEA_NPM_OWNER:-learning_ai_user}" # ── Resolve token file path ───────────────────────────────────── resolve_file() { for candidate in "$HOME/.gitea_npm_token_${NETWORK}" "$HOME/.gitea_npm_token"; do if [ -f "$candidate" ]; then echo "$candidate" return 0 fi done # If neither exists, prefer per-network path for new file creation echo "$HOME/.gitea_npm_token_${NETWORK}" } TOKEN_FILE="$(resolve_file)" read_token_file() { [ -f "$TOKEN_FILE" ] && tr -d '\n\r ' < "$TOKEN_FILE" || echo "" } # ── Resolve admin credentials (for rotate) ─────────────────────── # Priority: env vars → macOS Keychain → fail get_admin_credentials() { if [ -n "${GITEA_ADMIN_USER:-}" ] && [ -n "${GITEA_ADMIN_PASS:-}" ]; then echo "$GITEA_ADMIN_USER:$GITEA_ADMIN_PASS" return 0 fi if command -v security >/dev/null 2>&1; then local pass pass="$(security find-generic-password -s 'gitea-admin' -w 2>/dev/null || true)" local user user="$(security find-generic-password -s 'gitea-admin' 2>/dev/null \ | awk -F'"' '/"acct"/{print $4}' || true)" if [ -n "$pass" ] && [ -n "$user" ]; then echo "$user:$pass" return 0 fi fi return 1 } # ── Subcommand: status ─────────────────────────────────────────── do_status() { echo "Network: $NETWORK" echo "Host: $GITEA_NPM_HOST" echo "Owner: $GITEA_NPM_OWNER" echo "Token file: $TOKEN_FILE" local f="$(read_token_file)" local e="${GITEA_NPM_TOKEN:-}" if [ -z "$f" ]; then echo "File status: MISSING — run '$0 rotate' or create manually" else echo "File status: exists (${#f} chars, starts ${f:0:8}…)" fi if [ -z "$e" ]; then echo "Env status: UNSET in current shell" elif [ -z "$f" ]; then echo "Env status: set but no file to compare" elif [ "$e" = "$f" ]; then echo "Env vs file: ✓ MATCH" else echo "Env vs file: ✗ STALE — env=${e:0:8}…, file=${f:0:8}… (run: source ~/.zshrc)" fi } # ── Subcommand: print ──────────────────────────────────────────── do_print() { local export_flag=false [ "${1:-}" = "--export" ] && export_flag=true local t="$(read_token_file)" if [ -z "$t" ]; then echo "Error: no token in $TOKEN_FILE" >&2 exit 1 fi if $export_flag; then echo "export GITEA_NPM_TOKEN='$t'" else echo "$t" fi } # ── Subcommand: validate ───────────────────────────────────────── do_validate() { local t="${GITEA_NPM_TOKEN:-$(read_token_file)}" if [ -z "$t" ]; then echo "Error: no token available" >&2 exit 1 fi local url="http://${GITEA_NPM_HOST}:3300/api/packages/${GITEA_NPM_OWNER}/npm/@bytelyst%2Ferrors" local code code=$(curl -fsS -o /dev/null -w "%{http_code}" "$url" \ -H "Authorization: token $t" --noproxy '*' --max-time 5 2>/dev/null || echo "000") case "$code" in 200) echo "✓ Token valid (HTTP 200 against @bytelyst/errors)" ;; 401|403) echo "✗ Token rejected (HTTP $code) — rotate it"; exit 1 ;; 404) echo "✗ Owner '$GITEA_NPM_OWNER' not found on $GITEA_NPM_HOST (HTTP 404)"; exit 1 ;; 000) echo "✗ Registry unreachable at $url"; exit 1 ;; *) echo "✗ Unexpected HTTP $code"; exit 1 ;; esac } # ── Subcommand: rotate ─────────────────────────────────────────── do_rotate() { local creds if ! creds=$(get_admin_credentials); then cat >&2 < GITEA_ADMIN_PASS=

2. macOS Keychain: security add-generic-password -s 'gitea-admin' \\ -a '' -w '' Then re-run: $0 rotate EOF exit 1 fi local user="${creds%:*}" local pass="${creds#*:}" local token_name="npm-$(date +%Y%m%d-%H%M%S)-$(hostname -s)" echo "Minting token '$token_name' for user '$user' on $GITEA_NPM_HOST…" local response response=$(curl -fsS -u "$user:$pass" \ -X POST "http://${GITEA_NPM_HOST}:3300/api/v1/users/$user/tokens" \ -H 'Content-Type: application/json' \ --noproxy '*' --max-time 10 \ -d "{\"name\":\"$token_name\",\"scopes\":[\"write:package\",\"read:package\"]}" 2>&1) local rc=$? if [ $rc -ne 0 ]; then echo "✗ API call failed:" >&2 echo "$response" >&2 exit 1 fi # Gitea returns the secret as "sha1" (older) or "token" (newer) local new_token new_token=$(echo "$response" | grep -oE '"(sha1|token)":"[^"]+' | head -1 | sed 's/.*":"//') if [ -z "$new_token" ]; then echo "✗ Could not extract token from response:" >&2 echo "$response" >&2 exit 1 fi # Backup old, write new if [ -f "$TOKEN_FILE" ]; then cp "$TOKEN_FILE" "${TOKEN_FILE}.bak" fi printf '%s' "$new_token" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" echo "✓ Written to $TOKEN_FILE (mode 600, ${#new_token} chars)" echo "✓ Old token backed up to ${TOKEN_FILE}.bak" echo "" echo "To refresh the current shell:" echo " eval \"\$(bash '$0' print --export)\"" echo "" echo "Validating new token…" GITEA_NPM_TOKEN="$new_token" do_validate } case "$cmd" in status) do_status ;; print) do_print "$@" ;; validate) do_validate ;; rotate) do_rotate ;; -h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' ;; *) echo "Unknown command: $cmd (use: status|print|validate|rotate)" >&2; exit 2 ;; esac