@@ -114,11 +114,17 @@ setWorldConstructor(GdbWorld);
114114
115115/**
116116 * Internal helper: login with given credentials and write the token to CLI config.
117- * Retries up to 3 times to handle race conditions where the DB was just
118- * cleared and the server hasn't yet recreated users.
117+ * Retries to handle two failure windows:
118+ * - the DB was just cleared and the server hasn't yet recreated users
119+ * - the server's logout invalidates ALL of the user's tokens issued in the
120+ * same wall-clock second (`invalidatedBefore = now + 1s`), so a token minted
121+ * right after a logout in a preceding scenario is dead on arrival even
122+ * though the login itself succeeded (#142). The token is therefore verified
123+ * against /me before being handed to the scenario; the backoff spans more
124+ * than one second so at least one attempt lands past the invalidation window.
119125 */
120126async function loginAs ( world : GdbWorld , email : string , password : string ) : Promise < Record < string , unknown > > {
121- const maxRetries = 3 ;
127+ const maxRetries = 5 ;
122128 let lastError : Error | undefined ;
123129
124130 for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
@@ -142,6 +148,14 @@ async function loginAs(world: GdbWorld, email: string, password: string): Promis
142148 continue ;
143149 }
144150
151+ const meRes = await fetch ( new URL ( "/me" , world . serverUrl ) . toString ( ) , {
152+ headers : { Authorization : `Bearer ${ token } ` } ,
153+ } ) ;
154+ if ( ! meRes . ok ) {
155+ lastError = new Error ( `Fresh token rejected by /me: HTTP ${ meRes . status } (token invalidation window, see #142)` ) ;
156+ continue ;
157+ }
158+
145159 const profile : Record < string , unknown > = { url : world . serverUrl , token } ;
146160 if ( data . refreshToken ) profile . refreshToken = data . refreshToken ;
147161 world . writeConfig ( {
0 commit comments