The main application browser window.
export function addAuthEventListeners(mainWindow: BrowserWindow) {
// Open the OAuth window when requested by the renderer
secureHandle(
"auth:openOAuthWindow",
async (event, oauthUrl: string, redirectUri: string) => {
return withGroupAsync(
`[AuthIPC] OAuth Flow: ${redirectUri}`,
async () => {
try {
// Reset cancellation flag
authCancelled = false;
// Validate OAuth URL and parameters EARLY before allocating any resources
const validation = validateOAuthUrl(oauthUrl, redirectUri);
if (!validation.valid) {
console.error(
`[AuthIPC] ❌ OAuth URL validation failed: ${validation.error}`,
);
mainWindow.webContents.send(
"auth:status",
`Security error: ${validation.error}`,
);
return {
success: false,
error: validation.error,
};
}
console.info(
`[AuthIPC] ✅ OAuth URL validated: ${new URL(oauthUrl).hostname}`,
);
// Extract redirect URI parts
const redirectUrl = new URL(redirectUri);
// Use a non-privileged port by default
const port = redirectUrl.port || DEFAULT_PORT.toString();
// Update the redirectUri with our port if none was specified
if (!redirectUrl.port) {
redirectUrl.port = port;
const updatedRedirectUri = redirectUrl.toString();
// If the redirect URI in the oauth URL doesn't match the updated one,
// we need to update the oauth URL too
if (redirectUri !== updatedRedirectUri) {
const oauthUrlObj = new URL(oauthUrl);
const redirectParam =
oauthUrlObj.searchParams.get("redirect_uri");
if (redirectParam) {
oauthUrlObj.searchParams.set(
"redirect_uri",
updatedRedirectUri,
);
oauthUrl = oauthUrlObj.toString();
}
}
// Update the redirect URI to include the port
redirectUri = updatedRedirectUri;
}
// Start the temporary HTTP server first
try {
await startAuthServer(port, redirectUrl.pathname, mainWindow);
// Send status update
mainWindow.webContents.send(
"auth:status",
`Server started on port ${port}, opening browser for authentication...`,
);
// IMPORTANT: Set up the auth code promise AFTER server is started
const authCodePromise = new Promise<string>((resolve, reject) => {
authResolve = resolve;
authReject = reject;
// Set timeout for the entire auth process and store handle for cleanup
loadTimeout = createAuthTimeout();
});
// Open the authorization URL in the default browser
await shell.openExternal(oauthUrl);
// Notify the user about the browser
mainWindow.webContents.send(
"auth:status",
"Browser opened for authentication. Please complete the process in your browser.",
);
// Set up the background handling of the auth code
// This needs to be done after we return the response
// to avoid the "reply was never sent" error
setTimeout(() => {
handleAuthCodePromise(authCodePromise, mainWindow, redirectUri);
}, 100);
// IMPORTANT: Return success immediately so the IPC call resolves
// The actual code handling will happen via the auth:codeReceived event
return { success: true };
} catch (serverError) {
const errorMessage =
serverError instanceof Error
? serverError.message
: "Failed to start authentication server";
console.error("[AuthIPC] Server error:", serverError);
mainWindow.webContents.send(
"auth:status",
`Authentication error: ${errorMessage}`,
);
return { success: false, error: errorMessage };
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("[AuthIPC] Failed to open OAuth window:", error);
cleanupAuthServer();
return { success: false, error: errorMessage };
}
},
);
},
mainWindow,
);
// Handle storing and retrieving API credentials
secureHandle(
"auth:storeCredentials",
async (event, credentials: AuthCredentials) => {
try {
console.debug(
"[AuthIPC] Storing credentials for source:",
credentials?.source,
);
// Store the credentials in memory
if (credentials?.source) {
storedCredentials[credentials.source] = credentials;
}
return { success: true };
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("[AuthIPC] Failed to store credentials:", error);
return { success: false, error: errorMessage };
}
},
mainWindow,
);
// Get stored credentials
secureHandle(
"auth:getCredentials",
async (event, source: string) => {
try {
console.debug("[AuthIPC] Retrieving credentials for source:", source);
const credentials = storedCredentials[source];
if (!credentials) {
return {
success: false,
error: `No credentials found for source: ${source}`,
};
}
return {
success: true,
credentials,
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("[AuthIPC] Failed to retrieve credentials:", error);
return { success: false, error: errorMessage };
}
},
mainWindow,
);
// Add a way to manually cancel auth
secureHandle(
"auth:cancel",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(event) => {
authCancelled = true;
authReject?.(new Error("Authentication cancelled by user"));
cleanupAuthServer();
return { success: true };
},
mainWindow,
);
// Add a handler to exchange auth code for token in the main process
// This avoids network issues that can happen in the renderer process
secureHandle(
"auth:exchangeToken",
async (
_event,
params: {
code: string;
clientId: string;
clientSecret: string;
redirectUri: string;
},
) => {
return withGroupAsync(
`[AuthIPC] Token Exchange (${params.clientId.substring(0, 8)}...)`,
async () => {
try {
const { clientId, clientSecret, redirectUri, code } = params;
// Validate parameters
const validation = validateTokenExchangeParams({
clientId,
clientSecret,
redirectUri,
code,
});
if (!validation.isValid) {
return { success: false, error: validation.error };
}
console.info("[AuthIPC] Exchanging token in main process:", {
clientIdLength: clientId.length,
redirectUri,
codeLength: code.length,
});
const result = await exchangeTokenWithRetries(
{ clientId, clientSecret, redirectUri, code },
3,
);
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
"[AuthIPC] Token exchange handler error:",
errorMessage,
);
return { success: false, error: errorMessage };
}
},
);
},
mainWindow,
);
}
Registers IPC event listeners for all authentication-related operations. Handles OAuth flow, credentials management, and token exchange.