feat(09-02): retire the bare-pubkey session path (BOT-01)
CI / check (push) Failing after 6m11s

Client: useNostr.ts's auto-restore now calls GET /api/auth/me (a plain
authFetch, no body) instead of POSTing {pubkey} to /api/auth/login —
identity is derived server-side from the JWT alone, never claimed by
the client.

Server: POST /login is reduced to a pure, documented-deprecated read.
Removed the creator auto-create branch and the creator auto-upgrade
db.update block — an unauthenticated request can no longer mutate the
database via this endpoint. The identical creator auto-create/upgrade
logic already exists, correctly gated behind NIP-98 verification, in
POST /nostr/session, so a creator signing in with a real signer still
gets the same row created/upgraded. Added a handler doc comment plus a
new auth.test.ts case asserting an unregistered creator pubkey now
returns exists:false and leaves the bots table row count unchanged.

e2e/helpers/auth.ts: doc comments updated to describe loginWithPubkey
as a read-only test lookup helper, not a login; request/signature
unchanged so existing e2e specs keep working.

Verification: auth.test.ts + auth-edge.test.ts + auth-audit.test.ts +
auth-me.test.ts = 56/56 pass. Full server suite (bypassing pnpm's
install-gate via ./node_modules/.bin/vitest, since this environment's
pnpm needs an interactive build-approval step unrelated to this task)
= 810/817 pass, remaining 7 are pre-existing timing/perf flakes under
CPU load (lifecycle/speed-meta/tier-balance/bot-auth constant-time),
none touching auth. tsc (server) and vue-tsc (frontend) both exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-30 22:31:26 -04:00
co-authored by Claude Fable 5
parent 2dd9947516
commit 2a343ac746
4 changed files with 47 additions and 64 deletions
+11 -53
View File
@@ -89,7 +89,17 @@ authRouter.get('/me', async (c) => {
})
})
// Login with Nostr pubkey (rate limited: 10 per minute per IP)
// DEPRECATED — read-only lookup kept for backward compatibility only.
// This endpoint establishes NO session and issues NO token; it never trusts
// the pubkey it's given beyond looking up an existing row (D-01/BOT-01).
// It used to auto-create/auto-upgrade the creator's bot row on an
// unauthenticated request — that side effect has been removed. The
// identical creator auto-create/auto-upgrade logic runs, correctly gated
// behind NIP-98 signature verification, inside POST /nostr/session; a
// creator who signs in with a real signer still gets the same row
// created/upgraded there. Session establishment lives ONLY in
// POST /nostr/session; session restoration lives ONLY in GET /me.
// Rate limited: 10 per minute per IP.
authRouter.post('/login', rateLimit(60_000, 10), async (c) => {
const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
@@ -117,62 +127,10 @@ authRouter.post('/login', rateLimit(60_000, 10), async (c) => {
}).from(schema.bots).where(eq(schema.bots.publicKey, pubkey)).limit(1)
if (rows.length === 0) {
// Auto-create bot for the Creator if not registered
if (isCreatorPubkey(pubkey)) {
const id = nanoid(12)
const secret = randomBytes(32).toString('hex')
await db.insert(schema.bots).values({
id,
name: 'the_creator',
webhookUrl: 'http://poll.local/',
avatarSeed: 'the_creator',
archetype: 'the_creator',
secretHash: createHash('sha256').update(secret).digest('hex'),
publicKey: pubkey,
profilePicUrl: null,
customization: null,
createdAt: new Date().toISOString(),
})
return c.json({
exists: true,
bot: {
id,
name: 'the_creator',
avatarSeed: 'the_creator',
archetype: 'the_creator',
profilePicUrl: null,
eloRating: 1200,
wins: 0,
losses: 0,
winStreak: 0,
bestStreak: 0,
tier: 0,
isActive: true,
isHuman: false,
customization: null,
satsWon: 0,
satsWagered: 0,
hasWallet: false,
},
})
}
return c.json({ exists: false, pubkey })
}
const bot = rows[0]
// Auto-upgrade: if creator logs in, ensure archetype + bot mode are correct
if (isCreatorPubkey(pubkey)) {
const fixes: Record<string, string> = {}
if (bot.archetype !== "the_creator") fixes.archetype = "the_creator"
if (bot.webhookUrl === "http://human.local/") fixes.webhookUrl = "http://poll.local/"
if (Object.keys(fixes).length > 0) {
await db.update(schema.bots).set(fixes).where(eq(schema.bots.id, bot.id))
if (fixes.archetype) bot.archetype = "the_creator"
if (fixes.webhookUrl) bot.webhookUrl = "http://poll.local/"
}
}
const isHuman = bot.webhookUrl === 'http://human.local/'
return c.json({