mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 14:51:26 +02:00
14 KiB
14 KiB
Wownerogue Remediation & Cleanup Plan
Derived from the deep code review (May 2026). Goal: fix every BAD/UGLY finding, remove the spaghetti, and reach a real-funds launch gate. Phases are ordered so launch-blockers get surgical fixes first, then correctness hardening, then structural cleanup, then a test/ops overhaul to lock it in.
Guiding principles
- Stabilize before restructuring. Fix exploitable money bugs as minimal patches, ship, then refactor.
- Single source of truth for the three things that bite: money (one BigInt module), identity (one
user_id), randomness (one per-game seeded RNG). - Defense in depth. Every check at the DB and app level; every render escapes and the server escapes.
- Every change ships with a test that fails before and passes after.
Key: S ≤ half day · M ~1–3 days · L ~1 week+ · 🔴 launch-blocker · 🟠 pre-launch · 🟡 post-launch
Phase 0 — Stop the bleeding (launch-blockers)
- 0.1 🔴 S — Verify amount received ≥ expected before confirming. ✅ Gate on
status.completeinpaymentHandlers.js; underpaid → stay pending +payment_underpaidwarning (once).received_amountnow recorded for single-game + credits. Regression test added (payment_flow.integration.test.js). 113 tests green. - 0.2 🔴 M — Make provably-fair real. ✅ Per-game seeded RNG (
seedToInt) threaded throughdungeon.js(Digger reseed + allMath.random()→rng),monster.js(seededShuffle),game.js(chase + generation).DungeonGenerator.regenerateFromSeed+layoutFingerprint;/api/verifyregenerates & returns fingerprint. Determinism test suite added (provablyFair.test.js). Verifier can now reproduce the exact dungeon from the seed. - 0.3 🔴 M — Fix batch-payout
tx_hashcollision + double-pay. ✅ Migration 015 drops the payoutstx_hashunique index (batches legitimately share a tx_hash) and extends the one-per-game guard to coverprocessing. Batch completion now happens in ONE transaction viaid = ANY(no row stranded mid-batch); BIGINT stat math (no float). Ambiguous batch failures →needs_review(retry service won't re-send) + operator alert. NewbatchPayout.test.jscovers both paths. - 0.4 🔴 S — Harden identity transport. ✅ Client sends
anon_tokenin handshakeauth(not the URL query → no proxy/referer/access-log leakage); server readsauthfirst with a query-string fallback for older clients. Token is now rotated on every resume (rotateToken), so a leaked token is invalidated when the real owner reconnects; client already persists the returned token. Rotation + old-token-replay-rejection test added tosecurity.test.js. - 0.5 🟠 S — Single-game disconnect recovery. ✅
recoverPendingPaymentsnow also recovers confirmed-but-unconsumedsingle_gamepayments (nogames.payment_idlink) by granting the equivalent credits, idempotently (per-payment reason key, row-locked). Disconnect preservation made deterministic: the stabledbUserIdis stamped on the game at creation, so suspend/restore never needs a fragile disconnect-time DB lookup — every game (paid or free) is reconnectable whenever a session exists; the drop-branch is now only the genuine no-session edge. NewsingleGameRecovery.test.js(grant / consumed / idempotent). - 0.6 🟠 S — Client + admin XSS and CSP. ✅ Single shared
escapeHtmlhelper (html/js/core/escapeHtml.js, escapes all five chars incl. both quotes); leaderboard delegates to it. Chat sinks (onChatBroadcast/onChatHistory) now escape username + message. Admin panel escapes user-controlled fields (payout address, socket id, player name, status) and its localescapeHtmlnow escapes quotes (safe intitle="…"). CSP +X-Content-Type-Options/X-Frame-Options/Referrer-Policyheaders added via middleware inindex.js(connect-src 'self'keeps Socket.IO working; nonce-based tightening tracked as Phase 4.4).
Phase 1 — Money correctness hardening (🟠)
- 1.1 M —
src/money/atomic.jsBigInt module. ✅ Exact integer money math (toBig/toSafe/sum/add/mulByDecimal/format).calculatePayoutnow computesbase * multipliervia BigInt (half-up rounding, narrows to number only when exact).walletRPCServicebatch total + fee sum usemoney.suminstead of floatreduce. Property tests (moneyAtomic.test.js) prove exactness on amounts above 2^53 where the old float path drifted. - 1.2 S — Snapshot payout config at game start. ✅ Migration 016 adds
payout_escape_amount/payout_treasure_amount(BIGINT) + multipliers togames. Both start paths stamp the resolved payout amounts (via_computePayoutSnapshot);completeGamepays from the snapshot (falls back to live calc only for pre-snapshot games). NewpayoutSnapshot.test.jsproves a mid-game config change can't alter an in-flight payout. - 1.3 S — Close payout uniqueness holes. ✅
processingcovered in the unique index (migration 015) and in the in-code existing-payout guard. Migration 017 adds idempotentNOT VALIDCHECK constraints onpayments.status,payments.payment_type, andpayouts.statusso a typo'd status can't silently orphan a money row (enforces new writes, won't choke on legacy rows). (games.status/game_mode CHECKs deferred — transient states, lower money-impact.) - 1.4 S — Fix retry stat guard + non-atomic tx_hash write. ✅ The "already on-chain" path used a
NOT EXISTSguard against the row it had just marked completed (always skipped → never counted); replaced with a conditionalUPDATE … WHERE status <> 'completed' RETURNINGso stats count exactly once. Successful retries now storetx_hash+ mark completed + count stats in ONE transaction (no stranded-tx_hash window). NewpayoutRetry.test.jscovers count-once, no-double-count, and atomic completion. - 1.5 S — Fix dead refund send-path. ✅ The admin refund called a nonexistent
walletRPCService.sendPayment()(guarded by atypeofcheck), so refunds were silently never sent while still reporting success. Now uses the realprocessPayout()(validates address + transfers, returns{success, txHash}) and refunds the actually received amount (received_amount, falling back to expected). - 1.6 S —
credits→ BIGINT; idempotent 010; atomic migration runner. ✅ Migration 018 widensusers.credits+credit_transactions.amount/balance_afterto BIGINT. Migration 010 rewritten with conname-guardedDOblocks (re-runnable, no startup wedge). Runner now applies each migration and records the ledger row in one transaction (no applied-but-unrecorded state). Verified end-to-end: all 18 migrations applied cleanly to a throwaway Postgres DB, re-run skipped via the ledger,creditsconfirmedbigint, all CHECKs present.
Phase 2 — Identity, abuse & concurrency (🟠)
- 2.1 L — Unify identity resolution on stable
user_id. ✅getOrCreateUsernow resolves through SessionManager's stableanon_token→ididentity first (re-selecting a fresh row by stable id), falling back to the legacysocket_idlookup only when no session exists — eliminating the duplicate "orphan" user row that caused credit/payout desync.gameModeManager.sessionManagerwired in SocketHandlers.anon_tokenis alreadyUNIQUE(migration 003), so the stable-identity constraint exists. NewidentityResolution.test.jsproves no duplicate row is created when a session exists. Deferred: re-keying the in-memoryactiveGamesmap byuser_id(invasive; already mitigated in 0.5 by stampingdbUserIdon the game) — tracked as a follow-up. - 2.2 M — Reconnect-proof rate limiting. ✅ New
rateLimitContexthelper resolves a STABLE id (anon_token→id, falling back to socket id) + the real client IP. All socket-event limiters (game:start, address:set, chat:message) now pass both — previously they passedsocket.idand no IP at all, so reconnecting reset every limit and the "IP-limited" prefixes never actually applied.chat:added to IP-limited prefixes;connection:newuses the real IP. Expresstrust proxy+ XFF honored only whenTRUST_PROXY=true(documented in.env.example). Contradictory default limit values aligned. NewrateLimit.test.js(reconnect can't bypass, IP backstop, XFF gating). - 2.3 M — Input validation + leak fixes. ✅
register_clientvalidatesclientId(type/length/charset) before it entersclientSocketMap— closes the map-poisoning vector.mempoolNotifiedis now a Map with real TTL eviction in PaymentHandlers (the dead duplicate Set + its random-10% eviction removed from SocketHandlers).sessionscache evicted on disconnect (removeSocket, called after suspend reads it).sessionManager.dispose()/suspendedGameManager.cleanup()/paymentHandlers.dispose()wired intoshutdown().checkGamesTimeoutsnapshots before iterating + awaits sequentially (was mutate-during-forEach+ fire-and-forget). NewinputValidation.test.js.
Phase 3 — Game integrity (🟠)
- 3.1 S — Anti-instant-death grace only (NO fairness floor). ✅ Random block timing is the game's core mechanic and is deliberately preserved — early entry is a knowingly-risky bet, not a bug. The only change is a tiny configurable grace (
GAME_START_GRACE_MS, default 2000ms, 0 to disable) so a block landing the instant a game starts can't kill the player before the dungeon renders / before their first move is possible. (Rejected the originally-planned uniform time budget — it would gut the random-timing mechanic.) - 3.2 S — Reachability guarantee. ✅
DungeonGenerator.generatenow BFS-checks entrance→exit reachability and regenerates if unreachable (paid-but-unwinnable guard). Retries are deterministic (per-attempt ROT seed =seedInt+attempt, seeded rng stream advances), so verify-regeneration replays the same attempts and reproduces the exact dungeon — provably-fair determinism preserved. NewisReachable+ tests (25 seeds all reachable; walled-off target rejected). - 3.3 S — Multi-step monster collision fix. ✅ Collision is now checked after each monster sub-step, not just after the loop. With
movesPerPlayerMove > 1(casino's 1.5× speed) the monster could previously step onto the player's tile and off again between checks, phasing through and missing the catch (player-favorable, eroded the house edge). NewmonsterCollision.test.js.
Phase 4 — De-spaghetti / structural cleanup (🟡)
- 4.1 L — Break up
index.js(substantially). ✅ Extracted the verify HTML →views/verifyPage.js(now HTML-escaped), all 15 admin endpoints →routes/admin.js, and the 3 smirk endpoints →routes/auth.js, each as DI factories with smoke tests. index.js: 2029 → 882 lines (-57%). Remaining user/public routes left inline — they're interleaved with bootstrap and have no route tests, so further slicing is diminishing-return/higher-risk; the structure + pattern are established for later. - [~] 4.2 M — Kill duplicates (partial). ✅ Deleted the dead
handlePlayerMovehandler (a footgun that double-stepped the monster + bypassed escape/collision detection) and its orphanedplayerMoveTimestampsmap/cleanup — the live path isMovementManager. Remaining (lower-value/higher-effort): mergerpcService+rpccalls, unifypayout_addresscolumn widths, address theQueryValidatorfalse-positive — deferred. - [⏸] 4.3 M — Config snapshots — DEFERRED (risk > value). The
process.envconfig write-back is ugly but load-bearing: it's how DB-persisted hot-reload config reaches the env-based readers —difficultyConfigselects the difficulty preset fromprocess.env.GAME_MODE, anddebugManager's simulated-blocks safety gate reads it. A properConfigServicemeans rewiring those sensitive readers; with no config-reload tests and zero correctness/safety upside, the risk isn't justified right now. Documented rather than rewritten. - [~] 4.4 M — Frontend de-spaghetti (safe wins done). ✅ Shared
escapeHtmlalready added in 0.6. Removed the duplicate_gameModedeclaration (the laternullsilently shadowed the earlier'free'). Removed the deadSocketHandlers._appendMessagecalls insmirkAuth.js(the method never existed; always fell through to the working.text()append). GatedbroadcastManager's high-volume per-event logs (user-count, chat-message-with-PII, game-update, game-start) behind debug mode. Upgraded the same-origin vendored jQuery from 3.4.1 to distro-sourced 3.7.1 with a recorded package version and SHA-256. Deferred: the nested-setTimeoutinit-ordering rewrite and a complete nonce-based CSP migration (both require broader browser regression coverage). - 4.5 S — Delete stale
test/scratch files. ✅ Removed 14 non-jest leftovers (5.jsscratch scripts + 9.htmldebug pages) — confirmed they contained no jest constructs and were referenced nowhere.test/is now exclusively the 21 real jest suites.
Phase 5 — Test & ops overhaul (🟠 — the launch gate)
- 5.1 L — Real money-path tests against Postgres (no stubbed
withTransaction): under/overpay, double-confirm, batch dispatch, credit race, retry idempotency; property-testmoney/atomic.js. - 5.2 S — Run real
wownero-wallet-rpcintegration test (open in TODO since day one). - 5.3 S —
environmentValidatorhard-stop (process.exit(1)); add admin-key/mainnet/simulated-block checks (src/index.js:33). - 5.4 S — Reconcile docs (README vs
.env.examplehouse odds); document real provably-fair. - 5.5 M — Durability: DB/Redis-backed idempotency + rate limiter.
- 5.6 S — TLS via reverse proxy.
Sequencing
Build money/atomic.js (1.1) and per-game RNG (0.2) early — Phases 1/3/4 depend on them.
Phase 0 (days) → freeze features, ship → Phase 1 (+5.1/5.2 in parallel)
→ Phase 2 → Phase 3 → Phase 4 (safe now) → finish Phase 5 → LAUNCH GATE
Launch gate (real funds)
- Under/overpayment correct + reconciled (0.1)
- Provably-fair reproduces dungeon, or claim removed (0.2)
- No double-payout under batch+retry, test-proven (0.3, 1.3, 1.4, 5.1)
- All money math BigInt, property-tested (1.1)
- One identity, impersonation closed (0.4, 2.1)
- XSS closed + CSP (0.6)
- Real wallet-rpc integration test executed (5.2)
- Validator hard-stops on misconfig (5.3)
- TLS in front (5.6)
Estimate: ~3–4 weeks to launch gate; Phase 0 in days.