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,40 @@ 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+ class InvalidClientTokenError extends Error {
60+ constructor ( ) {
61+ super ( "Invalid/Expired client token" )
62+ Object . setPrototypeOf ( this , InvalidClientTokenError . prototype )
63+ }
64+ }
65+
3366export class AuthService extends EventEmitter < AuthServiceEvents > {
3467 private context : vscode . ExtensionContext
3568 private timer : RefreshTimer
@@ -208,7 +241,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
208241 throw new Error ( "Invalid state parameter. Authentication request may have been tampered with." )
209242 }
210243
211- const { credentials } = await this . clerkSignIn ( code )
244+ const credentials = await this . clerkSignIn ( code )
212245
213246 await this . storeCredentials ( credentials )
214247
@@ -285,7 +318,6 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
285318 private async refreshSession ( ) : Promise < void > {
286319 if ( ! this . credentials ) {
287320 this . log ( "[auth] Cannot refresh session: missing credentials" )
288- this . state = "inactive-session"
289321 return
290322 }
291323
@@ -300,6 +332,10 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
300332 this . fetchUserInfo ( )
301333 }
302334 } catch ( error ) {
335+ if ( error instanceof InvalidClientTokenError ) {
336+ this . log ( "[auth] Invalid/Expired client token: clearing credentials" )
337+ this . clearCredentials ( )
338+ }
303339 this . log ( "[auth] Failed to refresh session" , error )
304340 throw error
305341 }
@@ -323,120 +359,117 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
323359 return this . userInfo
324360 }
325361
326- private async clerkSignIn ( ticket : string ) : Promise < { credentials : AuthCredentials ; sessionToken : string } > {
362+ private async clerkSignIn ( ticket : string ) : Promise < AuthCredentials > {
327363 const formData = new URLSearchParams ( )
328364 formData . append ( "strategy" , "ticket" )
329365 formData . append ( "ticket" , ticket )
330366
331- const response = await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sign_ins` , formData , {
367+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sign_ins` , {
368+ method : "POST" ,
332369 headers : {
333370 "Content-Type" : "application/x-www-form-urlencoded" ,
334371 "User-Agent" : this . userAgent ( ) ,
335372 } ,
373+ body : formData . toString ( ) ,
374+ signal : AbortSignal . timeout ( 10000 ) ,
336375 } )
337376
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" )
377+ if ( ! response . ok ) {
378+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
343379 }
344380
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" )
350- }
351-
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- }
381+ const {
382+ response : { created_session_id : sessionId } ,
383+ } = clerkSignInResponseSchema . parse ( await response . json ( ) )
358384
359- // Extract the session token (JWT) and store it .
360- const sessionToken = session . last_active_token ?. jwt
385+ // 3. Extract the client token from the Authorization header .
386+ const clientToken = response . headers . get ( "authorization" )
361387
362- if ( ! sessionToken ) {
363- throw new Error ( "Session does not have a token " )
388+ if ( ! clientToken ) {
389+ throw new Error ( "No authorization header found in the response " )
364390 }
365391
366- const credentials = authCredentialsSchema . parse ( { clientToken, sessionId } )
367-
368- return { credentials, sessionToken }
392+ return authCredentialsSchema . parse ( { clientToken, sessionId } )
369393 }
370394
371395 private async clerkCreateSessionToken ( ) : Promise < string > {
372396 const formData = new URLSearchParams ( )
373397 formData . append ( "_is_native" , "1" )
374398
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- } ,
399+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ this . credentials ! . sessionId } /tokens` , {
400+ method : "POST" ,
401+ headers : {
402+ "Content-Type" : "application/x-www-form-urlencoded" ,
403+ Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
404+ "User-Agent" : this . userAgent ( ) ,
384405 } ,
385- )
386-
387- const sessionToken = response . data ?. jwt
406+ body : formData . toString ( ) ,
407+ signal : AbortSignal . timeout ( 10000 ) ,
408+ } )
388409
389- if ( ! sessionToken ) {
390- throw new Error ( "No JWT found in refresh response" )
410+ if ( response . status >= 400 && response . status < 500 ) {
411+ throw new InvalidClientTokenError ( )
412+ } else if ( ! response . ok ) {
413+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
391414 }
392415
393- return sessionToken
416+ const data = clerkCreateSessionTokenResponseSchema . parse ( await response . json ( ) )
417+
418+ return data . jwt
394419 }
395420
396421 private async clerkMe ( ) : Promise < CloudUserInfo > {
397- const response = await axios . get ( `${ getClerkBaseUrl ( ) } /v1/me` , {
422+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/me` , {
398423 headers : {
399424 Authorization : `Bearer ${ this . credentials ! . clientToken } ` ,
400425 "User-Agent" : this . userAgent ( ) ,
401426 } ,
427+ signal : AbortSignal . timeout ( 10000 ) ,
402428 } )
403429
404- const userData = response . data ?. response
405-
406- if ( ! userData ) {
407- throw new Error ( "No response user data" )
430+ if ( ! response . ok ) {
431+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
408432 }
409433
434+ const { response : userData } = clerkMeResponseSchema . parse ( await response . json ( ) )
435+
410436 const userInfo : CloudUserInfo = { }
411437
412- userInfo . name = `${ userData ? .first_name } ${ userData ? .last_name } `
413- const primaryEmailAddressId = userData ? .primary_email_address_id
414- const emailAddresses = userData ? .email_addresses
438+ userInfo . name = `${ userData . first_name } ${ userData . last_name } `
439+ const primaryEmailAddressId = userData . primary_email_address_id
440+ const emailAddresses = userData . email_addresses
415441
416442 if ( primaryEmailAddressId && emailAddresses ) {
417443 userInfo . email = emailAddresses . find (
418- ( email : { id : string } ) => primaryEmailAddressId === email ? .id ,
444+ ( email : { id : string } ) => primaryEmailAddressId === email . id ,
419445 ) ?. email_address
420446 }
421447
422- userInfo . picture = userData ? .image_url
448+ userInfo . picture = userData . image_url
423449 return userInfo
424450 }
425451
426452 private async clerkLogout ( credentials : AuthCredentials ) : Promise < void > {
427453 const formData = new URLSearchParams ( )
428454 formData . append ( "_is_native" , "1" )
429455
430- await axios . post ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , formData , {
456+ const response = await fetch ( `${ getClerkBaseUrl ( ) } /v1/client/sessions/${ credentials . sessionId } /remove` , {
457+ method : "POST" ,
431458 headers : {
459+ "Content-Type" : "application/x-www-form-urlencoded" ,
432460 Authorization : `Bearer ${ credentials . clientToken } ` ,
433461 "User-Agent" : this . userAgent ( ) ,
434462 } ,
463+ body : formData . toString ( ) ,
464+ signal : AbortSignal . timeout ( 10000 ) ,
435465 } )
466+
467+ if ( ! response . ok ) {
468+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` )
469+ }
436470 }
437471
438472 private userAgent ( ) : string {
439473 return getUserAgent ( this . context )
440474 }
441-
442475}
0 commit comments