- checkAnswer now returns highest score across all accepted answers
instead of first match, fixing 95 false-low-confidence results
- Skip string containment for purely numeric strings to prevent
false positives like "1000" matching inside "10000"
- Preserve decimal points in normalize() (42.0 no longer becomes 420)
- Use word-boundary regex for number matching in responses
- Fix 47 wrong choices scoring too high (comma-formatted numbers,
verbose choices matching terse answers)
- Fix 17 prompts where no choice matched any accepted answer
- Challenge audit now reports zero failures across all 1472 prompts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useNostr (9), useFightCache (5), useOnlineStatus (4) composable tests.
Added fake-indexeddb dev dependency for IDB tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only invalidates __alltime__ and current season cache keys on fight completion,
preserving historical season caches. Test verifies selective behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global app.onError handler already in app.ts catches all unhandled route
exceptions. Production mode returns "Internal server error" only.
Tests verify no stack traces or file paths leak in responses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Both bots timing out every round: verified draw with zero damage
- Perfect victory (10-0): verified correct ELO calculation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
blacklistJwt() adds token to in-memory blacklist until its natural expiry.
verifyJwt() checks blacklist before signature verification.
Cleanup interval removes expired entries every 10 minutes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents finding: no replay protection in NIP-98 verification.
Token replay within 120s window succeeds (mitigated by JWT issuance being idempotent).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests the actual rateLimit middleware with production mode via dynamic import.
Covers: under-limit allows, over-limit returns 429, window reset, per-IP
isolation, and poll endpoint config (30 req/1s window).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Validates challenge type against the full CHALLENGE_TYPES enum before
processing. Invalid types now return 400 instead of silently falling
back to speed_blitz.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Production now requires JWT_SECRET env var. Added comprehensive JWT
tests: creation, verification, expiry, tampered payload, tampered
signature, and malformed token rejection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added getDecodedToken validation that rejects malformed tokens with
400 before any DB lookups. Tests cover empty, non-base64, truncated,
and random base64 tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add rateLimit(1_000, 30) middleware to GET /poll endpoint
- Fix Challenge type errors in human-responses test files (missing baseDamage)
- Add rate-limit unit test verifying 429 after exceeding limit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
better-sqlite3 driver is synchronous — .get() does NOT need await.
Added tests for unknown pubkey (404) and missing pubkey (400) on join.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Frontend: vitest.config.ts with vue plugin + jsdom, dummy component test
- Server: in-memory SQLite test DB factory + Hono testClient helper + smoke test
- CI: add pnpm audit and server coverage threshold steps
- Root: vitest workspace config for multi-project test discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scoring: both-wrong is now a DRAW — equal scores (3/3), no winner,
symmetric minimal damage. Garbage answers no longer beat reasonable
ones just by being faster. Both-wrong narrations reflect the draw.
Entrance: removed duplicate announceDeepIntro() call from FightViewer
(was already called inside playEntrance). Removed _resetPositions()
after entrance (entrance already places fighters at home positions,
the extra reset caused a visible snap/reset).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Polling bots have no external script running during practice mode,
so the poll would time out giving empty answers. Now overrides the
webhook URL to human.local so the browser UI handles challenges.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Allow huggingface.co in CSP connect-src (fixes Kokoro TTS model download)
- Add registerSW.js route (fixes PWA service worker 404)
- Add _resetPositions() safety after entrance (fixes invisible fighters)
- Fight end sequence works without canvas scene (KO/overlays/log always play)
- Pre-fight instructions in battle log for human players
- NIP-55 visibility sync and cleanup handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security:
- Move CREATOR_PUBKEY from hardcoded constant to BOTFIGHTS_CREATOR_PUBKEYS
env var. Shared isCreatorPubkey() in constants.ts used by auth, admin,
tournaments. Frontend checks authorization via API, not client-side.
Mobile fixes:
- Nostr signer: poll for window.nostr up to 3s (Amber injects late).
- TTS: auto-unlock AudioContext on first user interaction via
installAutoUnlock() on fight page mount.
UX:
- Add loading spinners to "I BUILD BOTS" and "I FIGHT MYSELF" buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand all 5 creative answer pools from 5-10 to 25-30 entries each
- Fix factual wrong answers to pick from challenge's own wrong choices
instead of random non-sequiturs like "banana" and "purple?"
- Reduce bad answer rate: 25% → 2.5% at elo 1200, 0% at elo 1300+
- Improve BAD_ANSWERS and WRONG_FACTUAL to be funny when they do appear
- 90%+ of all mock answers are now good attempts that make sense
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix invisible characters in human mode: init live scene BEFORE starting
challenge polling so entrance plays before first question appears
- Cap creative writing timer to 10s for multiple choice (just tapping buttons)
- Fix TTS reliability: precache priority phrases (Round 1-7, Fight!, K.O.)
all at once instead of in slow batches; prevent duplicate precache runs
- Add 10 vibe-coded narrations (~20% chance): "I was vibe coded into existence"
- Add 15 sweary narrations (~30% chance): raw unhinged fight commentary
- Add sweary draw and retro narrations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace insecure raw-pubkey auth with cryptographic NIP-98 signed
requests and server-issued JWT sessions. Logout now fully clears
all state including nsec. Add yellow "Use Nostr Signer" button
for Amber/NIP-07 remote signers.
- Server: JWT middleware (HMAC-SHA256, 24h expiry), NIP-98 verification
- Server: POST /api/auth/nostr/session endpoint
- Frontend: NIP-98 token builder + authFetch wrapper with JWT Bearer
- Frontend: All authenticated API calls use authFetch
- Security: logout clears JWT, pubkey, bot, nsec, and profile pic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Practice button to BotProfilePage for quick sparring
- Fix rate limiter bug: all rateLimit() instances shared one counter map,
causing global and per-route limits to corrupt each other. Each limiter
now gets its own isolated map.
- Replace 8-digit hex colors (#ffd70066) with rgba() in sprite rendering
for mobile browser compatibility (iOS Safari renders them as black boxes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mobile nav menu now overlays content (absolute positioning) instead of
pushing it down
- Speech bubbles stay visible for minimum 400ms even when TTS resolves
instantly or fails
- kokoroPlayCached checks audio cache and loads static files even when
Kokoro worker hasn't loaded — fixes TTS not playing on production
- CORS_ORIGIN env now supports comma-separated origins
- Rename "VIDEO REPLAY" to play icon + "REPLAY"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Track active fight IDs per bot in orchestrator (Set → Map)
- Return fightId in "already in fight" error responses (409)
- Frontend shows "REJOIN FIGHT" link instead of generic error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Benchmarks the scoring pipeline (challenge → response → score → elo → tier)
without I/O. Currently achieves ~8500 fights/sec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>