11import type { AppAuthClient , AuthSession , AuthUser } from '#nuxt-better-auth'
22import type { ComputedRef , Ref } from 'vue'
33import createAppAuthClient from '#auth/client'
4- import { computed , navigateTo , nextTick , useNuxtApp , useRequestFetch , useRequestHeaders , useRequestURL , useRuntimeConfig , useState , watch } from '#imports'
4+ import { computed , navigateTo , useNuxtApp , useRequestURL , useRuntimeConfig , useState , watch } from '#imports'
55import { normalizeAuthActionError } from '../internal/auth-action-error'
6+ import { resolvePostAuthSuccessRedirect , withFallbackSocialCallbackURL } from '../internal/redirect-helpers'
7+ import { fetchSessionClient , fetchSessionServer , stripToken } from '../internal/session-fetch'
8+ import { isRecord } from '../internal/utils'
9+ import { wrapAuthMethod } from '../internal/wrap-auth-method'
610
711export interface SignOutOptions { onSuccess ?: ( ) => void | Promise < void > }
812interface RuntimeFlags { client : boolean , server : boolean }
9- interface SessionResponse { session : AuthSession & { token ?: string } , user : AuthUser }
1013
1114let _sessionSignalListenerBound = false
1215let _signOutPromise : Promise < void > | null = null
@@ -29,23 +32,12 @@ export interface UseUserSessionReturn {
2932let _client : AppAuthClient | null = null
3033interface UpdateUserResponse { error ?: unknown }
3134
32- function isRecord ( value : unknown ) : value is Record < string , unknown > {
33- return Boolean ( value && typeof value === 'object' )
34- }
35-
3635function getClient ( baseURL : string ) : AppAuthClient {
3736 if ( ! _client )
3837 _client = createAppAuthClient ( baseURL )
3938 return _client
4039}
4140
42- function isExpectedSignedOutSessionError ( error : unknown ) : boolean {
43- const normalizedError = normalizeAuthActionError ( error )
44- if ( normalizedError . status === 401 )
45- return true
46- return normalizedError . code === 'UNAUTHORIZED'
47- }
48-
4941function getRuntimeFlags ( ) : RuntimeFlags {
5042 const globalFlags = ( globalThis as { __NUXT_BETTER_AUTH_TEST_FLAGS__ ?: RuntimeFlags } ) . __NUXT_BETTER_AUTH_TEST_FLAGS__
5143 if ( globalFlags )
@@ -81,7 +73,6 @@ export function useUserSession(): UseUserSessionReturn {
8173 const requestURL = useRequestURL ( )
8274 const nuxtApp = useNuxtApp ( )
8375
84- // Client only - create better-auth client for client-side operations (singleton)
8576 const client : AppAuthClient | null = runtimeFlags . client
8677 ? getClient ( runtimeConfig . public . siteUrl || requestURL . origin )
8778 : null
@@ -114,10 +105,8 @@ export function useUserSession(): UseUserSessionReturn {
114105 return false
115106 if ( ! nuxtApp . payload . serverRendered )
116107 return false
117- // Don't skip for prerendered/cached payloads; we want a real session check after mount.
118108 if ( isPrerenderedPayload . value )
119109 return false
120- // SSR already hydrated state: avoid duplicate /api/auth/get-session on first paint.
121110 return Boolean ( session . value && user . value )
122111 } )
123112
@@ -142,6 +131,13 @@ export function useUserSession(): UseUserSessionReturn {
142131 user . value = null
143132 }
144133
134+ async function fetchSession ( options : { headers ?: HeadersInit , force ?: boolean } = { } ) {
135+ if ( runtimeFlags . server )
136+ return fetchSessionServer ( session , user , authReady , options )
137+ if ( client )
138+ return fetchSessionClient ( client , session , user , authReady , options )
139+ }
140+
145141 async function updateUser ( updates : Partial < AuthUser > ) {
146142 if ( ! user . value )
147143 return
@@ -173,11 +169,9 @@ export function useUserSession(): UseUserSessionReturn {
173169 }
174170
175171 // On client, subscribe to better-auth's reactive session store
176- // This auto-updates when signIn/signUp/signOut triggers the session signal
177172 if ( runtimeFlags . client && client && ! shouldSkipInitialClientSessionFetch . value ) {
178173 const clientSession = client . useSession ( )
179174
180- // Sync better-auth's reactive session to our useState
181175 watch (
182176 ( ) => clientSession . value ,
183177 ( newSession ) => {
@@ -190,9 +184,7 @@ export function useUserSession(): UseUserSessionReturn {
190184 return
191185
192186 if ( newSession ?. data ?. session && newSession ?. data ?. user ) {
193- // Filter out sensitive token field
194- const { token : _ , ...safeSession } = newSession . data . session as AuthSession & { token ?: string }
195- session . value = safeSession as AuthSession
187+ session . value = stripToken ( newSession . data . session as AuthSession & { token ?: string } )
196188 user . value = newSession . data . user as AuthUser
197189 }
198190 else if ( ! newSession ?. isPending && ! newSession ?. isRefetching ) {
@@ -235,142 +227,22 @@ export function useUserSession(): UseUserSessionReturn {
235227 resolve ( )
236228 }
237229 } )
238- // Timeout fallback to prevent hanging
239230 setTimeout ( ( ) => {
240231 unwatch ( )
241232 resolve ( )
242233 } , 5000 )
243234 } )
244235 }
245236
246- function isSafeLocalRedirect ( redirect : unknown ) : string | undefined {
247- if ( typeof redirect !== 'string' )
248- return
249- if ( ! redirect . startsWith ( '/' ) || redirect . startsWith ( '//' ) )
250- return
251- return redirect
252- }
253-
254- function resolvePostAuthRedirect ( ) : string | undefined {
255- const authConfig = runtimeConfig . public . auth as { redirects ?: { authenticated ?: string } , redirectQueryKey ?: string } | undefined
256- const redirectQueryKey = authConfig ?. redirectQueryKey ?? 'redirect'
257- const queryRedirect = requestURL . searchParams ?. get ( redirectQueryKey )
258- const safeQueryRedirect = isSafeLocalRedirect ( queryRedirect )
259- if ( safeQueryRedirect )
260- return safeQueryRedirect
261- return isSafeLocalRedirect ( authConfig ?. redirects ?. authenticated )
262- }
263-
264- function resolvePostAuthSuccessRedirect ( ) : ( ( ) => Promise < void > ) | undefined {
265- const target = resolvePostAuthRedirect ( )
266- if ( ! target )
267- return
268- return async ( ) => {
269- await navigateTo ( target )
270- }
271- }
272-
273- function withFallbackSocialCallbackURL ( data : unknown ) : unknown {
274- const callbackURL = resolvePostAuthRedirect ( )
275- if ( ! callbackURL )
276- return data
277-
278- if ( ! isRecord ( data ) )
279- return { callbackURL }
280- if ( typeof data . callbackURL === 'string' )
281- return data
282-
283- return {
284- ...data ,
285- callbackURL,
286- }
287- }
288-
289- // Wrap signIn methods to wait for session sync before calling onSuccess
237+ // Wrap signIn/signUp methods to sync session before executing onSuccess
290238 type SignIn = NonNullable < AppAuthClient > [ 'signIn' ]
291239 type SignUp = NonNullable < AppAuthClient > [ 'signUp' ]
292240
293- // Wraps onSuccess callback to sync session before executing
294- function wrapOnSuccess ( cb : ( ctx : unknown ) => void | Promise < void > ) {
295- return async ( ctx : unknown ) => {
296- await fetchSession ( { force : true } )
297- if ( ! loggedIn . value )
298- await waitForSession ( )
299- await nextTick ( )
300- await cb ( ctx )
301- }
302- }
303-
304- function wrapAuthMethod < T extends ( ...args : unknown [ ] ) => Promise < unknown > > (
305- method : T ,
306- wrapOptions : {
307- shouldSkipSessionSync ?: ( data : unknown , options : unknown ) => boolean
308- transformData ?: ( data : unknown , options : unknown ) => unknown
309- } = { } ,
310- ) : T {
311- return ( async ( ...args : unknown [ ] ) => {
312- const originalData = args [ 0 ]
313- const options = args [ 1 ]
314- const data = wrapOptions . transformData ?.( originalData , options ) ?? originalData
315- const dataRecord = isRecord ( data ) ? data : undefined
316- const optionsRecord = isRecord ( options ) ? options : undefined
317-
318- if ( wrapOptions . shouldSkipSessionSync ?.( data , options ) )
319- return method ( data , options )
320-
321- type OnSuccess = ( ctx : unknown ) => void | Promise < void >
322- const fetchOptions = isRecord ( dataRecord ?. fetchOptions ) ? dataRecord . fetchOptions : undefined
323- const nestedOnSuccess = fetchOptions ?. onSuccess
324- const topLevelOnSuccess = optionsRecord ?. onSuccess
325-
326- const fallbackOnSuccess = resolvePostAuthSuccessRedirect ( )
327- const wrappedFallbackOnSuccess = fallbackOnSuccess && wrapOnSuccess ( async ( ) => {
328- if ( ! loggedIn . value )
329- return
330- await fallbackOnSuccess ( )
331- } )
332-
333- // Passkey pattern: onSuccess in data.fetchOptions
334- if ( typeof nestedOnSuccess === 'function' ) {
335- const nextData = {
336- ...dataRecord ,
337- fetchOptions : {
338- ...fetchOptions ,
339- onSuccess : wrapOnSuccess ( nestedOnSuccess as OnSuccess ) ,
340- } ,
341- }
342- return method ( nextData as unknown as Parameters < T > [ 0 ] , options as unknown as Parameters < T > [ 1 ] )
343- }
344- // Email/social pattern: onSuccess in options
345- if ( typeof topLevelOnSuccess === 'function' ) {
346- const nextOptions = {
347- ...optionsRecord ,
348- onSuccess : wrapOnSuccess ( topLevelOnSuccess as OnSuccess ) ,
349- }
350- return method ( data as unknown as Parameters < T > [ 0 ] , nextOptions as unknown as Parameters < T > [ 1 ] )
351- }
352-
353- if ( wrappedFallbackOnSuccess ) {
354- if ( fetchOptions ) {
355- const nextData = {
356- ...dataRecord ,
357- fetchOptions : {
358- ...fetchOptions ,
359- onSuccess : wrappedFallbackOnSuccess ,
360- } ,
361- }
362- return method ( nextData as unknown as Parameters < T > [ 0 ] , options as unknown as Parameters < T > [ 1 ] )
363- }
364-
365- const nextOptions = {
366- ...optionsRecord ,
367- onSuccess : wrappedFallbackOnSuccess ,
368- }
369- return method ( data as unknown as Parameters < T > [ 0 ] , nextOptions as unknown as Parameters < T > [ 1 ] )
370- }
371-
372- return method ( data , options )
373- } ) as T
241+ const wrapDeps = {
242+ fetchSession,
243+ loggedIn,
244+ waitForSession,
245+ resolvePostAuthSuccessRedirect : ( ) => resolvePostAuthSuccessRedirect ( requestURL ) ,
374246 }
375247
376248 const signIn : SignIn = client ?. signIn
@@ -386,10 +258,10 @@ export function useUserSession(): UseUserSessionReturn {
386258 return socialData ?. disableRedirect !== true
387259 }
388260 : undefined
389- const transformData = prop === 'social' ? withFallbackSocialCallbackURL : undefined
390- // Don't bind - call through target to preserve better-auth's Proxy context
261+ const transformData = prop === 'social' ? ( data : unknown ) => withFallbackSocialCallbackURL ( data , requestURL ) : undefined
391262 return wrapAuthMethod (
392263 ( ...args : unknown [ ] ) => ( targetRecord [ prop ] as ( ...a : unknown [ ] ) => Promise < unknown > ) ( ...args ) ,
264+ wrapDeps ,
393265 { shouldSkipSessionSync, transformData } ,
394266 )
395267 } ,
@@ -405,70 +277,13 @@ export function useUserSession(): UseUserSessionReturn {
405277 const method = targetRecord [ prop ]
406278 if ( typeof method !== 'function' )
407279 return method
408- // Don't bind - call through target to preserve better-auth's Proxy context
409- return wrapAuthMethod ( ( ...args : unknown [ ] ) => ( targetRecord [ prop ] as ( ...a : unknown [ ] ) => Promise < unknown > ) ( ...args ) )
280+ return wrapAuthMethod ( ( ...args : unknown [ ] ) => ( targetRecord [ prop ] as ( ...a : unknown [ ] ) => Promise < unknown > ) ( ...args ) , wrapDeps )
410281 } ,
411282 } )
412283 : new Proxy ( { } as SignUp , {
413284 get : ( _ , prop ) => { throw new Error ( `signUp.${ String ( prop ) } () can only be called on client-side` ) } ,
414285 } )
415286
416- async function fetchSession ( options : { headers ?: HeadersInit , force ?: boolean } = { } ) {
417- if ( runtimeFlags . server ) {
418- try {
419- const headers = options . headers || useRequestHeaders ( [ 'cookie' ] )
420- const requestFetch = useRequestFetch ( )
421- const data = await requestFetch < SessionResponse | null > ( '/api/auth/get-session' , { headers } )
422-
423- if ( data ?. session && data ?. user ) {
424- const { token : _ , ...safeSession } = data . session
425- session . value = safeSession as AuthSession
426- user . value = data . user
427- }
428- else {
429- clearSession ( )
430- }
431- }
432- catch {
433- clearSession ( )
434- }
435- finally {
436- if ( ! authReady . value )
437- authReady . value = true
438- }
439- return
440- }
441-
442- if ( client ) {
443- try {
444- const headers = options . headers || useRequestHeaders ( [ 'cookie' ] )
445- const fetchOptions = headers ? { headers } : undefined
446- const query = options . force ? { disableCookieCache : true } : undefined
447- const result = await client . getSession ( { query } , fetchOptions )
448- const data = result . data as SessionResponse | null
449-
450- if ( data ?. session && data ?. user ) {
451- // Filter out sensitive token field
452- const { token : _ , ...safeSession } = data . session
453- session . value = safeSession as AuthSession
454- user . value = data . user
455- }
456- else {
457- clearSession ( )
458- }
459- }
460- catch ( error ) {
461- clearSession ( )
462- if ( ! isExpectedSignedOutSessionError ( error ) )
463- console . error ( '[nuxt-better-auth] Failed to fetch session:' , error )
464- }
465- finally {
466- if ( ! authReady . value )
467- authReady . value = true
468- }
469- }
470- }
471-
472287 if ( runtimeFlags . client && client && shouldSkipInitialClientSessionFetch . value ) {
473288 ensureSessionSignalListener ( client , ( ) => fetchSession ( { force : true } ) )
474289 }
0 commit comments