# Production Hardening v2 — Final Status ## Context The original hardening plan (`greedy-skipping-lollipop.md`) was 95% complete. A full security audit confirmed the fight engine, scoring, auth, SSRF, rate limiting, and webhook systems were solid. This session addressed remaining production gaps AND added Creator ₿ animation/morph enhancements AND fixed a production ranked-fight bug. --- ## Phase 1: Security Headers & Error Hardening — DONE ### 1.1 Security headers middleware — DONE **File:** `server/src/app.ts` - Added `secureHeaders` from `hono/secure-headers` with CSP (production only), X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy ### 1.2 Request body size limit — DONE **File:** `server/src/app.ts` - Added `bodyLimit` from `hono/body-limit` — 256KB max for API requests ### 1.3 Hide error details in production — DONE **File:** `server/src/app.ts` - `onError` returns generic "Internal server error" in production, full message in dev --- ## Phase 2: Graceful Shutdown & Env Validation — DONE ### 2.1 Graceful shutdown handler — DONE **File:** `server/src/index.ts` - SIGTERM/SIGINT handlers wait 10s for in-flight fights, clear pending human challenges, then exit ### 2.2 Export helpers for shutdown — DONE - `server/src/engine/orchestrator.ts` — `getActiveFighterCount()` - `server/src/engine/human-responses.ts` — `clearAllPending()` ### 2.3 Stricter env validation — DONE **File:** `server/src/index.ts` - Warns if CORS_ORIGIN not set in production --- ## Phase 3: Payment Race Condition Fix — DONE ### 3.1 Atomic consumePaymentForQueue — DONE **File:** `server/src/engine/payments.ts` - Replaced check-then-set with atomic `UPDATE ... WHERE` (single SQL statement) - Imported raw `sqlite` from db/index.js ### 3.2 releasePayment reverts DB state — DONE **File:** `server/src/engine/payments.ts` - Now also reverts `status` from `consumed` back to `confirmed` in DB --- ## Phase 4: Live Round-by-Round TUI — DONE ### 4.1 onRoundComplete callback — DONE **File:** `server/src/engine/fight-loop.ts` - New `onRoundComplete` option, subscribes to `fightEvents.onAll()` for `round_end` events ### 4.2 TUI wired to round updates — DONE **File:** `server/src/fight-loop-cli.ts` - `onRoundComplete` updates `state.currentFight` HP/round/challengeType and re-renders --- ## Phase 5: Creator ₿ Animation Enhancements — DONE ### 5.1 Persistent orbiting ₿ letters on entrance — DONE **File:** `frontend/src/game/FightScene.ts` (Creator entrance) - Replaced 6 plain circle dots with 10 ₿ text letters in 2 counter-rotating rings - Varied sizes (7-14px), 5 colors, gentle rocking animation - Visible for the entire fight ### 5.2 Upgraded omni-morph ₿ orbits — DONE **File:** `frontend/src/game/FightScene.ts` (`applyCreatorOmniMorph`) - Increased from 4 to 8 ₿ in dual rings (inner 5, outer counter-rotating 3) - Varied sizes (10-18px), 3 colors, wobble animation ### 5.3 bitcoinRain uses ₿ text — DONE **File:** `frontend/src/game/FightScene.ts` (`bitcoinRain`) - Changed from circles to actual ₿ text with 3 colors, varied sizes (6-18px), spinning as they fall ### 5.4 Four new ₿ swarm choreographies — DONE **File:** `frontend/src/game/FightScene.ts` - `bitcoinSwarm` — 18-30 ₿ letters zigzag from Creator to defender - `satoshiTornado` — 14-24 ₿ spiral in tightening vortex around defender, then explode - `hodlWave` — wall of ₿ letters (4-6 rows x 7-10 cols) advances like a tidal wave - `bitcoinBarrage` — rapid-fire stream of 20-35 spinning ₿ from the laptop - All added to `CREATOR_MOVES` array and `choreographyMap` --- ## Phase 6: Creator Omni-Morph Visual Fix — DONE ### 6.1 Sprite swap on morph — DONE **File:** `frontend/src/game/FightScene.ts` (`applyCreatorOmniMorph`) - Was: only added overlays (aura, ₿, color tint) — sprite stayed as Creator - Now: generates a full sprite sheet for the picked archetype via `generateSpriteSheet(seed, tier, colors, archetypeOverride)` - Loads it as a Kaplay sprite and swaps with `fighter.use(k.sprite(morphKey))` - Function changed from sync to `async` - Unique sprite keys via incrementing counter prevent collisions ### 6.2 Sprite revert after attack — DONE **File:** `frontend/src/game/FightScene.ts` (morph trigger in playRound) - `morphRevert` now calls `attacker.use(k.sprite(originalSpriteKey))` to swap back to Creator - Added `await` on `applyCreatorOmniMorph` call (now async) --- ## Phase 7: Production Ranked Fight Fix — DONE ### 7.1 Missing pubkey in ranked endpoints — DONE **File:** `frontend/src/composables/useWallet.ts` - `payEntryFee()` now sends `{ botId, pubkey: pubkey.value }` to `/api/payments/create-invoice` **File:** `frontend/src/pages/JoinBoutPage.vue` - `fightRanked()` now sends `{ paymentId, pubkey: pubkey.value }` to `/api/queue/join-ranked` **Root cause:** Server requires `pubkey` in production for ownership verification, but frontend wasn't sending it. Worked in dev because dev mode skips pubkey checks. --- ## All Files Modified | File | Phase | Changes | |------|-------|---------| | `server/src/app.ts` | 1 | +secureHeaders, +bodyLimit, error hiding | | `server/src/index.ts` | 2 | +graceful shutdown, +env validation | | `server/src/engine/orchestrator.ts` | 2 | +getActiveFighterCount() | | `server/src/engine/human-responses.ts` | 2 | +clearAllPending() | | `server/src/engine/payments.ts` | 3 | Atomic consume, releasePayment DB revert | | `server/src/engine/fight-loop.ts` | 4 | +onRoundComplete via fightEvents | | `server/src/fight-loop-cli.ts` | 4 | Wire onRoundComplete to TUI | | `frontend/src/game/FightScene.ts` | 5,6 | ₿ animations, omni-morph sprite swap, 4 new choreographies | | `frontend/src/composables/useWallet.ts` | 7 | +pubkey in create-invoice | | `frontend/src/pages/JoinBoutPage.vue` | 7 | +pubkey in join-ranked | | `frontend/src/game/sprites/archetypes/the_creator.ts` | — | No changes (reference only) | --- ## Verification 1. `pnpm build` — verify no type errors (both server and frontend pass) 2. Production server: `curl -I /api/health` — X-Frame-Options, X-Content-Type-Options, CSP headers present 3. Body limit: oversized POST returns 413 4. Error hiding: bad endpoint returns "Internal server error" (not stack trace) 5. Graceful shutdown: `kill -TERM` shows drain log 6. TUI: `pnpm fight-loop --max=3` shows live round-by-round HP updates 7. Payment race: concurrent ranked joins with same paymentId — only 1 succeeds 8. Ranked fights: no more "Missing pubkey" error in production 9. Creator fights: omni-morph visually transforms into random archetypes (sprite swaps) 10. Creator fights: ₿ letters orbit persistently, new swarm attacks fire regularly