@@ -30,6 +30,73 @@ export function resolveProtocolName(model: string): string {
3030// Internal helpers
3131// ---------------------------------------------------------------------------
3232
33+ /**
34+ * better-auth datetime columns (snake_case) per model.
35+ *
36+ * When the underlying driver stored these as JavaScript `Date` objects
37+ * (legacy behaviour), the libsql HTTP transport coerces the value to a REAL
38+ * column and round-trips it as a string like `"1779497911249.0"`. That
39+ * string is not a valid Date string (it has a trailing `.0`), so
40+ * `new Date(...)` produces `Invalid Date` and better-auth's client treats
41+ * the session as expired — causing a login/redirect loop.
42+ *
43+ * We normalise these legacy values back to ISO strings on **read** so the
44+ * factory's `supportsDates: false` parser can turn them into real Date
45+ * objects. New writes always go through better-auth's own
46+ * `Date → ISO string` conversion (because we declare `supportsDates: false`
47+ * below), so no further `.0`-suffixed values will ever be created.
48+ */
49+ const LEGACY_DATETIME_FIELDS_BY_MODEL : Record < string , string [ ] > = {
50+ user : [ 'created_at' , 'updated_at' ] ,
51+ session : [ 'expires_at' , 'created_at' , 'updated_at' ] ,
52+ account : [
53+ 'access_token_expires_at' ,
54+ 'refresh_token_expires_at' ,
55+ 'created_at' ,
56+ 'updated_at' ,
57+ ] ,
58+ verification : [ 'expires_at' , 'created_at' , 'updated_at' ] ,
59+ } ;
60+
61+ const NUMERIC_STRING_RE = / ^ - ? \d + ( \. \d + ) ? $ / ;
62+
63+ /**
64+ * If `value` looks like a stringified epoch-ms (optionally with `.0`),
65+ * convert it to an ISO 8601 string. Otherwise return it unchanged.
66+ */
67+ function normaliseLegacyDate ( value : unknown ) : unknown {
68+ if ( typeof value !== 'string' ) return value ;
69+ if ( ! NUMERIC_STRING_RE . test ( value ) ) return value ;
70+ const n = parseFloat ( value ) ;
71+ if ( ! Number . isFinite ( n ) ) return value ;
72+ // Heuristic: epoch milliseconds are at least 10 digits (year 2001+).
73+ if ( Math . abs ( n ) < 1e10 ) return value ;
74+ const d = new Date ( n ) ;
75+ if ( Number . isNaN ( d . getTime ( ) ) ) return value ;
76+ return d . toISOString ( ) ;
77+ }
78+
79+ /**
80+ * Walk a record and rewrite any legacy `.0`-suffixed datetime values
81+ * into ISO strings. Mutates and returns the record.
82+ */
83+ function normaliseLegacyDates < T extends Record < string , any > | null | undefined > (
84+ model : string ,
85+ record : T ,
86+ ) : T {
87+ if ( ! record ) return record ;
88+ const cols = LEGACY_DATETIME_FIELDS_BY_MODEL [ model ] ;
89+ if ( ! cols ) return record ;
90+ for ( const col of cols ) {
91+ if ( col in record ) {
92+ ( record as Record < string , unknown > ) [ col ] = normaliseLegacyDate (
93+ ( record as Record < string , unknown > ) [ col ] ,
94+ ) ;
95+ }
96+ }
97+ return record ;
98+ }
99+
33100/**
34101 * Convert better-auth where clause to ObjectQL query format.
35102 *
@@ -88,17 +155,21 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) {
88155 return createAdapterFactory ( {
89156 config : {
90157 adapterId : 'objectql' ,
91- // ObjectQL natively supports these types — no extra conversion needed
92- supportsBooleans : true ,
93- supportsDates : true ,
158+ // We let better-auth handle Date↔string and boolean↔0/1 conversion so
159+ // that values land in the underlying SQL driver as primitive strings
160+ // and integers. Some drivers (e.g. libsql over the HTTP transport)
161+ // otherwise mangle `Date` objects into `"<epoch>.0"` strings that
162+ // break the client-side session parser.
163+ supportsBooleans : false ,
164+ supportsDates : false ,
94165 supportsJSON : true ,
95166 } ,
96167 adapter : ( ) => ( {
97168 create : async < T extends Record < string , any > > (
98169 { model, data, select : _select } : { model : string ; data : T ; select ?: string [ ] } ,
99170 ) : Promise < T > => {
100171 const result = await dataEngine . insert ( model , data ) ;
101- return result as T ;
172+ return normaliseLegacyDates ( model , result ) as T ;
102173 } ,
103174
104175 findOne : async < T > (
@@ -108,7 +179,7 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) {
108179
109180 const result = await dataEngine . findOne ( model , { where : filter , fields : select } ) ;
110181
111- return result ? ( result as T ) : null ;
182+ return result ? ( normaliseLegacyDates ( model , result ) as T ) : null ;
112183 } ,
113184
114185 findMany : async < T > (
@@ -130,7 +201,7 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) {
130201 orderBy,
131202 } ) ;
132203
133- return results as T [ ] ;
204+ return results . map ( ( r ) => normaliseLegacyDates ( model , r as Record < string , any > ) ) as T [ ] ;
134205 } ,
135206
136207 count : async (
@@ -150,7 +221,7 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) {
150221 if ( ! record ) return null ;
151222
152223 const result = await dataEngine . update ( model , { ...( update as any ) , id : record . id } ) ;
153- return result ? ( result as T ) : null ;
224+ return result ? ( normaliseLegacyDates ( model , result ) as T ) : null ;
154225 } ,
155226
156227 updateMany : async (
0 commit comments