Skip to content

Fix OAuth account enrichment with tooltip explanation - #148

Closed
Kzoeps wants to merge 16 commits into
mainfrom
143-oauth-enrichment-cover-all-pages-hide-handles-in-random-mode-explain-via-tooltip
Closed

Fix OAuth account enrichment with tooltip explanation#148
Kzoeps wants to merge 16 commits into
mainfrom
143-oauth-enrichment-cover-all-pages-hide-handles-in-random-mode-explain-via-tooltip

Conversation

@Kzoeps

@Kzoeps Kzoeps commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR restores account identity enrichment across the OAuth and account-management surfaces for ePDS deployments that use generated/random handles.
The user-facing goal is simple: when a handle is system-generated and not meaningful to the user, ePDS should identify the account primarily by email while still keeping the public AT Protocol handle available where it is useful for context, accessibility, or app interoperability.
closes #143

What changed

  • Restores scoped account identity enrichment on:

    • OAuth account chooser pages
    • OAuth consent / approval pages
    • /account
    • /account/:did
    • preview chooser and preview consent routes
  • Makes random-handle flows email-first:

    • chooser rows show email as the visible primary identifier
    • underlying handles are preserved through accessible descriptions
    • consent pages show email as the primary account identifier
    • public handles remain available through the identity information tooltip
  • Tightens the DOM enrichment logic so only known account identity elements are rewritten.
    This avoids accidentally mutating legal copy, technical details, connected-app rows, device rows, footer text, or arbitrary prose that happens to contain handle-like text.

  • Makes epds_handle_mode resolution consistent between auth-service and pds-core:

    • explicit authorize/callback query values take precedence
    • OAuth client metadata is used when the authorize URL only has a PAR request_uri
    • valid handle mode is preserved through /oauth/epds-callback
    • invalid callback values are ignored instead of forwarded
  • Shares OAuth request-context resolution between chooser enrichment and client CSS injection so PAR-backed authorize pages can still resolve the client id correctly.

  • Updates random-handle e2e coverage to assert the accessible hidden-handle behavior instead of the previous stale title tooltip behavior.

Summary by CodeRabbit

  • New Features
    • Account email is now displayed more consistently across sign-in, consent, account chooser, and account management screens.
    • Consent identity information is available through an accessible tooltip, including email and public handle details where relevant.
    • OAuth flows now preserve valid handle-selection modes across approval and account selection.
  • Bug Fixes
    • Prevented random handles from appearing as primary consent identifiers.
    • Improved recovery when authorization requests provide limited client information.
    • Sign-in actions remain disabled until the page is ready, reducing failed submissions.
  • Accessibility
    • Hidden handles are now exposed through accessible descriptions instead of title-only tooltips.

UI Changes

Screenshot_05-May_17-29-13_4449

when handle mode is picker or picker with random

Screenshot_05-May_17-31-05_26694

when handle mode is random

@vercel

vercel Bot commented May 4, 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 Aug 4, 2026 10:07pm

Request Review

@changeset-bot

changeset-bot Bot commented May 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7fe24bb

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

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change propagates epds_handle_mode through signed OAuth callbacks, resolves it from OAuth request context or metadata, and applies email-first account presentation with accessible handle descriptions and consent tooltips. It also adds coverage for callback integrity, enrichment behavior, consent flows, and login readiness.

Changes

Handle mode callback contracts

Layer / File(s) Summary
Callback signing and redirect contracts
packages/shared/src/crypto.ts, packages/pds-core/src/lib/epds-callback-authorize.ts, packages/pds-core/src/lib/oauth-request-context.ts, packages/shared/src/__tests__/crypto.test.ts, packages/pds-core/src/__tests__/epds-callback-authorize.test.ts
Signed callback payloads include epds_handle_mode; redirect construction validates the mode and resolves client IDs from client_id or request_uri.
Auth-service callback propagation
packages/auth-service/src/routes/complete.ts, packages/auth-service/src/routes/choose-handle.ts, packages/pds-core/src/index.ts, packages/auth-service/src/__tests__/callback-handle-mode.test.ts
Stored handle modes are passed through completion and handle-selection redirects into verified EPDS callback URLs, with identity resolution retained.
Chooser and consent enrichment
packages/pds-core/src/chooser-enrichment.ts, packages/pds-core/src/lib/preview-consent.ts, packages/pds-core/src/lib/client-css-injection.ts
Enrichment resolves handle mode from query parameters, pushed-request context, or metadata, then updates chooser, consent, account, and preview presentation.
Enrichment and consent validation
packages/pds-core/src/__tests__/chooser-enrichment.test.ts, e2e/step-definitions/consent.steps.ts, e2e/step-definitions/session-reuse-bugs.steps.ts, features/consent-screen.feature, features/session-reuse-bugs.feature
Tests cover email-primary identifiers, public-handle tooltips, accessible hidden-handle descriptions, route scoping, metadata fallback, and consent interactions.
Login readiness and abort handling
packages/auth-service/src/routes/login-page.ts, packages/auth-service/src/__tests__/login-page.test.ts, .changeset/sign-in-account-presentation.md
Email OTP submission is gated until JavaScript setup completes, with no-script guidance and updated resend/abort behavior documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthService
  participant EPDSCallback
  participant PDSCore
  participant EnrichmentScript
  participant ConsentPage
  AuthService->>EPDSCallback: send signed epds_handle_mode
  EPDSCallback->>PDSCore: verify callback and redirect to authorize
  PDSCore->>EnrichmentScript: inject resolved handle mode
  EnrichmentScript->>ConsentPage: replace identifier and attach tooltip
  ConsentPage-->>EnrichmentScript: expose email and accessible handle description
Loading

Possibly related issues

Possibly related PRs

  • hypercerts-org/ePDS#103 — Directly overlaps chooser enrichment, random handle visibility, tooltip behavior, and handle-mode metadata.
  • hypercerts-org/ePDS#154 — Directly relates to propagating epds_handle_mode through auth-service callback redirects.
  • hypercerts-org/ePDS#47 — Shares consent OAuth E2E coverage and identity display assertions.

Suggested reviewers: aspiers

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Login-page readiness, noscript, and resend/start-over changes are unrelated to the enrichment and tooltip objectives. Split the login-page behavior into a separate PR or issue to keep this changeset focused on OAuth enrichment and handle-mode plumbing.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: OAuth account enrichment with tooltip explanation.
Linked Issues check ✅ Passed The PR implements the requested OAuth/account enrichment scope, random-handle hiding, accessible tooltip, and handle-mode plumbing.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 143-oauth-enrichment-cover-all-pages-hide-handles-in-random-mode-explain-via-tooltip

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.

@railway-app

railway-app Bot commented May 4, 2026

Copy link
Copy Markdown

🚅 Deployed to the pr-5c336d-148 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web Aug 4, 2026 at 10:07 pm
@certified-app/demo ✅ Success (View Logs) Web Aug 4, 2026 at 10:07 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Aug 4, 2026 at 10:07 pm
@certified-app/pds-core ✅ Success (View Logs) Web Aug 4, 2026 at 10:07 pm

@blacksmith-sh

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/auth-service/src/routes/complete.ts (1)

164-169: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Sign epds_handle_mode as part of the callback contract.

epds_handle_mode is being appended after signCallback(), so the browser can alter it before /oauth/epds-callback reaches pds-core. Because the callback value is later used to drive the chooser/consent presentation, this makes the privacy/display mode user-tamperable. Please either include it in the signed payload end-to-end or stop trusting the callback query for this field.

As per coding guidelines, packages/{auth-service,pds-core}/**/*.{ts,tsx,js,mjs}: All epds-callback redirects must be HMAC-SHA256 signed using signCallback()/verifyCallback() from @certified-app/shared.

Also applies to: 208-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/auth-service/src/routes/complete.ts` around lines 164 - 169, The
fix: include epds_handle_mode in the signed callback instead of appending it
afterward — add epds_handle_mode: flow.handleMode into the callbackParams object
before calling signCallback(callbackParams, ctx.config.epdsCallbackSecret) so
the generated ts/sig cover that field, and keep using URLSearchParams(params)
afterwards; ensure any other occurrences (the similar block around the later
208-213) follow the same pattern so all epds-callback redirects are HMAC-SHA256
signed via signCallback()/verifyCallback().
packages/auth-service/src/routes/choose-handle.ts (1)

366-371: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid adding epds_handle_mode outside the signed callback payload.

This POST path has the same integrity gap as /auth/complete: epds_handle_mode is mutated onto the redirect URL after signCallback(). That leaves the mode browser-controlled on the hop to pds-core, so the final consent/chooser UI can be flipped without invalidating the callback signature.

As per coding guidelines, packages/{auth-service,pds-core}/**/*.{ts,tsx,js,mjs}: All epds-callback redirects must be HMAC-SHA256 signed using signCallback()/verifyCallback() from @certified-app/shared.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/auth-service/src/routes/choose-handle.ts` around lines 366 - 371,
The code currently appends epds_handle_mode to params after calling
signCallback, leaving it unsigned; instead, if flow.handleMode is present add it
into the callbackParams object before calling signCallback (e.g. set
callbackParams.epds_handle_mode = flow.handleMode), then call
signCallback(callbackParams, ctx.config.epdsCallbackSecret) and build
URLSearchParams from the resulting signed callback (remove the later
params.set('epds_handle_mode', ...) call); this ensures the handle mode is
included in the HMAC produced by signCallback/verifyCallback.
🧹 Nitpick comments (2)
.changeset/preserve-handle-mode-callback.md (1)

5-5: ⚡ Quick win

Rewrite the summary line in plainer end-user language.

app-requested handle display mode reads like implementation terminology. Since this first line is the headline release note for End users, I’d rephrase it in terms of what people actually see during sign-up/approval.

As per coding guidelines, .changeset/*.md: Changeset summary line is the first non-frontmatter line, read by every listed audience; if End users is an audience, write in plain language without OTP/DID/PAR/OAuth jargon or implementation concepts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/preserve-handle-mode-callback.md at line 5, Rewrite the first
non-frontmatter line (currently: "Sign-up screens now keep the app-requested
handle display mode through the final approval step.") into plain end-user
language that describes what the user sees during sign-up/approval without
implementation jargon; e.g. state that the app will remember and show the
preferred handle/display option throughout the sign-up and final approval
screens, using simple terms like "remember" or "keep showing" instead of
"app-requested handle display mode" or other technical phrases.
e2e/step-definitions/session-reuse-bugs.steps.ts (1)

439-506: ⚡ Quick win

Assert the exact hidden handle, not just a non-empty suffix.

This still passes if aria-describedby points at a generic placeholder or the wrong row's description. Since the callback already has handleLabel, capture its text and compare the description body to that exact hidden handle.

Suggested tightening
 type HiddenHandleDescriptionRow = {
   describedBy: string | null
   descriptions: {
     id: string
     isHiddenHandleDescription: boolean
     text: string
   }[]
   emailTitle: string | null
+  hiddenHandleText: string
   rowIndex: number
 }
 ...
             return {
               describedBy,
               descriptions,
               emailTitle: emailLabel.getAttribute('title'),
+              hiddenHandleText: handleLabel.textContent?.trim() ?? '',
               rowIndex,
             }
 ...
-      expect(
-        descriptionText.slice(prefixIndex + prefix.length).trim().length,
-      ).toBeGreaterThan(0)
+      expect(descriptionText.slice(prefixIndex + prefix.length).trim()).toBe(
+        row.hiddenHandleText,
+      )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/step-definitions/session-reuse-bugs.steps.ts` around lines 439 - 506, The
test currently only asserts that the hidden-handle description contains a
non-empty suffix; instead capture the actual handle text from the DOM (use
handleLabel.textContent.trim() inside the evaluateAll mapping and include it on
the returned HiddenHandleDescriptionRow), then in the outer assertions compare
description.text exactly (or ensure it contains) that captured handleText (not
just non-empty trimmed suffix). Update references in the mapping for handleLabel
and the returned object (e.g., add handleText) and replace the loose suffix
checks on descriptionText with a precise comparison against that handleText.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/step-definitions/consent.steps.ts`:
- Around line 162-172: The test reads aria-describedby into describedBy and
constructs a locator with page.locator(`#${describedBy?.trim()}`), but
aria-describedby can contain multiple space-separated IDs; split describedBy on
whitespace (or use describedBy?.split(/\s+/)[0]) and guard for null/empty before
building the selector so you only target the first ID; update the code around
describedBy, tooltipControl and the tooltip locator to use the first token (and
bail or assert if none) when calling page.locator and subsequent expects.

In `@packages/pds-core/src/chooser-enrichment.ts`:
- Around line 895-910: The catch block that silences failures when resolving
request_uri/metadata should log the failure at debug level: thread the pds-core
logger into this middleware (or import the existing logger) and in the catch for
the resolveOAuthClientIdFromQuery/resolveMeta sequence emit logger.debug with a
concise message that includes the failing inputs (e.g. the query/request_uri and
resolved clientId if available) and the caught error/stack; keep behavior
unchanged otherwise so metaMode still falls back to query/env. Ensure the log
uses logger.debug(...) and references resolveOAuthClientIdFromQuery,
resolveClientIdFromRequestUri and resolveMeta to locate the code to modify.

---

Outside diff comments:
In `@packages/auth-service/src/routes/choose-handle.ts`:
- Around line 366-371: The code currently appends epds_handle_mode to params
after calling signCallback, leaving it unsigned; instead, if flow.handleMode is
present add it into the callbackParams object before calling signCallback (e.g.
set callbackParams.epds_handle_mode = flow.handleMode), then call
signCallback(callbackParams, ctx.config.epdsCallbackSecret) and build
URLSearchParams from the resulting signed callback (remove the later
params.set('epds_handle_mode', ...) call); this ensures the handle mode is
included in the HMAC produced by signCallback/verifyCallback.

In `@packages/auth-service/src/routes/complete.ts`:
- Around line 164-169: The fix: include epds_handle_mode in the signed callback
instead of appending it afterward — add epds_handle_mode: flow.handleMode into
the callbackParams object before calling signCallback(callbackParams,
ctx.config.epdsCallbackSecret) so the generated ts/sig cover that field, and
keep using URLSearchParams(params) afterwards; ensure any other occurrences (the
similar block around the later 208-213) follow the same pattern so all
epds-callback redirects are HMAC-SHA256 signed via
signCallback()/verifyCallback().

---

Nitpick comments:
In @.changeset/preserve-handle-mode-callback.md:
- Line 5: Rewrite the first non-frontmatter line (currently: "Sign-up screens
now keep the app-requested handle display mode through the final approval
step.") into plain end-user language that describes what the user sees during
sign-up/approval without implementation jargon; e.g. state that the app will
remember and show the preferred handle/display option throughout the sign-up and
final approval screens, using simple terms like "remember" or "keep showing"
instead of "app-requested handle display mode" or other technical phrases.

In `@e2e/step-definitions/session-reuse-bugs.steps.ts`:
- Around line 439-506: The test currently only asserts that the hidden-handle
description contains a non-empty suffix; instead capture the actual handle text
from the DOM (use handleLabel.textContent.trim() inside the evaluateAll mapping
and include it on the returned HiddenHandleDescriptionRow), then in the outer
assertions compare description.text exactly (or ensure it contains) that
captured handleText (not just non-empty trimmed suffix). Update references in
the mapping for handleLabel and the returned object (e.g., add handleText) and
replace the loose suffix checks on descriptionText with a precise comparison
against that handleText.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cebbd6d-6d03-4125-9514-8c20d849467e

📥 Commits

Reviewing files that changed from the base of the PR and between 2acbc43 and 8d66b71.

📒 Files selected for processing (21)
  • .beads/issues.jsonl
  • .changeset/preserve-handle-mode-callback.md
  • .changeset/random-handle-consent-metadata.md
  • .changeset/scoped-account-enrichment.md
  • e2e/step-definitions/consent.steps.ts
  • e2e/step-definitions/session-reuse-bugs.steps.ts
  • features/consent-screen.feature
  • features/session-reuse-bugs.feature
  • packages/auth-service/src/__tests__/callback-handle-mode.test.ts
  • packages/auth-service/src/routes/choose-handle.ts
  • packages/auth-service/src/routes/complete.ts
  • packages/pds-core/src/__tests__/chooser-enrichment.test.ts
  • packages/pds-core/src/__tests__/epds-callback-authorize.test.ts
  • packages/pds-core/src/__tests__/preview-chooser.test.ts
  • packages/pds-core/src/__tests__/preview-consent.test.ts
  • packages/pds-core/src/chooser-enrichment.ts
  • packages/pds-core/src/index.ts
  • packages/pds-core/src/lib/client-css-injection.ts
  • packages/pds-core/src/lib/epds-callback-authorize.ts
  • packages/pds-core/src/lib/oauth-request-context.ts
  • packages/pds-core/src/lib/preview-consent.ts

Comment thread e2e/step-definitions/consent.steps.ts Outdated
Comment thread packages/pds-core/src/chooser-enrichment.ts
@Kzoeps

Kzoeps commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

(reply generated by gpt-5.5)

Review follow-up status:

  • Fixed in 49c64e1: aria-describedby token handling, chooser-enrichment debug logging, Sonar mechanical code-smell items, test HTTP hotspot annotation, complete-route complexity refactor, session-reuse hidden-handle assertion tightening, and chooser-enrichment test duplication refactor.
  • Addressed earlier in 60aa045: consolidated the generated-handle changeset wording.
  • Leaving the epds_handle_mode HMAC suggestions as-is by maintainer decision: this field is UI/display-only and is not used as an authorization/security decision.
  • Blacksmith E2E failures are intentionally left for separate follow-up per maintainer.

@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-148 May 5, 2026 06:44 Destroyed
@Kzoeps

Kzoeps commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

(reply generated by gpt-5.5)

Follow-up: added 02b4c01 to further reduce the remaining chooser-enrichment test duplication reported by Sonar after the first fix. Local validation for that follow-up passed: pnpm format:check, pnpm lint, pnpm typecheck, and pnpm vitest run packages/pds-core/src/__tests__/chooser-enrichment.test.ts.

@Kzoeps
Kzoeps marked this pull request as ready for review May 5, 2026 06:45

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@blacksmith-sh

This comment has been minimized.

@aspiers

aspiers commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Note on the e2e failure seen on this branch (resolved — flaky, not a regression)

The first E2E run on 1104efa failed one scenario of 72, Expired OTP is rejected, resend recovers the flow:

Locator: locator('#error-msg')
Expected substring: "OTP expired"
Received string:    "Invalid OTP"

Re-running the same job on the identical commit passed, so this is flake rather than a regression from the rebase. All checks on 1104efa are now green. Recording the diagnosis since the symptom is misleading and will recur.

Why it looks like a product bug but isn't: better-auth throws Invalid OTP both for a wrong code and for no pending code at all — noted already in packages/auth-service/src/better-auth.ts above OTP_FAILURE_MESSAGES. An absent verification row and a mistyped code are therefore indistinguishable in #error-msg.

Suspected root cause, unfixed and pre-existing: POST /_internal/test/expire-otp in packages/auth-service/src/routes/test-hooks.ts runs

UPDATE verification SET expiresAt = ? WHERE identifier = ?

with no liveness guard, whereas the sibling expire-auth-flow hook a few lines above does filter AND expires_at > ?. The step asserts only updated >= 1, which a stale row can satisfy while the live verification path finds no pending OTP — yielding Invalid OTP instead of OTP expired.

I have deliberately not fixed this here: it is unrelated to this PR's scope and would widen the diff. Worth a separate issue if the scenario keeps failing.

Tracked separately as #214 (with the better-auth INVALID_OTP vs OTP_EXPIRED branch logic and a suggested one-line hook fix). Not addressed in this PR.

@aspiers

aspiers commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

(reply generated by Claude Opus 5 via Claude Code)

Re: the four nitpicks in CodeRabbit's latest review summary

These were in the review body rather than as inline threads, so replying here. Three addressed, one declined.

1. Escape-to-dismiss for the identity tooltip (chooser-enrichment.ts) — fixed in 7f76cd6. Valid and worth more than "trivial": the tooltip had no key handler at all, and hide() returns early while pinned, so a keyboard/AT user who clicked to pin it genuinely could not dismiss it without moving focus back to the button. That is the WCAG 1.4.13 Dismissible failure as described. Escape now clears pinned then hides, bound on keyup so it does not race the surrounding page for the same key, and it no-ops when the tooltip is already hidden rather than swallowing the event. Covered by a new test that asserts the pinned path, the unpinned path, a non-Escape key, and non-propagation when already hidden — verified it fails against the pre-fix code.

2. Hoist the duplicated handle-mode resolver (preview-consent.ts) — fixed in ea43ebc. This PR added a byte-identical second copy of resolveQueryHandleMode next to the existing one in preview-chooser.ts. Since both previews exist specifically to exercise the production resolver path, two copies is a drift risk rather than harmless repetition. Moved into preview-shared.ts, which already owns the helpers both routes share, and dropped the now-dead resolveHandleMode / VALID_HANDLE_MODES / ClientMetadata imports from both call sites.

3. Document the fake createTreeWalker divergence (chooser-enrichment.test.ts) — done in 7f76cd6. Agreed with your own assessment that it is benign today: nothing the script inserts can match isConsentIdentityElement() (which needs a <b>/<strong> inside an approved consent phrasing) or matchAccountIdentifier(). Added a comment recording that the snapshot-vs-live difference is deliberate and that the fake should be made live if either predicate loosens, so the limitation is not silently trusted later.

4. Use a CSS class for runtime visibility (login-page.ts:879-917) — declined, out of scope. refreshResendVisibility() is pre-existing code from main, not part of this PR: git diff origin/main HEAD -- packages/auth-service/src/routes/login-page.ts adds zero lines touching style.display. Inline style.display is also the established idiom throughout that file (7 call sites, including errorEl and termsEl), so converting only the three lines you flagged would leave the file half-migrated and inconsistent. Worth doing as its own change across the whole file if the guideline is to be enforced, but folding it into an OAuth-enrichment PR would mix unrelated churn into the diff.

All green on 7f76cd6 locally: build, lint, format, and 1132 unit tests (Node 20, matching CI).

@blacksmith-sh

This comment has been minimized.

Kzoeps and others added 16 commits August 4, 2026 23:03
Three suites drifted against changes that landed on main while this
branch was open:

- callback-handle-mode: /auth/complete now signs client_id as well, so
  the verification helper must include it or the signature never matches.
- login-page: the readiness-gate test banned setTimeout/setInterval
  across the whole page, which now trips on the unrelated PAR heartbeat
  interval. Scope it to the gap between the last handler registration
  and the enable, which is the actual invariant.
- chooser-enrichment: main renders the chooser email label with a
  leading space for visual separation (e873e7a); trim it in the
  assertions since that space is presentation, not identity.
Matches the sibling flowRequestUri / flowClientId args, and the name the
branch had already converged on before the rebase collapsed it against
main's buildEpdsCallbackUrl extraction. Optional so main's existing
callers in build-epds-callback-url.test.ts stay valid.
The metadata/request-uri fallback path logs via `logger?.debug`, and a
unit test covers it when a logger is injected — but the production call
site in index.ts never passed one, so the optional chain made it a no-op
everywhere it mattered. That left handle-mode mismatches undebuggable in
production, which is the problem the logging was added to solve.
Address CodeRabbit review findings on #148.

The description id was derived from the per-tick match index. enrich()
rebuilds that list on every MutationObserver tick and skips rows already
marked epdsEnriched, so a row enriched on a later re-render restarts at
index 0 and collides with an earlier row's id. Duplicate ids make
aria-describedby resolve to the first matching node, so a row could
announce a different account's handle. Use a monotonic counter that
survives across ticks, with a regression test that replays a tick and
fails on the old code.

Also:
- assert the login-page readiness gate against the submit binding rather
  than the last click handler, which was a proxy that would still pass if
  the submit binding moved below the enable
- take .first() in the e2e identity-tooltip helper: enrichment adds an
  icon per matching identity node, so a page with two approved phrasings
  tripped Playwright strict mode
- add direct unit coverage for resolveOAuthClientIdFromQuery, previously
  only exercised through its callers
Address a CodeRabbit nitpick on #148. This PR added a second byte-identical
copy of resolveQueryHandleMode to preview-consent.ts alongside the existing
one in preview-chooser.ts. Both previews must interpret an unknown metadata
value the same way as the production chooserEnrichment middleware, so the
duplication is a drift risk rather than harmless repetition.

preview-shared.ts already owns the helpers both preview routes share, so
move it there and drop the now-unused resolveHandleMode / VALID_HANDLE_MODES
/ ClientMetadata imports from both call sites.
Address CodeRabbit nitpicks on #148.

WCAG 1.4.13 (Content on Hover or Focus) requires hover/focus content to be
dismissible without moving the pointer or focus. The tooltip had no key
handler at all, and once pinned by click hide() returns early, so a
keyboard or AT user who pinned it could not close it without moving focus
back to the button. Escape now clears pinned and hides, on keyup so we do
not race the surrounding page for the same key.

Also document the deliberate divergence in the test's fake createTreeWalker:
it snapshots descendants where a real walker is live, so enrichment-inserted
nodes are not revisited. Benign today because nothing the script inserts can
match the identity predicates, but worth flagging before those loosen.
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

@aspiers

aspiers commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@Kzoeps I've split this into four stacked PRs to make it reviewable.

The motivation: this PR is +2918/−173, but ~1450 of that is a single test file, so the real change is ~700 lines covering four independent concerns. In particular the HMAC payload change is currently reviewed alongside 1400 lines of DOM tests, and it deserves its own scrutiny.

PR Change Base
#241 Extract OAuth client-id resolution from the CSS middleware main
#242 Gate email sign-in until its handlers are installed main
#243 Sign epds_handle_mode through the callback hop #241
#244 Identify generated-handle accounts by email #243

#242 is entirely independent and could merge today. #241 is a near-trivial refactor. #243 carries the HMAC change. #244 is the actual user-visible enrichment work.

Nothing was changed in the process. Merging all four branches produces a tree byte-identical to this PR's head (7fe24bb) — verified by diffing the union against it. The only difference is the single sign-in-account-presentation.md changeset, which I split into three so each PR carries the part describing its own change (#241 needs none — it's an internal refactor).

Each branch independently passes typecheck, lint, format and the full vitest suite.

Two things I flagged in the split that are worth your eyes:

Your two review threads here are both marked outdated; the login-page.ts one ("these seem to trigger unconditionally on page load") looks answered by 69a1456, which is the disabled-button gating now in #242.

🤖 Generated with Claude Code

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.

OAuth enrichment: cover all pages, hide handles in random mode, explain via tooltip

2 participants