-- v0.25.0 / security audit 0026, finding H1. -- -- The OTC verify path had no attempt-limit or lockout, unlike the -- passcode path (015_passcode.sql gave users.passcode_failed_attempts + -- passcode_locked_until). This table gives the OTC verify endpoint the -- same per-identity lockout shape. It is keyed by email rather than -- user_id because an OTC sign-in may not have a users row yet — the row -- is provisioned only on a *successful* verify, so the lockout state has -- to survive independently of it. -- -- The per-IP rate limiter (app/ratelimit.py) is the primary brute-force -- defense; this table is the parity layer that mirrors the passcode -- lockout and persists across restarts. CREATE TABLE IF NOT EXISTS otc_verify_state ( email TEXT PRIMARY KEY, failed_attempts INTEGER NOT NULL DEFAULT 0, locked_until TEXT );