- register_client now validates the client-supplied clientId (type, length,
charset) before it enters clientSocketMap as both key and value, closing a
map-poisoning / mapping-impersonation vector.
- mempoolNotified is now a Map with real TTL eviction owned by PaymentHandlers
(the actual dedup set). The dead duplicate Set in SocketHandlers and its
random-10%-per-cycle eviction (which could drop live entries) are removed;
addresses are also evicted promptly when monitoring stops.
- The SessionManager `sessions` cache is evicted on disconnect (removeSocket,
called after the suspend logic reads it) so it no longer grows unbounded
with every socket ever seen.
- sessionManager.dispose(), suspendedGameManager.cleanup() and
paymentHandlers.dispose() are wired into SocketHandlers.shutdown() so their
timers/maps don't leak on shutdown.
- checkGamesTimeout snapshots activeGames before iterating and awaits each
game-over sequentially (was mutating the Map during forEach and firing
async game-overs concurrently, which could skip games / double-process).
Adds inputValidation.test.js. Full suite: 19 suites / 149 tests.
Socket-event rate limits keyed on socket.id (new on every reconnect) and
passed NO ip argument, so the "IP-limited" prefixes never actually applied —
reconnecting reset every limit, and chat had no IP limit at all.
- New rateLimitContext helper resolves a STABLE identity (anon_token -> id,
falling back to socket id) and the real client IP (X-Forwarded-For honoured
only when TRUST_PROXY=true, else handshake address — no spoofing on direct
exposure).
- game:start, address:set and chat:message now pass (stableId, action, ip);
connection:new uses the real IP. 'chat:' added to IP-limited prefixes.
- Express 'trust proxy' enabled when TRUST_PROXY=true; documented in
.env.example. Aligned the contradictory default limit values with the
production override.
Adds rateLimit.test.js (reconnect can't bypass, IP backstop for fresh
identities, XFF gating). Full suite: 18 suites / 143 tests.
The admin "send funds" refund path called walletRPCService.sendPayment(),
which does not exist — guarded by a typeof check, so it silently never sent
while still reporting the refund as processed.
Now uses the real processPayout() (validates the address and transfers,
returning { success, txHash }), and refunds the amount the user actually paid
(received_amount, recorded at confirmation) rather than the expected amount,
falling back to expected for older records.
The client trusted the server to pre-escape chat content and rendered
user-supplied fields via .html()/innerHTML — one server regression away from
stored XSS, and the admin panel injected user-controlled payout addresses
into its authenticated DOM.
- Add a single shared escapeHtml helper (escapes all five HTML-significant
chars including both quotes); leaderboard now delegates to it.
- Escape chat username + message in onChatBroadcast/onChatHistory.
- Escape user-controlled fields in the admin panel (payout address, socket
id, player name, status) and make its local escapeHtml quote-safe so values
in title="..." can't break out.
- Add CSP plus X-Content-Type-Options, X-Frame-Options and Referrer-Policy
headers via middleware. connect-src 'self' keeps Socket.IO working;
removing 'unsafe-inline' (nonces) is tracked as a later cleanup.
Full suite: 13 suites / 119 tests.
Phase 0.1 — Underpayment confirmation bug (CRITICAL):
Payment confirmation gated on `status.confirmed`, which checkPaymentStatus
sets for ANY incoming tx regardless of amount, so sending a single atomic
unit granted a full game/credits. Now gate on `status.complete` (received
>= expected). Underpayers are warned once and the payment stays pending so
a top-up can complete it. Persist `received_amount` for single-game and
credits confirmations for reconciliation. Adds an underpayment regression
test.
Phase 0.2 — Provably-fair was decorative (CRITICAL):
The committed seed had zero effect on the dungeon (created seededRNG was
unused; generation used Math.random + a globally-seeded ROT.RNG). Thread a
per-game seeded RNG derived from the committed seed through dungeon
generation (reseed the Digger via seedToInt + replace all Math.random) and
monster movement (seededShuffle). Add DungeonGenerator.regenerateFromSeed
and layoutFingerprint; /api/verify now regenerates the dungeon from the
seed and returns a fingerprint a player can independently reproduce. Adds a
determinism test suite.
Full suite: 11 suites / 113 tests passing.
Plan and progress tracked in docs/REMEDIATION_PLAN.md.
Smirk extension is migrating from SHA256 pre-hash to standard RFC 8032
Ed25519 signatures. Accept both formats during transition:
- Try raw message bytes first (new standard)
- Fall back to SHA256(message) (legacy, remove after 2026-07-01)
- Fix wallet balance always showing 'Disconnected' in admin dashboard
- Always attempt balance fetch even if isHealthy=false
- Show error state with message instead of generic 'Disconnected'
- Add POST /api/admin/alerts/test-email endpoint to verify Resend config
- Move alertService to module scope for endpoint access
- Fix game start failure in direct payment mode when payment confirms
before mempool detection (add player to queue if not already there)
- Fix debug logging in production: correct indentation in debugRegisteredUsers,
swap console_logging config (false in prod, true in debug)
- Add close button to QR code container
- Auto-hide QR on payment detected, confirmed, and game start
Payout retry system:
- Add migration 007 with retry_count, last_error, last_retry_at columns
- Payouts now retry up to PAYOUT_MAX_RETRIES times before permanent failure
- Each failure logs the attempt number and error message
- Console shows ⚠️ for retryable failures, 💀 for permanent failures
- Added SQL examples in .env.example for querying/resetting failed payouts
HOSTED_BY feature:
- Add HOSTED_BY and HOSTED_BY_NAME env vars
- /api/game-modes endpoint returns hostedBy info
- Footer displays 'Hosted by: <link>' when configured
Also improved .env.example:
- Better documentation for credit packages with reasonable defaults
- Clearer explanation of retry behavior