diff --git a/scripts/gitea/doctor.sh b/scripts/gitea/doctor.sh new file mode 100755 index 00000000..27217502 --- /dev/null +++ b/scripts/gitea/doctor.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# gitea-doctor β€” pre-flight validation for Gitea npm registry connectivity. +# +# Run this before any deploy, before any `pnpm install` that needs @bytelyst/* +# from Gitea, and after any token rotation. Exits non-zero if anything is wrong. +# +# Usage: +# bash scripts/gitea/doctor.sh # full check +# bash scripts/gitea/doctor.sh --quiet # only print failures +# bash scripts/gitea/doctor.sh --probe PKG # also try resolving PKG@latest +# +# Checks: +# 1. NETWORK env var sane +# 2. GITEA_NPM_HOST set + DNS resolves +# 3. GITEA_NPM_OWNER set +# 4. GITEA_NPM_TOKEN in env matches the on-disk file (catches stale shells) +# 5. Registry HTTP 200 with token +# 6. Token has write scope (best-effort detection) +# 7. (Optional) given package@version resolves +# +# Exit codes: +# 0 all checks pass +# 1 one or more checks fail + +set -uo pipefail + +QUIET=false +PROBE_PKG="" +while [ $# -gt 0 ]; do + case "$1" in + --quiet) QUIET=true ;; + --probe) PROBE_PKG="${2:-}"; shift ;; + -h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac + shift +done + +fail=0 +say() { $QUIET || echo "$@"; } +ok() { say " βœ“ $1"; } +warn() { echo " ⚠ $1"; } +err() { echo " βœ— $1"; fail=1; } + +say "πŸ” gitea-doctor β€” Gitea npm registry pre-flight" +say "" + +# ── 1. NETWORK ─────────────────────────────────────────────────── +NETWORK="${NETWORK:-home}" +case "$NETWORK" in + corp|home) ok "NETWORK=$NETWORK" ;; + *) err "NETWORK='$NETWORK' (must be 'corp' or 'home')" ;; +esac + +# ── 2. GITEA_NPM_HOST ──────────────────────────────────────────── +if [ -z "${GITEA_NPM_HOST:-}" ]; then + err "GITEA_NPM_HOST not set. Source switch-network.sh." +else + ok "GITEA_NPM_HOST=$GITEA_NPM_HOST" + # DNS / reachability β€” only matters for non-localhost + if [ "$GITEA_NPM_HOST" != "localhost" ] && [ "$GITEA_NPM_HOST" != "127.0.0.1" ]; then + if ! getent hosts "$GITEA_NPM_HOST" >/dev/null 2>&1 && ! host "$GITEA_NPM_HOST" >/dev/null 2>&1; then + err "DNS does not resolve $GITEA_NPM_HOST" + else + ok "DNS resolves $GITEA_NPM_HOST" + fi + fi +fi + +# ── 3. GITEA_NPM_OWNER ─────────────────────────────────────────── +if [ -z "${GITEA_NPM_OWNER:-}" ]; then + err "GITEA_NPM_OWNER not set. Source switch-network.sh (or export manually)." +else + ok "GITEA_NPM_OWNER=$GITEA_NPM_OWNER" +fi + +# ── 4. Token consistency (env vs file) ─────────────────────────── +env_token="${GITEA_NPM_TOKEN:-}" +file_token="" +token_file="" +for candidate in "$HOME/.gitea_npm_token_${NETWORK}" "$HOME/.gitea_npm_token"; do + if [ -f "$candidate" ]; then + file_token="$(tr -d '\n\r ' < "$candidate")" + token_file="$candidate" + break + fi +done + +if [ -z "$file_token" ]; then + err "No token file found (~/.gitea_npm_token or ~/.gitea_npm_token_$NETWORK)" +elif [ -z "$env_token" ]; then + warn "GITEA_NPM_TOKEN not in env, but $token_file exists (re-source ~/.zshrc)" + env_token="$file_token" # use file for further checks +elif [ "$env_token" != "$file_token" ]; then + err "STALE TOKEN: env GITEA_NPM_TOKEN β‰  $token_file" + err " env starts: ${env_token:0:8}…" + err " file starts: ${file_token:0:8}…" + err " Fix: source ~/.zshrc (or open a new terminal)" + env_token="$file_token" # still try registry check with fresh value +else + ok "Token consistent (env matches $token_file, ${#env_token} chars)" +fi + +# ── 5. Registry connectivity + auth ────────────────────────────── +if [ -n "${GITEA_NPM_HOST:-}" ] && [ -n "${GITEA_NPM_OWNER:-}" ] && [ -n "$env_token" ]; then + url="http://${GITEA_NPM_HOST}:3300/api/packages/${GITEA_NPM_OWNER}/npm/@bytelyst%2Ferrors" + code=$(curl -fsS -o /dev/null -w "%{http_code}" "$url" \ + -H "Authorization: token $env_token" --noproxy '*' --max-time 5 2>/dev/null || echo "000") + case "$code" in + 200) ok "Registry HTTP 200 on @bytelyst/errors" ;; + 401|403) err "Registry HTTP $code β€” token rejected. Rotate token." ;; + 404) err "Registry HTTP 404 β€” owner '$GITEA_NPM_OWNER' may be wrong" ;; + 000) err "Registry unreachable at $url (Gitea down? SSH tunnel? VPN?)" ;; + *) err "Registry HTTP $code (unexpected)" ;; + esac +else + warn "Skipping registry probe (host/owner/token incomplete)" +fi + +# ── 6. Token scope (best-effort) ───────────────────────────────── +# Gitea exposes /api/v1/users//tokens for the authenticated user; +# we can't enumerate scopes for the current token directly, so probe a +# write endpoint with a HEAD request that won't actually publish. +if [ -n "$env_token" ] && [ -n "${GITEA_NPM_HOST:-}" ]; then + url="http://${GITEA_NPM_HOST}:3300/api/v1/repos/search?limit=1" + code=$(curl -fsS -o /dev/null -w "%{http_code}" "$url" \ + -H "Authorization: token $env_token" --noproxy '*' --max-time 5 2>/dev/null || echo "000") + if [ "$code" = "200" ]; then + ok "Token authenticates against Gitea API" + else + warn "Could not verify token via Gitea API (HTTP $code)" + fi +fi + +# ── 7. Optional package probe ──────────────────────────────────── +if [ -n "$PROBE_PKG" ] && [ -n "$env_token" ]; then + encoded="${PROBE_PKG//\//%2F}" + url="http://${GITEA_NPM_HOST}:3300/api/packages/${GITEA_NPM_OWNER}/npm/${encoded}" + code=$(curl -fsS -o /dev/null -w "%{http_code}" "$url" \ + -H "Authorization: token $env_token" --noproxy '*' --max-time 5 2>/dev/null || echo "000") + if [ "$code" = "200" ]; then + versions=$(curl -fsS "$url" -H "Authorization: token $env_token" --noproxy '*' --max-time 5 \ + 2>/dev/null | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | sort -V | tail -3 | tr '\n' ' ') + ok "$PROBE_PKG resolvable (latest versions: $versions)" + else + err "$PROBE_PKG not found in registry (HTTP $code)" + fi +fi + +say "" +if [ "$fail" -eq 0 ]; then + say "βœ… All Gitea pre-flight checks passed" +else + echo "❌ Gitea pre-flight failed β€” fix issues above before deploying" +fi +exit "$fail" diff --git a/scripts/gitea/token.sh b/scripts/gitea/token.sh new file mode 100755 index 00000000..07f8baf7 --- /dev/null +++ b/scripts/gitea/token.sh @@ -0,0 +1,199 @@ +#!/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 diff --git a/scripts/npmrc.template b/scripts/npmrc.template index bddc33e6..a14197cc 100644 --- a/scripts/npmrc.template +++ b/scripts/npmrc.template @@ -1,5 +1,5 @@ -@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/learning_ai_user/npm/ -//localhost:3300/api/packages/learning_ai_user/npm/:_authToken=${GITEA_NPM_TOKEN} +@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/${GITEA_NPM_OWNER:-learning_ai_user}/npm/ +//${GITEA_NPM_HOST:-localhost}:3300/api/packages/${GITEA_NPM_OWNER:-learning_ai_user}/npm/:_authToken=${GITEA_NPM_TOKEN} strict-ssl=false link-workspace-packages=true prefer-workspace-packages=true diff --git a/scripts/switch-network.sh b/scripts/switch-network.sh index 4cad71b6..7c300e22 100755 --- a/scripts/switch-network.sh +++ b/scripts/switch-network.sh @@ -105,6 +105,11 @@ unset _CORP_PROXY _CORP_NPM_REGISTRY # Create: curl -s -u admin:PASSWORD http://:3300/api/v1/users/admin/tokens \ # -H 'Content-Type: application/json' -d '{"name":"npm"}' | jq -r '.sha1 // .token' +# Gitea npm package owner β€” single env var drives every .npmrc + Dockerfile. +# Override per-shell if you have multiple Gitea orgs; default is the canonical +# ByteLyst owner. Renaming the owner is now a one-line env change. +export GITEA_NPM_OWNER="${GITEA_NPM_OWNER:-learning_ai_user}" + _GITEA_VM_HOST_FILE="$HOME/.gitea_vm_host" if [ "${NETWORK:-home}" = "corp" ]; then export GITEA_NPM_HOST="localhost" @@ -119,10 +124,18 @@ else fi unset _GITEA_VM_HOST_FILE -# Token: load from file if not already set -_GITEA_TOKEN_FILE="$HOME/.gitea_npm_token" -if [ -z "${GITEA_NPM_TOKEN:-}" ] && [ -f "$_GITEA_TOKEN_FILE" ]; then - export GITEA_NPM_TOKEN - GITEA_NPM_TOKEN="$(cat "$_GITEA_TOKEN_FILE")" -fi -unset _GITEA_TOKEN_FILE +# Token: per-network file preferred, fallback to shared file +# Layout: +# ~/.gitea_npm_token_corp β†’ corp (local Gitea via SSH tunnel) +# ~/.gitea_npm_token_home β†’ home / prod (cloud VM Gitea) +# ~/.gitea_npm_token β†’ fallback (used if per-network file missing) +unset GITEA_NPM_TOKEN +_NET="${NETWORK:-home}" +for _f in "$HOME/.gitea_npm_token_${_NET}" "$HOME/.gitea_npm_token"; do + if [ -f "$_f" ]; then + GITEA_NPM_TOKEN="$(tr -d '\n\r ' < "$_f")" + export GITEA_NPM_TOKEN + break + fi +done +unset _NET _f