feat(payments): wire Cashu as the primary entry-fee UX, fix anonymous-bot ranked auth
CI / check (push) Has been cancelled
CI / check (push) Has been cancelled
Two pieces, both user-directed, completing what 7341ca0 only configured:
1. WalletConnect.vue: Cashu token paste is now the PRIMARY entry-fee path
(submitCashuToken() already existed in useWallet.ts but was never called
from any UI — added the missing wiring). Lightning/NWC is now secondary,
behind an explicit "or connect a Lightning wallet instead" toggle.
Emits `cashu-paid` with the redeemed paymentId; JoinBoutPage.vue's
fightRanked() uses it directly instead of calling payEntryFee()
(Lightning-only) when present — no duplicate invoice/charge.
2. queue.ts's POST /join-ranked/:botId required a nostr pubkey for
ownership verification, full stop. Confirmed live during testing:
anonymous poll-mode bots (the primary registration path for AI agents
per BOTFIGHTS.md) have publicKey: null — staked fights were completely
unusable for that entire audience, silently. Now accepts EITHER a
pubkey OR Authorization: Bot <id>:<secret> (same bot-auth every other
anonymous-bot endpoint already uses) as proof of ownership.
Verified: full server typecheck clean; payments.test.ts (23) and
queue.test.ts (8) unchanged and passing; full frontend suite (101 tests,
13 files) passing, including useWallet.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { joinQueue, leaveQueue, getQueueSize, getQueueSnapshot } from '../engine
|
||||
import { joinRankedQueue, getRankedQueueStatus } from '../engine/ranked-queue.js'
|
||||
import { rateLimit } from '../middleware/rate-limit.js'
|
||||
import { joinRankedSchema, sanitizeError } from '../lib/validators.js'
|
||||
import { authenticateBot } from '../middleware/bot-auth.js'
|
||||
|
||||
export const queueRouter = new Hono()
|
||||
|
||||
@@ -53,7 +54,15 @@ queueRouter.get('/ranked-status', (c) => {
|
||||
return c.json(getRankedQueueStatus())
|
||||
})
|
||||
|
||||
// Join ranked queue — requires confirmed payment + bot ownership
|
||||
// Join ranked queue — requires confirmed payment + bot ownership.
|
||||
// Ownership can be proven either way, since ranked/staked fights are for
|
||||
// BOTH audiences (not just nostr-signed-in humans):
|
||||
// 1. pubkey (nostr-authenticated bots, the web UI's own JWT session flow)
|
||||
// 2. Authorization: Bot <id>:<secret> (anonymous poll-mode bots — the
|
||||
// primary registration path for AI agents per BOTFIGHTS.md, which
|
||||
// never have a publicKey at all: confirmed live, publicKey is null
|
||||
// for every bot registered via POST /api/bots). Without this, staking
|
||||
// was silently unusable for the whole poll-mode/AI-agent audience.
|
||||
queueRouter.post('/join-ranked/:botId', async (c) => {
|
||||
const botId = c.req.param('botId')
|
||||
const parsed = joinRankedSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
@@ -64,13 +73,20 @@ queueRouter.post('/join-ranked/:botId', async (c) => {
|
||||
|
||||
// Verify bot ownership in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
return c.json({ error: 'Missing pubkey' }, 400)
|
||||
}
|
||||
const botRows = await db.select({ publicKey: schema.bots.publicKey })
|
||||
.from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
|
||||
if (botRows.length === 0 || botRows[0].publicKey !== pubkey) {
|
||||
return c.json({ error: 'Unauthorized' }, 403)
|
||||
if (pubkey && typeof pubkey === 'string' && pubkey.length === 64) {
|
||||
const botRows = await db.select({ publicKey: schema.bots.publicKey })
|
||||
.from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
|
||||
if (botRows.length === 0 || botRows[0].publicKey !== pubkey) {
|
||||
return c.json({ error: 'Unauthorized' }, 403)
|
||||
}
|
||||
} else {
|
||||
// No pubkey supplied — fall back to bot-secret auth (Authorization
|
||||
// header or ?bot_id=&secret= query params, same as /api/fights/poll).
|
||||
const botOrRes = await authenticateBot(c)
|
||||
if (botOrRes instanceof Response) return botOrRes
|
||||
if (botOrRes.botId !== botId) {
|
||||
return c.json({ error: 'Unauthorized' }, 403)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user