mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 19:42:45 +00:00
67f7d09fe5
## What A trusted, server-side HTTP API so a bot authenticated with a shared secret can **create private games, change their settings, start them, kick players, and pause/resume** — without opening a WebSocket or joining as a player. Two endpoints under `/api/adminbot/`, reaching the owning worker via the existing `/wN/` nginx routing. They reuse the existing Zod schemas and `GameServer` methods, mirroring the WebSocket intent flow rather than inventing a new wire protocol. | Endpoint | Purpose | | --- | --- | | `POST /api/adminbot/create_game` | Create a private game; the worker mints a self-owned id and returns it (body: `GameConfigSchema.partial()`) | | `POST /api/adminbot/game/:id/intent` | Send a lobby-management intent (body: base `IntentSchema`) | ## How it works - **Auth:** `ADMIN_BOT_API_KEY` env var via the `x-admin-bot-key` header (timing-safe compare). The whole API is **disabled — 404 — when the var is unset**, so non-configured environments expose nothing. It's distinct from the per-instance `ADMIN_TOKEN`, which an external bot can't know. - **`GameServer.handleIntent`** is the unified intent dispatch for both the WebSocket `case "intent"` path and the admin-bot HTTP API. An `IntentActor` carries identity + authority (per-connection lobby-creator/role checks for the WS path; admin authority for the bot). It honors `update_game_config`, `toggle_game_start_timer`, `kick_player`, and `toggle_pause` — **on private games only** (`isPublic()` → 403). Gameplay intents and `mark_disconnected` are rejected (400). - **Private games only.** `create_game` rejects any `gameType` other than `Private` (Public *and* Singleplayer → 400); an omitted `gameType` defaults to `Private`. - **The bot is never a player.** It sends no `clientID`; the server stamps a placeholder `ADMIN_BOT_CLIENT_ID = "ADMINBOT"` (collision-proof — contains `I`/`O`, which `generateID()` never emits). A gameplay intent stamped with it would resolve to no player, so puppeteering is structurally impossible on top of the explicit 400. - **Determinism unchanged:** the only intent that reaches the sim is `toggle_pause`, via the same `addIntent` → turn queue → `ServerTurnMessage` path the WS uses. ## Notable details for review - **`hostCheats` is assigned unconditionally — on purpose.** `updateGameConfig` sets `this.gameConfig.hostCheats = gameConfig.hostCheats` unconditionally, unlike its sibling fields (which are guarded on `!== undefined`). The WS host clears cheats by re-sending the *full* config with `hostCheats: undefined`, so here `undefined` must mean "clear", not "leave unchanged". **Caveat for the admin bot**, which is a *partial*-update client: a partial `update_game_config` that omits `hostCheats` will clear it — the bot should send `hostCheats` explicitly (or a full config) when it wants to keep a previously-set value. - **Deploy wiring:** `ADMIN_BOT_API_KEY` is piped through the deploy steps' `env:` in `deploy.yml`/`release.yml` → `deploy.sh` heredoc → container via `update.sh`'s `--env-file`. The remaining manual step is creating the GitHub secret itself. ## Tests 19 new tests: - `GameServer.handleIntent` admin-bot behavior (per-intent, private-only, post-start guards, placeholder clientID, rejected gameplay/`mark_disconnected` intents). - `create_game` gameType guard (Public and Singleplayer both rejected). - `requireAdminBotKey` middleware (404 disabled / 401 missing / 401 wrong / pass). tsc + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
164 lines
4.9 KiB
Bash
Executable File
164 lines
4.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# deploy.sh - Deploy application to Hetzner server
|
|
# This script:
|
|
# 1. Copies the update script to Hetzner server
|
|
# 2. Executes the update script on the Hetzner server
|
|
|
|
set -e # Exit immediately if a command exits with a non-zero status
|
|
|
|
# Function to print section headers
|
|
print_header() {
|
|
echo "======================================================"
|
|
echo "🚀 $1"
|
|
echo "======================================================"
|
|
}
|
|
|
|
# Check command line arguments
|
|
if [ $# -ne 4 ]; then
|
|
echo "Error: Please specify environment, host, version tag, and subdomain"
|
|
echo "Usage: $0 [prod|staging] [nbg1|staging|masters|falk2] [version_tag] [subdomain]"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate first argument (environment)
|
|
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
|
|
echo "Error: First argument must be either 'prod' or 'staging'"
|
|
echo "Usage: $0 [prod|staging] [nbg1|staging|masters|falk2] [version_tag] [subdomain]"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate second argument (host)
|
|
if [ "$2" != "falk2" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
|
|
echo "Error: Second argument must be either 'falk2', 'nbg1', 'staging', or 'masters'"
|
|
echo "Usage: $0 [prod|staging] [nbg1|staging|masters|falk2] [version_tag] [subdomain]"
|
|
exit 1
|
|
fi
|
|
|
|
ENV=$1
|
|
HOST=$2
|
|
VERSION_TAG=$3
|
|
SUBDOMAIN=$4
|
|
|
|
# Set subdomain - use the provided subdomain
|
|
echo "Using subdomain: $SUBDOMAIN"
|
|
|
|
# Load common environment variables first
|
|
if [ -f .env ]; then
|
|
echo "Loading common configuration from .env file..."
|
|
export $(grep -v '^#' .env | xargs)
|
|
fi
|
|
|
|
# Load environment-specific variables
|
|
if [ -f .env.$ENV ]; then
|
|
echo "Loading $ENV-specific configuration from .env.$ENV file..."
|
|
export $(grep -v '^#' .env.$ENV | xargs)
|
|
fi
|
|
|
|
# Check required environment variables for deployment
|
|
if [ -z "$GHCR_USERNAME" ] || [ -z "$GHCR_REPO" ]; then
|
|
echo "Error: GHCR_USERNAME or GHCR_REPO not defined in .env file or environment"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$VERSION_TAG" == sha256:* ]]; then
|
|
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}@${VERSION_TAG}"
|
|
else
|
|
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
|
|
fi
|
|
|
|
if [ -z "$DOMAIN" ]; then
|
|
echo "Error: DOMAIN not defined in .env file or environment"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$HOST" == "staging" ]; then
|
|
print_header "DEPLOYING TO STAGING HOST"
|
|
SERVER_HOST=$SERVER_HOST_STAGING
|
|
elif [ "$HOST" == "nbg1" ]; then
|
|
print_header "DEPLOYING TO NBG1 HOST"
|
|
SERVER_HOST=$SERVER_HOST_NBG1
|
|
elif [ "$HOST" == "masters" ]; then
|
|
print_header "DEPLOYING TO MASTERS HOST"
|
|
SERVER_HOST=$SERVER_HOST_MASTERS
|
|
elif [ "$HOST" == "falk2" ]; then
|
|
print_header "DEPLOYING TO FALK2 HOST"
|
|
SERVER_HOST=$SERVER_HOST_FALK2
|
|
fi
|
|
|
|
# Check required environment variables
|
|
if [ -z "$SERVER_HOST" ]; then
|
|
echo "Error: ${HOST} not defined in .env file or environment"
|
|
exit 1
|
|
fi
|
|
|
|
# Configuration
|
|
UPDATE_SCRIPT="./update.sh" # Path to your update script
|
|
REMOTE_USER="openfront"
|
|
REMOTE_UPDATE_PATH="/home/$REMOTE_USER"
|
|
REMOTE_UPDATE_SCRIPT="$REMOTE_UPDATE_PATH/update-openfront.sh" # Where to place the script on server
|
|
|
|
# Check if update script exists
|
|
if [ ! -f "$UPDATE_SCRIPT" ]; then
|
|
echo "Error: Update script $UPDATE_SCRIPT not found!"
|
|
exit 1
|
|
fi
|
|
|
|
# Display deployment information
|
|
print_header "DEPLOYMENT INFORMATION"
|
|
echo "Environment: ${ENV}"
|
|
echo "Host: ${HOST}"
|
|
echo "Subdomain: ${SUBDOMAIN}"
|
|
echo "Image: $GHCR_IMAGE"
|
|
echo "Target Server: $SERVER_HOST"
|
|
|
|
# Copy update script to Hetzner server
|
|
print_header "COPYING UPDATE SCRIPT TO SERVER"
|
|
echo "Target: $REMOTE_USER@$SERVER_HOST"
|
|
|
|
# Make sure the update script is executable
|
|
chmod +x $UPDATE_SCRIPT
|
|
|
|
# Copy the update script to the server
|
|
scp -i $SSH_KEY $UPDATE_SCRIPT $REMOTE_USER@$SERVER_HOST:$REMOTE_UPDATE_SCRIPT
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "❌ Failed to copy update script to server. Stopping deployment."
|
|
exit 1
|
|
fi
|
|
|
|
# Generate a random filename for the environment file to prevent conflicts
|
|
# when multiple deployments are happening at the same time.
|
|
ENV_FILE="${REMOTE_UPDATE_PATH}/${SUBDOMAIN}-${RANDOM}.env"
|
|
|
|
print_header "EXECUTING UPDATE SCRIPT ON SERVER"
|
|
|
|
ssh -i $SSH_KEY $REMOTE_USER@$SERVER_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && \
|
|
cat > $ENV_FILE << 'EOL'
|
|
GAME_ENV=$ENV
|
|
ENV=$ENV
|
|
HOST=$HOST
|
|
GHCR_IMAGE=$GHCR_IMAGE
|
|
GHCR_TOKEN=$GHCR_TOKEN
|
|
API_KEY=$API_KEY
|
|
ADMIN_BOT_API_KEY=$ADMIN_BOT_API_KEY
|
|
DOMAIN=$DOMAIN
|
|
SUBDOMAIN=$SUBDOMAIN
|
|
CDN_BASE=$CDN_BASE
|
|
NUM_WORKERS=$NUM_WORKERS
|
|
TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
|
|
OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT
|
|
OTEL_AUTH_HEADER=$OTEL_AUTH_HEADER
|
|
EOL
|
|
chmod 600 $ENV_FILE && \
|
|
$REMOTE_UPDATE_SCRIPT $ENV_FILE"
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "❌ Failed to execute update script on server."
|
|
exit 1
|
|
fi
|
|
|
|
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
|
|
echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!"
|
|
echo "🌐 Check your server to verify the deployment."
|
|
echo "======================================================="
|