192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
# 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 in `auth.test.ts`, `auth-audit.test.ts`,
|
|
`auth-edge.test.ts`, `tournaments.test.ts` — all going through the real
|
|
`db/index.ts` singleton against a table missing columns that `schema.ts`
|
|
declares (e.g. `satsWon` / `sats_won` in the `bots` table).
|
|
- **Root cause:** `server/src/db/migrate.ts` hand-writes `CREATE TABLE IF NOT
|
|
EXISTS` DDL that has drifted out of sync with `schema.ts` (payments/wallet
|
|
columns were added to the Drizzle schema without updating the raw migration
|
|
script). `drizzle-kit push` also 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_URL` is unset in the test env, so
|
|
`arenaProxy` is a pure `next()` no-op; the SQL error occurs deep in
|
|
`db.select(...)` inside `routes/auth.ts`/`routes/tournaments.ts`, completely
|
|
independent of the proxy mount added in `app.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
|
|
match `schema.ts` column-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", "throughput
|
|
>500 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`'s `backfillCompletedRounds()` — `loadFight()` already
|
|
fetched already-completed rounds via `GET /api/fights/:id`'s `rounds`
|
|
array, but nothing rendered them into the visible log; only live SSE
|
|
`round_end` events ever pushed into `liveLogItems`. Backfill now renders
|
|
a compact (non-animated) summary of already-completed rounds and sets
|
|
HP/round-counter to current state on mount, before `wireSSE()` 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 commit `2512265`, part of 1.2.6):
|
|
`promptUrl` now resolves the real arena origin by parsing it out of the
|
|
fetched prompt's own (correctly proxy-resolved) content, instead of
|
|
`window.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.vue` and
|
|
`BotProfilePage.vue` fetched the static `/docs/BOTFIGHTS.md` file and
|
|
substituted `{{ARENA_URL}}` client-side with `window.location.origin` —
|
|
on a proxy-mode instance (`ARENA_UPSTREAM_URL` set), that's whatever
|
|
address the browser happens to be on, not the real externally-reachable
|
|
arena. **Fixed same session** (botfight commits `2512265`/`ffd4dfd`, part
|
|
of 1.2.6): both flows now fetch the server-rendered `/api/docs/prompt`
|
|
instead, which is under `/api/*` and therefore correctly forwarded by
|
|
`arena-proxy` to 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.ts` falls back to `new URL(c.req.url).origin`
|
|
when `PUBLIC_ARENA_URL` isn't set — same class of problem for an operator
|
|
only reachable over Tailscale/Tor/FIPS. `PUBLIC_ARENA_URL` is already the
|
|
correct escape hatch (explicit config, same pattern as our own
|
|
`ARENA_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 (no `BOT_SECRET` exists 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`, `secretHash`
|
|
unconditionally stored at registration). **Fixed in the doc itself**
|
|
(`BOTFIGHTS.md` section 4 + Option B example + troubleshooting table,
|
|
botfight commit `ffd4dfd`, part of 1.2.6) to special-case
|
|
`type === '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 CSP `img-src` only 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 commit `877d1f6`, part
|
|
of 1.2.8): added `https:` (broad) to `img-src` — safe since images can't
|
|
execute script even from an untrusted origin, unlike `script-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.ts` only 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/info` endpoint. `BOTFIGHTS_CASHU_MINT_URL` set on the canonical
|
|
arena (botfight commit `7341ca0`) — config-only, moves no funds by
|
|
itself (the existing payout branch is only reached from ranked-mode
|
|
fights, which still requires the still-unset `BOTFIGHTS_NWC_URL`).
|
|
- **Threat register worked through before any implementation:**
|
|
1. *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).
|
|
2. *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).
|
|
3. *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.
|
|
4. *Double-posting / replay* — must validate/swap with the mint (NUT-7)
|
|
at submission time, not trust the token string at face value.
|
|
5. *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.
|
|
6. *Payout destination* — resolved: NUT-11 P2PK lets a payout be locked
|
|
to the winner's own pubkey with no interactive receive step required.
|
|
7. *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.
|