11import * as crypto from "node:crypto" ;
22import * as http from "node:http" ;
33import type { Socket } from "node:net" ;
4- import { ipcMain , shell } from "electron" ;
4+ import { shell } from "electron" ;
5+ import { injectable } from "inversify" ;
56import {
67 getCloudUrlFromRegion ,
78 getOauthClientIdFromRegion ,
89 OAUTH_PORT ,
910 OAUTH_SCOPES ,
10- } from "../../constants/oauth" ;
11+ } from "../../../ constants/oauth.js " ;
1112import type {
13+ CancelFlowOutput ,
1214 CloudRegion ,
13- OAuthConfig ,
1415 OAuthTokenResponse ,
15- } from "../../shared/types/oauth" ;
16+ RefreshTokenOutput ,
17+ StartFlowOutput ,
18+ } from "./schemas.js" ;
19+
20+ interface OAuthConfig {
21+ scopes : string [ ] ;
22+ cloudRegion : CloudRegion ;
23+ }
1624
1725function generateCodeVerifier ( ) : string {
1826 return crypto . randomBytes ( 32 ) . toString ( "base64url" ) ;
@@ -155,7 +163,6 @@ async function startCallbackServer(authUrl: string): Promise<{
155163 }
156164 } ) ;
157165
158- // Track connections
159166 server . on ( "connection" , ( conn ) => {
160167 connections . add ( conn ) ;
161168 conn . on ( "close" , ( ) => {
@@ -164,7 +171,6 @@ async function startCallbackServer(authUrl: string): Promise<{
164171 } ) ;
165172
166173 const closeServer = ( ) => {
167- // Destroy all active connections
168174 for ( const conn of connections ) {
169175 conn . destroy ( ) ;
170176 }
@@ -233,76 +239,23 @@ async function refreshTokenRequest(
233239 return response . json ( ) ;
234240}
235241
236- let activeCloseServer : ( ( ) => void ) | null = null ;
237-
238- export async function performOAuthFlow (
239- config : OAuthConfig ,
240- ) : Promise < OAuthTokenResponse > {
241- const cloudUrl = getCloudUrlFromRegion ( config . cloudRegion ) ;
242- const codeVerifier = generateCodeVerifier ( ) ;
243- const codeChallenge = generateCodeChallenge ( codeVerifier ) ;
244-
245- const authUrl = new URL ( `${ cloudUrl } /oauth/authorize` ) ;
246- authUrl . searchParams . set (
247- "client_id" ,
248- getOauthClientIdFromRegion ( config . cloudRegion ) ,
249- ) ;
250- authUrl . searchParams . set (
251- "redirect_uri" ,
252- `http://localhost:${ OAUTH_PORT } /callback` ,
253- ) ;
254- authUrl . searchParams . set ( "response_type" , "code" ) ;
255- authUrl . searchParams . set ( "code_challenge" , codeChallenge ) ;
256- authUrl . searchParams . set ( "code_challenge_method" , "S256" ) ;
257- authUrl . searchParams . set ( "scope" , config . scopes . join ( " " ) ) ;
258- authUrl . searchParams . set ( "required_access_level" , "project" ) ;
259-
260- const localLoginUrl = `http://localhost:${ OAUTH_PORT } /authorize` ;
261-
262- const { closeServer, waitForCallback } = await startCallbackServer (
263- authUrl . toString ( ) ,
264- ) ;
265-
266- activeCloseServer = closeServer ;
267-
268- await shell . openExternal ( localLoginUrl ) ;
269-
270- try {
271- const code = await Promise . race ( [
272- waitForCallback ( ) ,
273- new Promise < never > ( ( _ , reject ) =>
274- setTimeout ( ( ) => reject ( new Error ( "Authorization timed out" ) ) , 180_000 ) ,
275- ) ,
276- ] ) ;
277-
278- const token = await exchangeCodeForToken ( code , codeVerifier , config ) ;
279-
280- closeServer ( ) ;
281- activeCloseServer = null ;
282-
283- return token ;
284- } catch ( error ) {
285- closeServer ( ) ;
286- activeCloseServer = null ;
287- throw error ;
288- }
289- }
242+ @injectable ( )
243+ export class OAuthService {
244+ private activeCloseServer : ( ( ) => void ) | null = null ;
290245
291- export function registerOAuthHandlers ( ) : void {
292- ipcMain . handle ( "oauth:start-flow" , async ( _ , region : CloudRegion ) => {
246+ async startFlow ( region : CloudRegion ) : Promise < StartFlowOutput > {
293247 try {
294- // Close any existing server before starting a new flow
295- if ( activeCloseServer ) {
296- activeCloseServer ( ) ;
297- activeCloseServer = null ;
248+ if ( this . activeCloseServer ) {
249+ this . activeCloseServer ( ) ;
250+ this . activeCloseServer = null ;
298251 }
299252
300253 const config : OAuthConfig = {
301254 scopes : OAUTH_SCOPES ,
302255 cloudRegion : region ,
303256 } ;
304257
305- const tokenResponse = await performOAuthFlow ( config ) ;
258+ const tokenResponse = await this . performOAuthFlow ( config ) ;
306259
307260 return {
308261 success : true ,
@@ -314,31 +267,31 @@ export function registerOAuthHandlers(): void {
314267 error : error instanceof Error ? error . message : "Unknown error" ,
315268 } ;
316269 }
317- } ) ;
270+ }
318271
319- ipcMain . handle (
320- "oauth:refresh-token" ,
321- async ( _ , refreshToken : string , region : CloudRegion ) => {
322- try {
323- const tokenResponse = await refreshTokenRequest ( refreshToken , region ) ;
324- return {
325- success : true ,
326- data : tokenResponse ,
327- } ;
328- } catch ( error ) {
329- return {
330- success : false ,
331- error : error instanceof Error ? error . message : "Unknown error" ,
332- } ;
333- }
334- } ,
335- ) ;
272+ async refreshToken (
273+ refreshToken : string ,
274+ region : CloudRegion ,
275+ ) : Promise < RefreshTokenOutput > {
276+ try {
277+ const tokenResponse = await refreshTokenRequest ( refreshToken , region ) ;
278+ return {
279+ success : true ,
280+ data : tokenResponse ,
281+ } ;
282+ } catch ( error ) {
283+ return {
284+ success : false ,
285+ error : error instanceof Error ? error . message : "Unknown error" ,
286+ } ;
287+ }
288+ }
336289
337- ipcMain . handle ( "oauth:cancel-flow" , async ( ) => {
290+ cancelFlow ( ) : CancelFlowOutput {
338291 try {
339- if ( activeCloseServer ) {
340- activeCloseServer ( ) ;
341- activeCloseServer = null ;
292+ if ( this . activeCloseServer ) {
293+ this . activeCloseServer ( ) ;
294+ this . activeCloseServer = null ;
342295 }
343296 return { success : true } ;
344297 } catch ( error ) {
@@ -347,5 +300,61 @@ export function registerOAuthHandlers(): void {
347300 error : error instanceof Error ? error . message : "Unknown error" ,
348301 } ;
349302 }
350- } ) ;
303+ }
304+
305+ private async performOAuthFlow (
306+ config : OAuthConfig ,
307+ ) : Promise < OAuthTokenResponse > {
308+ const cloudUrl = getCloudUrlFromRegion ( config . cloudRegion ) ;
309+ const codeVerifier = generateCodeVerifier ( ) ;
310+ const codeChallenge = generateCodeChallenge ( codeVerifier ) ;
311+
312+ const authUrl = new URL ( `${ cloudUrl } /oauth/authorize` ) ;
313+ authUrl . searchParams . set (
314+ "client_id" ,
315+ getOauthClientIdFromRegion ( config . cloudRegion ) ,
316+ ) ;
317+ authUrl . searchParams . set (
318+ "redirect_uri" ,
319+ `http://localhost:${ OAUTH_PORT } /callback` ,
320+ ) ;
321+ authUrl . searchParams . set ( "response_type" , "code" ) ;
322+ authUrl . searchParams . set ( "code_challenge" , codeChallenge ) ;
323+ authUrl . searchParams . set ( "code_challenge_method" , "S256" ) ;
324+ authUrl . searchParams . set ( "scope" , config . scopes . join ( " " ) ) ;
325+ authUrl . searchParams . set ( "required_access_level" , "project" ) ;
326+
327+ const localLoginUrl = `http://localhost:${ OAUTH_PORT } /authorize` ;
328+
329+ const { closeServer, waitForCallback } = await startCallbackServer (
330+ authUrl . toString ( ) ,
331+ ) ;
332+
333+ this . activeCloseServer = closeServer ;
334+
335+ await shell . openExternal ( localLoginUrl ) ;
336+
337+ try {
338+ const code = await Promise . race ( [
339+ waitForCallback ( ) ,
340+ new Promise < never > ( ( _ , reject ) =>
341+ setTimeout (
342+ ( ) => reject ( new Error ( "Authorization timed out" ) ) ,
343+ 180_000 ,
344+ ) ,
345+ ) ,
346+ ] ) ;
347+
348+ const token = await exchangeCodeForToken ( code , codeVerifier , config ) ;
349+
350+ closeServer ( ) ;
351+ this . activeCloseServer = null ;
352+
353+ return token ;
354+ } catch ( error ) {
355+ closeServer ( ) ;
356+ this . activeCloseServer = null ;
357+ throw error ;
358+ }
359+ }
351360}
0 commit comments