@@ -3,6 +3,7 @@ import EventEmitter from "events"
33
44import axios from "axios"
55import * as vscode from "vscode"
6+ import { z } from "zod"
67
78import type { CloudUserInfo } from "@roo-code/types"
89
@@ -15,8 +16,14 @@ export interface AuthServiceEvents {
1516 "user-info" : [ data : { userInfo : CloudUserInfo } ]
1617}
1718
18- const CLIENT_TOKEN_KEY = "clerk-client-token"
19- const SESSION_ID_KEY = "clerk-session-id"
19+ const authCredentialsSchema = z . object ( {
20+ clientToken : z . string ( ) . min ( 1 , "Client token cannot be empty" ) ,
21+ sessionId : z . string ( ) . min ( 1 , "Session ID cannot be empty" ) ,
22+ } )
23+
24+ type AuthCredentials = z . infer < typeof authCredentialsSchema >
25+
26+ const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials"
2027const AUTH_STATE_KEY = "clerk-auth-state"
2128
2229type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
@@ -26,9 +33,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
2633 private timer : RefreshTimer
2734 private state : AuthState = "initializing"
2835
29- private clientToken : string | null = null
36+ private credentials : AuthCredentials | null = null
3037 private sessionToken : string | null = null
31- private sessionId : string | null = null
3238 private userInfo : CloudUserInfo | null = null
3339
3440 constructor ( context : vscode . ExtensionContext ) {
@@ -47,6 +53,55 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
4753 } )
4854 }
4955
56+ private async handleCredentialsChange ( ) : Promise < void > {
57+ try {
58+ const credentials = await this . loadCredentials ( )
59+
60+ if ( credentials ) {
61+ if (
62+ this . credentials === null ||
63+ this . credentials . clientToken !== credentials . clientToken ||
64+ this . credentials . sessionId !== credentials . sessionId
65+ ) {
66+ this . transitionToInactiveSession ( credentials )
67+ }
68+ } else {
69+ if ( this . state !== "logged-out" ) {
70+ this . transitionToLoggedOut ( )
71+ }
72+ }
73+ } catch ( error ) {
74+ console . error ( "[auth] Error handling credentials change:" , error )
75+ }
76+ }
77+
78+ private transitionToLoggedOut ( ) : void {
79+ this . timer . stop ( )
80+
81+ const previousState = this . state
82+
83+ this . credentials = null
84+ this . sessionToken = null
85+ this . userInfo = null
86+ this . state = "logged-out"
87+
88+ this . emit ( "logged-out" , { previousState } )
89+
90+ console . log ( "[auth] Transitioned to logged-out state" )
91+ }
92+
93+ private transitionToInactiveSession ( credentials : AuthCredentials ) : void {
94+ this . credentials = credentials
95+ this . state = "inactive-session"
96+
97+ this . sessionToken = null
98+ this . userInfo = null
99+
100+ this . timer . start ( )
101+
102+ console . log ( "[auth] Transitioned to inactive-session state" )
103+ }
104+
50105 /**
51106 * Initialize the auth state
52107 *
@@ -59,29 +114,42 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
59114 return
60115 }
61116
117+ await this . handleCredentialsChange ( )
118+
119+ this . context . subscriptions . push (
120+ this . context . secrets . onDidChange ( ( e ) => {
121+ if ( e . key === AUTH_CREDENTIALS_KEY ) {
122+ this . handleCredentialsChange ( )
123+ }
124+ } ) ,
125+ )
126+ }
127+
128+ private async storeCredentials ( credentials : AuthCredentials ) : Promise < void > {
129+ await this . context . secrets . store ( AUTH_CREDENTIALS_KEY , JSON . stringify ( credentials ) )
130+ }
131+
132+ private async loadCredentials ( ) : Promise < AuthCredentials | null > {
133+ const credentialsJson = await this . context . secrets . get ( AUTH_CREDENTIALS_KEY )
134+ if ( ! credentialsJson ) return null
135+
62136 try {
63- this . clientToken = ( await this . context . secrets . get ( CLIENT_TOKEN_KEY ) ) || null
64- this . sessionId = this . context . globalState . get < string > ( SESSION_ID_KEY ) || null
65-
66- // Determine initial state.
67- if ( ! this . clientToken || ! this . sessionId ) {
68- // TODO: it may be possible to get a new session with the client,
69- // but the obvious Clerk endpoints don't support that.
70- const previousState = this . state
71- this . state = "logged-out"
72- this . emit ( "logged-out" , { previousState } )
137+ const parsedJson = JSON . parse ( credentialsJson )
138+ return authCredentialsSchema . parse ( parsedJson )
139+ } catch ( error ) {
140+ if ( error instanceof z . ZodError ) {
141+ console . error ( "[auth] Invalid credentials format:" , error . errors )
73142 } else {
74- this . state = "inactive-session"
75- this . timer . start ( )
143+ console . error ( "[auth] Failed to parse stored credentials:" , error )
76144 }
77-
78- console . log ( `[auth] Initialized with state: ${ this . state } ` )
79- } catch ( error ) {
80- console . error ( `[auth] Error initializing AuthService: ${ error } ` )
81- this . state = "logged-out"
145+ return null
82146 }
83147 }
84148
149+ private async clearCredentials ( ) : Promise < void > {
150+ await this . context . secrets . delete ( AUTH_CREDENTIALS_KEY )
151+ }
152+
85153 /**
86154 * Start the login process
87155 *
@@ -132,21 +200,9 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
132200 throw new Error ( "Invalid state parameter. Authentication request may have been tampered with." )
133201 }
134202
135- const { clientToken, sessionToken, sessionId } = await this . clerkSignIn ( code )
136-
137- await this . context . secrets . store ( CLIENT_TOKEN_KEY , clientToken )
138- await this . context . globalState . update ( SESSION_ID_KEY , sessionId )
203+ const { credentials } = await this . clerkSignIn ( code )
139204
140- this . clientToken = clientToken
141- this . sessionId = sessionId
142- this . sessionToken = sessionToken
143-
144- const previousState = this . state
145- this . state = "active-session"
146- this . emit ( "active-session" , { previousState } )
147- this . timer . start ( )
148-
149- this . fetchUserInfo ( )
205+ await this . storeCredentials ( credentials )
150206
151207 vscode . window . showInformationMessage ( "Successfully authenticated with Roo Code Cloud" )
152208 console . log ( "[auth] Successfully authenticated with Roo Code Cloud" )
@@ -165,30 +221,21 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
165221 * This method removes all stored tokens and stops the refresh timer.
166222 */
167223 public async logout ( ) : Promise < void > {
168- try {
169- this . timer . stop ( )
224+ const oldCredentials = this . credentials
170225
171- await this . context . secrets . delete ( CLIENT_TOKEN_KEY )
172- await this . context . globalState . update ( SESSION_ID_KEY , undefined )
226+ try {
227+ // Clear credentials from storage - onDidChange will handle state transitions
228+ await this . clearCredentials ( )
173229 await this . context . globalState . update ( AUTH_STATE_KEY , undefined )
174230
175- const oldClientToken = this . clientToken
176- const oldSessionId = this . sessionId
177-
178- this . clientToken = null
179- this . sessionToken = null
180- this . sessionId = null
181- this . userInfo = null
182- const previousState = this . state
183- this . state = "logged-out"
184- this . emit ( "logged-out" , { previousState } )
185-
186- if ( oldClientToken && oldSessionId ) {
187- await this . clerkLogout ( oldClientToken , oldSessionId )
231+ if ( oldCredentials ) {
232+ try {
233+ await this . clerkLogout ( oldCredentials )
234+ } catch ( error ) {
235+ console . error ( "[auth] Error calling clerkLogout:" , error )
236+ }
188237 }
189238
190- this . fetchUserInfo ( )
191-
192239 vscode . window . showInformationMessage ( "Logged out from Roo Code Cloud" )
193240 console . log ( "[auth] Logged out from Roo Code Cloud" )
194241 } catch ( error ) {
@@ -228,8 +275,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
228275 * This method refreshes the session token using the client token.
229276 */
230277 private async refreshSession ( ) : Promise < void > {
231- if ( ! this . sessionId || ! this . clientToken ) {
232- console . log ( "[auth] Cannot refresh session: missing session ID or token " )
278+ if ( ! this . credentials ) {
279+ console . log ( "[auth] Cannot refresh session: missing credentials " )
233280 this . state = "inactive-session"
234281 return
235282 }
@@ -239,13 +286,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
239286 this . state = "active-session"
240287
241288 if ( previousState !== "active-session" ) {
289+ console . log ( "[auth] Transitioned to active-session state" )
242290 this . emit ( "active-session" , { previousState } )
243291 this . fetchUserInfo ( )
244292 }
245293 }
246294
247295 private async fetchUserInfo ( ) : Promise < void > {
248- if ( ! this . clientToken ) {
296+ if ( ! this . credentials ) {
249297 return
250298 }
251299
@@ -262,9 +310,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
262310 return this . userInfo
263311 }
264312
265- private async clerkSignIn (
266- ticket : string ,
267- ) : Promise < { clientToken : string ; sessionToken : string ; sessionId : string } > {
313+ private async clerkSignIn ( ticket : string ) : Promise < { credentials : AuthCredentials ; sessionToken : string } > {
268314 const formData = new URLSearchParams ( )
269315 formData . append ( "strategy" , "ticket" )
270316 formData . append ( "ticket" , ticket )
@@ -284,14 +330,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
284330 }
285331
286332 // 4. Find the session using created_session_id and extract the JWT.
287- const createdSessionId = response . data ?. response ?. created_session_id
333+ const sessionId = response . data ?. response ?. created_session_id
288334
289- if ( ! createdSessionId ) {
335+ if ( ! sessionId ) {
290336 throw new Error ( "No session ID found in the response" )
291337 }
292338
293339 // Find the session in the client sessions array.
294- const session = response . data ?. client ?. sessions ?. find ( ( s : { id : string } ) => s . id === createdSessionId )
340+ const session = response . data ?. client ?. sessions ?. find ( ( s : { id : string } ) => s . id === sessionId )
295341
296342 if ( ! session ) {
297343 throw new Error ( "Session not found in the response" )
@@ -304,20 +350,22 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
304350 throw new Error ( "Session does not have a token" )
305351 }
306352
307- return { clientToken, sessionToken, sessionId : session . id }
353+ const credentials = authCredentialsSchema . parse ( { clientToken, sessionId } )
354+
355+ return { credentials, sessionToken }
308356 }
309357
310358 private async clerkCreateSessionToken ( ) : Promise < string > {
311359 const formData = new URLSearchParams ( )
312360 formData . append ( "_is_native" , "1" )
313361
314362 const response = await axios . post (
315- `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . sessionId } /tokens` ,
363+ `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . credentials ! . sessionId } /tokens` ,
316364 formData ,
317365 {
318366 headers : {
319367 "Content-Type" : "application/x-www-form-urlencoded" ,
320- Authorization : `Bearer ${ this . clientToken } ` ,
368+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
321369 "User-Agent" : this . userAgent ( ) ,
322370 } ,
323371 } ,
@@ -335,7 +383,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
335383 private async clerkMe ( ) : Promise < CloudUserInfo > {
336384 const response = await axios . get ( `${ getClerkBaseUrl ( ) } /v1/me` , {
337385 headers : {
338- Authorization : `Bearer ${ this . clientToken } ` ,
386+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
339387 "User-Agent" : this . userAgent ( ) ,
340388 } ,
341389 } )
@@ -362,13 +410,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
362410 return userInfo
363411 }
364412
365- private async clerkLogout ( clientToken : string , sessionId : string ) : Promise < void > {
413+ private async clerkLogout ( credentials : AuthCredentials ) : Promise < void > {
366414 const formData = new URLSearchParams ( )
367415 formData . append ( "_is_native" , "1" )
368416
369- await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ sessionId } /remove` , formData , {
417+ await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , formData , {
370418 headers : {
371- Authorization : `Bearer ${ clientToken } ` ,
419+ Authorization : `Bearer ${ credentials . clientToken } ` ,
372420 "User-Agent" : this . userAgent ( ) ,
373421 } ,
374422 } )
0 commit comments