feat: configurable OTP length with 6-digit default support#14
feat: configurable OTP length with 6-digit default support#14
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
.beads/issues.jsonl.env.examplepackages/auth-service/.env.examplepackages/auth-service/src/__tests__/consent.test.tspackages/auth-service/src/__tests__/recovery.test.tspackages/auth-service/src/better-auth.tspackages/auth-service/src/context.tspackages/auth-service/src/index.tspackages/auth-service/src/routes/account-login.tspackages/auth-service/src/routes/login-page.tspackages/auth-service/src/routes/recovery.tspackages/shared/src/crypto.ts
| 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) | ||
| }) |
There was a problem hiding this comment.
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.
| 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.
| otpLength: number, | ||
| otpCharset: 'numeric' | 'alphanumeric' = 'numeric', | ||
| ): Promise<void> { |
There was a problem hiding this comment.
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 generationAs 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).
| otpLength: parseInt(process.env.OTP_LENGTH || '8', 10), | ||
| otpCharset: (process.env.OTP_CHARSET || 'numeric') as | ||
| | 'numeric' | ||
| | 'alphanumeric', |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.
| maxlength="${opts.otpLength}" pattern="[0-9]{${opts.otpLength}}" inputmode="numeric" autocomplete="one-time-code" | ||
| placeholder="${'0'.repeat(opts.otpLength)}" class="otp-input" |
There was a problem hiding this comment.
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.
| var otpLength = ${opts.otpLength}; | ||
| var otpCharset = ${JSON.stringify(opts.otpCharset)}; | ||
| var article = /^[aeiou]/i.test(otpLength.toString()) ? 'an' : 'a'; | ||
|
|
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.
| 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"> |
There was a problem hiding this comment.
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.
| 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.
Summary
OTP_LENGTHenv var toauth-service, validated on startup and propagated throughAuthServiceConfigto all route handlers and better-auth setupotpCharsetfield toAuthServiceConfigand alphanumeric OTP generation support inbetter-auth.tsgenerateOtpCodeinshared/crypto.tsin favour of the configurable path.env.examplefiles8Changes
AuthServiceConfigotpLengthandotpCharsetfieldsindex.tsOTP_LENGTHfrom env, passes to contextbetter-auth.tsOTP_LENGTH, adds alphanumericgenerateOTPsupportlogin-page.ts,recovery.ts,account-login.tsctx.config.otpLengthinstead of hardcoded valueshared/crypto.tsgenerateOtpCodemarked deprecated.env.exampleOTP_LENGTHrecovery.test.ts; alphanumeric OTP variants addedSummary by CodeRabbit
New Features
Chores