feat(09-02): GET /api/auth/me — identity from JWT, never a claimed pubkey (BOT-01)
CI / check (push) Has been cancelled
CI / check (push) Has been cancelled
Adds a JWT-gated, read-only session-restore route. Mirrors POST /login's projection and 200 body shape exactly so normalizeBotData on the client is unchanged. extractPubkeyFromAuth (already imported later in the file for /regenerate-secret) covers missing/malformed/forged/expired/ blacklisted tokens via verifyJwt; deduped the now-redundant import at the /nostr/session section. All 7 auth-me.test.ts cases pass; tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { testWebhook } from '../engine/webhook-test.js'
|
|||||||
import { rateLimit } from '../middleware/rate-limit.js'
|
import { rateLimit } from '../middleware/rate-limit.js'
|
||||||
import { isCreatorPubkey } from '../lib/constants.js'
|
import { isCreatorPubkey } from '../lib/constants.js'
|
||||||
import { loginSchema, registerSchema, registerHumanSchema, updateBotSchema, pubkeySchema, formatZodError } from '../lib/validators.js'
|
import { loginSchema, registerSchema, registerHumanSchema, updateBotSchema, pubkeySchema, formatZodError } from '../lib/validators.js'
|
||||||
|
import { extractPubkeyFromAuth } from '../middleware/jwt.js'
|
||||||
|
|
||||||
export const authRouter = new Hono()
|
export const authRouter = new Hono()
|
||||||
|
|
||||||
@@ -25,6 +26,69 @@ authRouter.get("/check-name/:name", async (c) => {
|
|||||||
return c.json({ available: existing.length === 0 })
|
return c.json({ available: existing.length === 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// GET /me — restore the caller's own identity from their JWT alone.
|
||||||
|
// This is the ONLY session-restore path: it derives the pubkey from a
|
||||||
|
// verified, non-expired, non-blacklisted Bearer token (extractPubkeyFromAuth
|
||||||
|
// delegates to verifyJwt, which covers all of those cases) and never trusts
|
||||||
|
// a client-claimed pubkey. Read-only — performs no writes of any kind.
|
||||||
|
// Covered by global /api/* rate limiting (see app.ts); no per-route limiter
|
||||||
|
// needed for a session-restore call issued on every page load.
|
||||||
|
authRouter.get('/me', async (c) => {
|
||||||
|
const pubkey = extractPubkeyFromAuth(c.req.header('Authorization'))
|
||||||
|
if (!pubkey) {
|
||||||
|
return c.json({ error: 'Authentication required.' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.select({
|
||||||
|
id: schema.bots.id,
|
||||||
|
name: schema.bots.name,
|
||||||
|
avatarSeed: schema.bots.avatarSeed,
|
||||||
|
archetype: schema.bots.archetype,
|
||||||
|
profilePicUrl: schema.bots.profilePicUrl,
|
||||||
|
eloRating: schema.bots.eloRating,
|
||||||
|
wins: schema.bots.wins,
|
||||||
|
losses: schema.bots.losses,
|
||||||
|
winStreak: schema.bots.winStreak,
|
||||||
|
bestStreak: schema.bots.bestStreak,
|
||||||
|
tier: schema.bots.tier,
|
||||||
|
isActive: schema.bots.isActive,
|
||||||
|
customization: schema.bots.customization,
|
||||||
|
webhookUrl: schema.bots.webhookUrl,
|
||||||
|
satsWon: schema.bots.satsWon,
|
||||||
|
satsWagered: schema.bots.satsWagered,
|
||||||
|
}).from(schema.bots).where(eq(schema.bots.publicKey, pubkey)).limit(1)
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return c.json({ exists: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = rows[0]
|
||||||
|
const isHuman = bot.webhookUrl === 'http://human.local/'
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
exists: true,
|
||||||
|
bot: {
|
||||||
|
id: bot.id,
|
||||||
|
name: bot.name,
|
||||||
|
avatarSeed: bot.avatarSeed,
|
||||||
|
archetype: bot.archetype,
|
||||||
|
profilePicUrl: bot.profilePicUrl,
|
||||||
|
eloRating: bot.eloRating,
|
||||||
|
wins: bot.wins,
|
||||||
|
losses: bot.losses,
|
||||||
|
winStreak: bot.winStreak,
|
||||||
|
bestStreak: bot.bestStreak,
|
||||||
|
tier: bot.tier,
|
||||||
|
isActive: bot.isActive,
|
||||||
|
isHuman,
|
||||||
|
customization: bot.customization ? JSON.parse(bot.customization) : null,
|
||||||
|
satsWon: bot.satsWon ?? 0,
|
||||||
|
satsWagered: bot.satsWagered ?? 0,
|
||||||
|
hasWallet: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Login with Nostr pubkey (rate limited: 10 per minute per IP)
|
// Login with Nostr pubkey (rate limited: 10 per minute per IP)
|
||||||
authRouter.post('/login', rateLimit(60_000, 10), async (c) => {
|
authRouter.post('/login', rateLimit(60_000, 10), async (c) => {
|
||||||
const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({})))
|
const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||||
@@ -369,7 +433,7 @@ authRouter.post('/update', rateLimit(60_000, 10), async (c) => {
|
|||||||
|
|
||||||
// --- NIP-98 Authenticated Session ---
|
// --- NIP-98 Authenticated Session ---
|
||||||
import { verifyNip98Token } from '../middleware/nip98.js'
|
import { verifyNip98Token } from '../middleware/nip98.js'
|
||||||
import { createJwt, extractPubkeyFromAuth } from '../middleware/jwt.js'
|
import { createJwt } from '../middleware/jwt.js'
|
||||||
|
|
||||||
// POST /nostr/session — authenticate with NIP-98, receive JWT
|
// POST /nostr/session — authenticate with NIP-98, receive JWT
|
||||||
authRouter.post('/nostr/session', rateLimit(60_000, 10), async (c) => {
|
authRouter.post('/nostr/session', rateLimit(60_000, 10), async (c) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user