Skip to content

feat: configurable OTP length with 6-digit default support#14

Draft
Kzoeps wants to merge 17 commits intomainfrom
karma/hyper-113-switch-to-6-digit-otp
Draft

feat: configurable OTP length with 6-digit default support#14
Kzoeps wants to merge 17 commits intomainfrom
karma/hyper-113-switch-to-6-digit-otp

Conversation

@Kzoeps
Copy link
Contributor

@Kzoeps Kzoeps commented Mar 13, 2026

Summary

  • Adds OTP_LENGTH env var to auth-service, validated on startup and propagated through AuthServiceConfig to all route handlers and better-auth setup
  • Adds otpCharset field to AuthServiceConfig and alphanumeric OTP generation support in better-auth.ts
  • Deprecates legacy generateOtpCode in shared/crypto.ts in favour of the configurable path
  • Documents new env vars in .env.example files
  • Updates tests to use dynamic OTP length instead of hardcoded 8

Changes

Area What changed
AuthServiceConfig Added otpLength and otpCharset fields
index.ts Reads and validates OTP_LENGTH from env, passes to context
better-auth.ts Reads OTP_LENGTH, adds alphanumeric generateOTP support
login-page.ts, recovery.ts, account-login.ts Use ctx.config.otpLength instead of hardcoded value
shared/crypto.ts generateOtpCode marked deprecated
.env.example Documents OTP_LENGTH
Tests Dynamic OTP length in recovery.test.ts; alphanumeric OTP variants added

Summary by CodeRabbit

  • New Features

    • OTP configuration now supports configurable length (4-12 characters, default 8) and character set (numeric or alphanumeric).
    • OTP forms dynamically adapt input validation and display based on configuration settings.
  • Chores

    • Added documentation for OTP environment variables.
    • Marked legacy OTP generation function as deprecated.

@vercel
Copy link

vercel bot commented Mar 13, 2026

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

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Mar 13, 2026 10:53am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

This pull request adds configurable OTP length (4-12 characters) and character set (numeric or alphanumeric) throughout the authentication service. Configuration is loaded from environment variables, validated on startup, threaded through initialization, and used to dynamically render OTP forms and UI messaging.

Changes

Cohort / File(s) Summary
Environment Configuration
.env.example, packages/auth-service/.env.example
Added documentation comments for OTP_LENGTH (range 4-12, default 8) and OTP_CHARSET (numeric or alphanumeric) configuration options as commented-out examples.
Configuration Type & Initialization
packages/auth-service/src/context.ts, packages/auth-service/src/better-auth.ts, packages/auth-service/src/index.ts
Extended AuthServiceConfig interface with otpLength (number) and otpCharset ('numeric' | 'alphanumeric') properties. Updated createBetterAuth and runBetterAuthMigrations signatures to accept these parameters; added validation (otpLength 4-12 range), conditional generateOTP function for alphanumeric mode, and configuration propagation through initialization.
Route Handlers
packages/auth-service/src/routes/account-login.ts, packages/auth-service/src/routes/login-page.ts, packages/auth-service/src/routes/recovery.ts
Updated router signatures to accept AuthServiceContext; passed otpLength and otpCharset to OTP form rendering functions. Dynamically computed OTP input attributes (maxlength, pattern, placeholder, inputmode) and messaging (article selection, character type labels) based on configured OTP parameters. Removed fixed letter-spacing CSS constraints.
Tests
packages/auth-service/src/__tests__/consent.test.ts, packages/auth-service/src/__tests__/recovery.test.ts
Updated AuthServiceConfig usage in tests to include otpLength and otpCharset. Generalized OTP length from hardcoded 8 to configurable OTP_LENGTH; expanded test coverage for alphanumeric and numeric OTP patterns, placeholder generation, and length validation.
Deprecation Documentation
packages/shared/src/crypto.ts
Added JSDoc deprecation notice to generateOtpCode, indicating it is superseded by better-auth's emailOTP plugin and referencing better-auth.ts configuration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Eight digits now become flexible strings,
Numeric or alphanumeric wings,
Configuration threads through routes with care,
Dynamic validation floating in the air,
The OTP flows with grace and length.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding configurable OTP length with support for 6-digit OTPs instead of hardcoded 8-digit defaults.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch karma/hyper-113-switch-to-6-digit-otp
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/auth-service/src/__tests__/recovery.test.ts`:
- Around line 153-159: Rename the failing test title to match the assertion:
change the it(...) description currently "numeric-only OTP does not match
alphanumeric-exclusive pattern" to something like "numeric-only OTP matches
alphanumeric pattern" (or "numeric-only OTP matches [A-Za-z0-9] pattern") so the
test name accurately reflects that pattern.test(otp) is expected toBe(true);
update the string in the it(...) call surrounding the test that creates pattern
and otp.

In `@packages/auth-service/src/better-auth.ts`:
- Around line 80-82: The function in better-auth.ts exposes otpLength and
otpCharset parameters which allow non-8-digit or alphanumeric OTPs; update the
implementation to enforce the auth-service policy by hardcoding or overriding
these values to otpLength = 8 and otpCharset = 'numeric' (ignore or throw on
incoming variants) wherever OTPs are generated/validated (references: otpLength,
otpCharset and the OTP generation/validation functions in better-auth.ts),
ensure the generator only produces 8 numeric digits and single-use semantics
remain, and apply the same enforcement to the other occurrences noted in this
file (the other OTP-related helper functions referenced in the review).

In `@packages/auth-service/src/index.ts`:
- Around line 138-141: The OTP configuration widens policy beyond the service
contract by allowing variable lengths and alphanumeric chars; change the otp
config so otpLength is fixed to 8 and otpCharset is fixed to 'numeric' (do not
read these from process.env), and ensure any other occurrences in this file that
set OTP policy (e.g., the otpLength and otpCharset bindings and related settings
in the same config block) are updated to use the fixed 8-digit numeric policy
and single-use handling via better-auth integration (preserve surrounding config
structure and types, only replace the env-driven values with the constant 8 and
'numeric').

In `@packages/auth-service/src/routes/account-login.ts`:
- Line 176: The current article selection uses
/^[aeiou]/i.test(opts.otpLength.toString()), which misidentifies numeric lengths
(e.g., "8" or "11"); update the logic around the article variable (where article
is computed from opts.otpLength) to detect numeric values and choose "an" when
the spoken form begins with a vowel sound (handle common numeric cases such as
numbers starting with "8" and "11" at minimum) otherwise fall back to the
vowel-letter test for non-numeric strings; refer to the article variable and
opts.otpLength/opts.otpLength.toString() when locating and replacing the check.
- Around line 196-197: The OTP input currently forces digits by hardcoding
pattern="[0-9]{...}", inputmode="numeric", and a zero-filled placeholder, which
breaks alphanumeric OTPs; update the template that renders the input
(referencing opts.otpLength and opts.otpCharset and the "otp-input" input
element) to choose attributes based on opts.otpCharset: for numeric use
pattern="[0-9]{N}", inputmode="numeric", and placeholder of
'0'.repeat(opts.otpLength); for alphanumeric use a more permissive pattern (e.g.
[A-Za-z0-9]{N} or no pattern), remove/adjust inputmode (or use "text"), and set
a neutral placeholder (e.g. repeated '•' or spaces) so valid alphanumeric codes
are not blocked client-side.

In `@packages/auth-service/src/routes/login-page.ts`:
- Around line 366-369: The article calculation using /^[aeiou]/ on
otpLength.toString() is wrong for numerals (e.g., 8, 11); change the logic that
sets article (where otpLength is used) to detect numeric cases instead of vowel
letters — e.g., convert otpLength to string and set article =
otpStr.startsWith('8') || otpStr.startsWith('11') ? 'an' : 'a' (use the
otpLength and article symbols from the diff), and replace the three occurrences
noted with this numeric-aware check.

In `@packages/auth-service/src/routes/recovery.ts`:
- Around line 269-272: The OTP input pattern currently uses [A-Za-z0-9] when
opts.otpCharset === 'alphanumeric', which permits lowercase even though
generated alphanumeric OTPs are uppercase-only; update the pattern expression to
[A-Z0-9] and ensure autocapitalize remains "characters" (and inputmode stays
'text') so the HTML validation and UX match the generated OTP alphabet; locate
the pattern string and related attributes in the OTP input markup that reference
opts.otpCharset and opts.otpLength and replace the lowercase-permitting class
with an uppercase-only character class.
- Line 248: The current vowel-regex on opts.otpLength.toString() produces
incorrect articles for numeric lengths (e.g., "a 8-digit"); change the logic to
decide the article from the numeric string's prefix instead (for example check
opts.otpLength.toString().startsWith('8') ||
opts.otpLength.toString().startsWith('11') ? 'an' : 'a') so numbers like 8 and
11 get "an"; update the const article assignment in recovery.ts (the article
computation shown in the diff) and apply the same fix to the similar occurrence
around the other instance at line 261.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d9ed776-a4bf-40a8-8806-3c9f1e350998

📥 Commits

Reviewing files that changed from the base of the PR and between 9351243 and 995ea71.

📒 Files selected for processing (12)
  • .beads/issues.jsonl
  • .env.example
  • packages/auth-service/.env.example
  • packages/auth-service/src/__tests__/consent.test.ts
  • packages/auth-service/src/__tests__/recovery.test.ts
  • packages/auth-service/src/better-auth.ts
  • packages/auth-service/src/context.ts
  • packages/auth-service/src/index.ts
  • packages/auth-service/src/routes/account-login.ts
  • packages/auth-service/src/routes/login-page.ts
  • packages/auth-service/src/routes/recovery.ts
  • packages/shared/src/crypto.ts

Comment on lines +153 to +159
it('numeric-only OTP does not match alphanumeric-exclusive pattern', () => {
// Verify that a purely numeric OTP still matches [A-Za-z0-9] (it should — digits are a subset)
const OTP_LENGTH = parseInt(process.env.OTP_LENGTH ?? '8', 10)
const pattern = new RegExp(`^[A-Za-z0-9]{${OTP_LENGTH}}$`)
const otp = '1'.repeat(OTP_LENGTH)
expect(pattern.test(otp)).toBe(true)
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test title contradicts the assertion outcome.

The case name says “does not match”, but the assertion expects true. Please rename the test to reflect the actual expectation.

✏️ Suggested rename
-  it('numeric-only OTP does not match alphanumeric-exclusive pattern', () => {
+  it('numeric-only OTP matches alphanumeric-inclusive pattern', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('numeric-only OTP does not match alphanumeric-exclusive pattern', () => {
// Verify that a purely numeric OTP still matches [A-Za-z0-9] (it should — digits are a subset)
const OTP_LENGTH = parseInt(process.env.OTP_LENGTH ?? '8', 10)
const pattern = new RegExp(`^[A-Za-z0-9]{${OTP_LENGTH}}$`)
const otp = '1'.repeat(OTP_LENGTH)
expect(pattern.test(otp)).toBe(true)
})
it('numeric-only OTP matches alphanumeric-inclusive pattern', () => {
// Verify that a purely numeric OTP still matches [A-Za-z0-9] (it should — digits are a subset)
const OTP_LENGTH = parseInt(process.env.OTP_LENGTH ?? '8', 10)
const pattern = new RegExp(`^[A-Za-z0-9]{${OTP_LENGTH}}$`)
const otp = '1'.repeat(OTP_LENGTH)
expect(pattern.test(otp)).toBe(true)
})
🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 155-155: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^[A-Za-z0-9]{${OTP_LENGTH}}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/__tests__/recovery.test.ts` around lines 153 - 159,
Rename the failing test title to match the assertion: change the it(...)
description currently "numeric-only OTP does not match alphanumeric-exclusive
pattern" to something like "numeric-only OTP matches alphanumeric pattern" (or
"numeric-only OTP matches [A-Za-z0-9] pattern") so the test name accurately
reflects that pattern.test(otp) is expected toBe(true); update the string in the
it(...) call surrounding the test that creates pattern and otp.

Comment on lines +80 to 82
otpLength: number,
otpCharset: 'numeric' | 'alphanumeric' = 'numeric',
): Promise<void> {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Configurable OTP length/charset conflicts with auth-service OTP policy.

These paths now permit non-8-digit and alphanumeric OTPs, which breaks the current auth-service requirement.

🔧 Suggested direction
-  otpLength: number,
-  otpCharset: 'numeric' | 'alphanumeric' = 'numeric',
+  // OTP policy: fixed 8-digit numeric
...
-        otpLength,
+        otpLength: 8,
...
-        ...(otpCharset === 'alphanumeric'
-          ? { generateOTP: () => generateRandomString(otpLength, 'A-Z', '0-9') }
-          : {}),
+        // keep default numeric OTP generation

As per coding guidelines: **/auth-service/**/*.{ts,tsx}: OTP codes must be 8-digit, single-use, managed by better-auth.

Also applies to: 91-97, 122-127, 164-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/better-auth.ts` around lines 80 - 82, The function
in better-auth.ts exposes otpLength and otpCharset parameters which allow
non-8-digit or alphanumeric OTPs; update the implementation to enforce the
auth-service policy by hardcoding or overriding these values to otpLength = 8
and otpCharset = 'numeric' (ignore or throw on incoming variants) wherever OTPs
are generated/validated (references: otpLength, otpCharset and the OTP
generation/validation functions in better-auth.ts), ensure the generator only
produces 8 numeric digits and single-use semantics remain, and apply the same
enforcement to the other occurrences noted in this file (the other OTP-related
helper functions referenced in the review).

Comment on lines +138 to +141
otpLength: parseInt(process.env.OTP_LENGTH || '8', 10),
otpCharset: (process.env.OTP_CHARSET || 'numeric') as
| 'numeric'
| 'alphanumeric',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

OTP policy is widened beyond the service contract.

This change allows 4–12 and alphanumeric OTPs, but auth-service policy requires 8-digit OTPs.

💡 Suggested fix
-    otpLength: parseInt(process.env.OTP_LENGTH || '8', 10),
-    otpCharset: (process.env.OTP_CHARSET || 'numeric') as
-      | 'numeric'
-      | 'alphanumeric',
+    otpLength: parseInt(process.env.OTP_LENGTH || '8', 10),
+    otpCharset: 'numeric',
@@
-  if (
-    isNaN(config.otpLength) ||
-    config.otpLength < 4 ||
-    config.otpLength > 12
-  ) {
+  if (isNaN(config.otpLength) || config.otpLength !== 8) {
     throw new Error(
-      `Invalid OTP_LENGTH: must be between 4 and 12, got "${process.env.OTP_LENGTH}"`,
+      `Invalid OTP_LENGTH: must be exactly 8, got "${process.env.OTP_LENGTH}"`,
     )
   }
-
-  const validCharsets = ['numeric', 'alphanumeric']
-  if (!validCharsets.includes(config.otpCharset)) {
-    throw new Error(
-      `Invalid OTP_CHARSET: must be 'numeric' or 'alphanumeric', got "${process.env.OTP_CHARSET}"`,
-    )
-  }

As per coding guidelines, "**/auth-service/**/*.{ts,tsx}: OTP codes must be 8-digit, single-use, managed by better-auth".

Also applies to: 144-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/index.ts` around lines 138 - 141, The OTP
configuration widens policy beyond the service contract by allowing variable
lengths and alphanumeric chars; change the otp config so otpLength is fixed to 8
and otpCharset is fixed to 'numeric' (do not read these from process.env), and
ensure any other occurrences in this file that set OTP policy (e.g., the
otpLength and otpCharset bindings and related settings in the same config block)
are updated to use the fixed 8-digit numeric policy and single-use handling via
better-auth integration (preserve surrounding config structure and types, only
replace the env-driven values with the constant 8 and 'numeric').

error?: string
}): string {
const maskedEmail = maskEmail(opts.email)
const article = /^[aeiou]/i.test(opts.otpLength.toString()) ? 'an' : 'a'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Article selection is incorrect for numeric lengths like 8 and 11.

/^[aeiou]/ on a number string yields “a 8-digit” instead of “an 8-digit”.

💡 Suggested fix
-  const article = /^[aeiou]/i.test(opts.otpLength.toString()) ? 'an' : 'a'
+  const article = [8, 11].includes(opts.otpLength) ? 'an' : 'a'

Also applies to: 189-189

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/account-login.ts` at line 176, The current
article selection uses /^[aeiou]/i.test(opts.otpLength.toString()), which
misidentifies numeric lengths (e.g., "8" or "11"); update the logic around the
article variable (where article is computed from opts.otpLength) to detect
numeric values and choose "an" when the spoken form begins with a vowel sound
(handle common numeric cases such as numbers starting with "8" and "11" at
minimum) otherwise fall back to the vowel-letter test for non-numeric strings;
refer to the article variable and opts.otpLength/opts.otpLength.toString() when
locating and replacing the check.

Comment on lines +196 to +197
maxlength="${opts.otpLength}" pattern="[0-9]{${opts.otpLength}}" inputmode="numeric" autocomplete="one-time-code"
placeholder="${'0'.repeat(opts.otpLength)}" class="otp-input"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

OTP input is hardcoded numeric, which breaks alphanumeric OTP mode.

When otpCharset is alphanumeric, the current pattern, inputmode, and placeholder still enforce digits only, so valid codes can be blocked client-side.

💡 Suggested fix
 function renderOtpForm(opts: {
   email: string
   csrfToken: string
   otpLength: number
+  otpCharset: 'numeric' | 'alphanumeric'
   error?: string
 }): string {
@@
-        <input type="text" id="otp" name="otp" required autofocus
-               maxlength="${opts.otpLength}" pattern="[0-9]{${opts.otpLength}}" inputmode="numeric" autocomplete="one-time-code"
-               placeholder="${'0'.repeat(opts.otpLength)}" class="otp-input"
+        <input type="text" id="otp" name="otp" required autofocus
+               maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Za-z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
+               placeholder="${opts.otpCharset === 'alphanumeric' ? 'X'.repeat(opts.otpLength) : '0'.repeat(opts.otpLength)}" class="otp-input"
+               autocapitalize="${opts.otpCharset === 'alphanumeric' ? 'characters' : 'off'}"
                style="letter-spacing: ${Math.max(2, Math.round(32 / opts.otpLength))}px">
       renderOtpForm({
         email,
         csrfToken: res.locals.csrfToken,
         otpLength: ctx.config.otpLength,
+        otpCharset: ctx.config.otpCharset,
       }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/account-login.ts` around lines 196 - 197,
The OTP input currently forces digits by hardcoding pattern="[0-9]{...}",
inputmode="numeric", and a zero-filled placeholder, which breaks alphanumeric
OTPs; update the template that renders the input (referencing opts.otpLength and
opts.otpCharset and the "otp-input" input element) to choose attributes based on
opts.otpCharset: for numeric use pattern="[0-9]{N}", inputmode="numeric", and
placeholder of '0'.repeat(opts.otpLength); for alphanumeric use a more
permissive pattern (e.g. [A-Za-z0-9]{N} or no pattern), remove/adjust inputmode
(or use "text"), and set a neutral placeholder (e.g. repeated '•' or spaces) so
valid alphanumeric codes are not blocked client-side.

Comment on lines +366 to +369
var otpLength = ${opts.otpLength};
var otpCharset = ${JSON.stringify(opts.otpCharset)};
var article = /^[aeiou]/i.test(otpLength.toString()) ? 'an' : 'a';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Article logic yields incorrect text for numbers like 8 and 11.

Current logic can render “a 8-digit code” instead of “an 8-digit code”.

💡 Suggested fix
-      var article = /^[aeiou]/i.test(otpLength.toString()) ? 'an' : 'a';
+      var article = [8, 11].includes(otpLength) ? 'an' : 'a';

Also applies to: 374-374, 506-506

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/login-page.ts` around lines 366 - 369, The
article calculation using /^[aeiou]/ on otpLength.toString() is wrong for
numerals (e.g., 8, 11); change the logic that sets article (where otpLength is
used) to detect numeric cases instead of vowel letters — e.g., convert otpLength
to string and set article = otpStr.startsWith('8') || otpStr.startsWith('11') ?
'an' : 'a' (use the otpLength and article symbols from the diff), and replace
the three occurrences noted with this numeric-aware check.

}): string {
const maskedEmail = maskEmail(opts.email)
const encodedUri = encodeURIComponent(opts.requestUri)
const article = /^[aeiou]/i.test(opts.otpLength.toString()) ? 'an' : 'a'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Article selection is grammatically wrong for numeric lengths like 8 and 11.

Using a vowel regex on opts.otpLength.toString() yields “a 8-digit” instead of “an 8-digit”.

✏️ Suggested fix
-  const article = /^[aeiou]/i.test(opts.otpLength.toString()) ? 'an' : 'a'
+  const article = [8, 11].includes(opts.otpLength) ? 'an' : 'a'

Also applies to: 261-261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/recovery.ts` at line 248, The current
vowel-regex on opts.otpLength.toString() produces incorrect articles for numeric
lengths (e.g., "a 8-digit"); change the logic to decide the article from the
numeric string's prefix instead (for example check
opts.otpLength.toString().startsWith('8') ||
opts.otpLength.toString().startsWith('11') ? 'an' : 'a') so numbers like 8 and
11 get "an"; update the const article assignment in recovery.ts (the article
computation shown in the diff) and apply the same fix to the similar occurrence
around the other instance at line 261.

Comment on lines +269 to +272
maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Za-z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
${opts.otpCharset === 'alphanumeric' ? 'autocapitalize="characters"' : 'autocapitalize="off"'}
placeholder="${opts.otpCharset === 'alphanumeric' ? 'X'.repeat(opts.otpLength) : '0'.repeat(opts.otpLength)}" class="otp-input"
style="letter-spacing: ${Math.max(2, Math.round(32 / opts.otpLength))}px">
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Alphanumeric input validation is looser than generated OTP alphabet.

The form allows lowercase ([A-Za-z0-9]), but generated alphanumeric OTPs are uppercase-only. This can let lowercase entries pass HTML validation and then fail verification.

🔧 Suggested fix
-               maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Za-z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
+               maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Za-z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
${opts.otpCharset === 'alphanumeric' ? 'autocapitalize="characters"' : 'autocapitalize="off"'}
placeholder="${opts.otpCharset === 'alphanumeric' ? 'X'.repeat(opts.otpLength) : '0'.repeat(opts.otpLength)}" class="otp-input"
style="letter-spacing: ${Math.max(2, Math.round(32 / opts.otpLength))}px">
maxlength="${opts.otpLength}" pattern="${opts.otpCharset === 'alphanumeric' ? `[A-Z0-9]{${opts.otpLength}}` : `[0-9]{${opts.otpLength}}`}" inputmode="${opts.otpCharset === 'alphanumeric' ? 'text' : 'numeric'}" autocomplete="one-time-code"
${opts.otpCharset === 'alphanumeric' ? 'autocapitalize="characters"' : 'autocapitalize="off"'}
placeholder="${opts.otpCharset === 'alphanumeric' ? 'X'.repeat(opts.otpLength) : '0'.repeat(opts.otpLength)}" class="otp-input"
style="letter-spacing: ${Math.max(2, Math.round(32 / opts.otpLength))}px">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/recovery.ts` around lines 269 - 272, The OTP
input pattern currently uses [A-Za-z0-9] when opts.otpCharset ===
'alphanumeric', which permits lowercase even though generated alphanumeric OTPs
are uppercase-only; update the pattern expression to [A-Z0-9] and ensure
autocapitalize remains "characters" (and inputmode stays 'text') so the HTML
validation and UX match the generated OTP alphabet; locate the pattern string
and related attributes in the OTP input markup that reference opts.otpCharset
and opts.otpLength and replace the lowercase-permitting class with an
uppercase-only character class.

@Kzoeps Kzoeps marked this pull request as draft March 16, 2026 08:26
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