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
156 lines
6.2 KiB
Markdown
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`.
|