Rate-limiter now accepts string | string[] so callers can key on
multiple buckets simultaneously. If any bucket is exhausted the
request is denied, which lets login/TOTP/reset-password throttle on
BOTH user identifier and source IP without either becoming a bypass.
Fail-closed: empty/whitespace-only keys now deny by default instead
of silently allowing unbounded attempts (was CWE-307 gap).
Degraded-fallback divisor reduced from /10 to /2 — the old aggressive
clamp forced-logged-out legitimate users during brief Redis outages;
/2 still meaningfully slows distributed brute-force.
Callers updated:
- auth.ts (login): both email: and ip: buckets
- auth router requestPasswordReset: email + IP
- auth router resetPassword: IP before lookup, email-reset after
- invite router getInvite/acceptInvite: IP
- user-self-service verifyTotp: userId + IP
TRPCContext now carries clientIp; web tRPC route extracts it from
X-Forwarded-For / X-Real-IP.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Move CI_AUTH_SECRET from plaintext to ${{ secrets.CI_AUTH_SECRET }}
- Wrap password reset (update + session kill + token mark) in $transaction
to prevent stale sessions on partial failure (CWE-613)
- Rate limiter Redis fallback now uses stricter degraded limits
(maxRequests/10) and logs at error level instead of warn
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>