@@ -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 ) {
@@ -60,19 +66,16 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
6066 }
6167
6268 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
69+ const credentials = await this . loadCredentials ( )
6570
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.
71+ if ( credentials ) {
72+ this . credentials = credentials
73+ this . state = "inactive-session"
74+ this . timer . start ( )
75+ } else {
7076 const previousState = this . state
7177 this . state = "logged-out"
7278 this . emit ( "logged-out" , { previousState } )
73- } else {
74- this . state = "inactive-session"
75- this . timer . start ( )
7679 }
7780
7881 console . log ( `[auth] Initialized with state: ${ this . state } ` )
@@ -82,6 +85,32 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
8285 }
8386 }
8487
88+ private async storeCredentials ( credentials : AuthCredentials ) : Promise < void > {
89+ await this . context . secrets . store ( AUTH_CREDENTIALS_KEY , JSON . stringify ( credentials ) )
90+ }
91+
92+ private async loadCredentials ( ) : Promise < AuthCredentials | null > {
93+ const credentialsJson = await this . context . secrets . get ( AUTH_CREDENTIALS_KEY )
94+ if ( ! credentialsJson ) return null
95+
96+ try {
97+ const parsedJson = JSON . parse ( credentialsJson )
98+ // Validate using Zod schema
99+ return authCredentialsSchema . parse ( parsedJson )
100+ } catch ( error ) {
101+ if ( error instanceof z . ZodError ) {
102+ console . error ( "[auth] Invalid credentials format:" , error . errors )
103+ } else {
104+ console . error ( "[auth] Failed to parse stored credentials:" , error )
105+ }
106+ return null
107+ }
108+ }
109+
110+ private async clearCredentials ( ) : Promise < void > {
111+ await this . context . secrets . delete ( AUTH_CREDENTIALS_KEY )
112+ }
113+
85114 /**
86115 * Start the login process
87116 *
@@ -132,13 +161,11 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
132161 throw new Error ( "Invalid state parameter. Authentication request may have been tampered with." )
133162 }
134163
135- const { clientToken , sessionToken, sessionId } = await this . clerkSignIn ( code )
164+ const { credentials , sessionToken } = await this . clerkSignIn ( code )
136165
137- await this . context . secrets . store ( CLIENT_TOKEN_KEY , clientToken )
138- await this . context . globalState . update ( SESSION_ID_KEY , sessionId )
166+ await this . storeCredentials ( credentials )
139167
140- this . clientToken = clientToken
141- this . sessionId = sessionId
168+ this . credentials = credentials
142169 this . sessionToken = sessionToken
143170
144171 const previousState = this . state
@@ -168,23 +195,20 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
168195 try {
169196 this . timer . stop ( )
170197
171- await this . context . secrets . delete ( CLIENT_TOKEN_KEY )
172- await this . context . globalState . update ( SESSION_ID_KEY , undefined )
198+ await this . clearCredentials ( )
173199 await this . context . globalState . update ( AUTH_STATE_KEY , undefined )
174200
175- const oldClientToken = this . clientToken
176- const oldSessionId = this . sessionId
201+ const oldCredentials = this . credentials
177202
178- this . clientToken = null
203+ this . credentials = null
179204 this . sessionToken = null
180- this . sessionId = null
181205 this . userInfo = null
182206 const previousState = this . state
183207 this . state = "logged-out"
184208 this . emit ( "logged-out" , { previousState } )
185209
186- if ( oldClientToken && oldSessionId ) {
187- await this . clerkLogout ( oldClientToken , oldSessionId )
210+ if ( oldCredentials ) {
211+ await this . clerkLogout ( oldCredentials )
188212 }
189213
190214 this . fetchUserInfo ( )
@@ -228,8 +252,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
228252 * This method refreshes the session token using the client token.
229253 */
230254 private async refreshSession ( ) : Promise < void > {
231- if ( ! this . sessionId || ! this . clientToken ) {
232- console . log ( "[auth] Cannot refresh session: missing session ID or token " )
255+ if ( ! this . credentials ) {
256+ console . log ( "[auth] Cannot refresh session: missing credentials " )
233257 this . state = "inactive-session"
234258 return
235259 }
@@ -245,7 +269,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
245269 }
246270
247271 private async fetchUserInfo ( ) : Promise < void > {
248- if ( ! this . clientToken ) {
272+ if ( ! this . credentials ) {
249273 return
250274 }
251275
@@ -262,9 +286,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
262286 return this . userInfo
263287 }
264288
265- private async clerkSignIn (
266- ticket : string ,
267- ) : Promise < { clientToken : string ; sessionToken : string ; sessionId : string } > {
289+ private async clerkSignIn ( ticket : string ) : Promise < { credentials : AuthCredentials ; sessionToken : string } > {
268290 const formData = new URLSearchParams ( )
269291 formData . append ( "strategy" , "ticket" )
270292 formData . append ( "ticket" , ticket )
@@ -284,14 +306,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
284306 }
285307
286308 // 4. Find the session using created_session_id and extract the JWT.
287- const createdSessionId = response . data ?. response ?. created_session_id
309+ const sessionId = response . data ?. response ?. created_session_id
288310
289- if ( ! createdSessionId ) {
311+ if ( ! sessionId ) {
290312 throw new Error ( "No session ID found in the response" )
291313 }
292314
293315 // Find the session in the client sessions array.
294- const session = response . data ?. client ?. sessions ?. find ( ( s : { id : string } ) => s . id === createdSessionId )
316+ const session = response . data ?. client ?. sessions ?. find ( ( s : { id : string } ) => s . id === sessionId )
295317
296318 if ( ! session ) {
297319 throw new Error ( "Session not found in the response" )
@@ -304,20 +326,22 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
304326 throw new Error ( "Session does not have a token" )
305327 }
306328
307- return { clientToken, sessionToken, sessionId : session . id }
329+ const credentials = authCredentialsSchema . parse ( { clientToken, sessionId } )
330+
331+ return { credentials, sessionToken }
308332 }
309333
310334 private async clerkCreateSessionToken ( ) : Promise < string > {
311335 const formData = new URLSearchParams ( )
312336 formData . append ( "_is_native" , "1" )
313337
314338 const response = await axios . post (
315- `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . sessionId } /tokens` ,
339+ `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . credentials ! . sessionId } /tokens` ,
316340 formData ,
317341 {
318342 headers : {
319343 "Content-Type" : "application/x-www-form-urlencoded" ,
320- Authorization : `Bearer ${ this . clientToken } ` ,
344+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
321345 "User-Agent" : this . userAgent ( ) ,
322346 } ,
323347 } ,
@@ -335,7 +359,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
335359 private async clerkMe ( ) : Promise < CloudUserInfo > {
336360 const response = await axios . get ( `${ getClerkBaseUrl ( ) } /v1/me` , {
337361 headers : {
338- Authorization : `Bearer ${ this . clientToken } ` ,
362+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
339363 "User-Agent" : this . userAgent ( ) ,
340364 } ,
341365 } )
@@ -362,13 +386,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
362386 return userInfo
363387 }
364388
365- private async clerkLogout ( clientToken : string , sessionId : string ) : Promise < void > {
389+ private async clerkLogout ( credentials : AuthCredentials ) : Promise < void > {
366390 const formData = new URLSearchParams ( )
367391 formData . append ( "_is_native" , "1" )
368392
369- await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ sessionId } /remove` , formData , {
393+ await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , formData , {
370394 headers : {
371- Authorization : `Bearer ${ clientToken } ` ,
395+ Authorization : `Bearer ${ credentials . clientToken } ` ,
372396 "User-Agent" : this . userAgent ( ) ,
373397 } ,
374398 } )
0 commit comments