mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 16:44:36 +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>
124 lines
4.5 KiB
JSON
124 lines
4.5 KiB
JSON
{
|
|
"name": "openfront-client",
|
|
"scripts": {
|
|
"build-dev": "concurrently \"tsc --noEmit\" \"vite build --mode development\"",
|
|
"build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
|
|
"start:client": "vite",
|
|
"start:server": "tsx src/server/Server.ts",
|
|
"start:server-dev": "cross-env GAME_ENV=dev NUM_WORKERS=2 TURNSTILE_SITE_KEY=1x00000000000000000000AA API_KEY=WARNING_DEV_API_KEY_DO_NOT_USE_IN_PRODUCTION ADMIN_BOT_API_KEY=WARNING_DEV_ADMIN_BOT_KEY_DO_NOT_USE_IN_PRODUCTION DOMAIN=localhost GIT_COMMIT=DEV tsx src/server/Server.ts",
|
|
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
|
"dev:host": "cross-env GAME_ENV=dev VITE_HOST=lan concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
|
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
|
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
|
"docs:map-generator": "cd map-generator && go doc -cmd -u -all",
|
|
"tunnel": "npm run build-prod && npm run start:server",
|
|
"test": "vitest run && vitest run tests/server",
|
|
"perf": "npx tsx tests/perf/run-all.ts",
|
|
"perf:game": "npx tsx tests/perf/fullgame/FullGamePerf.ts",
|
|
"perf:client": "npx tsx tests/perf/client/ClientUpdatePerf.ts",
|
|
"test:coverage": "vitest run --coverage",
|
|
"format": "prettier --ignore-unknown --write .",
|
|
"format:map-generator": "cd map-generator && go fmt .",
|
|
"lint": "eslint",
|
|
"lint:fix": "eslint --fix",
|
|
"prepare": "husky",
|
|
"gen-maps": "cd map-generator && go run . && npm run format",
|
|
"inst": "npm ci --ignore-scripts"
|
|
},
|
|
"lint-staged": {
|
|
"**/*": [
|
|
"eslint --fix",
|
|
"prettier --ignore-unknown --write"
|
|
]
|
|
},
|
|
"devDependencies": {
|
|
"@datastructures-js/priority-queue": "^6.3.5",
|
|
"@eslint/compat": "^2.0.5",
|
|
"@eslint/js": "^10.0.1",
|
|
"@tailwindcss/vite": "^4.2.4",
|
|
"@types/benchmark": "^2.1.5",
|
|
"@types/d3": "^7.4.3",
|
|
"@types/ejs": "^3.1.5",
|
|
"@types/express": "^5.0.6",
|
|
"@types/hammerjs": "^2.0.46",
|
|
"@types/howler": "^2.2.12",
|
|
"@types/js-yaml": "^4.0.9",
|
|
"@types/msgpack5": "^3.4.6",
|
|
"@types/node": "^24.12.0",
|
|
"@types/pg": "^8.20.0",
|
|
"@types/ws": "^8.18.1",
|
|
"@vitest/coverage-v8": "^4.1.5",
|
|
"@vitest/ui": "^4.1.5",
|
|
"autoprefixer": "^10.5.0",
|
|
"benchmark": "^2.1.4",
|
|
"canvas": "^3.2.3",
|
|
"concurrently": "^9.2.3",
|
|
"cross-env": "^10.1.0",
|
|
"d3": "^7.9.0",
|
|
"eslint": "^10.3.0",
|
|
"eslint-config-prettier": "^10.1.8",
|
|
"eslint-formatter-gha": "^2.0.1",
|
|
"glob": "^13.0.6",
|
|
"globals": "^17.6.0",
|
|
"husky": "^9.1.7",
|
|
"jsdom": "^29.1.1",
|
|
"lint-staged": "^16.4.0",
|
|
"lit": "^3.3.2",
|
|
"marked": "^18.0.5",
|
|
"mrmime": "^2.0.1",
|
|
"pixi-filters": "^6.1.5",
|
|
"pixi.js": "^8.18.1",
|
|
"prettier": "^3.8.3",
|
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
"prettier-plugin-sh": "^0.18.1",
|
|
"tailwindcss": "^4.2.4",
|
|
"tsconfig-paths": "^4.2.0",
|
|
"typescript": "^6.0.3",
|
|
"typescript-eslint": "^8.59.1",
|
|
"vite": "^8.0.16",
|
|
"vite-plugin-html": "^3.2.2",
|
|
"vitest": "^4.1.5",
|
|
"vitest-canvas-mock": "^1.1.4"
|
|
},
|
|
"dependencies": {
|
|
"@lit-labs/virtualizer": "^2.1.1",
|
|
"@opentelemetry/api": "^1.9.1",
|
|
"@opentelemetry/api-logs": "^0.216.0",
|
|
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
|
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.219.0",
|
|
"@opentelemetry/resources": "^2.8.0",
|
|
"@opentelemetry/sdk-logs": "^0.219.0",
|
|
"@opentelemetry/sdk-metrics": "^2.8.0",
|
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
"@opentelemetry/winston-transport": "^0.26.0",
|
|
"@types/compression": "^1.8.1",
|
|
"colord": "^2.9.3",
|
|
"compression": "^1.8.1",
|
|
"dompurify": "^3.4.11",
|
|
"dotenv": "^17.4.2",
|
|
"ejs": "^5.0.2",
|
|
"express": "^5.2.1",
|
|
"express-rate-limit": "^8.5.1",
|
|
"fastpriorityqueue": "^0.8.0",
|
|
"howler": "^2.2.4",
|
|
"intl-messageformat": "^11.2.3",
|
|
"ip-anonymize": "^0.1.0",
|
|
"jose": "^6.2.3",
|
|
"js-yaml": "^4.2.0",
|
|
"lil-gui": "^0.21.0",
|
|
"limiter": "^3.0.0",
|
|
"nanoid": "^5.1.11",
|
|
"node-html-parser": "^7.1.0",
|
|
"obscenity": "^0.4.6",
|
|
"ts-node": "^10.9.2",
|
|
"tsx": "^4.22.4",
|
|
"winston": "^3.19.0",
|
|
"ws": "^8.21.0",
|
|
"zod": "^4.4.2"
|
|
},
|
|
"overrides": {
|
|
"sanitize-html": ">=2.17.4"
|
|
},
|
|
"type": "module"
|
|
}
|