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

6.2 KiB

NiManyatta

Real-time chat server built on xitca-web, SurrealDB (in-memory), and a custom WebSocket gateway.

Quick start

# 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).

# 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.