• /api/admin/* accepted only the T6 ADMIN_PASSWORD cookie until now —
    MVCP M.1/M.10's stated rule is users.role='admin' + a real session, and
    M.2's credit_ledger needs a real admin identity to attribute manual
    balance corrections to. isAdminAuthed() now accepts either path (the
    ADMIN_PASSWORD channel stays as a fallback so nobody's locked out
    before any role='admin' account exists by hand — dev-plan-4.md T30's
    explicit requirement). New POST /api/admin/credit/adjust wraps the
    new creditLedger.adjust() (db.js), which — unlike spend/refund — takes
    either sign of delta and records actorUserId (null over the legacy
    ADMIN_PASSWORD channel, which has no per-admin identity to attribute
    to).
    
    credit_ledger's actorUserId column needed an idempotent ALTER TABLE
    guard (no migration framework exists anywhere in this project) since
    the table's CREATE TABLE IF NOT EXISTS already shipped in T23 without
    it. Also switched db.js to WAL journal mode — surfaced by two processes
    legitimately touching the same data.sqlite at once in this session
    (the long-running server + a test run) hitting "database is locked" on
    the default rollback-journal mode; worth having before this bites in
    production too, not just to unblock local test runs.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • credit_ledger (dev-plan-4.md T23; schema M.2) joins users/sessions in
    the same SQLite db.js — architecture.md 1.4 named this as the other
    half of why SQLite got picked in T18 ("扣积分 + 创建生成任务 + 扣减片段
    配额" needs real atomicity, T18 only used the credential-storage half).
    spend()/refund()/topup() each read-modify-write users.creditBalance
    inside a transaction; spend() refuses to take a balance negative
    (INSUFFICIENT_BALANCE) instead of allowing debt, and topup() dedupes
    by paymentRef so a Stripe webhook's guaranteed at-least-once delivery
    can't double-credit (T24 will rely on this).
    
    pricing_config (M.12) stays a JSON file via a new pricing.js — it's
    operator-edited config, not on the atomic spend path, so SQLite would
    be over-engineering here; keyed by `key` rather than an id, hence its
    own tiny module instead of store.js's createStore. Seeded with
    placeholder credit amounts (dev-plan-4.md §1.2 item 3 — real numbers
    are still TBD).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • New db.js opens data.sqlite (node:sqlite's DatabaseSync — a Node 24
    built-in, experimental but functional) and creates the users/sessions
    tables per dressup-dance-room-mvcp.md M.1/M.10. Zero new npm
    dependency: no bcrypt/argon2 (node:crypto's scryptSync covers password
    hashing in T19), no better-sqlite3, no package.json needed.
    
    Mirrors store.js's ergonomics (create/get/update, plain JS objects in
    and out — booleans and the oauthProviders array marshalled to/from
    SQLite's TEXT/INTEGER columns internally) rather than reaching for an
    ORM. Exports the openDb/createUsersStore/createSessionsStore factories
    alongside a default singleton so tests can point at an isolated tmp
    file instead of the real data.sqlite (same pattern
    dressup-generate.test.js uses for library.json).
    
    data.sqlite is gitignored under the secrets section, not the "runtime
    state" one — unlike library.json it holds real credential material
    (password hashes, session tokens), not just regenerable content.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse File »