feat: THE CREATOR god-tier character, bitcoin choreographies, omni-morph, cameos
- Guy Fawkes mask archetype with golden outline, bitcoin chest symbol, laptop, tier-gated effects - 12 custom bitcoin-themed choreographies (8 regular + 4 exclusive ultimates) - Omni-morph system: creator morphs into any of 80+ archetypes instead of cycling 3 - 6% per-round cameo in non-creator fights — drops golden ₿ gifts to fighters - Custom golden code rain entrance with persistent orbiting particles - pickChoreography: 50% ultimate chance (100% on crits), all tier ultimates + 4 exclusive - Server auto-assigns the_creator archetype on login/register for creator pubkey - FightViewer, payments, queue, orchestrator improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8a3480e5ab
commit
6d390f69b2
@@ -10,6 +10,9 @@ import { rateLimit } from '../middleware/rate-limit.js'
|
||||
|
||||
export const authRouter = new Hono()
|
||||
|
||||
// The Creator — game founder pubkey (auto-assigns the_creator archetype)
|
||||
const CREATOR_PUBKEY = "da5e0c1b646bdb13c2300f805b0ca3e5afe5b052c594ce78bac8978d21c3fa39"
|
||||
|
||||
// Check name availability
|
||||
authRouter.get("/check-name/:name", async (c) => {
|
||||
const name = c.req.param("name")?.trim().toLowerCase()
|
||||
@@ -58,6 +61,12 @@ authRouter.post('/login', async (c) => {
|
||||
const bot = rows[0]
|
||||
const isHuman = bot.webhookUrl === 'http://human.local/'
|
||||
|
||||
// Auto-upgrade: if creator logs in, ensure archetype is always the_creator
|
||||
if (pubkey === CREATOR_PUBKEY && bot.archetype !== "the_creator") {
|
||||
await db.update(schema.bots).set({ archetype: "the_creator" }).where(eq(schema.bots.id, bot.id))
|
||||
bot.archetype = "the_creator"
|
||||
}
|
||||
|
||||
return c.json({
|
||||
exists: true,
|
||||
bot: {
|
||||
@@ -154,7 +163,8 @@ authRouter.post('/register', rateLimit(3600_000, 15), async (c) => {
|
||||
const id = nanoid(12)
|
||||
const secret = randomBytes(32).toString('hex')
|
||||
|
||||
const effectiveArchetype = custResult.data.archetype || archetype || 'standard'
|
||||
const baseArchetype = custResult.data.archetype || archetype || 'standard'
|
||||
const effectiveArchetype = pubkey === CREATOR_PUBKEY ? 'the_creator' : baseArchetype
|
||||
const custJson = Object.keys(custResult.data).length > 0 ? JSON.stringify(custResult.data) : null
|
||||
|
||||
await db.insert(schema.bots).values({
|
||||
@@ -248,7 +258,7 @@ authRouter.post('/update', async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = body
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string') {
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
return c.json({ error: 'Invalid pubkey.' }, 400)
|
||||
}
|
||||
|
||||
@@ -264,6 +274,9 @@ authRouter.post('/update', async (c) => {
|
||||
const updates: Record<string, unknown> = {}
|
||||
|
||||
if (webhookUrl) {
|
||||
if (typeof webhookUrl !== 'string' || webhookUrl.length > 2048) {
|
||||
return c.json({ error: 'Invalid webhookUrl.' }, 400)
|
||||
}
|
||||
if (!isAllowedWebhookUrl(webhookUrl)) {
|
||||
return c.json({ error: 'webhookUrl must not point to private/internal addresses.' }, 400)
|
||||
}
|
||||
@@ -279,7 +292,12 @@ authRouter.post('/update', async (c) => {
|
||||
updates.isActive = true
|
||||
}
|
||||
|
||||
if (profilePicUrl) updates.profilePicUrl = profilePicUrl
|
||||
if (profilePicUrl) {
|
||||
if (typeof profilePicUrl !== 'string' || profilePicUrl.length > 2048 || !/^https?:\/\//.test(profilePicUrl)) {
|
||||
return c.json({ error: 'profilePicUrl must be a valid HTTP(S) URL.' }, 400)
|
||||
}
|
||||
updates.profilePicUrl = profilePicUrl
|
||||
}
|
||||
|
||||
if (rawCustomization !== undefined) {
|
||||
const custResult = validateCustomization(rawCustomization)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createEntryInvoice, checkPaymentStatus, redeemCashuToken } from '../engine/payments.js'
|
||||
import { encrypt, decrypt } from '../engine/crypto.js'
|
||||
import { rateLimit } from '../middleware/rate-limit.js'
|
||||
|
||||
export const paymentsRouter = new Hono()
|
||||
|
||||
@@ -82,11 +83,23 @@ paymentsRouter.get('/wallet-status', async (c) => {
|
||||
return c.json({ connected: true, method: walletRows[0].method })
|
||||
})
|
||||
|
||||
// POST /create-invoice
|
||||
paymentsRouter.post('/create-invoice', async (c) => {
|
||||
const { botId } = await c.req.json<{ botId: string }>()
|
||||
// POST /create-invoice — rate limited: 10 per minute per IP
|
||||
paymentsRouter.post('/create-invoice', rateLimit(60_000, 10), async (c) => {
|
||||
const { botId, pubkey } = await c.req.json<{ botId: string; pubkey?: string }>()
|
||||
if (!botId) return c.json({ error: 'Missing botId' }, 400)
|
||||
|
||||
// In production, verify bot ownership
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createEntryInvoice(botId)
|
||||
return c.json(result)
|
||||
@@ -96,9 +109,12 @@ paymentsRouter.post('/create-invoice', async (c) => {
|
||||
}
|
||||
})
|
||||
|
||||
// GET /check/:paymentId
|
||||
paymentsRouter.get('/check/:paymentId', async (c) => {
|
||||
// GET /check/:paymentId — rate limited: 30 per minute per IP
|
||||
paymentsRouter.get('/check/:paymentId', rateLimit(60_000, 30), async (c) => {
|
||||
const paymentId = c.req.param('paymentId')
|
||||
if (!paymentId || paymentId.length > 24) {
|
||||
return c.json({ error: 'Invalid paymentId' }, 400)
|
||||
}
|
||||
try {
|
||||
const status = await checkPaymentStatus(paymentId)
|
||||
return c.json({ status })
|
||||
@@ -109,8 +125,12 @@ paymentsRouter.get('/check/:paymentId', async (c) => {
|
||||
})
|
||||
|
||||
// POST /confirm/:paymentId — frontend confirms after NWC pay returns preimage
|
||||
paymentsRouter.post('/confirm/:paymentId', async (c) => {
|
||||
paymentsRouter.post('/confirm/:paymentId', rateLimit(60_000, 20), async (c) => {
|
||||
const paymentId = c.req.param('paymentId')
|
||||
if (!paymentId || paymentId.length > 24) {
|
||||
return c.json({ error: 'Invalid paymentId' }, 400)
|
||||
}
|
||||
|
||||
const { preimage, pubkey } = await c.req.json<{ preimage?: string; pubkey?: string }>().catch(() => ({ preimage: undefined, pubkey: undefined }))
|
||||
|
||||
const rows = await db.select().from(schema.payments)
|
||||
@@ -119,16 +139,37 @@ paymentsRouter.post('/confirm/:paymentId', async (c) => {
|
||||
|
||||
const payment = rows[0]
|
||||
if (payment.status === 'confirmed') return c.json({ status: 'confirmed' })
|
||||
if (payment.status !== 'pending') return c.json({ error: 'Payment is not pending' }, 400)
|
||||
|
||||
// Must be an inbound entry payment
|
||||
if (payment.direction !== 'in') return c.json({ error: 'Cannot confirm outbound payments' }, 400)
|
||||
|
||||
// Verify caller owns this payment's bot
|
||||
if (pubkey) {
|
||||
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, payment.botId)).limit(1)
|
||||
if (botRows.length === 0 || botRows[0].publicKey !== pubkey) {
|
||||
return c.json({ error: 'Unauthorized' }, 403)
|
||||
}
|
||||
} else if (process.env.NODE_ENV === 'production') {
|
||||
return c.json({ error: 'Missing pubkey' }, 400)
|
||||
return c.json({ error: 'Missing or invalid pubkey' }, 400)
|
||||
}
|
||||
|
||||
// In production, also verify payment via NWC lookup (belt and suspenders)
|
||||
if (process.env.NODE_ENV === 'production' && payment.invoice && payment.invoice !== 'dev_auto_confirmed') {
|
||||
try {
|
||||
const serverStatus = await checkPaymentStatus(paymentId)
|
||||
if (serverStatus !== 'confirmed') {
|
||||
return c.json({ error: 'Server could not verify payment. Try again.' }, 402)
|
||||
}
|
||||
// checkPaymentStatus already updated the DB
|
||||
return c.json({ status: 'confirmed' })
|
||||
} catch {
|
||||
// NWC check failed — fall through to client-confirmed path with preimage
|
||||
if (!preimage) {
|
||||
return c.json({ error: 'Payment verification failed and no preimage provided.' }, 402)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(schema.payments).set({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { joinQueue, leaveQueue, getQueueSize, getQueueSnapshot } from '../engine/queue.js'
|
||||
import { joinRankedQueue, getRankedQueueStatus } from '../engine/ranked-queue.js'
|
||||
import { rateLimit } from '../middleware/rate-limit.js'
|
||||
|
||||
export const queueRouter = new Hono()
|
||||
|
||||
@@ -48,15 +49,27 @@ queueRouter.get('/ranked-status', (c) => {
|
||||
return c.json(getRankedQueueStatus())
|
||||
})
|
||||
|
||||
// Join ranked queue — requires confirmed payment
|
||||
// Join ranked queue — requires confirmed payment + bot ownership
|
||||
queueRouter.post('/join-ranked/:botId', async (c) => {
|
||||
const botId = c.req.param('botId')
|
||||
const { paymentId } = await c.req.json<{ paymentId: string }>()
|
||||
const { paymentId, pubkey } = await c.req.json<{ paymentId: string; pubkey?: string }>()
|
||||
|
||||
if (!paymentId) {
|
||||
return c.json({ error: 'Missing paymentId' }, 400)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const fightId = await joinRankedQueue(botId, paymentId)
|
||||
return c.json({ fightId, message: 'Ranked match found! Fight starting.' })
|
||||
|
||||
Reference in New Issue
Block a user