Skip to content

Unbundle #165: hide Resend when sign-in can't recover + demo cookie outlasts OTP wait#187

Draft
aspiers wants to merge 2 commits into
mainfrom
unbundle/otp-resend-par-liveness
Draft

Unbundle #165: hide Resend when sign-in can't recover + demo cookie outlasts OTP wait#187
aspiers wants to merge 2 commits into
mainfrom
unbundle/otp-resend-par-liveness

Conversation

@aspiers

@aspiers aspiers commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Unbundles the two Tier 1 items from #165 into a focused, reviewable branch: the OTP sign-in fixes that defend against a user acting on a code (or a browser session) that has silently died during a long wait. These are the changes that motivated #165 in the first place; the other 18 changesets in that PR are separable follow-ups.

Two commits, cleanly separated:

  1. fix(auth-service): hide Resend on OTP screen when sign-in cannot recover — once the upstream authorization request has lapsed, the OTP screen swaps Resend code for Start over, so the only action offered is one that can actually complete the flow. Reconciles on heartbeat ticks and visibilitychange. (Cherry-picked from fix(auth-service): hide Resend on OTP screen when sign-in cannot recover #165 ccf4ece.)

  2. fix(demo): cookie outlasts OTP form sit times + honest error when it doesn't — the demo's OAuth-state cookie expired at maxAge: 600 (10 min), exactly the OTP lifetime, so an email arriving near expiry could outlive the cookie and break the token exchange. Raised to 60 * 60 (1 h) to match the auth service's auth_flow TTL, and the callback now surfaces session_expired (honest "took too long") rather than a generic auth_failed. (Cherry-picked from fix(auth-service): hide Resend on OTP screen when sign-in cannot recover #165 a5cb519, which shipped without a changeset — one has been added here.)

Order matters: commit 1 must land before commit 2, since both touch the shared e2e/feature files. Cherry-picking in this order auto-merges cleanly.

Why draft

Test plan

  • Cherry-picked both commits onto current origin/main in a fresh worktree; no conflicts in the correct order.
  • pnpm format:check, pnpm lint, pnpm typecheck all green.
  • pnpm test — 1025 unit tests pass (70 files).
  • CI on this branch.
  • E2E (@resend-hidden-when-par-dead, @demo-cookie-expiry) in CI.

Relationship to #165

Carves out 2 of the 20+ changes in #165. The remaining items (OTP-UX adjacencies, accessibility polish, error-copy rewrites, handle-picker fixes, Account Settings flash banners) are independent and will follow as separate PRs.

Previously the OTP screen always offered "Resend code", even when the
upstream PAR row had silently lapsed (suspended tab, mobile background,
heartbeat throttling). The user could click Resend, receive a fresh
email, type the new code, and only then see "Sign in failed" — wasting
their time on a code that could not have worked.

The screen now never surfaces actions that cannot complete the flow:

- Track lastSuccessfulHeartbeatAt; treat the PAR as dead once we cross
  upstream's 5 min AUTHORIZATION_INACTIVITY_TIMEOUT without a fresh ok
  ping (the upstream death point is exact — no margin needed).
- Hide #btn-resend and surface a #btn-start-over (→ /auth/abort) the
  moment parLikelyDead() flips. Reconciled on every heartbeat tick
  (including transient ticks, so a stale-by-time case still hides the
  button) and on the visibilitychange event (so a backgrounded tab
  returning to focus reflects reality immediately).
- Inline "Send a new code" action on the OTP-expired error now branches:
  parLikelyDead() → "Start over"; otherwise existing "Send a new code".

This is the proactive UI complement to the existing reactive abort gate.
Server-side enforcement of the same invariant on /email-otp/send-
verification-otp and /sign-in/email-otp is a separate follow-up.

Test: new @resend-hidden-when-par-dead scenario; full
@otp-and-par-expiry / @par-heartbeat / @resend-after-par-dead /
@otp-expiry suite still passes (7 scenarios, 78 steps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c4240cd

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Jul 6, 2026 5:05pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56d8e989-febc-4c02-bcca-2a160ed6975b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch unbundle/otp-resend-par-liveness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls-official

coveralls-official Bot commented Jul 6, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28809119087

Coverage increased (+0.01%) to 56.084%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 3 uncovered changes across 1 file (2 of 5 lines covered, 40.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
packages/demo/src/app/api/oauth/callback/route.ts 3 0 0.0%
Total (2 files) 5 2 40.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 2955
Covered Lines: 1650
Line Coverage: 55.84%
Relevant Branches: 1820
Covered Branches: 1028
Branch Coverage: 56.48%
Branches in Coverage %: Yes
Coverage Strength: 5.87 hits per line

💛 - Coveralls

@railway-app

railway-app Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-187 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/demo ✅ Success (View Logs) Web Jul 6, 2026 at 5:06 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Jul 6, 2026 at 5:06 pm
@certified-app/pds-core ✅ Success (View Logs) Web Jul 6, 2026 at 3:05 pm
@certified-app/auth-service ✅ Success (View Logs) Web Jul 6, 2026 at 3:05 pm

…doesn't

Two changes that close the bug-report hole on the demo client:

1. Bump oauth_state cookie maxAge from 600s to 3600s, matching
   auth-service's auth_flow row TTL. The demo's cookie carries the
   state, code verifier, token endpoint and issuer for the OAuth
   callback to complete; if it expires before the user submits the
   OTP, the callback can't find any of that and bounces silently.
   600s was shorter than realistic OTP-form sit times (the bug
   report was an 11-minute wait). Aligning with auth_flow's 60-min
   budget means as long as the auth-service can still recover the
   flow, the demo can too.

2. Distinguish "cookie missing" from "auth failed" on the callback.
   Previously every silent-fail path bounced to /?error=auth_failed
   ("Authentication failed. Please try again."), which is misleading
   when the sign-in itself succeeded — the user typed a fresh OTP
   correctly, the auth-service issued the OAuth code, the demo just
   couldn't finish because its own session cookie had aged out. Now
   the cookie-missing branch redirects to /?error=session_expired
   ("Your sign-in took too long to finish. Please sign in again.")
   so the user understands what happened without thinking they
   typed the code wrong.

Test: new @demo-cookie-expiry @bug-report scenario clears the cookie
mid-flow and asserts the session-expired error surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant