Skip to content

fix(core): revive date strings in safeJSONParse for pre-parsed objects#8248

Merged
himself65 merged 2 commits intocanaryfrom
fix/safe-json-parse-dates
Mar 1, 2026
Merged

fix(core): revive date strings in safeJSONParse for pre-parsed objects#8248
himself65 merged 2 commits intocanaryfrom
fix/safe-json-parse-dates

Conversation

@himself65
Copy link
Copy Markdown
Contributor

Summary

  • Fix safeJSONParse to convert ISO 8601 date strings to Date instances when the input is an already-parsed object (e.g., from Redis clients that auto-parse JSON)
  • Previously, the date reviver only ran on JSON.parse string input — pre-parsed objects were returned as-is with date fields still as strings
  • This caused silent failures in 40+ call sites doing expiresAt < new Date(), since string < Date coerces to NaN and always returns false — expired tokens were never detected as expired

Test plan

  • Added 4 new tests in internal-adapter.test.ts under "safeJSONParse date revival in secondary storage":
    • Verifies findVerificationValue returns Date instances when storage returns pre-parsed objects
    • Verifies expiration comparisons work correctly (expiresAt > new Date())
    • Verifies Date objects are consistent across multiple reads
    • Verifies non-date string fields are preserved (not converted)
  • All existing tests pass (internal-adapter: 33/33, secondary-storage: 2/2, magic-link: 16/16, email-otp: 70/70, two-factor: 34/34, password: 16/16, one-time-token: 13/13)
  • pnpm typecheck, pnpm lint, pnpm build all pass

When a Redis client (or similar) returns already-parsed JSON objects,
safeJSONParse previously returned them as-is without converting ISO 8601
date strings to Date instances. This caused silent failures in date
comparisons like `expiresAt < new Date()` across 40+ call sites, since
string-vs-Date comparison coerces to NaN and always returns false —
meaning expired tokens were never detected as expired.

Split the reviver into two functions:
- reviveDate: leaf-level converter for JSON.parse reviver
- reviveDates: recursive walker for pre-parsed objects

Both paths now produce consistent Date objects for ISO 8601 strings.
Copilot AI review requested due to automatic review settings March 1, 2026 12:38
@himself65 himself65 requested a review from Bekacru as a code owner March 1, 2026 12:38
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 1, 2026

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
better-auth-demo Ignored Ignored Mar 1, 2026 0:38am
better-auth-docs Skipped Skipped Mar 1, 2026 0:38am

Request Review

@vercel vercel bot temporarily deployed to Preview – better-auth-docs March 1, 2026 12:38 Inactive
@himself65 himself65 enabled auto-merge March 1, 2026 12:39
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 1, 2026

Open in StackBlitz

@better-auth/api-key

npm i https://pkg.pr.new/@better-auth/api-key@8248

better-auth

npm i https://pkg.pr.new/better-auth@8248

auth

npm i https://pkg.pr.new/auth@8248

@better-auth/core

npm i https://pkg.pr.new/@better-auth/core@8248

@better-auth/drizzle-adapter

npm i https://pkg.pr.new/@better-auth/drizzle-adapter@8248

@better-auth/electron

npm i https://pkg.pr.new/@better-auth/electron@8248

@better-auth/expo

npm i https://pkg.pr.new/@better-auth/expo@8248

@better-auth/i18n

npm i https://pkg.pr.new/@better-auth/i18n@8248

@better-auth/kysely-adapter

npm i https://pkg.pr.new/@better-auth/kysely-adapter@8248

@better-auth/memory-adapter

npm i https://pkg.pr.new/@better-auth/memory-adapter@8248

@better-auth/mongo-adapter

npm i https://pkg.pr.new/@better-auth/mongo-adapter@8248

@better-auth/oauth-provider

npm i https://pkg.pr.new/@better-auth/oauth-provider@8248

@better-auth/passkey

npm i https://pkg.pr.new/@better-auth/passkey@8248

@better-auth/prisma-adapter

npm i https://pkg.pr.new/@better-auth/prisma-adapter@8248

@better-auth/redis-storage

npm i https://pkg.pr.new/@better-auth/redis-storage@8248

@better-auth/scim

npm i https://pkg.pr.new/@better-auth/scim@8248

@better-auth/sso

npm i https://pkg.pr.new/@better-auth/sso@8248

@better-auth/stripe

npm i https://pkg.pr.new/@better-auth/stripe@8248

@better-auth/telemetry

npm i https://pkg.pr.new/@better-auth/telemetry@8248

@better-auth/test-utils

npm i https://pkg.pr.new/@better-auth/test-utils@8248

commit: 5d4fa41

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes safeJSONParse so ISO 8601 date strings are revived into Date instances not only when parsing JSON strings, but also when the input is already a parsed object (e.g., from Redis clients that auto-parse JSON). This prevents incorrect expiry comparisons at call sites that assume expiresAt is a Date.

Changes:

  • Added recursive date revival for non-string (already-parsed) inputs to safeJSONParse.
  • Refactored date revival logic into reusable helpers (reviveDate / reviveDates).
  • Added tests covering date revival behavior when secondary storage returns pre-parsed objects.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/core/src/utils/json.ts Extends safeJSONParse to recursively revive ISO date strings to Date for already-parsed objects/arrays.
packages/better-auth/src/db/internal-adapter.test.ts Adds test coverage for date revival when secondary storage returns pre-parsed objects.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +38
if (typeof value === "object") {
const result: Record<string, any> = {};
for (const key of Object.keys(value)) {
result[key] = reviveDates((value as Record<string, any>)[key]);
}
return result;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

reviveDates builds a plain {} and assigns arbitrary keys from untrusted input. If a payload includes __proto__ (or similar magic keys), result[key] = ... can mutate the returned object’s prototype, enabling prototype-pollution-style behavior. Consider using a null-prototype object (e.g. Object.create(null)) and/or defining properties in a way that treats __proto__ as a normal key, or explicitly blocking these keys during the copy.

Copilot uses AI. Check for mistakes.
set(key: string, value: string, ttl?: number) {
// Store as pre-parsed object (simulating Redis auto-parse)
dataMap.set(key, JSON.parse(value));
if (ttl) ttlMap.set(key, ttl);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

In this test helper, if (ttl) ttlMap.set(key, ttl); will skip storing TTL when ttl is 0 (because of the truthiness check). Using an explicit undefined check (e.g. ttl !== undefined) makes the helper behave correctly for all numeric TTL values.

Suggested change
if (ttl) ttlMap.set(key, ttl);
if (ttl !== undefined) ttlMap.set(key, ttl);

Copilot uses AI. Check for mistakes.
@himself65 himself65 added this pull request to the merge queue Mar 1, 2026
Merged via the queue into canary with commit 0db2da5 Mar 1, 2026
17 of 18 checks passed
himself65 added a commit that referenced this pull request Mar 5, 2026
…rialized JSON)

Add tests covering the scenario where secondaryStorage (e.g. @vercel/kv)
returns already-parsed objects instead of strings, causing expiresAt to be
a string rather than a Date. The root cause was fixed in #8248 via
reviveDates in safeJSONParse. These tests prevent regression.
rae-fcm pushed a commit to FullCodeMedical/better-auth that referenced this pull request Mar 9, 2026
@better-auth better-auth locked as resolved and limited conversation to collaborators Mar 31, 2026
@bytaesu bytaesu added the locked Locked conversations after being closed for 7 days label Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

locked Locked conversations after being closed for 7 days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants