@@ -463,6 +463,101 @@ describe("Stateful Session Store", async () => {
463463 } ) ;
464464
465465 describe ( "rolling session race condition" , async ( ) => {
466+ it ( "should use store.update() atomically when implemented and bail if it returns false" , async ( ) => {
467+ // store.update() returns false → session deleted by concurrent logout.
468+ // The SDK must not fall through to store.set() — zero TOCTOU gap.
469+ const sessionId = "ses_atomic" ;
470+ const secret = await generateSecret ( 32 ) ;
471+ const session : SessionData = {
472+ user : { sub : "user_123" } ,
473+ tokenSet : {
474+ accessToken : "at_123" ,
475+ refreshToken : "rt_123" ,
476+ expiresAt : 123456
477+ } ,
478+ internal : {
479+ sid : "auth0-sid" ,
480+ createdAt : Math . floor ( Date . now ( ) / 1000 )
481+ }
482+ } ;
483+ const store = {
484+ get : vi . fn ( ) ,
485+ set : vi . fn ( ) ,
486+ delete : vi . fn ( ) ,
487+ update : vi . fn ( ) . mockResolvedValue ( false ) // session already gone
488+ } ;
489+ const maxAge = 60 * 60 ;
490+ const expiration = Math . floor ( Date . now ( ) / 1000 + maxAge ) ;
491+ const encryptedCookieValue = await encrypt (
492+ { id : sessionId } ,
493+ secret ,
494+ expiration
495+ ) ;
496+ const headers = new Headers ( ) ;
497+ headers . append ( "cookie" , `__session=${ encryptedCookieValue } ` ) ;
498+ const requestCookies = new RequestCookies ( headers ) ;
499+ const responseCookies = new ResponseCookies ( new Headers ( ) ) ;
500+
501+ const sessionStore = new StatefulSessionStore ( {
502+ secret,
503+ store,
504+ rolling : true
505+ } ) ;
506+ await sessionStore . set ( requestCookies , responseCookies , session ) ;
507+
508+ expect ( store . update ) . toHaveBeenCalledWith ( sessionId , session ) ;
509+ expect ( store . set ) . not . toHaveBeenCalled ( ) ;
510+ expect ( store . get ) . not . toHaveBeenCalled ( ) ; // no fallback read
511+ expect ( responseCookies . get ( "__session" ) ) . toBeUndefined ( ) ;
512+ } ) ;
513+
514+ it ( "should use store.update() atomically and refresh the cookie when it returns true" , async ( ) => {
515+ // store.update() returns true → session found and updated in one DB op.
516+ const sessionId = "ses_atomic_alive" ;
517+ const secret = await generateSecret ( 32 ) ;
518+ const session : SessionData = {
519+ user : { sub : "user_123" } ,
520+ tokenSet : {
521+ accessToken : "at_123" ,
522+ refreshToken : "rt_123" ,
523+ expiresAt : 123456
524+ } ,
525+ internal : {
526+ sid : "auth0-sid" ,
527+ createdAt : Math . floor ( Date . now ( ) / 1000 )
528+ }
529+ } ;
530+ const store = {
531+ get : vi . fn ( ) ,
532+ set : vi . fn ( ) ,
533+ delete : vi . fn ( ) ,
534+ update : vi . fn ( ) . mockResolvedValue ( true )
535+ } ;
536+ const maxAge = 60 * 60 ;
537+ const expiration = Math . floor ( Date . now ( ) / 1000 + maxAge ) ;
538+ const encryptedCookieValue = await encrypt (
539+ { id : sessionId } ,
540+ secret ,
541+ expiration
542+ ) ;
543+ const headers = new Headers ( ) ;
544+ headers . append ( "cookie" , `__session=${ encryptedCookieValue } ` ) ;
545+ const requestCookies = new RequestCookies ( headers ) ;
546+ const responseCookies = new ResponseCookies ( new Headers ( ) ) ;
547+
548+ const sessionStore = new StatefulSessionStore ( {
549+ secret,
550+ store,
551+ rolling : true
552+ } ) ;
553+ await sessionStore . set ( requestCookies , responseCookies , session ) ;
554+
555+ expect ( store . update ) . toHaveBeenCalledWith ( sessionId , session ) ;
556+ expect ( store . set ) . not . toHaveBeenCalled ( ) ;
557+ expect ( store . get ) . not . toHaveBeenCalled ( ) ;
558+ expect ( responseCookies . get ( "__session" ) ) . toBeDefined ( ) ;
559+ } ) ;
560+
466561 it ( "should not re-create a session that was deleted by a concurrent logout" , async ( ) => {
467562 const sessionId = "ses_123" ;
468563 const secret = await generateSecret ( 32 ) ;
@@ -609,6 +704,45 @@ describe("Stateful Session Store", async () => {
609704 expect ( store . set ) . toHaveBeenCalledOnce ( ) ;
610705 expect ( responseCookies . get ( "__session" ) ) . toBeDefined ( ) ;
611706 } ) ;
707+
708+ it ( "should call store.set() and not store.update() for a brand-new session even when update() is implemented" , async ( ) => {
709+ // When there is no existing session cookie, existingSessionId is null and the guard
710+ // is bypassed entirely. store.set() must be called to create the new session;
711+ // store.update() must NOT be called — it would immediately return false (nothing
712+ // exists yet) and silently swallow the new session creation.
713+ const secret = await generateSecret ( 32 ) ;
714+ const session : SessionData = {
715+ user : { sub : "user_123" } ,
716+ tokenSet : {
717+ accessToken : "at" ,
718+ refreshToken : "rt" ,
719+ expiresAt : 123456
720+ } ,
721+ internal : {
722+ sid : "sid" ,
723+ createdAt : Math . floor ( Date . now ( ) / 1000 )
724+ }
725+ } ;
726+ const store = {
727+ get : vi . fn ( ) ,
728+ set : vi . fn ( ) ,
729+ delete : vi . fn ( ) ,
730+ update : vi . fn ( ) // must NOT be called for new sessions
731+ } ;
732+ const requestCookies = new RequestCookies ( new Headers ( ) ) ; // no existing cookie
733+ const responseCookies = new ResponseCookies ( new Headers ( ) ) ;
734+
735+ const sessionStore = new StatefulSessionStore ( {
736+ secret,
737+ store,
738+ rolling : true
739+ } ) ;
740+ await sessionStore . set ( requestCookies , responseCookies , session ) ;
741+
742+ expect ( store . update ) . not . toHaveBeenCalled ( ) ;
743+ expect ( store . set ) . toHaveBeenCalledOnce ( ) ;
744+ expect ( responseCookies . get ( "__session" ) ) . toBeDefined ( ) ;
745+ } ) ;
612746 } ) ;
613747
614748 describe ( "session fixation" , async ( ) => {
0 commit comments