Records everything found/fixed live during 09-07's blocking human-verify checkpoints today (botfights 1.2.2-1.2.8): iframe embed, native signer bridge, mode-picker discoverability, proxy-URL leaks, round-jump backfill, broken profile images (CSP), AI-answer discoverability, and the webhook_test signature exception. Also records the full threat register worked through for the user-directed Cashu fixed-stake entry-fee request (21 sats, winner takes all) — mint configured and verified live (Minibits), actual token-accepting implementation deliberately NOT done yet, scoped as a follow-up. Note: this section was written once already earlier in the session and appears to have been lost to a shared-tree overwrite before it was committed (CLAUDE.md "Concurrent agent in shared tree" hazard) — recreated and committed immediately this time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 KiB
Deferred Items — Phase 9 Plan 01 (arena-proxy tracer)
Out-of-scope discoveries found while executing 09-01-PLAN.md. Not fixed (per Scope Boundary rule — these are pre-existing, unrelated to the files this plan touches).
1. server/src/db/migrate.ts is stale vs server/src/db/schema.ts (blocks a clean local pnpm vitest run --project server)
- Found during: Task 1/2 verification (
pnpm vitest run --project server). - Symptom:
SqliteError: no such column: "sats_won"(and similar) on a freshly migrated local dev DB (server/data/botfights.db, gitignored). Causes ~14-20 pre-existing test failures inauth.test.ts,auth-audit.test.ts,auth-edge.test.ts,tournaments.test.ts— all going through the realdb/index.tssingleton against a table missing columns thatschema.tsdeclares (e.g.satsWon/sats_wonin thebotstable). - Root cause:
server/src/db/migrate.tshand-writesCREATE TABLE IF NOT EXISTSDDL that has drifted out of sync withschema.ts(payments/wallet columns were added to the Drizzle schema without updating the raw migration script).drizzle-kit pushalso fails against the existing db for the same reason (introspection step queries a column drizzle-kit itself expects to exist). - Verified unrelated to this plan's changes: the failing requests never
touch
arena-proxy.ts—ARENA_UPSTREAM_URLis unset in the test env, soarenaProxyis a purenext()no-op; the SQL error occurs deep indb.select(...)insideroutes/auth.ts/routes/tournaments.ts, completely independent of the proxy mount added inapp.ts. Confirmed by reproducing the identical error on the fresh migrated DB before making any arena-proxy changes were involved in that code path. - Recommendation: A future plan should either update
migrate.ts's DDL to matchschema.tscolumn-for-column, or replace it with real drizzle-kit generated migrations (drizzle-kit generate+drizzle-kit migrate) so the two never diverge again.
2. Timing-sensitive perf tests are flaky under parallel CPU load
- Found during: same full-suite run.
- Symptom:
answers.test.ts("completes 1000 checks in under 50ms"),lifecycle.test.ts("10,000 automated fight simulations", "throughput500 fights/second"),
bot-auth.test.ts("constant-time comparison... variance is minimal") intermittently fail when Vitest's parallel workers contend for CPU on this machine. Pre-existing, unrelated to arena-proxy. - Recommendation: Not this plan's concern; note if a future hardening
pass wants to raise these budgets or mark them
--sequential.
Deferred Items — 2026-07-31 demo-day session (09-06/09-07 hotfix cycle)
Found live while verifying 09-07's blocking human checkpoints (real signer login, cross-node visibility, cloud bot from prompt). Fixed in-line where demo-blocking (shipped as botfights 1.2.2-1.2.8, see 09-07-SUMMARY.md deviations once written); these are the remainder — real but not that session's blocker. (This section was lost once already to a shared-tree overwrite mid-session — recreated and committed immediately this time; see CLAUDE.md "Concurrent agent in shared tree".)
3. FightViewer/round display can open mid-fight instead of at round 1
- Reported: live during demo verification — opening a fight ("botfights training") landed straight on round 5 instead of round 1.
- Fixed same session (botfight commit
ca5b634, part of 1.2.7):FightPage.vue'sbackfillCompletedRounds()—loadFight()already fetched already-completed rounds viaGET /api/fights/:id'sroundsarray, but nothing rendered them into the visible log; only live SSEround_endevents ever pushed intoliveLogItems. Backfill now renders a compact (non-animated) summary of already-completed rounds and sets HP/round-counter to current state on mount, beforewireSSE()connects.
4. DocsPage.vue's promptUrl display link was proxy-unaware
- Same root cause as the JoinBoutPage/BotProfilePage
{{ARENA_URL}}leak (item 5 below) but for a displayed URL string, not fetched content. Fixed same session (botfight commit2512265, part of 1.2.6):promptUrlnow resolves the real arena origin by parsing it out of the fetched prompt's own (correctly proxy-resolved) content, instead ofwindow.location.origin.
5. Client-side "copy AI setup guide" flows leaked proxy-mode local addresses
- Root cause, found live during a real demo incident: a bot got a
Tailscale address (
http://100.69.68.39:9100) in its AI setup guide and correctly refused to act on it, suspecting prompt injection — it wasn't injection, it was real output from real code.JoinBoutPage.vueandBotProfilePage.vuefetched the static/docs/BOTFIGHTS.mdfile and substituted{{ARENA_URL}}client-side withwindow.location.origin— on a proxy-mode instance (ARENA_UPSTREAM_URLset), that's whatever address the browser happens to be on, not the real externally-reachable arena. Fixed same session (botfight commits2512265/ffd4dfd, part of 1.2.6): both flows now fetch the server-rendered/api/docs/promptinstead, which is under/api/*and therefore correctly forwarded byarena-proxyto the real upstream arena in proxy mode.
6. Decentralization roadmap: Tor/FIPS-aware standalone-arena addressing
- User explicitly parked this for later. Today's fix (item 5) solves it for
proxy-mode instances by construction — the hub always reports its own
correct origin. The gap is standalone arenas (no
ARENA_UPSTREAM_URL):server/src/routes/docs.tsfalls back tonew URL(c.req.url).originwhenPUBLIC_ARENA_URLisn't set — same class of problem for an operator only reachable over Tailscale/Tor/FIPS.PUBLIC_ARENA_URLis already the correct escape hatch (explicit config, same pattern as our ownARENA_UPSTREAM_URL); wiring it to FIPS/Tor auto-discovery automatically is new scope, not a bug fix. Design deliberately later, not accidentally now.
7. Decentralization roadmap: arena-less (nostr-event-based) match state
- Also explicitly deferred. Today's implementation is federated-by-choice
(any node can be a hub via unset
ARENA_UPSTREAM_URL), not peer-to-peer — one SQLite DB per arena is still the single source of truth for whoever points at it. A genuinely arena-less design (fight results/registrations as signed, independently-verifiable nostr events propagated across many relays, no server authoritative) is a real architecture change. Design later.
8. External bots that "fix" a webhook_test 401 by disabling signature verification entirely
- Not a bug in this codebase — a warning for the unified prompt's audience.
webhook_test(the registration-time verification call) is deliberately unsigned (noBOT_SECRETexists yet at registration). A bot that generalizes "this one request was unsigned" into "disable signature checking entirely" reopens its own webhook to spoofed challenges from anyone, not just the real arena — confirmed real fight deliveries ARE always signed (server/src/engine/orchestrator.ts,secretHashunconditionally stored at registration). Fixed in the doc itself (BOTFIGHTS.mdsection 4 + Option B example + troubleshooting table, botfight commitffd4dfd, part of 1.2.6) to special-casetype === 'webhook_test'only.
9. Broken profile images — CSP img-src missing https:
- Reported live: "images in profile and such coming up as broken
links." Root cause:
secureHeaders()'s CSPimg-srconly allowed'self', data:, blob:'— nostr profile pictures come from kind:0 metadata, a URL the user sets via their own client, hosted on whatever domain they picked. There's no central image host for a decentralized identity system. Fixed same session (botfight commit877d1f6, part of 1.2.8): addedhttps:(broad) toimg-src— safe since images can't execute script even from an untrusted origin, unlikescript-src, which stays locked to'self'.
10. AI-answer feature discoverability
- The new "let BotFights answer for me" feature (1.2.7) was reported as
invisible after shipping — it was gated behind picking POLLING (not the
default WEBHOOK) AND behind a collapsed toggle within that. Fixed same
session (botfight commit
877d1f6, part of 1.2.8): POLLING is now the default connection mode (also BOTFIGHTS.md's own documented default), the AI section is expanded by default, and the POLLING button's own description mentions the option.
11. Cashu fixed-stake entry fee ("winner takes all, 21 sats each, only ever") — SCOPED, NOT IMPLEMENTED
- User-directed feature request, explicitly asked to be scoped properly before any code, given real bearer-instrument money is involved.
- What exists today:
server/src/engine/payments.tsonly mints outbound cashu tokens to pay a winner from the arena's own configured mint. There is no code path that accepts a user-submitted token as an entry stake — this is genuinely new work, not a wire-up. - Mint chosen and verified live (not guessed): Minibits,
https://mint.minibits.cash/Bitcoin— confirmed NUT-4 (mint, bolt11/sat), NUT-5 (melt), NUT-7 (spend-check), NUT-11 (P2PK) all present via its/v1/infoendpoint.BOTFIGHTS_CASHU_MINT_URLset on the canonical arena (botfight commit7341ca0) — config-only, moves no funds by itself (the existing payout branch is only reached from ranked-mode fights, which still requires the still-unsetBOTFIGHTS_NWC_URL). - Threat register worked through before any implementation:
- Bearer-token custody window — must swap a posted token into server-owned proofs immediately on receipt (NUT-7), never hold the raw wire-format token as state (crash-loses-money risk otherwise).
- Mint trust — inherent to any Cashu design; mitigated by choosing an established, verified-live mint whose own operators recommend small amounts (matches the 21-sat cap).
- Amount enforcement — must hard-reject anything that isn't exactly 21 sats, server-side, at submission — the whole point of the cap is bounding any bug's blast radius to a trivial amount.
- Double-posting / replay — must validate/swap with the mint (NUT-7) at submission time, not trust the token string at face value.
- Refund path — an unmatched/cancelled/timed-out stake must return to its poster; no refund path = silent fund loss on the (common, for anonymous poll-mode bots) timeout case.
- Payout destination — resolved: NUT-11 P2PK lets a payout be locked to the winner's own pubkey with no interactive receive step required.
- Logging hygiene — a raw token string is money; same rule as API keys, never logged, never in error messages.
- Recommendation for the actual implementation (not done): a new escrow-and-swap flow (post token → validate exact amount + NUT-7 check → immediate mint-side swap to server custody → hold server-owned proof reference in DB, never the raw token → on fight resolution, mint a fresh 42-sat P2PK token to the winner's pubkey → on timeout/cancellation, mint a refund token back to the poster's pubkey if they provided one, or require pubkey-at-submission specifically so a refund destination always exists). This should be its own properly-planned unit of work, not folded into an already-massive hotfix batch.