summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-06 09:29:41 +1200
committerChia <Chia@93.nz>2026-08-06 09:32:46 +1200
commit41e322c53d7b4b796eb377d0df9c29ecd10ba431 (patch)
treec730526150e55e39b822d5197e4a20318ecaa449 /scripts
parenteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (diff)
feat: complete commercial control plane, billing, auth, and model catalog
- add PostgreSQL control-plane persistence with Redis-degraded hot reload - implement prepaid balance, usage ledger, Stripe top-up and reconciliation - add registration, email verification, password reset, invitations and RBAC - support TOTP, Passkey MFA, device sessions, quotas and rate limits - add tenant billing profiles, audit logs and operational readiness checks - build authenticated admin console, Quickstart, Playground and usage analytics - add public model catalog with pricing, filtering and cost estimation - support OpenAI Responses providers and provider health failover - validate real upstream usage reporting and balance settlement
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/start-debug.sh97
-rwxr-xr-xscripts/stop-debug.sh66
2 files changed, 67 insertions, 96 deletions
diff --git a/scripts/start-debug.sh b/scripts/start-debug.sh
index bf18de5..80db5de 100755
--- a/scripts/start-debug.sh
+++ b/scripts/start-debug.sh
@@ -22,104 +22,43 @@ if [[ ! -f "$env_file" ]]; then
command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate local credentials"
credential_key="$(openssl rand -base64 32 | tr -d '\n')"
admin_token="aigw-admin-$(openssl rand -hex 24)"
- postgres_password="$(openssl rand -hex 24)"
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'
- printf 'AIGW_DATABASE_URL=postgres://aigw:%s@127.0.0.1:5432/aigw?sslmode=disable\n' "$postgres_password"
- printf 'AIGW_DATABASE_URL_DOCKER=postgres://aigw:%s@postgres:5432/aigw?sslmode=disable\n' "$postgres_password"
- 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:8081/admin/\n'
- printf 'AIGW_WEBAUTHN_RP_ID=localhost\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'
- printf 'AIGW_SMTP_USERNAME=\n'
- printf 'AIGW_SMTP_PASSWORD=\n'
- 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: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"
+for required_name in AIGW_CREDENTIAL_KEY AIGW_ADMIN_TOKEN; do
+ grep -q "^${required_name}=." "$env_file" || fail "$required_name is missing from $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
- grep -q "^${required_name}=." "$env_file" || fail "$required_name is missing from $env_file; compare it with .env.control.example"
-done
+compose=(docker compose --project-directory "$repo_dir")
+if [[ -n "${AIGW_COMPOSE_PROJECT_NAME:-}" ]]; then
+ compose+=(--project-name "$AIGW_COMPOSE_PROJECT_NAME")
+fi
+compose+=(--env-file "$env_file")
cd "$repo_dir"
if [[ "${AIGW_DEBUG_SKIP_BUILD:-0}" == "1" ]]; then
- log "skipping image build (AIGW_DEBUG_SKIP_BUILD=1)"
+ log "starting all services without rebuilding the gateway image"
+ up_args=(up -d --no-build --remove-orphans --wait --wait-timeout 120)
else
- build_network="${AIGW_DOCKER_BUILD_NETWORK:-host}"
- log "building gateway image (network: $build_network)"
- docker build --network="$build_network" -t aigw-debug:local .
+ log "building and starting all services"
+ up_args=(up -d --build --remove-orphans --wait --wait-timeout 120)
fi
-log "starting PostgreSQL, Redis, Mailpit, and gateway"
-if ! docker compose --env-file "$env_file" up -d --no-build --wait --wait-timeout 120; then
- docker compose --env-file "$env_file" ps >&2 || true
- gateway_logs="$(docker compose --env-file "$env_file" logs --no-color --tail=120 aigw 2>&1 || true)"
- printf '%s\n' "$gateway_logs" >&2
- if [[ "$gateway_logs" == *"decrypt credential"* ]]; then
- printf '[aigw-debug] the AIGW_CREDENTIAL_KEY in %s does not match credentials already stored in the PostgreSQL volume\n' "$env_file" >&2
- printf '[aigw-debug] restore the previous key, or explicitly remove the debug volumes if the stored control-plane data is disposable\n' >&2
- fi
+if ! "${compose[@]}" "${up_args[@]}"; then
+ "${compose[@]}" ps >&2 || true
+ "${compose[@]}" logs --no-color --tail=120 aigw >&2 || true
fail "services did not become healthy"
fi
-log "services are ready"
-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:9090/readyz\n'
+log "all services are ready"
+printf '\nAdmin UI: http://127.0.0.1:8080/admin/\n'
+printf 'Health: http://127.0.0.1:8080/readyz\n'
printf 'Secrets: %s (mode 0600)\n' "$env_file"
-printf '\nLogs: docker compose --env-file %q logs -f aigw\n' "$env_file"
+printf '\nLogs: docker compose --project-directory %q --env-file %q logs -f\n' "$repo_dir" "$env_file"
printf 'Stop: ./scripts/stop-debug.sh\n'
-
-if grep -q '^AIGW_STRIPE_API_KEY=rk_test_replace_me$' "$env_file" 2>/dev/null; then
- printf '\nStripe uses placeholders. Replace the two Stripe values in %s before testing Checkout.\n' "$env_file"
-fi
diff --git a/scripts/stop-debug.sh b/scripts/stop-debug.sh
index 43de07d..661d7c1 100755
--- a/scripts/stop-debug.sh
+++ b/scripts/stop-debug.sh
@@ -5,31 +5,63 @@ set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd -- "$script_dir/.." && pwd)"
env_file="${AIGW_DEBUG_ENV_FILE:-$repo_dir/.env.debug}"
+stop_timeout="${AIGW_DEBUG_STOP_TIMEOUT:-30}"
-command -v docker >/dev/null 2>&1 || {
- printf '[aigw-debug] error: docker is not installed\n' >&2
+log() {
+ printf '[aigw-debug] %s\n' "$*"
+}
+
+fail() {
+ printf '[aigw-debug] error: %s\n' "$*" >&2
exit 1
}
+command -v docker >/dev/null 2>&1 || fail "docker is not installed"
+docker info >/dev/null 2>&1 || fail "cannot access Docker; check that the daemon is running and your user belongs to the docker group"
+
+if [[ ! "$stop_timeout" =~ ^[0-9]+$ ]]; then
+ fail "AIGW_DEBUG_STOP_TIMEOUT must be a non-negative integer"
+fi
+
+compose=(docker compose --project-directory "$repo_dir")
+if [[ -n "${AIGW_COMPOSE_PROJECT_NAME:-}" ]]; then
+ compose+=(--project-name "$AIGW_COMPOSE_PROJECT_NAME")
+fi
+
if [[ -f "$env_file" ]]; then
- compose=(docker compose --env-file "$env_file")
+ compose+=(--env-file "$env_file")
else
- export AIGW_SERVER_ADDRESS=:8080
- export AIGW_POSTGRES_USER=debug-stop-placeholder
- export AIGW_POSTGRES_PASSWORD=debug-stop-placeholder
- export AIGW_POSTGRES_DB=debug-stop-placeholder
- export AIGW_DATABASE_URL_DOCKER=postgres://debug-stop-placeholder:debug-stop-placeholder@postgres:5432/debug-stop-placeholder
- export AIGW_REDIS_URL_DOCKER=
+ # Compose still interpolates required variables for `down`; these values are
+ # only used to resolve the file and are never sent to a running service.
export AIGW_CREDENTIAL_KEY=debug-stop-placeholder
export AIGW_ADMIN_TOKEN=debug-stop-placeholder
- export AIGW_STRIPE_API_KEY=debug-stop-placeholder
- export AIGW_STRIPE_WEBHOOK_SECRET=debug-stop-placeholder
- export AIGW_STRIPE_SUCCESS_URL=http://127.0.0.1:8080/admin/?topup=success
- export AIGW_STRIPE_CANCEL_URL=http://127.0.0.1:8080/admin/?topup=cancel
- compose=(docker compose)
fi
cd "$repo_dir"
-printf '[aigw-debug] stopping services\n'
-"${compose[@]}" down --remove-orphans
-printf '[aigw-debug] stopped; PostgreSQL and Redis volumes were preserved\n'
+log "stopping all Compose services"
+down_status=0
+if "${compose[@]}" down --remove-orphans --timeout "$stop_timeout"; then
+ :
+else
+ down_status=$?
+fi
+
+# A forced cleanup handles containers left behind by an interrupted `down` or
+# by a previous compose configuration, while leaving named data volumes intact.
+mapfile -t remaining_containers < <("${compose[@]}" ps -aq 2>/dev/null || true)
+if ((${#remaining_containers[@]} > 0)); then
+ log "removing ${#remaining_containers[@]} remaining service container(s)"
+ if ! docker rm -f -- "${remaining_containers[@]}"; then
+ fail "could not remove all remaining service containers"
+ fi
+fi
+
+mapfile -t remaining_containers < <("${compose[@]}" ps -aq 2>/dev/null || true)
+if ((${#remaining_containers[@]} > 0)); then
+ fail "${#remaining_containers[@]} service container(s) are still running or present"
+fi
+if ((down_status != 0)); then
+ fail "docker compose down failed with exit code $down_status"
+fi
+
+log "all services stopped; named PostgreSQL and Redis volumes were preserved"