@@ -19,13 +19,13 @@ export const AUTH_TYPE = "tobikodata"
1919export const AUTH_NAME = "Tobiko"
2020
2121const tokenSchema = z . object ( {
22- iss : z . string ( ) ,
23- aud : z . string ( ) ,
24- sub : z . string ( ) ,
25- scope : z . string ( ) ,
26- iat : z . number ( ) ,
27- exp : z . number ( ) ,
28- email : z . string ( ) ,
22+ iss : z . string ( ) ,
23+ aud : z . string ( ) ,
24+ sub : z . string ( ) ,
25+ scope : z . string ( ) ,
26+ iat : z . number ( ) ,
27+ exp : z . number ( ) ,
28+ email : z . string ( ) ,
2929} )
3030const statusResponseSchema = z . object ( {
3131 is_logged_in : z . boolean ( ) ,
@@ -39,6 +39,14 @@ const loginUrlResponseSchema = z.object({
3939 verifier_code : z . string ( ) ,
4040} )
4141
42+ const deviceCodeResponseSchema = z . object ( {
43+ device_code : z . string ( ) ,
44+ user_code : z . string ( ) ,
45+ verification_uri : z . string ( ) ,
46+ verification_uri_complete : z . string ( ) ,
47+ expires_in : z . number ( ) ,
48+ } )
49+
4250export class AuthenticationProviderTobikoCloud
4351 implements AuthenticationProvider
4452{
@@ -91,8 +99,8 @@ export class AuthenticationProviderTobikoCloud
9199 const session = {
92100 id : token . email ,
93101 account : {
94- id : token . email ,
95- label : "Tobiko" ,
102+ id : token . sub ,
103+ label : token . email ,
96104 } ,
97105 scopes : token . scope . split ( " " ) ,
98106 accessToken : "" ,
@@ -101,6 +109,60 @@ export class AuthenticationProviderTobikoCloud
101109 }
102110
103111 async createSession ( ) : Promise < AuthenticationSession > {
112+ await this . sign_in_oauth_flow ( )
113+ const status = await this . get_status ( )
114+ if ( isErr ( status ) ) {
115+ throw new Error ( "Failed to get tcloud auth status" )
116+ }
117+ const statusResponse = status . value
118+ if ( ! statusResponse . is_logged_in ) {
119+ throw new Error ( "Failed to login to tcloud" )
120+ }
121+ const token = statusResponse . id_token
122+ const session : AuthenticationSession = {
123+ id : token . email ,
124+ account : {
125+ id : token . email ,
126+ label : "Tobiko" ,
127+ } ,
128+ scopes : token . scope . split ( " " ) ,
129+ accessToken : "" ,
130+ }
131+ this . _sessionChangeEmitter . fire ( {
132+ added : [ session ] ,
133+ removed : [ ] ,
134+ changed : [ ] ,
135+ } )
136+ return session
137+ }
138+
139+ async removeSession ( ) : Promise < void > {
140+ // Get current sessions before logging out
141+ const currentSessions = await this . getSessions ( )
142+ const tcloudBin = await get_tcloud_bin ( )
143+ const workspacePath = await getProjectRoot ( )
144+ if ( isErr ( tcloudBin ) ) {
145+ throw new Error ( "Failed to get tcloud bin" )
146+ }
147+ const tcloudBinPath = tcloudBin . value
148+ const result = await execAsync ( tcloudBinPath , [ "auth" , "logout" ] , {
149+ cwd : workspacePath . uri . fsPath ,
150+ } )
151+ if ( result . exitCode !== 0 ) {
152+ throw new Error ( "Failed to logout from tcloud" )
153+ }
154+
155+ // Emit event with the actual sessions that were removed
156+ if ( currentSessions . length > 0 ) {
157+ this . _sessionChangeEmitter . fire ( {
158+ added : [ ] ,
159+ removed : currentSessions ,
160+ changed : [ ] ,
161+ } )
162+ }
163+ }
164+
165+ async sign_in_oauth_flow ( ) : Promise < void > {
104166 const workspacePath = await getProjectRoot ( )
105167 const tcloudBin = await get_tcloud_bin ( )
106168 if ( isErr ( tcloudBin ) ) {
@@ -117,83 +179,156 @@ export class AuthenticationProviderTobikoCloud
117179 if ( result . exitCode !== 0 ) {
118180 throw new Error ( "Failed to get tcloud login url" )
119181 }
120- const resultToJson = JSON . parse ( result . stdout )
121- const urlCode = loginUrlResponseSchema . parse ( resultToJson )
122- const url = urlCode . url
123182
124- const ac = new AbortController ( )
125- const timeout = setTimeout ( ( ) => ac . abort ( ) , 1000 * 60 * 5 )
126- const backgroundServerForLogin = execAsync (
127- tcloudBinPath ,
128- [ "auth" , "vscode" , "start-server" , urlCode . verifier_code ] ,
129- {
130- cwd : workspacePath . uri . fsPath ,
131- signal : ac . signal ,
183+ try {
184+ const resultToJson = JSON . parse ( result . stdout )
185+ const urlCode = loginUrlResponseSchema . parse ( resultToJson )
186+ const url = urlCode . url
187+
188+ if ( ! url ) {
189+ throw new Error ( "Invalid login URL received" )
132190 }
133- )
134191
135- const messageResult = await window . showInformationMessage (
136- "Please login to Tobiko Cloud" ,
137- {
138- modal : true ,
139- } ,
140- "Sign in with browser" ,
141- "Cancel"
142- )
192+ const ac = new AbortController ( )
193+ const timeout = setTimeout ( ( ) => ac . abort ( ) , 1000 * 60 * 5 )
194+ const backgroundServerForLogin = execAsync (
195+ tcloudBinPath ,
196+ [ "auth" , "vscode" , "start-server" , urlCode . verifier_code ] ,
197+ {
198+ cwd : workspacePath . uri . fsPath ,
199+ signal : ac . signal ,
200+ }
201+ )
143202
144- if ( messageResult === "Sign in with browser" ) {
145- await env . openExternal ( Uri . parse ( url ) )
146- }
147- if ( messageResult === "Cancel" ) {
148- ac . abort ( )
149- throw new Error ( "Login cancelled" )
150- }
203+ const messageResult = await window . showInformationMessage (
204+ "Please login to Tobiko Cloud" ,
205+ {
206+ modal : true ,
207+ } ,
208+ "Sign in with browser" ,
209+ "Cancel"
210+ )
151211
152- try {
153- const output = await backgroundServerForLogin
154- if ( output . exitCode !== 0 ) {
155- throw new Error ( `Failed to start server: ${ output . stderr } ` )
212+ if ( messageResult === "Sign in with browser" ) {
213+ await env . openExternal ( Uri . parse ( url ) )
214+ } else {
215+ // Always abort the server if not proceeding with sign in
216+ ac . abort ( )
217+ clearTimeout ( timeout )
218+ if ( messageResult === "Cancel" ) {
219+ throw new Error ( "Login cancelled" )
220+ }
221+ return
156222 }
157- } catch ( error ) {
158- traceError ( `Server error: ${ error } ` )
159- throw error
160- }
161223
162- clearTimeout ( timeout )
163-
164- const status = await this . get_status ( )
165- if ( isErr ( status ) ) {
166- throw new Error ( "Failed to get tcloud auth status" )
167- }
168- const statusResponse = status . value
169- if ( ! statusResponse . is_logged_in ) {
170- throw new Error ( "Failed to login to tcloud" )
171- }
172- const scopes = statusResponse . id_token . scope . split ( " " )
173- const session : AuthenticationSession = {
174- id : AuthenticationProviderTobikoCloud . id ,
175- account : {
176- id : AuthenticationProviderTobikoCloud . id ,
177- label : "Tobiko" ,
178- } ,
179- scopes : scopes ,
180- accessToken : ""
224+ try {
225+ const output = await backgroundServerForLogin
226+ if ( output . exitCode !== 0 ) {
227+ throw new Error (
228+ `Failed to complete authentication: ${ output . stderr } `
229+ )
230+ }
231+ // Get updated session and notify about the change
232+ const sessions = await this . getSessions ( )
233+ if ( sessions . length > 0 ) {
234+ this . _sessionChangeEmitter . fire ( {
235+ added : sessions ,
236+ removed : [ ] ,
237+ changed : [ ] ,
238+ } )
239+ }
240+ } catch ( error ) {
241+ if ( error instanceof Error && error . name === "AbortError" ) {
242+ throw new Error ( "Authentication timeout or aborted" )
243+ }
244+ traceError ( `Server error: ${ error } ` )
245+ throw error
246+ } finally {
247+ clearTimeout ( timeout )
248+ }
249+ } catch ( error ) {
250+ if ( error instanceof Error && error . message === "Login cancelled" ) {
251+ throw error
252+ }
253+ traceError ( `Authentication flow error: ${ error } ` )
254+ throw new Error ( "Failed to complete authentication flow" )
181255 }
182- return session
183256 }
184257
185- async removeSession ( ) : Promise < void > {
186- const tcloudBin = await get_tcloud_bin ( )
258+ async sign_in_device_flow ( ) : Promise < void > {
187259 const workspacePath = await getProjectRoot ( )
260+ const tcloudBin = await get_tcloud_bin ( )
188261 if ( isErr ( tcloudBin ) ) {
189262 throw new Error ( "Failed to get tcloud bin" )
190263 }
191264 const tcloudBinPath = tcloudBin . value
192- const result = await execAsync ( tcloudBinPath , [ "auth" , "logout" ] , {
193- cwd : workspacePath . uri . fsPath ,
194- } )
265+ const result = await execAsync (
266+ tcloudBinPath ,
267+ [ "auth" , "vscode" , "device" ] ,
268+ {
269+ cwd : workspacePath . uri . fsPath ,
270+ }
271+ )
195272 if ( result . exitCode !== 0 ) {
196- throw new Error ( "Failed to logout from tcloud" )
273+ throw new Error ( "Failed to get device code" )
274+ }
275+
276+ try {
277+ const resultToJson = JSON . parse ( result . stdout )
278+ const deviceCodeResponse = deviceCodeResponseSchema . parse ( resultToJson )
279+
280+ const ac = new AbortController ( )
281+ const timeout = setTimeout ( ( ) => ac . abort ( ) , 1000 * 60 * 5 )
282+ const waiting = execAsync (
283+ tcloudBinPath ,
284+ [ "auth" , "vscode" , "poll_device" , deviceCodeResponse . device_code ] ,
285+ {
286+ cwd : workspacePath . uri . fsPath ,
287+ signal : ac . signal ,
288+ }
289+ )
290+
291+ const messageResult = await window . showInformationMessage (
292+ `Confirm the code ${ deviceCodeResponse . user_code } at ${ deviceCodeResponse . verification_uri } ` ,
293+ {
294+ modal : true ,
295+ } ,
296+ "Open browser" ,
297+ "Cancel"
298+ )
299+
300+ if ( messageResult === "Open browser" ) {
301+ await env . openExternal ( Uri . parse ( deviceCodeResponse . verification_uri_complete ) )
302+ }
303+ if ( messageResult === "Cancel" ) {
304+ ac . abort ( )
305+ throw new Error ( "Login cancelled" )
306+ }
307+
308+ try {
309+ const output = await waiting
310+ if ( output . exitCode !== 0 ) {
311+ throw new Error ( `Failed to authenticate: ${ output . stderr } ` )
312+ }
313+
314+ // Get updated session and notify about the change
315+ const sessions = await this . getSessions ( )
316+ if ( sessions . length > 0 ) {
317+ this . _sessionChangeEmitter . fire ( {
318+ added : sessions ,
319+ removed : [ ] ,
320+ changed : [ ] ,
321+ } )
322+ }
323+ } catch ( error ) {
324+ traceError ( `Authentication error: ${ error } ` )
325+ throw error
326+ } finally {
327+ clearTimeout ( timeout )
328+ }
329+ } catch ( error ) {
330+ traceError ( `JSON parsing error: ${ error } ` )
331+ throw new Error ( "Failed to parse device code response" )
197332 }
198333 }
199334}
0 commit comments