nigig-org/nimanyatta/README.md
andodeki dfffe9e6e6 nimanyatta: Phase 8 — reconstruct nigig-common, repair load suites, close REST coverage gaps
Foundation:
- Vendor xitca-web (upstream commit 7fa07dae, see xitca-web/VENDORED.md);
  pin all six xitca crates via [patch.crates-io] in nimanyatta/Cargo.toml
- Reconstruct the lost nigig-common crate as nigig-lite/crates/common
  (21 unit tests, clippy-clean), recovered from the wire contract in
  crates/nimanyatta-protocol plus the server's own call sites

REST API:
- Register the four unwired room routes that were documented but never
  reachable: {room_id}/invite POST, /typing PUT, /read_receipt POST,
  /redact/{event_id} POST (root cause of the rooms.rs 50% coverage ceiling)
- REST read receipts now return 501 Not Implemented (documented) instead of
  a 500 — receipts are recorded via the WebSocket MarkRead path
- README API tables corrected to the real paths/methods; env table expanded
  (PROXY_TRUSTED_IPS, WS_ALLOWED_ORIGINS, CORS_ALLOWED_ORIGINS, ENABLE_OTEL,
  token durations) plus proxy-trust and WS-origin explainer sections

Security & correctness:
- WS_ALLOWED_ORIGINS is now fail-closed in production: an empty list is a
  configuration error at startup ('*' remains an explicit opt-out)
- Fix the OTP attempt counter: the BEGIN/IF SurrealQL block was a parse
  error, so every wrong OTP failed the query and the fail-closed handler
  masked it as a first-try 429 OTP_MAX_ATTEMPTS_EXCEEDED. Now a single
  atomic conditional UPDATE ... WHERE attempts < $max, the handler logs the
  underlying error before failing closed, and a regression test covers
  store -> fetch -> increment -> persist
- Gateway KNOWN_ISSUES #1/#2/#5 fixed (centralised idempotent
  cleanup_connection on all close paths; LoggedOut sent before
  remove_session); #3 verified already enforced via session eviction
- DeliveryReceipt receipts now carry by_user: Option<String> per the
  protocol crate (was Option<UserId> at the call sites)

Test suites:
- Repair every load-test binary: added mains for the three bins whose bodies
  were #[tokio::test] functions (test items are cfg(test)-gated and vanish
  from normal builds), fixed protocol-shape drift in the rest —
  cargo check --features load --bins is clean
- New route coverage tests: invite/join/409-reinvite/404-unknown, typing
  (member + 403 non-member), redact (happy path/404/403), read_receipt 501,
  custom-role denial, non-member send 403, pagination edges (limit,
  direction, invalid from-token), sync filter paths (room filter,
  timeline_limit, invalid since-token, empty filter semantics)
- cargo test --features b_server: 51 passed / 0 failed

Docs:
- PLAN.md Phase 8 section + post-Phase-8 roadmap (per-site channels, pinned
  document library, document read receipts, mentions/search/broadcast/
  moderation/retention, offline queue — documented as open scope gaps)
- KNOWN_ISSUES.md statuses updated with the fixes above
2026-09-26 16:24:41 +00:00

156 lines
6.2 KiB
Markdown

# NiManyatta
Real-time chat server built on xitca-web, SurrealDB (in-memory), and a custom WebSocket gateway.
## Quick start
```sh
# Build and run the server
cargo brct_srv # alias: cargo run --release --bin broadcast_server --features b_server
```
Server binds to `0.0.0.0:PORT` (default `8080`).
```sh
# REST API
http://127.0.0.1:8080/health
# WebSocket
ws://127.0.0.1:8080/ws?token=<paseto-access-token>
# Prometheus metrics
http://127.0.0.1:8080/metrics
```
## Environment variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `PASETO_KEY` | Yes (prod) | — | 32-byte base64 PASETO v4 symmetric key |
| `DATABASE_PATH` | No | in-memory | Path to persistent SurrealDB data file |
| `PORT` | No | `8080` | HTTP/WS listen port |
| `ENVIRONMENT` | No | `development` | `production` enforces stricter config |
| `TRUST_PROXY` | No | `false` | Trust `X-Forwarded-For` from upstream proxy |
| `PROXY_TRUSTED_IPS` | No | — | Comma-separated peer IPs allowed to set `X-Forwarded-For` (see note below) |
| `WS_ALLOWED_ORIGINS` | **Yes (prod)** | *(permissive)* | Comma-separated allowed `Origin` values for WebSocket upgrades (see note below) |
| `CORS_ALLOWED_ORIGINS` | No | — | Comma-separated allowed CORS origins |
| `SMS_PROVIDER` | No | `Console` | `Console` (prints OTP to stdout) or `Http` |
| `SMS_API_URL` | No | — | External SMS provider endpoint |
| `SMS_API_KEY` | No | — | SMS provider API key |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | No | `127.0.0.1:4317` | Set to `""` to disable OTLP |
| `ENABLE_OTEL` | No | `false` | Set `true` to enable the OTLP exporter |
| `ACCESS_TOKEN_DURATION_SECS` | No | `3600` | Access-token lifetime |
| `REFRESH_TOKEN_DURATION_SECS` | No | `604800` | Refresh-token lifetime |
### Proxy & forwarded headers (`PROXY_TRUSTED_IPS`)
`TRUST_PROXY=true` alone is **not** sufficient: the middleware only reads
`X-Forwarded-For` when the *TCP peer IP* of the connection is listed in
`PROXY_TRUSTED_IPS`. Requests from untrusted peers keep their raw peer
address and any `X-Forwarded-For` header is ignored — a client cannot spoof
its IP by sending the header directly.
```
# Example: reverse proxy on the same host and a dedicated LB subnet
TRUST_PROXY=true
PROXY_TRUSTED_IPS=127.0.0.1,::1,10.0.0.0/24
```
### WebSocket origin validation (`WS_ALLOWED_ORIGINS`)
WebSocket upgrade requests are rejected with `403` when the `Origin` header
is not in the allowlist. The allowlist supports `*` (any origin).
- **Development** (default): an empty allowlist is permissive — every origin
is accepted, so local tooling works with zero configuration.
- **Production** (`ENVIRONMENT=production`): the config **fails closed** —
startup is rejected unless `WS_ALLOWED_ORIGINS` is explicitly set. Set it
to your web client origins (`https://app.example.com`), or deliberately to
`*` if any origin is genuinely acceptable (e.g. an internal API).
See `src/shared.rs` for the full `Config` definition.
## API
### Auth (public)
| Method | Path | Body |
|--------|------|------|
| POST | `/api/v1/auth/register` | `{ "username", "password", "display_name"? }` |
| POST | `/api/v1/auth/login` | `{ "username", "password" }` |
| POST | `/api/v1/auth/refresh` | `{ "refresh_token" }` |
| GET | `/api/v1/auth/login_types` | — |
### Auth (token required)
| Method | Path | Body |
|--------|------|------|
| POST | `/api/v1/auth/logout` | — |
### Rooms (token required)
| Method | Path | Body / Notes |
|--------|------|--------------|
| POST | `/api/v1/rooms/create` | `{name?, topic?, is_direct, invite: [user_id]}` |
| POST | `/api/v1/rooms/{room_id}/join` | `{}` |
| POST | `/api/v1/rooms/{room_id}/leave` | `{reason?}` |
| POST | `/api/v1/rooms/{room_id}/invite` | `{user_id, reason?}` |
| POST | `/api/v1/rooms/{room_id}/send` | `{content: MessageContent, reply_to?}` |
| POST | `/api/v1/rooms/{room_id}/messages` | `{limit, direction, from?}` (RFC 3339 `from` token) |
| PUT | `/api/v1/rooms/{room_id}/typing` | `{typing: bool}` |
| POST | `/api/v1/rooms/{room_id}/read_receipt` | **501** — receipts are recorded via the WebSocket `MarkRead` path until the REST storage lands |
| POST | `/api/v1/rooms/{room_id}/redact/{event_id}` | `{reason?}` (requires `delete_any_message`) |
### Roles (token required)
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/v1/admin/server-role` | Set server role |
| POST | `/api/v1/rooms/{room_id}/role` | Set room role |
| GET | `/api/v1/rooms/{room_id}/roles` | List room custom roles |
| POST | `/api/v1/rooms/{room_id}/roles/create` | Create custom role |
| POST | `/api/v1/rooms/{room_id}/roles/{role_id}/update` | Update custom role |
| POST | `/api/v1/rooms/{room_id}/roles/{role_id}/delete` | Delete custom role |
| POST | `/api/v1/rooms/{room_id}/roles/assign` | Assign custom role |
| POST | `/api/v1/rooms/{room_id}/roles/remove` | Remove custom role assignment |
| GET | `/api/v1/rooms/{room_id}/permissions` | Get own resolved permissions |
### Sync (token required)
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/v1/sync` | Full sync |
### SMS Auth
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/v1/auth/sms/request` | `{phone_number}` — request OTP (Console provider logs it) |
| POST | `/api/v1/auth/sms/verify` | `{phone_number, code}` — verify OTP + login |
## WebSocket
Connect to `/ws?token=<access-token>`. Unauthenticated connections receive a guest identity.
Messages are JSON `ServerToClientMsg` / `ClientToServerMsg` per `nimanyatta-protocol`.
## Cargo aliases
Defined in `.cargo/config.toml`:
| Alias | Command |
|-------|---------|
| `brct_srv` | `cargo run --release --bin broadcast_server --features b_server` |
| `brct_srv_linux` | Same + `uring` feature |
| `build_srv` | `cargo build --bin broadcast_server --features b_server` |
| `stress_tester` | `cargo run --release --bin chat_load_tester --features load` |
| `consistency_tester` | `cargo run --release --bin chat_load_tester_consistency --features load` |
| `real_tester` | `cargo run --release --bin chat_load_tester_realistic --features load` |
## Deployment
See `deploy/` for Ubuntu VPS hardening and verification scripts.
## Known issues
See `KNOWN_ISSUES.md`.