-- 00004_recovery_and_sessions.sql -- -- Auth-A v3 rewrite: replace synthetic-identity-based recovery with a custom -- recovery_codes table (Argon2id hashed + HMAC-SHA256 prefix index for O(1) -- lookup) and a user_sessions table for server-side session tracking -- (revocation + 30d absolute cap). -- -- See: research/PLAN-AUTH-A.md (v3) and research/COOKIE-SHAPE-DECISION.md. ALTER TABLE public.users DROP COLUMN IF EXISTS recovery_code; CREATE TABLE public.recovery_codes ( user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, argon2_hash text NOT NULL, prefix_hmac bytea NOT NULL, -- HMAC-SHA256(pepper, code) truncated to first 8 bytes created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_recovery_codes_prefix ON public.recovery_codes(prefix_hmac); ALTER TABLE public.recovery_codes ENABLE ROW LEVEL SECURITY; -- No policies; service role only. COMMENT ON TABLE public.recovery_codes IS 'Argon2id-hashed recovery codes with HMAC prefix index. Service role only. Single-use; deleted on claim atomically via DELETE ... RETURNING.'; CREATE TABLE public.user_sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, iat_original timestamptz NOT NULL DEFAULT now(), last_seen_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz ); CREATE INDEX idx_user_sessions_user_active ON public.user_sessions(user_id) WHERE revoked_at IS NULL; ALTER TABLE public.user_sessions ENABLE ROW LEVEL SECURITY; -- No policies; service role only. COMMENT ON TABLE public.user_sessions IS 'Server-side session records. JWT carries session_id; touch endpoint validates revoked_at IS NULL and now() - iat_original < 30d.';