From a358ae7051f9e7009ac0d5732dfaeb25eb70c0ed Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:58:38 +0530 Subject: [PATCH 1/3] refactor oauth repo interface --- apps/x/apps/main/src/oauth-handler.ts | 20 ++--- apps/x/packages/core/src/auth/repo.ts | 77 +++---------------- .../src/knowledge/fireflies-client-factory.ts | 10 +-- .../src/knowledge/google-client-factory.ts | 14 ++-- apps/x/packages/shared/src/ipc.ts | 2 +- 5 files changed, 35 insertions(+), 88 deletions(-) diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index eba43956..ce1e83eb 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -84,7 +84,7 @@ async function getProviderConfiguration(provider: string, clientIdOverride?: str return clientIdOverride; } const oauthRepo = getOAuthRepo(); - const clientId = await oauthRepo.getClientId(provider); + const { clientId } = await oauthRepo.read(provider); if (clientId) { return clientId; } @@ -212,11 +212,11 @@ export async function connectProvider(provider: string, clientId?: string): Prom // Save tokens console.log(`[OAuth] Token exchange successful for ${provider}`); - await oauthRepo.saveTokens(provider, tokens); + await oauthRepo.upsert(provider, { tokens }); if (provider === 'google' && clientId) { - await oauthRepo.setClientId(provider, clientId); + await oauthRepo.upsert(provider, { clientId }); } - await oauthRepo.clearError(provider); + await oauthRepo.upsert(provider, { error: null }); // Trigger immediate sync for relevant providers if (provider === 'google') { @@ -281,7 +281,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom export async function disconnectProvider(provider: string): Promise<{ success: boolean }> { try { const oauthRepo = getOAuthRepo(); - await oauthRepo.clearTokens(provider); + await oauthRepo.delete(provider); return { success: true }; } catch (error) { console.error('OAuth disconnect failed:', error); @@ -297,7 +297,7 @@ export async function getAccessToken(provider: string): Promise { try { const oauthRepo = getOAuthRepo(); - let tokens = await oauthRepo.getTokens(provider); + const { tokens } = await oauthRepo.read(provider); if (!tokens) { return null; } @@ -306,7 +306,7 @@ export async function getAccessToken(provider: string): Promise { if (oauthClient.isTokenExpired(tokens)) { if (!tokens.refresh_token) { // No refresh token, need to reconnect - await oauthRepo.setError(provider, 'Missing refresh token. Please reconnect.'); + await oauthRepo.upsert(provider, { error: 'Missing refresh token. Please reconnect.' }); return null; } @@ -316,11 +316,11 @@ export async function getAccessToken(provider: string): Promise { // Refresh token, preserving existing scopes const existingScopes = tokens.scopes; - tokens = await oauthClient.refreshTokens(config, tokens.refresh_token, existingScopes); - await oauthRepo.saveTokens(provider, tokens); + const refreshedTokens = await oauthClient.refreshTokens(config, tokens.refresh_token, existingScopes); + await oauthRepo.upsert(provider, { tokens }); } catch (error) { const message = error instanceof Error ? error.message : 'Token refresh failed'; - await oauthRepo.setError(provider, message); + await oauthRepo.upsert(provider, { error: message }); console.error('Token refresh failed:', error); return null; } diff --git a/apps/x/packages/core/src/auth/repo.ts b/apps/x/packages/core/src/auth/repo.ts index 7a91e8b6..a53c2dcb 100644 --- a/apps/x/packages/core/src/auth/repo.ts +++ b/apps/x/packages/core/src/auth/repo.ts @@ -5,9 +5,9 @@ import { OAuthTokens } from './types.js'; import z from 'zod'; const ProviderConnectionSchema = z.object({ - tokens: OAuthTokens, - clientId: z.string().optional(), - error: z.string().optional(), + tokens: OAuthTokens.nullable().optional(), + clientId: z.string().nullable().optional(), + error: z.string().nullable().optional(), }); const OAuthConfigSchema = z.object({ @@ -17,7 +17,7 @@ const OAuthConfigSchema = z.object({ const ClientFacingConfigSchema = z.record(z.string(), z.object({ connected: z.boolean(), - error: z.string().optional(), + error: z.string().nullable().optional(), })); const LegacyOauthConfigSchema = z.record(z.string(), OAuthTokens); @@ -28,13 +28,9 @@ const DEFAULT_CONFIG: z.infer = { }; export interface IOAuthRepo { - getTokens(provider: string): Promise; - saveTokens(provider: string, tokens: OAuthTokens): Promise; - clearTokens(provider: string): Promise; - getClientId(provider: string): Promise; - setClientId(provider: string, clientId: string): Promise; - setError(provider: string, errorMessage: string): Promise; - clearError(provider: string): Promise; + read(provider: string): Promise>; + upsert(provider: string, connection: Partial>): Promise; + delete(provider: string): Promise; getClientFacingConfig(): Promise>; } @@ -92,71 +88,22 @@ export class FSOAuthRepo implements IOAuthRepo { await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); } - async getTokens(provider: string): Promise { + async read(provider: string): Promise> { const config = await this.readConfig(); - const tokens = config.providers[provider]?.tokens; - return tokens ?? null; + return config.providers[provider] ?? {}; } - - async saveTokens(provider: string, tokens: OAuthTokens): Promise { + async upsert(provider: string, connection: Partial>): Promise { const config = await this.readConfig(); - if (config.providers[provider]) { - delete config.providers[provider]; - } - config.providers[provider] = { - tokens, - }; + config.providers[provider] = { ...config.providers[provider] ?? {}, ...connection }; await this.writeConfig(config); } - async clearTokens(provider: string): Promise { + async delete(provider: string): Promise { const config = await this.readConfig(); delete config.providers[provider]; await this.writeConfig(config); } - async getClientId(provider: string): Promise { - const config = await this.readConfig(); - const clientId = config.providers[provider]?.clientId; - return clientId ?? null; - } - - async setClientId(provider: string, clientId: string): Promise { - const config = await this.readConfig(); - if (!config.providers[provider]) { - throw new Error(`Provider ${provider} not found`); - } - config.providers[provider].clientId = clientId; - await this.writeConfig(config); - } - - async clearClientId(provider: string): Promise { - const config = await this.readConfig(); - if (!config.providers[provider]) { - throw new Error(`Provider ${provider} not found`); - } - delete config.providers[provider].clientId; - await this.writeConfig(config); - } - - async setError(provider: string, errorMessage: string): Promise { - const config = await this.readConfig(); - if (!config.providers[provider]) { - throw new Error(`Provider ${provider} not found`); - } - config.providers[provider].error = errorMessage; - await this.writeConfig(config); - } - - async clearError(provider: string): Promise { - const config = await this.readConfig(); - if (!config.providers[provider]) { - throw new Error(`Provider ${provider} not found`); - } - delete config.providers[provider].error; - await this.writeConfig(config); - } - async getClientFacingConfig(): Promise> { const config = await this.readConfig(); const clientFacingConfig: z.infer = {}; diff --git a/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts b/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts index 67607bfe..1ee865e9 100644 --- a/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts +++ b/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts @@ -31,7 +31,7 @@ export class FirefliesClientFactory { */ static async getClient(): Promise { const oauthRepo = container.resolve('oauthRepo'); - const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); + const { tokens } = await oauthRepo.read(this.PROVIDER_NAME); if (!tokens) { this.clearCache(); @@ -49,7 +49,7 @@ export class FirefliesClientFactory { // Token expired, try to refresh if (!tokens.refresh_token) { console.log("[Fireflies] Token expired and no refresh token available."); - await oauthRepo.setError(this.PROVIDER_NAME, 'Missing refresh token. Please reconnect.'); + await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Missing refresh token. Please reconnect.' }); this.clearCache(); return null; } @@ -62,7 +62,7 @@ export class FirefliesClientFactory { tokens.refresh_token, existingScopes ); - await oauthRepo.saveTokens(this.PROVIDER_NAME, refreshedTokens); + await oauthRepo.upsert(this.PROVIDER_NAME, { tokens: refreshedTokens }); // Update cached tokens and recreate client this.cache.tokens = refreshedTokens; @@ -77,7 +77,7 @@ export class FirefliesClientFactory { return this.cache.client; } catch (error) { const message = error instanceof Error ? error.message : 'Failed to refresh token for Fireflies'; - await oauthRepo.setError(this.PROVIDER_NAME, message); + await oauthRepo.upsert(this.PROVIDER_NAME, { error: message }); console.error("[Fireflies] Failed to refresh token:", error); this.clearCache(); return null; @@ -107,7 +107,7 @@ export class FirefliesClientFactory { */ static async hasValidCredentials(): Promise { const oauthRepo = container.resolve('oauthRepo'); - const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); + const { tokens } = await oauthRepo.read(this.PROVIDER_NAME); return tokens !== null; } diff --git a/apps/x/packages/core/src/knowledge/google-client-factory.ts b/apps/x/packages/core/src/knowledge/google-client-factory.ts index e15d2513..3cb83cd1 100644 --- a/apps/x/packages/core/src/knowledge/google-client-factory.ts +++ b/apps/x/packages/core/src/knowledge/google-client-factory.ts @@ -27,9 +27,9 @@ export class GoogleClientFactory { private static async resolveClientId(): Promise { const oauthRepo = container.resolve('oauthRepo'); - const clientId = await oauthRepo.getClientId(this.PROVIDER_NAME); + const { clientId } = await oauthRepo.read(this.PROVIDER_NAME); if (!clientId) { - await oauthRepo.setError(this.PROVIDER_NAME, 'Google client ID missing. Please reconnect.'); + await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Google client ID missing. Please reconnect.' }); throw new Error('Google client ID missing. Please reconnect.'); } return clientId; @@ -40,7 +40,7 @@ export class GoogleClientFactory { */ static async getClient(): Promise { const oauthRepo = container.resolve('oauthRepo'); - const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); + const { tokens } = await oauthRepo.read(this.PROVIDER_NAME); if (!tokens) { this.clearCache(); @@ -64,7 +64,7 @@ export class GoogleClientFactory { // Token expired, try to refresh if (!tokens.refresh_token) { console.log("[OAuth] Token expired and no refresh token available for Google."); - await oauthRepo.setError(this.PROVIDER_NAME, 'Missing refresh token. Please reconnect.'); + await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Missing refresh token. Please reconnect.' }); this.clearCache(); return null; } @@ -77,7 +77,7 @@ export class GoogleClientFactory { tokens.refresh_token, existingScopes ); - await oauthRepo.saveTokens(this.PROVIDER_NAME, refreshedTokens); + await oauthRepo.upsert(this.PROVIDER_NAME, { tokens: refreshedTokens }); // Update cached tokens and recreate client this.cache.tokens = refreshedTokens; @@ -89,7 +89,7 @@ export class GoogleClientFactory { return this.cache.client; } catch (error) { const message = error instanceof Error ? error.message : 'Failed to refresh token for Google'; - await oauthRepo.setError(this.PROVIDER_NAME, message); + await oauthRepo.upsert(this.PROVIDER_NAME, { error: message }); console.error("[OAuth] Failed to refresh token for Google:", error); this.clearCache(); return null; @@ -116,7 +116,7 @@ export class GoogleClientFactory { */ static async hasValidCredentials(requiredScopes: string | string[]): Promise { const oauthRepo = container.resolve('oauthRepo'); - const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); + const { tokens } = await oauthRepo.read(this.PROVIDER_NAME); if (!tokens) { return false; } diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 1fa9b423..71491f8f 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -244,7 +244,7 @@ const ipcSchemas = { res: z.object({ config: z.record(z.string(), z.object({ connected: z.boolean(), - error: z.string().optional(), + error: z.string().nullable().optional(), })), }), }, From f5d585ac7edfe70045f1c1456bf1e40260896282 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:59:16 +0530 Subject: [PATCH 2/3] refactor build-oauth-url params --- apps/x/apps/main/src/oauth-handler.ts | 4 ++-- apps/x/packages/core/src/auth/oauth-client.ts | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index ce1e83eb..2efc77c2 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -179,9 +179,9 @@ export async function connectProvider(provider: string, clientId?: string): Prom // Build authorization URL const authUrl = oauthClient.buildAuthorizationUrl(config, { - redirectUri: REDIRECT_URI, + redirect_uri: REDIRECT_URI, scope: scopes.join(' '), - codeChallenge, + code_challenge: codeChallenge, state, }); diff --git a/apps/x/packages/core/src/auth/oauth-client.ts b/apps/x/packages/core/src/auth/oauth-client.ts index 613cee2e..762c8aae 100644 --- a/apps/x/packages/core/src/auth/oauth-client.ts +++ b/apps/x/packages/core/src/auth/oauth-client.ts @@ -152,19 +152,11 @@ export function generateState(): string { */ export function buildAuthorizationUrl( config: client.Configuration, - params: { - redirectUri: string; - scope: string; - codeChallenge: string; - state: string; - } + params: Record ): URL { return client.buildAuthorizationUrl(config, { - redirect_uri: params.redirectUri, - scope: params.scope, - code_challenge: params.codeChallenge, code_challenge_method: 'S256', - state: params.state, + ...params, }); } From 3ea416ac71b4865defbb898e6fc755b0d5117616 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:59:28 +0530 Subject: [PATCH 3/3] preserve refresh token if not reissued --- apps/x/packages/core/src/auth/oauth-client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/x/packages/core/src/auth/oauth-client.ts b/apps/x/packages/core/src/auth/oauth-client.ts index 762c8aae..0442d7d6 100644 --- a/apps/x/packages/core/src/auth/oauth-client.ts +++ b/apps/x/packages/core/src/auth/oauth-client.ts @@ -200,6 +200,11 @@ export async function refreshTokens( tokens.scopes = existingScopes; } + // Preserve existing refresh token if server didn't return it + if (!tokens.refresh_token) { + tokens.refresh_token = refreshToken; + } + console.log(`[OAuth] Token refresh successful`); return tokens; }