# Cosmic account registration service

This directory is an isolated, loopback-only JSON registration service. It does not modify Cosmic, the existing web server, or any game asset. It is intended to sit behind the site's existing HTTPS reverse proxy and to perform one operation: atomically insert a bcrypt-protected account into Cosmic's database.

## Verified Cosmic contract

The implementation was checked against pinned Cosmic commit `fec53bc7714dc0f1ae3f50b2986cdf2727e0912a`:

- `src/main/resources/db/tables/001-account.sql` defines a unique `accounts.name VARCHAR(13)` and `password VARCHAR(128)`.
- `net/server/handlers/login/LoginPasswordHandler.java` inserts `name`, `password`, `birthday`, and `tempban`; it uses bcrypt cost 12 when bcrypt migration is enabled.
- `tools/BCrypt.java` accepts `$2b$` hashes and UTF-8 password bytes.
- `client/DefaultDates.java` defines both Cosmic sentinel values as `2005-05-11` (midnight for `tempban`).
- The remaining account fields have schema defaults. In particular, first login continues through Cosmic's existing TOS, gender, PIN, and PIC flows.

The pinned sample `config.yaml` has `AUTOMATIC_REGISTER: true`. That permits registration directly through the game login socket and bypasses every web control described here. If web-only registration is the intended policy, the Cosmic operator must separately disable automatic registration through the server's authorized configuration process. This repository does not change server configuration.

## HTTP contract

All responses are JSON and carry `Cache-Control: no-store`.

| Method | Internal path | Purpose |
| --- | --- | --- |
| `GET` | `/healthz` | Process liveness only; deliberately reveals no database state. |
| `GET` | `/csrf` | Returns `{"csrfToken":"..."}` and a Secure, HttpOnly, SameSite=Strict CSRF cookie. |
| `POST` | `/register` | Accepts `{"username":"Maple83","password":"correct-horse"}`. |

Usernames are 4-13 ASCII letters/digits. Passwords are 8-64 printable, non-space ASCII characters. The API rejects unknown JSON fields, ambiguous security headers, chunked request bodies, requests larger than 4096 bytes, non-JSON media types, a missing or mismatched exact HTTPS `Origin`, and a missing/invalid CSRF token. It does not enable CORS.

Expected failures use the same response message: `Registration could not be completed.` Duplicate names and invalid input both return 400 so the endpoint cannot be used to enumerate account names; throttling returns 429 and unavailable storage returns 503. Duplicate JSON keys are rejected. No SQL message, credential, account name, or stack trace is returned to the browser.

## Runtime configuration

Install only the scoped dependencies:

```bash
python3 -m venv .venv-registration
.venv-registration/bin/pip install -r web/registration/requirements.txt
```

Pass configuration through the service manager or an external secret provider. Do not put database credentials in the repository, command line, logs, web root, or a browser-delivered file.

| Variable | Required/default | Notes |
| --- | --- | --- |
| `REG_ALLOWED_ORIGIN` | required | One exact canonical HTTPS origin, such as the public site origin; no path or wildcard. |
| `REG_DB_USER` | required | Dedicated registration-only database principal. Never use Cosmic's root/admin principal. |
| `REG_DB_PASSWORD` | required | Inject at process start from the deployment secret facility. |
| `REG_DB_NAME` | `cosmic` | Database name. |
| `REG_DB_HOST` / `REG_DB_PORT` | `127.0.0.1` / `3306` | Ignored when `REG_DB_UNIX_SOCKET` is set. |
| `REG_DB_UNIX_SOCKET` | unset | Prefer a local MariaDB socket when available. |
| `REG_BIND_HOST` / `REG_PORT` | `127.0.0.1` / `18081` | A non-loopback bind is rejected at startup. |
| `REG_TRUST_X_REAL_IP` | `false` | Enable only when the local reverse proxy overwrites `X-Real-IP`. |
| `REG_IP_ATTEMPTS` / `REG_IP_WINDOW_SECONDS` | `5` / `900` | In-process attempt limit per client IP. |
| `REG_USERNAME_ATTEMPTS` / `REG_USERNAME_WINDOW_SECONDS` | `5` / `3600` | In-process attempt limit per case-insensitive username. |
| `REG_MAX_PARALLEL_HASHES` | `2` | Caps concurrent cost-12 bcrypt work. |

From the repository root, start one process with:

```bash
python3 -m web.registration
```

The CSRF signing key is generated in memory at process start and is never persisted. Consequently, tokens expire on restart. The rate limiter is also process-local; run a single service process. A multi-process or multi-host deployment requires a shared rate-limit store and a deployment-managed CSRF key before it is safe.

## Least-privilege database principal

Have a database administrator create the principal interactively with a generated secret, restrict it to local socket/loopback access, and grant only column-level `INSERT` on these four columns:

```sql
GRANT INSERT (`name`, `password`, `birthday`, `tempban`)
ON `cosmic`.`accounts`
TO 'mapleweb_register'@'localhost';
```

The service neither needs nor should receive `SELECT`, `UPDATE`, `DELETE`, schema, file, process, grant, or privileges on any other table. Duplicate detection relies on the existing unique key and MariaDB error 1062, avoiding a separate read and its race window. Keep Cosmic and this service on the same host/socket where possible; never expose the database port for this API.

Run the process as a dedicated, unprivileged OS identity with no login shell and no access to Cosmic's server account. The installed code and virtual environment can be read-only. A systemd unit should have an empty capability set plus `NoNewPrivileges=yes`, `PrivateTmp=yes`, `ProtectSystem=strict`, `ProtectHome=yes`, `RestrictSUIDSGID=yes`, and address-family/network rules limited to loopback TCP and the MariaDB Unix socket actually used. Give it no writable directory unless the service manager itself requires one. Send logs to the service manager; this implementation intentionally omits bodies, headers, raw targets, SQL details, and secrets from logs.

## Reverse-proxy boundary

Publish the service as a same-origin path, for example `/mapleweb/api/registration/`, with a trailing-slash proxy mapping to `http://127.0.0.1:18081/`. At the proxy:

- terminate HTTPS and never publish port 18081;
- overwrite (do not append) `X-Real-IP` with the proxy's trusted resolved client address;
- set `client_max_body_size 4k`, buffer request bodies, and use short header/body/upstream timeouts;
- add an edge/IP request limit as the first DoS layer, since bcrypt is intentionally expensive;
- forward `Origin`, `Cookie`, `Content-Type`, and the `X-CSRF-Token` request header unchanged;
- do not add permissive CORS headers or cache `/csrf` or `/register` responses.

When Cloudflare proxies the domain, configure nginx's real-IP module with Cloudflare's maintained address ranges before using `$remote_addr`, then set `REG_TRUST_X_REAL_IP=true`. Without that trusted overwrite, leave the setting false; all proxied users will safely share the loopback rate bucket, but registration capacity will be poor.

## Tests

The tests use an ephemeral loopback port, fake database connection, and low-cost test bcrypt hash. They do not need a Cosmic database or any credential:

```bash
python3 -m unittest discover -s web/registration/tests -v
```

On Windows with Docker available, run the real-schema integration test with:

```powershell
.\scripts\test_registration_mysql.ps1
```

The script uses the pinned Cosmic account DDL from the project cache, a
digest-pinned MySQL 8.4 container, and a disposable identity that has only
`INSERT` on `cosmic.accounts`. It verifies the inserted bcrypt hash, first-login
defaults, duplicate handling, and grants, then removes the test row and
container. It never connects to an existing database.

To exercise the packaged registration form in a real browser against that same
disposable boundary, run:

```powershell
.\scripts\run_registration_browser_e2e.ps1
```

The command prints a loopback URL and one-time test credentials, waits for the
browser submission, verifies the stored bcrypt hash, and removes all disposable
state. The loopback proxy's HTTP/Origin shim is E2E-only and is never packaged
or used by the production service. Trusted HTTPS remains a separate staging
acceptance gate.

Before production, additionally perform a staging smoke test against a disposable Cosmic schema using the dedicated principal, then delete the disposable account through an authorized administrative path. Do not make the public health endpoint query the database.
