• 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
    yibo authored
     
    Browse File »