summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-05 22:01:29 +1200
committerChia <Chia@93.nz>2026-08-05 22:07:50 +1200
commiteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (patch)
tree1aba2536d57360da403aa35c9ced58b615c7064e /scripts
parentcd0dd91ab93653631904f2ea0e574ccde6d60339 (diff)
feat: harden prepaid billing and commercial operations
Diffstat (limited to '')
-rwxr-xr-xscripts/backup-postgres.sh14
-rwxr-xr-xscripts/load-smoke.sh22
-rwxr-xr-xscripts/redis-fault-drill.sh18
-rwxr-xr-xscripts/restore-drill.sh14
-rwxr-xr-xscripts/start-debug.sh44
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'