Skip to content

P2: fix(auth): filter unsupported OTP characters - #223

Merged
aspiers merged 3 commits into
mainfrom
split-pr165/server-otp-charset-filter
Aug 4, 2026
Merged

P2: fix(auth): filter unsupported OTP characters#223
aspiers merged 3 commits into
mainfrom
split-pr165/server-otp-charset-filter

Conversation

@aspiers

@aspiers aspiers commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Normalize full-length OTP inputs in account settings and account recovery as users type. Numeric codes reject non-digits, while alphanumeric codes uppercase and retain only supported characters.

Changes

  • Filter account-settings OTP input according to configured charset
  • Apply the same filtering to account recovery
  • Document the behavior with a changeset

Testing

  • pnpm format:check
  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm test:coverage

Screenshots

Before: a numeric recovery-code field accepted an unsupported letter.

Before: unsupported OTP character accepted

After: attempting the same input leaves the field empty and focused.

After: unsupported OTP character filtered

Notes

@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b308698

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 30, 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 6:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@aspiers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 91e06828-771e-4d28-b036-c43f94750b09

📥 Commits

Reviewing files that changed from the base of the PR and between dcefb95 and b308698.

📒 Files selected for processing (10)
  • .changeset/charset-filter-on-otp-forms.md
  • e2e/step-definitions/otp-character-filtering.steps.ts
  • features/otp-character-filtering.feature
  • packages/auth-service/src/__tests__/login-page.test.ts
  • packages/auth-service/src/__tests__/otp-input.test.ts
  • packages/auth-service/src/otp-input.ts
  • packages/auth-service/src/routes/account-login.ts
  • packages/auth-service/src/routes/login-page.ts
  • packages/auth-service/src/routes/preview.ts
  • packages/auth-service/src/routes/recovery.ts

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 Jul 30, 2026

Copy link
Copy Markdown

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

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

@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 13:38 Destroyed
@coveralls-official

coveralls-official Bot commented Jul 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30940616604

Coverage increased (+0.06%) to 57.909%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 3 uncovered changes across 2 files (4 of 7 lines covered, 57.14%).
  • 2 coverage regressions across 1 file.

Uncovered Changes

File Changed Covered %
packages/auth-service/src/routes/preview.ts 2 0 0.0%
packages/auth-service/src/routes/account-login.ts 1 0 0.0%
Total (5 files) 7 4 57.14%

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/auth-service/src/routes/preview.ts 2 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 3106
Covered Lines: 1789
Line Coverage: 57.6%
Relevant Branches: 1945
Covered Branches: 1136
Branch Coverage: 58.41%
Branches in Coverage %: Yes
Coverage Strength: 9.58 hits per line

💛 - Coveralls

@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 13:47 Destroyed
@aspiers aspiers changed the title fix(auth): filter unsupported OTP characters P2: fix(auth): filter unsupported OTP characters Jul 30, 2026
@aspiers
aspiers force-pushed the split-pr165/server-otp-charset-filter branch from 191c97b to d3646f1 Compare July 30, 2026 15:39
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 15:40 Destroyed
@aspiers
aspiers force-pushed the split-pr165/server-otp-charset-filter branch from d3646f1 to b163ef6 Compare July 30, 2026 15:50
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 15:50 Destroyed
@aspiers
aspiers force-pushed the split-pr165/server-otp-charset-filter branch from b163ef6 to d082ad6 Compare July 30, 2026 16:02
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 16:02 Destroyed
@aspiers
aspiers force-pushed the split-pr165/server-otp-charset-filter branch from d082ad6 to 5dc9cce Compare July 30, 2026 16:57
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 July 30, 2026 16:57 Destroyed
@aspiers

aspiers commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

(reply generated by OpenAI Codex)

Addressed the maintainability finding reported in the SonarQube summary in 5dc9cce by replacing the numeric filter’s [^0-9] with the equivalent concise \D. Format, lint, typecheck, production build, unit tests, coverage, and Cucumber dry-run validation pass.

@aspiers

aspiers commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

(reply generated by OpenAI Codex)

CI context: the latest PR-attached E2E job did not execute the suite; it timed out waiting for Railway to mark deployment 5dc9cce successful. Railway has an active build/deployment delay incident. Format, lint, typecheck, build, tests, coverage, SonarCloud, and Coveralls pass.

@aspiers

aspiers commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

(reply generated by OpenAI Codex)

Railway’s incident is resolved, the final deployment completed, and the current SHA now passes the full deployed E2E suite: https://github.com/hypercerts-org/ePDS/actions/runs/30563833577

@aspiers
aspiers marked this pull request as ready for review July 31, 2026 17:33
Copilot AI lite review requested due to automatic review settings July 31, 2026 17:34

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 18:20
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 August 4, 2026 18:20 Destroyed

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

aspiers and others added 2 commits August 4, 2026 19:23
The previous commit left the change in three inconsistent states.

The main sign-in screen renders a segmented grid of one-character
boxes rather than a single input, and its input/paste handlers only
stripped whitespace. Codes copied with surrounding punctuation, and
letters typed into a digits-only code, still reached the server and
failed verification. It also never upper-cased, while alphanumeric
codes are generated as A-Z0-9 and the other two forms upper-case on
submit — so on desktop, where autocapitalize does nothing, typing a
code in lowercase could never succeed. Route both handlers through a
shared filterOtpChars() built from buildOtpInputFilter(). The charset
filters are supersets of the old whitespace strip, so pasting a
spaced code still works, and auto-advance, backspace/arrow
navigation, paste-spreading and auto-submit are unaffected.

account-login.ts inlined the same regex as a literal ternary instead
of calling buildOtpInputFilter() the way recovery.ts does, so the
shared helper had two divergent copies from the moment it was
introduced. Use the helper. Only the rendered text changes (/\D/g
rather than /[^0-9]/g); in non-unicode mode \D is exactly [^0-9].

Hoist otpLength/otpCharset to sit with the other client-side config
vars so the filter does not depend on var hoisting across 130 lines.

The changeset and the feature narrative both claimed the main
sign-in form already had this filter, which was never true. Rewrite
them to describe all three forms, and rename the changeset since it
is no longer limited to the server-rendered ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aspiers
aspiers force-pushed the split-pr165/server-otp-charset-filter branch from 219a5f7 to 0637fa7 Compare August 4, 2026 18:26
Copilot AI review requested due to automatic review settings August 4, 2026 18:26
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 August 4, 2026 18:26 Destroyed

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

features/otp-character-filtering.feature:3

  • The feature description claims coverage for “every OTP form” (segmented sign-in grid, account-login, and recovery), but the only scenario in this feature file exercises the recovery OTP preview. Either add scenarios for the other forms or narrow the feature text so it matches what’s actually tested.
Feature: OTP character filtering
  Every OTP form — the segmented sign-in grid, account-login, and recovery —
  must apply the configured OTP character policy while the user types.

The feature narrative claimed all three OTP forms were covered while the
only scenario drove the recovery preview. The grid is the form nearly
every user meets, and it is the riskiest of the three: its filtering
lives in JS input/paste listeners rather than an oninput attribute, and
the paste listener also spreads the cleaned characters across boxes,
auto-advances and auto-submits when full. None of that is visible in the
rendered HTML, so login-page.test.ts can assert the filter was wired in
but never that it behaves — a regression in the spreading would pass
every unit test we have.

Plumb otp_charset through /preview/login-otp the way /preview/recovery-otp
already does, so a scenario can exercise both policies without redeploying
with a different OTP_CHARSET.

Reuse the existing .otp-box locator convention from auth.steps.ts. The
grid needs its own steps rather than the recovery ones because it is N
one-character boxes, not a single input, so the assertion is over all
boxes — checking only the filled prefix would miss characters scattered
into later boxes.

Paste is synthesised as a ClipboardEvent with a DataTransfer: Playwright
cannot portably seed the system clipboard, and the handler reads only
event.clipboardData, so this still drives the real listener.

Reword the narrative to say what is actually covered. account-login is
left out because its OTP form is only reachable by POSTing to
/account/send-otp, which needs a real better-auth send — there is no
preview route to drive it from a browser, and inventing one to serve a
test would add production surface for no user-facing gain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 18:51
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-223 August 4, 2026 18:51 Destroyed
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/auth-service/src/otp-input.ts:9

  • OtpCharset is already defined and exported in src/lib/otp-config-validation.ts. Introducing another OtpCharset type here duplicates the source of truth and makes it easier for these to diverge over time (e.g., adding a new charset would require updating multiple aliases). Consider importing (and optionally re-exporting) the existing type instead of redefining it in this module.
export type OtpCharset = 'numeric' | 'alphanumeric'

@aspiers
aspiers merged commit 284278f into main Aug 4, 2026
31 checks passed
@aspiers
aspiers deleted the split-pr165/server-otp-charset-filter branch August 4, 2026 20:29
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.

2 participants