The remote's protocol-crate commits pinned the client wire format, and the
pre-existing chat_load_tester_consistency suite already decoded
ServerToClientMsg with postcard — but the reconstructed nigig-common was
encoding with serde_json. Switch to_bytes/from_bytes (both message enums)
to postcard to match, and rebuild decode_all on postcard::take_from_bytes
so a fragmented frame re-buffers and bytes trailing a complete message are
preserved instead of dropped.
AuthMethod (13 variants) is shape- and order-aligned with the client
library. The remaining server/client shape divergences (missing
reaction/upload/history variants, LoginSuccess/MessageDeleted/MessageContent
field differences) are pre-existing in the original design — the gateway
itself destructures MessageContent::Text { body } while the client emits
Text { text } — and are now documented with a remediation path in
KNOWN_ISSUES ("WS wire parity" section).
nigig-common: 21/21 tests green with the postcard round-trips.
19 KiB
nimanyatta — Execution Plan
Completed Phases
Phase 0 — Baseline Hygiene (commit 3908dbc)
.gitignorefor target, logs,.env, concatenated dumps,.bak- Deleted:
src/database.rs,.bak/.newfiles, 11concatenated_code.txt,old/,instruments_output_*, runtime logs, stray binaries broadcast_server.rs: removed test modules, ~470 lines dead code, dead traits- Wrote real README with API reference, env vars, cargo aliases
KNOWN_ISSUES.mdcreated from prior review findings
Phase 1 — Bug Fixes (commit 39401c1)
- 500-vs-409 fix:
is_duplicate_error()shared by all three error mappers indb.rs - Session eviction fix:
register_token_conncalled after password login + token restore - In-memory DB safety:
DATABASE_PATHrequired whenENVIRONMENT=production
Phase 1 completion + Phase 2 (commit 73fa54a)
- Device revocation fix:
is_token_revokednow checksdevice:<device_id>markers; all 4 callers updated - Dead code removal (-1,513 lines):
chat_state.rs(orphaned, not a module)connection_multiplexer.rs+connection_pool.rs(never instantiated)io_backend/directory (not a module, references nonexistentServerError)error.rs: removedNigigErrorparent, 7 dead sub-sets, 5 deadFromimpls — kept onlyIggyError- Dead
pub useexports fromoptimized/mod.rs
Phase 3 — Architecture Cleanup (commit 75b2b41, -1,058 lines)
- Deleted distributor infrastructure:
BroadcastDistributor,AdaptiveBroadcaster,ShardedBroadcast,BackpressureConfig— all vestigial (hot-pathSendGroupalready bypassed viaget_sender()→try_send()) - Removed all distributor fields, construction, register/unregister calls, broadcast_rx select arm, is_under_pressure gate
- Replaced
log_direct_dropwith local static counter + inline function
Phase 4 — Request-Path Improvements (commit 32d237f, -38 lines)
- Registration rate limiting: added IP+username rate limit to
POST /auth/register(was unprotected) - Custom roles ownership check: replaced 4x full-table
get_room_custom_roles().iter().any()scans with boundedrole_belongs_to_room()query (LIMIT 1) - Dead code: deleted
get_active_devices_for_user()+ActiveDevice/ActiveDeviceReadstructs (zero callers) - Skipped token validation caching: in-memory
PasetoManagercache is only populated at startup; runtime revocations write to DB but don't update the HashMap. Query is fast (in-memory SurrealDB, LIMIT 1).
Phase 5 — Security Hardening (commit ee115ae, +59 lines)
- X-Forwarded-For IP filtering: added
PROXY_TRUSTED_IPSenv var; middleware now checks TCP peer IP against trusted set before reading headers - WebSocket Origin validation: added
WS_ALLOWED_ORIGINSenv var; middleware validates Origin header on WS upgrade, rejects with 403 if not in allowed list - UsernameOnly auth: already properly gated behind
#[cfg(debug_assertions)], rejected in release builds — no change needed
Phase 6 — Observability & Deployment (commit b08ad8c, +33 lines)
- OTEL gating: added
ENABLE_OTELenv var (default false); when disabled, skip OTLP exporter entirely — prevents startup hang when no collector is available - Env var fix: read
OTEL_EXPORTER_OTLP_ENDPOINTfirst, fall back toOTLP_ENDPOINT /readyendpoint: Kubernetes readiness probe returning 200 when running, 503 during shutdown; added to PUBLIC_PATHS- Cleanup: removed dead
health()function and commented-out route fromstart_xitca.rs
Phase 7 — Request-Path Perf + Shutdown (commit fcaec35, 8cc54e8)
- Token validation caching (commit
fcaec35): removed the per-requestis_token_revokedDB query from all 4 call sites (auth middleware, WS handshake, refresh endpoint, gateway token restore). Revocation is fully covered by the in-memoryPasetoManagercache, which is populated at startup and updated by everyrevoke_token/revoke_all_user_tokenscall (revoke_tokens_for_devicehas zero callers). Deleted deadDatabase::is_token_revoked. - Unbounded query caps: added safety caps + warnings to
get_room_member_ids(LIMIT 2,000) andget_user_joined_rooms(LIMIT 1,000) so huge rooms/users degrade with a logged warning instead of unbounded memory use. - Stale bin config: removed
[[bin]]entries for deleted sources (server,websocket_gateway_iggy,load_tester,simple_ws_server) and their broken cargo aliases — the full--features b_serverbuild was failing. - Graceful shutdown (commit
8cc54e8): disabled xitca's built-in signal handling (SIGINT mapped to an immediate force stop that defeated our sequencing), configuredshutdown_timeout, drive a gracefulstop(true)frombroadcast_server, wait for SIGINT or SIGTERM, and only force-abort if the drain exceedsSHUTDOWN_TIMEOUT_SECS.
Remaining / Future Work
Test Coverage Plan
-
Install
cargo-llvm-covand run with--features b_server— ✅ installed (v0.9.0) -
Added integration tests (commit
b9e5178):- DB write path:
revoke_token,revoke_all_user_tokens,revoke_tokens_for_device— each asserts idempotence of theUPSERT(single row) plus marker/user fields cleanup_expired_revocationsremoves only expired rowsload_revoked_tokensexcludes expired rowscreate_duplicate_user_returns_conflictexercises the409mapping with a real in-memory SurrealDB (Database::connect_fresh)- Config guard: production without
DATABASE_PATHand withoutPASETO_KEYrejected; production with both accepted (env-mutating tests serialized under a lock) is_duplicate_erroredge cases + rate limiter — already present
- DB write path:
-
Exclusions — no Makepad/generated code is part of the
b_serverbuild (it lives in separate crates), so nothing to configure there. Recommended command (excludes the stray<WORKSPACE>debuginfo path emitted by the../xitca-webpath-dependency macros):cargo llvm-cov --features b_server --bin broadcast_server \ --html --ignore-filename-regex '<WORKSPACE>|/target/'Baseline (46 tests): total 11.82% lines — pure-logic modules run 22–92% (
sms.rs92%,fingerprint.rs87%,session_limit.rs43%,shared.rs40%,db.rs23%); HTTP/WS handlers show 0% because they need a live server and are exercised by theload-feature stress/instrumentation suites.
Phase 7 coverage run (live, instrumented)
Ran against 127.0.0.1:8080 under cargo llvm-cov run --no-report so unit tests + live traffic merge into one profile:
- Executed
chat_load_tester_consistency(5×45s, 1000 users) andchat_load_tester(6 profiles, up to 3000 users) plus a REST/curl pass (register, login, refresh, SMS request/verify, auth failures,/admin/reset,/stats,/metrics,/ready). - Total: 45.86% lines (functions 43.79%, regions 45.64%) — up from 11.82%. Auth (
auth.rs63%), SMS (sms_auth.rs43%), WS/real-time (realtime.rs59%,hybrid_gateway_optimized.rs55%),db.rs32%,middleware.rs65%, monitoring/observability 20–80%, pure logic (sms 97%, fingerprint 90%, session_limit 84%). - Validated graceful shutdown (commit
8cc54e8) live: SIGTERM on the app →Graceful stopped xitca-server-worker-Nfor every worker,Shutting down observability,Shutdown complete., port released, exit in ~2–4s (timeout backstop not hit). - Stale load suites — repaired in Phase 8 (see below): all seven suites (
fast_integration_suite,chat_load_tester_realistic,security_suite,regression_suite,session_limit_test,enhanced_security_tests,enhanced_security) now compile undercargo check --features load --bins. - Instrumented-debug caveat: the
-C instrument-coveragebuild is ~10–50× slower than release (register/login 2–4 s, REST keep-alive connections hitEOF while parsing…timeouts after a burst). On this build the REST success paths forroutes/rooms.rs,routes/roles.rs,routes/sync.rsremain at 0% — the two working load testers drive those operations over WS instead and reach their handlers (create/join/sendmessaging paths covered via WS). To close the REST 0% gaps deterministically, add in‑process route tests using the existingDatabase::connect_freshpattern (buildAppState+ handlers against embedded SurrealDB, bind an ephemeral port) rather than timing windows against a live instrumented binary. Re-run the stress profiles against a release build for trustworthy pass/fail numbers.
Phase 7.5 — In-process route tests (closes REST 0% gaps)
Added src/routes/tests.rs (#[cfg(test)], wired via mod tests): a single rest_routes_in_process test builds the exact production service chain (App → /ws + auth/room/role/sync/monitoring routes → nimanyatta_middleware → HttpServiceBuilder::h1()), serves it on an ephemeral port over xitca_server::Builder (worker_threads 1), and drives it with reqwest + JSON bodies — no fixed port, no timing windows, no external processes.
- Dev-deps:
reqwest(plain HTTP,default-features = false, features["json"]),serde,serde_json(alongside existing xitca-web/tokio/tokio-tungstenite/futures-util).xitca-testwas tried first but its build.rs needsprotoc(grpc), so the harness reimplementstest_h1_server's ~15 lines locally and forces a graceful stop viaServerHandle::stop(true)+join.await(droppingServerFuturefrom async context panics — "Cannot drop a runtime"). - Flow covered: public
/health,/ready; loopback monitoring/stats,/metrics; unauthenticated/nowhere+/api/v1/sync→ 401; register (weak password → 400, duplicate → 409, wrong pw/unknown user → 401); login token extraction (tokens are nested{token, expiry}objects); refresh rotation (asserts token changed) + garbage → 401; SMS request/verify (Console provider); single-device logout (access and refresh → 401, device is deactivated); re-login → all-devices logout (both tokens → 401); group + direct room create, join + duplicate 409, sendMessageContent::Text(externally-tagged), messages pagination, room roles list/create/assign/update/remove/delete, set room role, set server role (owner seededServerRole::ServerAdminviaDatabase::set_server_rolesince granting requires an existing admin), sync with full filter. - Real bug found & fixed:
Database::role_belongs_to_roomcomparedid = $role_id(Surreal record id vs bare ULID string) → always false →assign/update/removecustom role 404'd. Nowid = type::record('custom_role', $role_id). - Test-run coverage now 49.01% lines (functions 43.54%, regions 46.78%) on 47 tests. The previously-0% REST handlers are now exercised:
routes/roles.rs92.86% (lines),routes/rooms.rs50.00%,routes/sync.rs36.09%. Combined with the Phase 7 live-traffic profile, total ≥ the 45.86% live number (test binary excludes thebroadcast_servermain/run path, which the live run covered at 70%).
Phase 8 — Protocol crate reconstruction, WS hardening, REST coverage, suite repairs
Context: the nigig-common dependency (../nigig-lite/crates/common) was reconstructed as a faithful, fully-tested crate (21 unit tests) mirroring the wire contract defined by crates/nimanyatta-protocol and the server's own call sites. The previously-lost shapes were recovered from the consumer code: UserProfile, DeliveryReceipt { message_id, state, timestamp, by_user } (+ sent/delivered/read constructors), DeliveryState five-state enum, MessageAck + message_ack()/group_message_with_ack() constructors, MessageDeleted { …, deleted_by }, AuthMethod::SsoCallback { access_token, user_id }, client_msg_id: String on SendGroup/SendPrivate, JoinRoomRequest { reason }, Room.created_at: DateTime<Utc> (non-optional), AppError::{DatabaseConnection{source}, db_connection(op, err), SmsSendFailed{reason}, OtpMaxAttemptsExceeded (unit), SessionLimitExceeded{limit: usize}}, From<Box<dyn Error>> for AppError, and enum helpers RoomRole/ServerRole/MembershipState::{as_str, from_str}, ServerRole: Default, PermissionSet::{merge, defaults_for_server_role, defaults_for_room_role}.
- WS origin fail-closed (production):
Config::from_envnow rejects an emptyWS_ALLOWED_ORIGINSwhenENVIRONMENT=production(startup fails with a clear message);*remains an explicit opt-out. Non-production keeps the permissive default for local dev. Tests:production_requires_ws_allowed_origins,production_with_database_path_and_key_ok(now also sets origins). - PROXY_TRUSTED_IPS documented: README env table expanded (PROXY_TRUSTED_IPS, WS_ALLOWED_ORIGINS, CORS_ALLOWED_ORIGINS, ENABLE_OTEL, token durations) plus two explainer sections — the proxy trust model (headers only honoured for TCP peers in the trusted set) and the WS origin fail-closed model.
- Four unwired REST routes registered (
src/routes/mod.rs):rooms/{room_id}/invitePOST,/typingPUT,/read_receiptPOST,/redact/{event_id}POST — the handlers existed and were documented in the README but never reachable (root cause of the rooms 50 % coverage ceiling). POST /rooms/{room_id}/read_receiptnow returns 501 (AppError::HttpError) instead of 500 — REST read receipts are not persisted yet; the WebSocketMarkReadpath does record receipts. README documents this explicitly.- README API tables corrected to the real paths/methods (rooms path-param form, roles, SMS).
- Gateway KNOWN_ISSUES fixes: #1 centralised + idempotent close-path cleanup via
cleanup_connection, #2LoggedOutnow sent beforeremove_session, #5 DRY'd eviction loop ontocleanup_connection; #3 verified already enforced (session_manager.authenticateevicts prior same-username connections on both handshake and in-band login paths, including token restore). - Gateway receipt bug fixed:
DeliveryReceipt::read/deliveredcall sites passedOption<UserId>where the (reconstructed) protocol wantsby_user: Option<String>— nowSome(username.clone()). - Load suites repaired (
cargo check --features load --bins→ 0 errors):fast_integration_suite,session_limit_test,regression_suiteare[[bin]]targets whose bodies are#[tokio::test]functions —#[test]items arecfg(test)-gated and vanish in normal builds, so the bins had nomain. Added smallmains (manual multi-thread runtime /#[tokio::main]) and stripped the test attributes from the orchestrator functions.- All other compile breaks (enum shapes, missing
JoinRoomRequest.reason, removed imports) resolved via the nigig-common reconstruction above;cargo check --features b_server --bin broadcast_serveris clean except the intentionalRocksDbimport that requires thekv-rocksdbfeature (restored for the final full build).
- Coverage tests added (
src/routes/tests.rs): invite → join → 409-on-reinvite → 404-unknown-user, typing PUT (member + 403 non-member), redact (happy path, 404 unknown event, 403 non-owner), read_receipt 501, custom-role creation denial (403 withoutManagePermissions), non-member send 403, messages pagination (limit/direction/invalidfrom→ 400), sync filtered (room filter,timeline_limit,include_state), sync invalidsince→ 400, empty room filter → zero rooms. - Real bug found & fixed — OTP attempt counter never incremented (
db.rs::increment_otp_attempts_if_under_limit): theBEGIN TRANSACTION; … IF … THEN ( UPDATE …; true ) …SurrealQL block is a parse error (;is not a valid statement separator inside an IF block), so every wrong OTP made the query fail — and the handler's fail-closed.unwrap_or(false)masked the DB error asOTP_MAX_ATTEMPTS_EXCEEDED(429) on the first wrong attempt. Rewritten as a single atomic conditional update:UPDATE type::record('otp', $id) SET attempts = attempts + 1 WHERE attempts < $max RETURN VALUE attempts— empty result set ⇒ at-limit, and the read-check-write race is gone entirely. The handler now logs the underlying error before failing closed (so a broken query can never hide as a 429 again). Regression test:db::tests::otp_attempts_increment_round_trip(store → fetch → increment → counter persisted). - Server-role permission baseline:
PermissionSet::defaults_for_server_role(User)grantsCreateRooms/SendMessages/ReadMessages/InviteUsers— room-scoped powers still come from room roles; membership checks precede permission checks in every handler. - WS wire format aligned to postcard (follow-up to the remote's protocol-crate wire pinning):
nigig-common'sto_bytes/from_bytesnow use postcard (they had been reconstructed with serde_json), matchingnimanyatta-protocoland the pre-existingchat_load_tester_consistencysuite, which already calledpostcard::from_bytesonServerToClientMsg.decode_allusespostcard::take_from_bytesso a fragmented frame re-buffers and bytes trailing a complete message are preserved. Remaining server/client shape divergences (variant sets,LoginSuccess/MessageDeleted/MessageContentfields) are pre-existing and documented with a remediation path in KNOWN_ISSUES ("WS wire parity"). - Final verification:
cargo test --features b_server→ 51 passed, 0 failed (47 prior + OTP round-trip + WS-origin fail-closed config tests + 2 new route-coverage suites);cargo test -p nigig-common→ 21 passed;cargo check --features load --bins→ clean (suite mains repaired); fullkv-rocksdbcompile check re-verified after restoring the production feature set.
Remaining / Future Work (post-Phase-8 roadmap)
Scope docs (crates/apps/nigig-site/construction-site-app-scope.md, construction-site-app-scope2.md) Phase-3 chat gaps documented as open (not yet implemented — see KNOWN_ISSUES "Scope gaps"):
- Channel structure per construction site (site channels auto-provisioned per project; management channel; DMs) — currently all rooms are flat, user-created.
- Pinned document library per room/site (pins exist in the permission model; no pin storage/REST yet).
- Read receipts on documents (message receipts land via WS
MarkRead; document-level receipts need schema work). - Mentions / search / broadcast / moderation (kick/ban enforcement) / retention policies — permission bits exist (
PinMessages,KickUsers,BanUsers); no handlers. - Offline queue — inbox structures exist in nigig-common (
InboxError); server-side offline inbox not wired.
Non-scope-gap follow-ups:
- X-Forwarded-For without trusted proxy: requests from untrusted peers keep the raw TCP peer IP (headers ignored) — documented in README; a real reverse proxy must set
PROXY_TRUSTED_IPS. cargo llvm-covre-run after Phase 8 to refresh the coverage numbers (rooms/sync percentages will rise with the newly registered routes + tests).