diff options
| author | Chia <Chia@93.nz> | 2026-08-05 22:01:29 +1200 |
|---|---|---|
| committer | Chia <Chia@93.nz> | 2026-08-05 22:07:50 +1200 |
| commit | eadb2ffe85c43cf6fc741c9823cd28eedb4a844c (patch) | |
| tree | 1aba2536d57360da403aa35c9ced58b615c7064e /scripts | |
| parent | cd0dd91ab93653631904f2ea0e574ccde6d60339 (diff) | |
feat: harden prepaid billing and commercial operations
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/backup-postgres.sh | 14 | ||||
| -rwxr-xr-x | scripts/load-smoke.sh | 22 | ||||
| -rwxr-xr-x | scripts/redis-fault-drill.sh | 18 | ||||
| -rwxr-xr-x | scripts/restore-drill.sh | 14 | ||||
| -rwxr-xr-x | scripts/start-debug.sh | 44 |
5 files changed, 106 insertions, 6 deletions
diff --git a/scripts/backup-postgres.sh b/scripts/backup-postgres.sh new file mode 100755 index 0000000..f6444c9 --- /dev/null +++ b/scripts/backup-postgres.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AIGW_DATABASE_URL:?AIGW_DATABASE_URL is required}" +backup_dir="${AIGW_BACKUP_DIR:-./backups}" +retention_days="${AIGW_BACKUP_RETENTION_DAYS:-30}" +mkdir -p -- "$backup_dir" +chmod 700 "$backup_dir" +timestamp="$(date -u +%Y%m%dT%H%M%SZ)" +target="$backup_dir/aigw-$timestamp.dump" +pg_dump --dbname="$AIGW_DATABASE_URL" --format=custom --compress=9 --file="$target" +sha256sum "$target" >"$target.sha256" +find "$backup_dir" -maxdepth 1 -type f -name 'aigw-*.dump*' -mtime "+$retention_days" -delete +printf 'backup=%s\nchecksum=%s.sha256\n' "$target" "$target" diff --git a/scripts/load-smoke.sh b/scripts/load-smoke.sh new file mode 100755 index 0000000..536edc1 --- /dev/null +++ b/scripts/load-smoke.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AIGW_LOAD_BASE_URL:?AIGW_LOAD_BASE_URL is required}" +: "${AIGW_LOAD_API_KEY:?AIGW_LOAD_API_KEY is required}" +: "${AIGW_LOAD_MODEL:?AIGW_LOAD_MODEL is required}" +requests="${AIGW_LOAD_REQUESTS:-1000}" +concurrency="${AIGW_LOAD_CONCURRENCY:-50}" +payload="$(mktemp)" +results="$(mktemp)" +trap 'rm -f -- "$payload" "$results"' EXIT +chmod 600 "$payload" "$results" +printf '{"model":"%s","messages":[{"role":"user","content":"health probe"}],"max_tokens":1}\n' "$AIGW_LOAD_MODEL" >"$payload" +export AIGW_LOAD_BASE_URL AIGW_LOAD_API_KEY payload results +seq "$requests" | xargs -P "$concurrency" -n 1 sh -c ' + curl --silent --show-error --output /dev/null --write-out "%{http_code}\n" \ + --header "Authorization: Bearer $AIGW_LOAD_API_KEY" --header "Content-Type: application/json" \ + --data-binary "@$payload" "$AIGW_LOAD_BASE_URL/v1/chat/completions" >>"$results" +' _ +failures="$(awk '$1 < 200 || $1 >= 300 { count++ } END { print count+0 }' "$results")" +printf 'requests=%s concurrency=%s failures=%s\n' "$requests" "$concurrency" "$failures" +test "$failures" -eq 0 diff --git a/scripts/redis-fault-drill.sh b/scripts/redis-fault-drill.sh new file mode 100755 index 0000000..8be92ff --- /dev/null +++ b/scripts/redis-fault-drill.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AIGW_READY_URL:?AIGW_READY_URL is required}" +: "${AIGW_REDIS_CONTAINER:?AIGW_REDIS_CONTAINER is required}" +cleanup() { docker start "$AIGW_REDIS_CONTAINER" >/dev/null 2>&1 || true; } +trap cleanup EXIT +docker stop "$AIGW_REDIS_CONTAINER" >/dev/null +for _ in $(seq 1 20); do + body="$(curl --silent --show-error --fail "$AIGW_READY_URL" || true)" + if printf '%s' "$body" | grep -q '"redis"' && printf '%s' "$body" | grep -q '"required":false'; then + printf 'redis degradation confirmed; gateway remained ready\n' + exit 0 + fi + sleep 1 +done +printf 'gateway did not report recoverable Redis degradation\n' >&2 +exit 1 diff --git a/scripts/restore-drill.sh b/scripts/restore-drill.sh new file mode 100755 index 0000000..53b8cd2 --- /dev/null +++ b/scripts/restore-drill.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AIGW_RESTORE_DATABASE_URL:?AIGW_RESTORE_DATABASE_URL must point to an isolated drill database}" +backup="${1:?usage: restore-drill.sh BACKUP.dump}" +[[ -f "$backup" && -f "$backup.sha256" ]] || { printf 'backup or checksum is missing\n' >&2; exit 2; } +sha256sum -c "$backup.sha256" +case "$AIGW_RESTORE_DATABASE_URL" in + *localhost*|*127.0.0.1*|*restore*|*drill*) ;; + *) printf 'refusing restore: target URL must visibly identify localhost, restore, or drill\n' >&2; exit 2 ;; +esac +pg_restore --dbname="$AIGW_RESTORE_DATABASE_URL" --clean --if-exists --no-owner "$backup" +psql "$AIGW_RESTORE_DATABASE_URL" -v ON_ERROR_STOP=1 -c "SELECT count(*) AS tenants FROM tenants; SELECT count(*) AS usage_events FROM usage_events; SELECT count(*) AS ledger_entries FROM billing_ledger;" +printf 'restore drill completed for %s\n' "$backup" diff --git a/scripts/start-debug.sh b/scripts/start-debug.sh index 7a22a89..bf18de5 100755 --- a/scripts/start-debug.sh +++ b/scripts/start-debug.sh @@ -26,6 +26,13 @@ if [[ ! -f "$env_file" ]]; then umask 077 { printf 'AIGW_SERVER_ADDRESS=:8080\n' + printf 'AIGW_PUBLIC_ADDRESS=:8080\n' + printf 'AIGW_ADMIN_ADDRESS=:8081\n' + printf 'AIGW_WEBHOOK_ADDRESS=:8082\n' + printf 'AIGW_OPERATIONS_ADDRESS=:9090\n' + printf 'AIGW_TRUSTED_PROXY_CIDRS=\n' + printf 'AIGW_REQUIRE_HTTPS=false\n' + printf 'AIGW_DEPLOYMENT_REGION=\n' printf 'AIGW_POSTGRES_USER=aigw\n' printf 'AIGW_POSTGRES_PASSWORD=%s\n' "$postgres_password" printf 'AIGW_POSTGRES_DB=aigw\n' @@ -34,10 +41,11 @@ if [[ ! -f "$env_file" ]]; then printf 'AIGW_REDIS_URL=redis://127.0.0.1:6379/0\n' printf 'AIGW_REDIS_URL_DOCKER=redis://redis:6379/0\n' printf 'AIGW_CREDENTIAL_KEY=%s\n' "$credential_key" + printf 'AIGW_CREDENTIAL_PREVIOUS_KEYS=\n' printf 'AIGW_ADMIN_TOKEN=%s\n' "$admin_token" - printf 'AIGW_PUBLIC_URL=http://localhost:8080/admin/\n' + printf 'AIGW_PUBLIC_URL=http://localhost:8081/admin/\n' printf 'AIGW_WEBAUTHN_RP_ID=localhost\n' - printf 'AIGW_WEBAUTHN_ORIGINS=http://localhost:8080\n' + printf 'AIGW_WEBAUTHN_ORIGINS=http://localhost:8081\n' printf 'AIGW_SMTP_FROM_ADDRESS=no-reply@aigw.local\n' printf 'AIGW_SMTP_ADDRESS=127.0.0.1:1025\n' printf 'AIGW_SMTP_ADDRESS_DOCKER=mailpit:1025\n' @@ -46,13 +54,37 @@ if [[ ! -f "$env_file" ]]; then printf 'AIGW_STRIPE_API_KEY=rk_test_replace_me\n' printf 'AIGW_STRIPE_CLI_API_KEY=rk_test_replace_me\n' printf 'AIGW_STRIPE_WEBHOOK_SECRET=whsec_replace_me\n' - printf 'AIGW_STRIPE_SUCCESS_URL=http://localhost:8080/admin/?topup=success\n' - printf 'AIGW_STRIPE_CANCEL_URL=http://localhost:8080/admin/?topup=cancel\n' + printf 'AIGW_STRIPE_SUCCESS_URL=http://localhost:8081/admin/?topup=success\n' + printf 'AIGW_STRIPE_CANCEL_URL=http://localhost:8081/admin/?topup=cancel\n' + printf 'AIGW_STRIPE_PORTAL_RETURN_URL=http://localhost:8081/admin/?billing=portal\n' + printf 'AIGW_STRIPE_AUTOMATIC_TAX_ENABLED=false\n' + printf 'AIGW_STRIPE_TAX_REGISTRATION_CONFIRMED=false\n' + printf 'AIGW_STRIPE_PRODUCT_TAX_CODE=\n' + printf 'AIGW_SETTLEMENT_SPOOL_PATH=/var/lib/aigw/settlements.jsonl\n' } >"$env_file" chmod 600 "$env_file" log "created $env_file" fi +# Backfill non-secret deployment settings when an older local environment file +# is reused. Exact legacy localhost values are moved to the split admin port. +sed -i \ + -e 's|^AIGW_PUBLIC_URL=http://localhost:8080/admin/$|AIGW_PUBLIC_URL=http://localhost:8081/admin/|' \ + -e 's|^AIGW_WEBAUTHN_ORIGINS=http://localhost:8080$|AIGW_WEBAUTHN_ORIGINS=http://localhost:8081|' \ + -e 's|^AIGW_STRIPE_SUCCESS_URL=http://localhost:8080/admin/?topup=success$|AIGW_STRIPE_SUCCESS_URL=http://localhost:8081/admin/?topup=success|' \ + -e 's|^AIGW_STRIPE_CANCEL_URL=http://localhost:8080/admin/?topup=cancel$|AIGW_STRIPE_CANCEL_URL=http://localhost:8081/admin/?topup=cancel|' \ + "$env_file" +for setting in \ + 'AIGW_PUBLIC_ADDRESS=:8080' \ + 'AIGW_ADMIN_ADDRESS=:8081' \ + 'AIGW_WEBHOOK_ADDRESS=:8082' \ + 'AIGW_OPERATIONS_ADDRESS=:9090' \ + 'AIGW_STRIPE_PORTAL_RETURN_URL=http://localhost:8081/admin/?billing=portal' \ + 'AIGW_SETTLEMENT_SPOOL_PATH=/var/lib/aigw/settlements.jsonl'; do + name="${setting%%=*}" + grep -q "^${name}=" "$env_file" || printf '%s\n' "$setting" >>"$env_file" +done + admin_token="$(sed -n 's/^AIGW_ADMIN_TOKEN=//p' "$env_file" | head -n 1)" [[ -n "$admin_token" ]] || fail "AIGW_ADMIN_TOKEN is missing from $env_file" for required_name in AIGW_SERVER_ADDRESS AIGW_POSTGRES_USER AIGW_POSTGRES_PASSWORD AIGW_POSTGRES_DB AIGW_DATABASE_URL_DOCKER AIGW_CREDENTIAL_KEY AIGW_PUBLIC_URL AIGW_WEBAUTHN_RP_ID AIGW_WEBAUTHN_ORIGINS AIGW_SMTP_FROM_ADDRESS AIGW_SMTP_ADDRESS_DOCKER AIGW_STRIPE_API_KEY AIGW_STRIPE_WEBHOOK_SECRET AIGW_STRIPE_SUCCESS_URL AIGW_STRIPE_CANCEL_URL; do @@ -81,9 +113,9 @@ if ! docker compose --env-file "$env_file" up -d --no-build --wait --wait-timeou fi log "services are ready" -printf '\nAdmin UI: http://localhost:8080/admin/\n' +printf '\nAdmin UI: http://localhost:8081/admin/\n' printf 'Mail inbox: http://127.0.0.1:8025/\n' -printf 'Health: http://127.0.0.1:8080/readyz\n' +printf 'Health: http://127.0.0.1:9090/readyz\n' printf 'Secrets: %s (mode 0600)\n' "$env_file" printf '\nLogs: docker compose --env-file %q logs -f aigw\n' "$env_file" printf 'Stop: ./scripts/stop-debug.sh\n' |
