docs(09): record demo-day deviations + Cashu fixed-stake feature scoping
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>
This commit is contained in:
parent
48a2ff7c16
commit
9ad31a7495
@ -41,3 +41,151 @@ touches).
|
||||
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user