# Development Guide ## Prerequisites - Go 1.27+ - Stockfish in PATH - PostgreSQL 18 (server for persistence; `psql` for the shell test suites) - Git - curl, jq (for testing) ## Building ```bash #git clone https://git.lixen.com/lixen/chess # Mirror git clone https://github.com/lixenwraith/chess cd chess go build ./cmd/chess-server go build ./cmd/chess-client-cli ``` ## Running ### Flags - `-api-host`: API server host (default: localhost) - `-api-port`: API server port (default: 8080) - `-serve`: Enable embedded web UI server - `-web-host`: Web UI server host (default: localhost) - `-web-port`: Web UI server port (default: 9090) - `-dev`: Development mode with relaxed rate limits and fixed JWT secret - `-dsn`: PostgreSQL connection string (default `$CHESS_DSN`); enables persistence and authentication - `-jwt-secret-file`: Stable JWT signing key file, mode 0600, at least 32 bytes (default `$CHESS_JWT_SECRET_FILE`) - `-trusted-proxies`: Comma-separated reverse-proxy IPs/CIDRs whose `-proxy-header` is trusted - `-proxy-header`: Client-IP header from a trusted proxy (default `X-Real-IP`) - `-pid`: PID file path for process tracking - `-pid-lock`: Enable exclusive locking (requires -pid) - `-log-level`: `debug`, `info`, `warn`, or `error` (default: `info`) - `-log-http`: Enable API and web request logs (default: `true`) - `-finished-game-ttl`: How long terminal games stay in memory (default: `1h`; `0` disables eviction) - `-anonymous-game-ttl`: How long games without a registered player survive after their last activity, in memory and in the database (default: `24h`; `0` keeps them) - `-max-users`: Accounts at which public registration closes (default: `100`; `0` = no limit; the CLI is not limited) - `-web-api-url`: Browser-visible API origin for the embedded web client, set on its script element as `data-api`; useful when its public origin differs from the listen address - `-smtp-host`, `-smtp-port`, `-smtp-user`, `-smtp-password-file`, `-mail-from`: SMTP relay for password-reset email; without a transport, recovery is off - `-mail-dir`: Development: write email to `.eml` files in this directory instead of sending - `-public-url`: The web client's address: emailed links open it, and its host is the `Site` of exported PGN (required with a transport) ### Modes ```bash # In-memory only (no persistence or auth) ./chess-server # With persistence and authentication ./chess-server -dsn 'dbname=chess' # Development with all features ./chess-server -dev -dsn 'dbname=chess' -pid /tmp/chess-server.pid -serve # Detailed persistence, engine-queue, cleanup, and request logs ./chess-server -dev -dsn 'dbname=chess' -serve -log-level debug -log-http=true # Behind a reverse proxy that sets X-Real-IP ./chess-server -dsn 'dbname=chess' -trusted-proxies 10.0.0.1 # Web UI is public at one origin while the API is exposed at another ./chess-server -serve -web-api-url https://api.example.test # Create or migrate the schema ./chess-server db init -dsn 'dbname=chess' ``` ### Local PostgreSQL Any PostgreSQL 18 works for development. With a local server and peer authentication for your OS user: ```bash sudo -u postgres createuser "$USER" sudo -u postgres createdb -O "$USER" chess export CHESS_DSN='dbname=chess' ``` Production provisioning (roles, schema, `pg_hba.conf`) is in [deployment.md](./deployment.md). ## Database Management All `db` subcommands take `-dsn`, defaulting to `$CHESS_DSN`; the examples below assume it is exported. Commands other than `init` and `delete` refuse to run against a missing or outdated schema. ### Schema Initialization ```bash # Create or migrate all tables; safe to repeat ./chess-server db init ``` ### User Management CLI ```bash # Add user with password ./chess-server db user add -username alice -password SecurePass123 # Add user with email ./chess-server db user add -username bob -email bob@example.com -password BobPass456 # Interactive password input ./chess-server db user add -username charlie -interactive # List all users ./chess-server db user list # Update password ./chess-server db user set-password -username alice -password NewPass789 # Update email ./chess-server db user set-email -username alice -email newemail@example.com # Update username ./chess-server db user set-username -current alice -new alice2 # Import with existing Argon2 hash ./chess-server db user set-hash -username alice -hash '$argon2id$v=19$m=65536,t=3,p=2$...' # Delete user ./chess-server db user delete -username alice ``` ### Game Query CLI ```bash # Query all games ./chess-server db query -gameId "*" # Query games for specific user ./chess-server db query -playerId "550e8400-e29b-41d4-a716-446655440000" # Query specific game ./chess-server db query -gameId "a1b2c3d4-e5f6-7890-1234-567890abcdef" # Drop every chess table in the DSN's search_path (destructive) ./chess-server db delete -confirm ``` ## Authentication Configuration ### JWT Secret Management - **Production**: Provide `-jwt-secret-file` so tokens survive restarts. The file must be a regular file with no group/other permissions and hold at least 32 bytes (`openssl rand -base64 48`). Without it, a random key is generated per process and every token is invalidated on restart, although the session rows remain. - **Development** (`-dev`): Fixed secret for testing consistency when no key file is given - **Scope**: Tokens are issued for `chess-server` / `chess-api`, carry only the subject and session ID, and are accepted only while their session row exists - **Sessions**: Stored for 7 days and renewed on each login ### Password Requirements - Minimum 8 characters - At least one letter and one number - Argon2id hashing with secure defaults ### User Account Features - Case-insensitive username and email matching - Optional email addresses - Last login tracking - Unique constraint enforcement with transaction isolation ## Project Structure ``` chess/ ├── cmd/ │ ├── chess-server/ # Server app │ │ ├── main.go # Server entry point │ │ ├── pid.go # PID file management │ │ └── cli/ # Database and user CLI │ └── chess-client/ # Client app │ └── main.go # Interactive debugging client ├── internal/ │ ├── client/ # Client components │ │ ├── api/ # HTTP client for server API │ │ ├── commands/ # Command registry and handlers │ │ ├── display/ # Terminal output formatting │ │ └── session/ # Session state management │ └── server/ # Server components │ ├── board/ # FEN/ASCII operations │ ├── core/ # Shared types and API models │ ├── engine/ # Stockfish UCI wrapper │ ├── game/ # Game state with player associations │ ├── http/ # Fiber handlers and auth endpoints │ │ ├── handler.go # Game endpoints │ │ ├── auth.go # Authentication endpoints │ │ └── middleware.go # JWT validation │ ├── processor/ # Command processing with user context │ ├── service/ # State and user management │ │ ├── service.go # Core service │ │ ├── game.go # Game operations │ │ └── user.go # User and auth operations │ └── storage/ # PostgreSQL persistence (pgtest: per-test schemas) │ ├── storage.go # Async writer for games │ ├── game.go # Game persistence │ ├── user.go # User persistence (synchronous) │ └── schema.go # Database schema └── test/ # Test scripts ``` ## Testing See [test documentation](../test/README.md) for comprehensive test suites covering API, authentication, and database operations. ### Quick Test Commands ```bash # API functionality tests ./test/test-api.sh # User authentication and database tests ./test/test-db.sh # Run test server with sample users (needs a disposable CHESS_TEST_DSN) ./test/run-test-server.sh # Test real-time game updates via long-polling ./test/test-longpoll.sh # Unit, migration, and persistence tests; database tests are skipped unless # CHESS_TEST_DSN names a database the test role may create schemas in export CHESS_TEST_DSN='postgres://chess_test:chess_test@localhost:5432/chess_test?sslmode=disable' go test ./... # Concurrency checks for the state/persistence boundary go test -race ./internal/server/storage ./internal/server/service # Web client end-to-end (Playwright): see test/README.md WEB_PORT=9090 MAIL_DIR=/tmp/chess-mail test/run-test-server.sh # terminal 1 CHESS_E2E_MAIL_DIR=/tmp/chess-mail make test-e2e # terminal 2 ``` ## Configuration ### Fixed Values - Engine path: `"stockfish"` (internal/engine/engine.go) - Worker count: 2 (internal/processor/processor.go) - Queue capacity: 100 (internal/processor/queue.go) - Min search time: 100ms (internal/processor/processor.go) - Write queue: 1000 operations (internal/server/storage/storage.go) - DB connections: 10 max, 5 idle (internal/server/storage/storage.go) - DB operation deadline: 5 seconds; write transactions 10 seconds - Transient write retries: 6 attempts, 100 ms doubling backoff - JWT expiration: 7 days (internal/service/user.go) - Concurrent Argon2id derivations: 4, five-second wait (internal/service/user.go) - Long-poll timeout: 30 seconds (internal/server/service/waiter.go) - Long-poll channel buffer: 1 (internal/service/waiter.go) ### Authentication Configuration - Password minimum: 8 characters with letter and number - Username format: 1-40 characters, alphanumeric and underscore - Email validation: Standard RFC 5322 format - Hash algorithm: Argon2id (memory-hard, side-channel resistant) ### Storage Configuration - PostgreSQL via pgx; schema chosen by `search_path`; migrations run at startup - Async write pattern for games; shutdown drains every accepted write - Transient failures before COMMIT are retried; others degrade storage - Replay reads wait for prior queued writes and use one REPEATABLE READ transaction - Synchronous writes for user operations (data consistency) - Registration limit check, user creation, and initial session are atomic - Games without a registered player are deleted 24 hours after their last activity - A full queue or unrecovered write failure degrades to memory-only and is visible in logs and `/health` - Usernames and emails stored lowercase with unique constraints ### Rate Limiting Configuration - General endpoints: 10 req/s (20 in dev mode) - User registration: 5 req/min - User login: 10 req/min - Rate limit key: `-proxy-header` (default `X-Real-IP`) from `-trusted-proxies`, otherwise the TCP peer ### PID Management - Singleton enforcement requires same PID file path - Stale PID detection via signal 0 checking - Exclusive file locking with LOCK_EX|LOCK_NB - Automatic cleanup on graceful shutdown ### Validation Rules - Player type: 1 (human) or 2 (computer) - Skill level: 0-20 - Search time: 100-10000ms - UCI moves: 4-5 characters ([a-h][1-8][a-h][1-8][qrbn]?) - Undo count: 1-300 - Username: 1-40 characters, [a-zA-Z0-9_] - Password: 8-128 characters, requires letter and number ## Security Considerations ### Authentication Security - Passwords hashed with Argon2id before storage - JWT tokens signed with HS256 and scoped by issuer/audience - Constant-time password comparison; unknown accounts verify a dummy hash - Case-insensitive matching prevents duplicate accounts - Rate limiting on auth endpoints prevents brute force - Argon2id concurrency is bounded to cap memory under load ### Input Validation - All user inputs validated and sanitized - SQL injection prevented via parameterized queries - UCI command injection blocked via character validation - FEN strings validated against strict regex pattern ### Session Management - JWT tokens expire after 7 days - No token refresh mechanism (re-login required) - Tokens carry only the user ID and session ID - Key is stable with `-jwt-secret-file`; otherwise it rotates on restart (fixed in dev mode) ## Limitations - JWT tokens don't support refresh (must re-login after expiry) - User deletion doesn't cascade to games (games keep player and claim IDs) - Games played without a registered player are deleted 24 hours after their last activity - Password recovery needs an SMTP relay and an email on the account - No email verification for registration - Fixed worker pool size for engine calculations - No push-based game updates (30-second long-polling is used) - Live games are not rehydrated after restart; persisted games are currently replay-only - Database history has no automatic retention policy - Curated games are planned in [Replay Plan](./todo.md) - REST API only