From 03bb4876da779170feed559994eaf2694efeec7d Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Fri, 26 Dec 2025 15:32:22 -0800 Subject: [PATCH 1/5] office sign in fix --- .../desktop/src/main/lib/auth/auth-service.ts | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index 558fb8e5422..604090e0466 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -63,14 +63,25 @@ class AuthService extends EventEmitter { if (session.refreshToken && session.refreshTokenExpiresAt > Date.now()) { console.log("[auth] Attempting to refresh tokens on startup"); this.session = session; // Temporarily set to allow refresh - const refreshed = await this.refreshTokens(); - if (refreshed) { + const result = await this.refreshTokens(); + + if (result === "success") { console.log("[auth] Session restored via token refresh"); return; } + + if (result === "network_error") { + // Offline - keep session with expired access token + // User can work offline, and we'll refresh when online + console.log("[auth] Offline - keeping session for offline use"); + this.session = session; + return; + } + + // result === "invalid" - tokens are revoked, must clear } - // Refresh failed or no valid refresh token + // Refresh token invalid/expired or no refresh token console.log("[auth] Session fully expired, clearing"); await this.clearSession(); return; @@ -93,6 +104,7 @@ class AuthService extends EventEmitter { /** * Get access token for API calls * Automatically refreshes if access token is expired but refresh token is valid + * Returns null if offline or tokens invalid (caller should handle gracefully) */ async getAccessToken(): Promise { if (!this.session) { @@ -108,10 +120,17 @@ class AuthService extends EventEmitter { this.session.refreshToken && this.session.refreshTokenExpiresAt > Date.now() ) { - const refreshed = await this.refreshTokens(); - if (refreshed) { + const result = await this.refreshTokens(); + if (result === "success") { return this.session.accessToken; } + if (result === "network_error") { + // Offline - return null but don't clear session + // User stays signed in, but can't make API calls until online + console.log("[auth] Offline - cannot refresh token"); + return null; + } + // result === "invalid" - fall through to clear session } // Refresh failed or no valid refresh token @@ -125,10 +144,14 @@ class AuthService extends EventEmitter { /** * Refresh tokens using the refresh token + * Returns: 'success' | 'invalid' | 'network_error' + * - 'success': Tokens refreshed successfully + * - 'invalid': Tokens are invalid/revoked (should clear session) + * - 'network_error': Network unavailable (should keep session for offline use) */ - private async refreshTokens(): Promise { + private async refreshTokens(): Promise<"success" | "invalid" | "network_error"> { if (!this.session?.refreshToken) { - return false; + return "invalid"; } try { @@ -147,7 +170,12 @@ class AuthService extends EventEmitter { if (!response.ok) { console.error("[auth] Token refresh failed:", response.status); - return false; + // 401/403 means tokens are actually invalid, not a network issue + if (response.status === 401 || response.status === 403) { + return "invalid"; + } + // Other errors (500, etc) - treat as temporary, keep session + return "network_error"; } const tokens = (await response.json()) as { @@ -167,10 +195,11 @@ class AuthService extends EventEmitter { await tokenStorage.save(this.session); console.log("[auth] Tokens refreshed successfully"); - return true; + return "success"; } catch (err) { - console.error("[auth] Token refresh error:", err); - return false; + // Network errors (offline, DNS failure, etc) - keep session for offline use + console.error("[auth] Token refresh network error:", err); + return "network_error"; } } From f0139f59bb4cab99645c28b7881b46c6082d561e Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Fri, 26 Dec 2025 15:42:58 -0800 Subject: [PATCH 2/5] fix lint --- apps/desktop/src/main/lib/auth/auth-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index 604090e0466..2c4a3ce2772 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -149,7 +149,9 @@ class AuthService extends EventEmitter { * - 'invalid': Tokens are invalid/revoked (should clear session) * - 'network_error': Network unavailable (should keep session for offline use) */ - private async refreshTokens(): Promise<"success" | "invalid" | "network_error"> { + private async refreshTokens(): Promise< + "success" | "invalid" | "network_error" + > { if (!this.session?.refreshToken) { return "invalid"; } From c839d644e33b59cd43fb8a1a70a279787f3b66f8 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Fri, 26 Dec 2025 17:34:06 -0800 Subject: [PATCH 3/5] invalid check --- apps/desktop/src/main/lib/auth/auth-service.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index 2c4a3ce2772..df60237b062 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -180,13 +180,24 @@ class AuthService extends EventEmitter { return "network_error"; } - const tokens = (await response.json()) as { + let tokens: { accessToken: string; accessTokenExpiresAt: number; refreshToken: string; refreshTokenExpiresAt: number; }; + try { + tokens = (await response.json()) as typeof tokens; + } catch (parseErr) { + // JSON parse error indicates malformed server response - treat as invalid + console.error( + "[auth] Token refresh JSON parse error:", + parseErr instanceof Error ? parseErr.message : parseErr, + ); + return "invalid"; + } + // Update session with new tokens this.session = { accessToken: tokens.accessToken, @@ -200,7 +211,9 @@ class AuthService extends EventEmitter { return "success"; } catch (err) { // Network errors (offline, DNS failure, etc) - keep session for offline use - console.error("[auth] Token refresh network error:", err); + const errType = err instanceof Error ? err.constructor.name : typeof err; + const errMsg = err instanceof Error ? err.message : String(err); + console.error(`[auth] Token refresh network error (${errType}):`, errMsg); return "network_error"; } } From 655f011444bb852ca0f2c682d7d484d823e6c904 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Fri, 26 Dec 2025 23:57:35 -0800 Subject: [PATCH 4/5] address coderabbit --- .../desktop/src/main/lib/auth/auth-service.ts | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index df60237b062..c95c6824f83 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -36,6 +36,33 @@ function verifyState(state: string): boolean { return true; } +interface TokenResponse { + accessToken: string; + accessTokenExpiresAt: number; + refreshToken: string; + refreshTokenExpiresAt: number; +} + +/** + * Type guard to validate token response shape at runtime + */ +function isValidTokenResponse(data: unknown): data is TokenResponse { + if (typeof data !== "object" || data === null) { + return false; + } + const obj = data as Record; + return ( + typeof obj.accessToken === "string" && + obj.accessToken.length > 0 && + typeof obj.accessTokenExpiresAt === "number" && + obj.accessTokenExpiresAt > 0 && + typeof obj.refreshToken === "string" && + obj.refreshToken.length > 0 && + typeof obj.refreshTokenExpiresAt === "number" && + obj.refreshTokenExpiresAt > 0 + ); +} + import { tokenStorage } from "./token-storage"; /** @@ -180,17 +207,10 @@ class AuthService extends EventEmitter { return "network_error"; } - let tokens: { - accessToken: string; - accessTokenExpiresAt: number; - refreshToken: string; - refreshTokenExpiresAt: number; - }; - + let data: unknown; try { - tokens = (await response.json()) as typeof tokens; + data = await response.json(); } catch (parseErr) { - // JSON parse error indicates malformed server response - treat as invalid console.error( "[auth] Token refresh JSON parse error:", parseErr instanceof Error ? parseErr.message : parseErr, @@ -198,12 +218,21 @@ class AuthService extends EventEmitter { return "invalid"; } - // Update session with new tokens + // Validate response shape before persisting + if (!isValidTokenResponse(data)) { + console.error( + "[auth] Token refresh response missing required fields:", + data, + ); + return "invalid"; + } + + // Update session with validated tokens this.session = { - accessToken: tokens.accessToken, - accessTokenExpiresAt: tokens.accessTokenExpiresAt, - refreshToken: tokens.refreshToken, - refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, + accessToken: data.accessToken, + accessTokenExpiresAt: data.accessTokenExpiresAt, + refreshToken: data.refreshToken, + refreshTokenExpiresAt: data.refreshTokenExpiresAt, }; await tokenStorage.save(this.session); From bf899f5451722477620a0b3f41483ba1724796ac Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sat, 27 Dec 2025 00:08:18 -0800 Subject: [PATCH 5/5] move import statement --- apps/desktop/src/main/lib/auth/auth-service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index c95c6824f83..09819129a3d 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -3,6 +3,7 @@ import { EventEmitter } from "node:events"; import { type BrowserWindow, shell } from "electron"; import { env } from "main/env.main"; import type { AuthProvider, AuthSession, SignInResult } from "shared/auth"; +import { tokenStorage } from "./token-storage"; /** * Store for state parameter (CSRF protection) @@ -63,8 +64,6 @@ function isValidTokenResponse(data: unknown): data is TokenResponse { ); } -import { tokenStorage } from "./token-storage"; - /** * Main authentication service * Handles direct Google OAuth flow, with token exchange via API