-
createCheckoutSession() is the seam (dev-plan-4.md §1.2 item 4): with no STRIPE_SECRET_KEY set — no real test-mode key exists yet, the user said "先做一个 mock,我明天再给你真实测试的 key" — POST /api/credits/checkout returns a same-origin /api/credits/mock-checkout URL that completes the topup itself, standing in for "redirected to Stripe, paid, redirected back" so the whole checkout -> credited loop (and T29's UI on top of it) can be built and tested today. With a key set, it creates a real Stripe Checkout Session via plain REST (fetch, no stripe SDK — this project has no dependencies at all) — untested against a live account until the key lands. POST /api/credits/webhook hand-rolls Stripe's documented HMAC-SHA256 signature scheme and refuses anything unsigned or unverifiable, including every request while STRIPE_WEBHOOK_SECRET is unset (not just badly-signed ones). Both the mock and real paths route through one completeTopup() -> creditLedger.topup() (T23) so Stripe's guaranteed at-least-once webhook delivery can't double-credit — verified in credits.test.js by redelivering the same event id twice. Also added creditBalance to GET /api/auth/me's response (T29's need) instead of a dedicated balance endpoint — it's the same "logged-in user's own state" this route already serves, and live.html/dance.html already poll it after login. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo