Reviewed draft rev 1 at `49eaacf0`. These findings concern the proposed design and its integration with the existing code. No files were edited; no tests were run.

1. **P1 — The link POST permits login CSRF.**

   **Spec:** §5.1.

   **Evidence:** [server.go:2124](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:2124) sets `SameSite=Lax`; [server.go:1683](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:1683) dispatches without a general origin check.

   **Failure:** An attacker requests a link for their own address, then auto-submits its token from an attacker-controlled page into the victim’s browser. The proposed POST signs that browser into the attacker’s account without displaying the confirmation page. The attacker knows the token legitimately. Withholding existing cookies does not prevent the response from installing a new session.

   **Fix:** Require browser-bound confirmation state minted by the GET, checked on POST alongside the link token. Reject cross-origin submissions. Apply this protection to the legacy confirmation POST too.

2. **P1 — Suppressing sends alone leaves unlimited six-digit guessing budgets.**

   **Spec:** §§4, 5, 7.

   **Evidence:** The existing challenge-creation precedent, [store.go:2456](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/store.go:2456), creates a redeemable token independently of delivery. [authcli.go:1270](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/authcli.go:1270) explicitly addresses the analogous problem: a fresh caller-controlled key must not provide a fresh guessing budget.

   **Failure:** The spec records challenges but says rate limiting skips only the send. An attacker can repeatedly create challenges for a victim using fresh browser cookies and make five guesses against each, including challenges whose emails were suppressed. A correct guess still authenticates; mailbox access is unnecessary. Five attempts per challenge therefore does not bound attempts per victim.

   **Fix:** Rate-limit creation of *redeemable* challenges, preserving an indistinguishable response for refused requests. Add verification limits across challenges, keyed by address and client IP. Test repeated challenge replacement after delivery limits are exhausted.

3. **P1 — The stored pending-action hash cannot be passed to the existing redemption helper.**

   **Spec:** §5’s `pending` column and §6’s success path.

   **Evidence:** [stepup.go:181](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/stepup.go:181) returns a raw token while storing its hash. [stepup.go:214](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/stepup.go:214) hashes the token supplied to `TakePendingAction` before lookup.

   **Failure:** The challenge retains only the pending token’s hash, but §6 calls the existing `TakePendingAction`. Passing the stored value hashes it twice and finds no action. No other place retaining the raw token is specified. Email verification can succeed while every pending action fails to resume.

   **Fix:** Specify an internal redemption helper accepting the stored hash, retaining the transaction, session binding, expiry check and single-use deletion. Alternatively, explicitly preserve and bind the raw token in browser state.

4. **P2 — The delivery rules still allow SMTP sends to Keymail addresses.**

   **Spec:** §3’s no-SMTP anti-goal, §4, and §11’s CLI notification change.

   **Evidence:** [keymailfed.go:77](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/keymailfed.go:77) wraps **every** lookup failure as `ErrNoKeymailMailbox`, including transport failures and server errors. [keymailfed_test.go:84](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/keymailfed_test.go:84) also establishes that a nonexistent local part under `keymail.dev` produces that sentinel. [authcli.go:1319](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/authcli.go:1319) checks grant presence, not mailbox type.

   **Failure:** Following §4 during a Keymail lookup outage sends a Keymail-addressed authentication message through SES. Separately, someone signing in through federation has no stored grant, so §11 sends their CLI security notification directly through SES even though their mailbox is Keymail.

   **Fix:** Distinguish definitive mailbox/domain results from transient lookup failures, and explicitly prevent SMTP fallback for Keymail-hosted addresses. Route CLI notifications through the same delivery policy rather than treating “no grant” as “ordinary email.”

5. **P2 — The shared 60-second limit suppresses the first CLI step-up email.**

   **Spec:** §§5.3, 6, 7, 12.

   **Evidence:** [store.go:2404](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/store.go:2404) creates a session without recording fresh authentication. [authcli.go:1468](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/authcli.go:1468) requires step-up before CLI approval.

   **Failure:** A person receives a sign-in email, signs in promptly, and approves their terminal within 60 seconds. Step-up requires another email, but the shared address cooldown silently suppresses it. They see “check your email” for a message that will never arrive.

   **Fix:** Reserve capacity for an authenticated step-up immediately after sign-in while retaining aggregate address limits—for example, separate purpose cooldowns under a shared hourly/daily budget. Test the complete CLI flow without advancing the clock between sign-in and approval.

6. **P2 — Email challenges outlive the actions they promise to resume.**

   **Spec:** §§5–6.

   **Evidence:** [stepup.go:75](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/stepup.go:75) defines a ten-minute pending-action lifetime; [stepup.go:191](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/stepup.go:191) applies it when stashing. [stepup.go:232](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/stepup.go:232) refuses expired actions.

   **Failure:** A person uses their valid 15-minute step-up link after eleven minutes. Authentication succeeds, but the pending action has expired and cannot resume.

   **Fix:** Align email step-up challenge and pending-action expiry. If pending actions remain ten minutes, give step-up challenges that same deadline and communicate it consistently.

7. **P2 — Reusing `/auth/continue` needs stricter recipient validation.**

   **Spec:** §§7 and 11.

   **Evidence:** [server.go:2733](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:2733) normalizes the submitted text but checks only `ValidAddress`. [mint.go:85](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/domains/mint.go:85) merely requires an `@` and no whitespace. The invite handler already explains and closes this gap using `net/mail.ParseAddress` plus exact address equality at [server.go:2462](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:2462).

   **Failure:** Inputs such as `a<victim@example.com>` pass the existing sign-in validator. Different display-name forms can identify the same email recipient while receiving different address-rate-limit keys and console identities.

   **Fix:** Require one normalized bare address before challenge creation, identity lookup or rate limiting, using the invite handler’s stricter validation. Use that same canonical value throughout delivery and redemption.

8. **P2 — `next` bypasses the personal-account creation promised by R2.**

   **Spec:** §§2/R2, 5.3, 8.

   **Evidence:** [store.go:2147](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/store.go:2147) creates only the user row. Account creation happens through `redirectToCurrentAccount` at [server.go:2179](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:2179), which reaches the mint at [server.go:2209](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/console/server.go:2209).

   **Failure:** A new user signs up while returning to `/auth/cli`. The proposed `finishSignin` follows `next` instead of calling `redirectToCurrentAccount`, so no personal account is created. The same branch changes onboarding for invite-driven sign-ups.

   **Fix:** Separate required onboarding from redirect selection. Ensure the R2 account-creation behavior completes before following `next`, and test new-user CLI and invite round trips.

9. **P2 — The rollout omits ownership proof for an existing SES identity.**

   **Spec:** §§9 and 13.

   **Evidence:** [walk.go:249](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/mail/walk.go:249) refuses an existing identity that CARLOS does not own unless DNS proof succeeds. [domainclaim.go:363](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/email-signin/internal/mail/domainclaim.go:363) defines the required proof record and value.

   **Failure:** If `carloku.com` already has an SES identity—or an operator creates it while preparing the rollout—verified DKIM and the absence of a tenant claim are insufficient. `EnsureDomain` refuses adoption, leaving ordinary email sign-in unavailable despite the send grant.

   **Fix:** Add an explicit existing-identity check. Where adoption is required, publish TXT `_carlos-mail.carloku.com` with value `carlos-mail-account=bab`, verify that the mailer adopts the identity successfully, then run the live sign-in checks.