11import crypto from "crypto"
22import EventEmitter from "events"
33
4- import axios from "axios"
54import * as vscode from "vscode"
65import { z } from "zod"
76
@@ -30,6 +29,61 @@ const AUTH_STATE_KEY = "clerk-auth-state"
3029
3130type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
3231
32+ const clerkSignInResponseSchema = z . object ( {
33+ response : z . object ( {
34+ created_session_id : z . string ( ) ,
35+ } ) ,
36+ } )
37+
38+ const clerkCreateSessionTokenResponseSchema = z . object ( {
39+ jwt : z . string ( ) ,
40+ } )
41+
42+ const clerkMeResponseSchema = z . object ( {
43+ response : z . object ( {
44+ first_name : z . string ( ) . optional ( ) ,
45+ last_name : z . string ( ) . optional ( ) ,
46+ image_url : z . string ( ) . optional ( ) ,
47+ primary_email_address_id : z . string ( ) . optional ( ) ,
48+ email_addresses : z
49+ . array (
50+ z . object ( {
51+ id : z . string ( ) ,
52+ email_address : z . string ( ) ,
53+ } ) ,
54+ )
55+ . optional ( ) ,
56+ } ) ,
57+ } )
58+
59+ const clerkOrganizationMembershipsSchema = z . object ( {
60+ response : z . array (
61+ z . object ( {
62+ id : z . string ( ) ,
63+ role : z . string ( ) ,
64+ permissions : z . array ( z . string ( ) ) . optional ( ) ,
65+ created_at : z . number ( ) . optional ( ) ,
66+ updated_at : z . number ( ) . optional ( ) ,
67+ organization : z . object ( {
68+ id : z . string ( ) ,
69+ name : z . string ( ) ,
70+ slug : z . string ( ) . optional ( ) ,
71+ image_url : z . string ( ) . optional ( ) ,
72+ has_image : z . boolean ( ) . optional ( ) ,
73+ created_at : z . number ( ) . optional ( ) ,
74+ updated_at : z . number ( ) . optional ( ) ,
75+ } ) ,
76+ } ) ,
77+ ) ,
78+ } )
79+
80+ class InvalidClientTokenError extends Error {
81+ constructor ( ) {
82+ super ( "Invalid/Expired client token" )
83+ Object . setPrototypeOf ( this , InvalidClientTokenError . prototype )
84+ }
85+ }
86+
3387export class AuthService extends EventEmitter < AuthServiceEvents > {
3488 private context : vscode . ExtensionContext
3589 private timer : RefreshTimer
@@ -208,7 +262,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
208262 throw new Error ( "Invalid state parameter. Authentication request may have been tampered with." )
209263 }
210264
211- const { credentials } = await this . clerkSignIn ( code )
265+ const credentials = await this . clerkSignIn ( code )
212266
213267 await this . storeCredentials ( credentials )
214268
@@ -285,7 +339,6 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
285339 private async refreshSession ( ) : Promise < void > {
286340 if ( ! this . credentials ) {
287341 this . log ( "[auth] Cannot refresh session: missing credentials" )
288- this . state = "inactive-session"
289342 return
290343 }
291344
@@ -300,6 +353,10 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
300353 this . fetchUserInfo ( )
301354 }
302355 } catch ( error ) {
356+ if ( error instanceof InvalidClientTokenError ) {
357+ this . log ( "[auth] Invalid/Expired client token: clearing credentials" )
358+ this . clearCredentials ( )
359+ }
303360 this . log ( "[auth] Failed to refresh session" , error )
304361 throw error
305362 }
@@ -323,103 +380,93 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
323380 return this . userInfo
324381 }
325382
326- private async clerkSignIn ( ticket : string ) : Promise < { credentials : AuthCredentials ; sessionToken : string } > {
383+ private async clerkSignIn ( ticket : string ) : Promise < AuthCredentials > {
327384 const formData = new URLSearchParams ( )
328385 formData . append ( "strategy" , "ticket" )
329386 formData . append ( "ticket" , ticket )
330387
331- const response = await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sign_ins` , formData , {
388+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sign_ins` , {
389+ method : "POST" ,
332390 headers : {
333391 "Content-Type" : "application/x-www-form-urlencoded" ,
334392 "User-Agent" : this . userAgent ( ) ,
335393 } ,
394+ body : formData . toString ( ) ,
395+ signal : AbortSignal . timeout ( 10000 ) ,
336396 } )
337397
338- // 3. Extract the client token from the Authorization header.
339- const clientToken = response . headers . authorization
340-
341- if ( ! clientToken ) {
342- throw new Error ( "No authorization header found in the response" )
343- }
344-
345- // 4. Find the session using created_session_id and extract the JWT.
346- const sessionId = response . data ?. response ?. created_session_id
347-
348- if ( ! sessionId ) {
349- throw new Error ( "No session ID found in the response" )
398+ if ( ! response . ok ) {
399+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
350400 }
351401
352- // Find the session in the client sessions array.
353- const session = response . data ?. client ?. sessions ?. find ( ( s : { id : string } ) => s . id === sessionId )
354-
355- if ( ! session ) {
356- throw new Error ( "Session not found in the response" )
357- }
402+ const {
403+ response : { created_session_id : sessionId } ,
404+ } = clerkSignInResponseSchema . parse ( await response . json ( ) )
358405
359- // Extract the session token (JWT) and store it .
360- const sessionToken = session . last_active_token ?. jwt
406+ // 3. Extract the client token from the Authorization header .
407+ const clientToken = response . headers . get ( "authorization" )
361408
362- if ( ! sessionToken ) {
363- throw new Error ( "Session does not have a token " )
409+ if ( ! clientToken ) {
410+ throw new Error ( "No authorization header found in the response " )
364411 }
365412
366- const credentials = authCredentialsSchema . parse ( { clientToken, sessionId } )
367-
368- return { credentials, sessionToken }
413+ return authCredentialsSchema . parse ( { clientToken, sessionId } )
369414 }
370415
371416 private async clerkCreateSessionToken ( ) : Promise < string > {
372417 const formData = new URLSearchParams ( )
373418 formData . append ( "_is_native" , "1" )
374419
375- const response = await axios . post (
376- `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . credentials ! . sessionId } /tokens` ,
377- formData ,
378- {
379- headers : {
380- "Content-Type" : "application/x-www-form-urlencoded" ,
381- Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
382- "User-Agent" : this . userAgent ( ) ,
383- } ,
420+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . credentials ! . sessionId } /tokens` , {
421+ method : "POST" ,
422+ headers : {
423+ "Content-Type" : "application/x-www-form-urlencoded" ,
424+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
425+ "User-Agent" : this . userAgent ( ) ,
384426 } ,
385- )
386-
387- const sessionToken = response . data ?. jwt
427+ body : formData . toString ( ) ,
428+ signal : AbortSignal . timeout ( 10000 ) ,
429+ } )
388430
389- if ( ! sessionToken ) {
390- throw new Error ( "No JWT found in refresh response" )
431+ if ( response . status >= 400 && response . status < 500 ) {
432+ throw new InvalidClientTokenError ( )
433+ } else if ( ! response . ok ) {
434+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
391435 }
392436
393- return sessionToken
437+ const data = clerkCreateSessionTokenResponseSchema . parse ( await response . json ( ) )
438+
439+ return data . jwt
394440 }
395441
396442 private async clerkMe ( ) : Promise < CloudUserInfo > {
397- const response = await axios . get ( `${ getClerkBaseUrl ( ) } /v1/me` , {
443+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/me` , {
398444 headers : {
399445 Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
400446 "User-Agent" : this . userAgent ( ) ,
401447 } ,
448+ signal : AbortSignal . timeout ( 10000 ) ,
402449 } )
403450
404- const userData = response . data ?. response
405-
406- if ( ! userData ) {
407- throw new Error ( "No response user data" )
451+ if ( ! response . ok ) {
452+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
408453 }
409454
455+ const { response : userData } = clerkMeResponseSchema . parse ( await response . json ( ) )
456+
410457 const userInfo : CloudUserInfo = { }
411458
412- userInfo . name = `${ userData ? .first_name } ${ userData ? .last_name } `
413- const primaryEmailAddressId = userData ? .primary_email_address_id
414- const emailAddresses = userData ? .email_addresses
459+ userInfo . name = `${ userData . first_name } ${ userData . last_name } `
460+ const primaryEmailAddressId = userData . primary_email_address_id
461+ const emailAddresses = userData . email_addresses
415462
416463 if ( primaryEmailAddressId && emailAddresses ) {
417464 userInfo . email = emailAddresses . find (
418- ( email : { id : string } ) => primaryEmailAddressId === email ? .id ,
465+ ( email : { id : string } ) => primaryEmailAddressId === email . id ,
419466 ) ?. email_address
420467 }
421468
422- userInfo . picture = userData ? .image_url
469+ userInfo . picture = userData . image_url
423470
424471 // Fetch organization memberships separately
425472 try {
@@ -444,51 +491,38 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
444491 }
445492
446493 private async clerkGetOrganizationMemberships ( ) : Promise < CloudOrganizationMembership [ ] > {
447- const response = await axios . get ( `${ getClerkBaseUrl ( ) } /v1/me/organization_memberships` , {
494+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/me/organization_memberships` , {
448495 headers : {
449496 Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
450497 "User-Agent" : this . userAgent ( ) ,
451498 } ,
499+ signal : AbortSignal . timeout ( 10000 ) ,
452500 } )
453501
454- // The response structure is: { response: [...] }
455- // Extract the organization memberships from the response.response array
456- return response . data ?. response || [ ]
502+ return clerkOrganizationMembershipsSchema . parse ( await response . json ( ) ) . response
457503 }
458504
459505 private async clerkLogout ( credentials : AuthCredentials ) : Promise < void > {
460506 const formData = new URLSearchParams ( )
461507 formData . append ( "_is_native" , "1" )
462508
463- await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , formData , {
509+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , {
510+ method : "POST" ,
464511 headers : {
512+ "Content-Type" : "application/x-www-form-urlencoded" ,
465513 Authorization : `Bearer ${ credentials . clientToken } ` ,
466514 "User-Agent" : this . userAgent ( ) ,
467515 } ,
516+ body : formData . toString ( ) ,
517+ signal : AbortSignal . timeout ( 10000 ) ,
468518 } )
469- }
470-
471- private userAgent ( ) : string {
472- return getUserAgent ( this . context )
473- }
474519
475- private static _instance : AuthService | null = null
476-
477- static get instance ( ) {
478- if ( ! this . _instance ) {
479- throw new Error ( "AuthService not initialized" )
520+ if ( ! response . ok ) {
521+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
480522 }
481-
482- return this . _instance
483523 }
484524
485- static async createInstance ( context : vscode . ExtensionContext , log ?: ( ...args : unknown [ ] ) => void ) {
486- if ( this . _instance ) {
487- throw new Error ( "AuthService instance already created" )
488- }
489-
490- this . _instance = new AuthService ( context , log )
491- await this . _instance . initialize ( )
492- return this . _instance
525+ private userAgent ( ) : string {
526+ return getUserAgent ( this . context )
493527 }
494528}
0 commit comments