1+ import crypto from "crypto" ;
12import http from "http" ;
3+ import chalk from "chalk" ;
24import open from "open" ;
35
46import { authStore } from "../stores/auth.js" ;
57import { configStore } from "../stores/config.js" ;
68
79import { loginUrl } from "./constants.js" ;
810
9- function corsHeaders ( baseUrl : string ) : Record < string , string > {
10- return {
11- "Access-Control-Allow-Origin" : baseUrl ,
12- "Access-Control-Allow-Methods" : "GET" ,
13- "Access-Control-Allow-Headers" : "Authorization" ,
14- } ;
15- }
16-
17- export async function authenticateUser ( baseUrl : string ) {
18- return new Promise < string > ( ( resolve , reject ) => {
19- let isResolved = false ;
11+ const successUrl = ( baseUrl : string ) => `${ baseUrl } /cli-login/success` ;
12+ const errorUrl = ( baseUrl : string , error : string ) =>
13+ `${ baseUrl } /cli-login/error?error=${ error } ` ;
2014
21- const server = http . createServer ( async ( req , res ) => {
22- const url = new URL ( req . url ?? "/" , "http://127.0.0.1" ) ;
23- const headers = corsHeaders ( baseUrl ) ;
15+ interface waitForAccessToken {
16+ accessToken : string ;
17+ expiresAt : Date ;
18+ }
2419
25- // Ensure we don't process requests after resolution
26- if ( isResolved ) {
27- res . writeHead ( 503 , headers ) . end ( ) ;
28- return ;
29- }
20+ export async function waitForAccessToken ( baseUrl : string , apiUrl : string ) {
21+ let resolve : ( args : waitForAccessToken ) => void ,
22+ reject : ( arg0 : Error ) => void ;
23+ const promise = new Promise < waitForAccessToken > ( ( res , rej ) => {
24+ resolve = res ;
25+ reject = rej ;
26+ } ) ;
3027
31- if ( url . pathname !== "/cli-login" ) {
32- res . writeHead ( 404 ) . end ( "Invalid path" ) ;
33- cleanupAndReject ( new Error ( "Could not authenticate: Invalid path" ) ) ;
34- return ;
35- }
28+ // PCKE code verifier and challenge
29+ const codeVerifier = crypto . randomUUID ( ) ;
30+ const codeChallenge = crypto
31+ . createHash ( "sha256" )
32+ . update ( codeVerifier )
33+ . digest ( "base64" )
34+ . replace ( / = / g, "" )
35+ . replace ( / \+ / g, "-" )
36+ . replace ( / \/ / g, "_" ) ;
37+
38+ const timeout = setTimeout ( ( ) => {
39+ cleanupAndReject ( new Error ( "Authentication timed out after 30 seconds" ) ) ;
40+ } , 30000 ) ;
41+
42+ function cleanupAndReject ( error : Error ) {
43+ cleanup ( ) ;
44+ reject ( error ) ;
45+ }
3646
37- // Handle preflight request
38- if ( req . method === "OPTIONS" ) {
39- res . writeHead ( 200 , headers ) ;
40- res . end ( ) ;
41- return ;
42- }
47+ function cleanup ( ) {
48+ clearTimeout ( timeout ) ;
49+ server . close ( ) ;
50+ server . closeAllConnections ( ) ;
51+ }
4352
44- if ( ! req . headers . authorization ?. startsWith ( "Bearer " ) ) {
45- res . writeHead ( 400 , headers ) . end ( "Could not authenticate" ) ;
46- cleanupAndReject ( new Error ( "Could not authenticate" ) ) ;
47- return ;
48- }
53+ const server = http . createServer ( async ( req , res ) => {
54+ const url = new URL ( req . url ?? "/" , "http://127.0.0.1" ) ;
4955
50- const token = req . headers . authorization . slice ( "Bearer " . length ) ;
51- headers [ "Content-Type" ] = "application/json" ;
52- res . writeHead ( 200 , headers ) ;
53- res . end ( JSON . stringify ( { result : "success" } ) ) ;
54-
55- try {
56- await authStore . setToken ( baseUrl , token ) ;
57- cleanupAndResolve ( token ) ;
58- } catch ( error ) {
59- cleanupAndReject (
60- error instanceof Error ? error : new Error ( "Failed to store token" ) ,
61- ) ;
62- }
63- } ) ;
56+ if ( url . pathname !== "/cli-login" ) {
57+ res . writeHead ( 404 ) . end ( "Invalid path" ) ;
58+ cleanupAndReject ( new Error ( "Could not authenticate: Invalid path" ) ) ;
59+ return ;
60+ }
6461
65- const timeout = setTimeout ( ( ) => {
66- cleanupAndReject ( new Error ( "Authentication timed out after 60 seconds" ) ) ;
67- } , 60000 ) ;
62+ const fullUrl = new URL ( `http://localhost${ req . url } ` ) ;
6863
69- function cleanupAndResolve ( token : string ) {
70- if ( isResolved ) return ;
71- isResolved = true ;
72- cleanup ( ) ;
73- resolve ( token ) ;
74- }
64+ const code = fullUrl . searchParams . get ( "code" ) ;
7565
76- function cleanupAndReject ( error : Error ) {
77- if ( isResolved ) return ;
78- isResolved = true ;
79- cleanup ( ) ;
80- reject ( error ) ;
66+ if ( ! code ) {
67+ res . writeHead ( 400 ) . end ( "Could not authenticate" ) ;
68+ cleanupAndReject ( new Error ( "Could not authenticate: no code provided" ) ) ;
69+ return ;
8170 }
8271
83- function cleanup ( ) {
84- clearTimeout ( timeout ) ;
85- server . close ( ) ;
86- // Force-close any remaining connections
87- server . getConnections ( ( err , count ) => {
88- if ( err || count === 0 ) return ;
89- server . closeAllConnections ( ) ;
90- } ) ;
91- }
72+ const response = await fetch ( `${ apiUrl } /oauth/cli/access-token` , {
73+ method : "POST" ,
74+ headers : {
75+ "Content-Type" : "application/json" ,
76+ } ,
77+ body : JSON . stringify ( {
78+ code,
79+ codeVerifier,
80+ } ) ,
81+ } ) ;
9282
93- server . listen ( ) ;
94- const address = server . address ( ) ;
95- if ( address && typeof address === "object" ) {
96- const port = address . port ;
97- void open ( loginUrl ( baseUrl , port ) ) ;
83+ if ( ! response . ok ) {
84+ res
85+ . writeHead ( 302 , {
86+ location : errorUrl (
87+ baseUrl ,
88+ "Could not authenticate: Unable to fetch access token" ,
89+ ) ,
90+ } )
91+ . end ( "Could not authenticate" ) ;
92+ cleanupAndReject ( new Error ( "Could not authenticate" ) ) ;
93+ return ;
9894 }
95+ res
96+ . writeHead ( 302 , {
97+ location : successUrl ( baseUrl ) ,
98+ } )
99+ . end ( "Authentication successful" ) ;
100+
101+ const jsonResponse = await response . json ( ) ;
102+
103+ cleanup ( ) ;
104+ resolve ( {
105+ accessToken : jsonResponse . accessToken ,
106+ expiresAt : new Date ( jsonResponse . expiresAt ) ,
107+ } ) ;
99108 } ) ;
109+
110+ server . listen ( ) ;
111+ const address = server . address ( ) ;
112+ if ( address == null || typeof address !== "object" ) {
113+ throw new Error ( "Could not start server" ) ;
114+ }
115+
116+ const port = address . port ;
117+ const browserUrl = loginUrl ( apiUrl , port , codeChallenge ) ;
118+
119+ console . log (
120+ `Opened web browser to facilitate login: ${ chalk . cyan ( browserUrl ) } ` ,
121+ ) ;
122+
123+ void open ( browserUrl ) ;
124+
125+ return promise ;
100126}
101127
102128export async function authRequest < T = Record < string , unknown > > (
@@ -110,7 +136,8 @@ export async function authRequest<T = Record<string, unknown>>(
110136 const token = authStore . getToken ( baseUrl ) ;
111137
112138 if ( ! token ) {
113- await authenticateUser ( baseUrl ) ;
139+ const accessToken = await waitForAccessToken ( baseUrl , apiUrl ) ;
140+ await authStore . setToken ( baseUrl , accessToken . accessToken ) ;
114141 return authRequest ( url , options ) ;
115142 }
116143
@@ -133,7 +160,7 @@ export async function authRequest<T = Record<string, unknown>>(
133160 if ( response . status === 401 ) {
134161 await authStore . setToken ( baseUrl , undefined ) ;
135162 if ( retryCount < 1 ) {
136- await authenticateUser ( baseUrl ) ;
163+ await waitForAccessToken ( baseUrl , apiUrl ) ;
137164 return authRequest ( url , options , retryCount + 1 ) ;
138165 }
139166 }
0 commit comments