UNPKG

sourcewizard

Version:

SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration

234 lines (233 loc) 8.38 kB
import fs from "fs/promises"; import path from "path"; import os from "os"; export class TokenStorage { configDir; tokenFilePath; logsDir; auth; refreshPromise; constructor(configDir, authClient) { // Use standard config directories based on OS this.configDir = path.join(os.homedir(), ".config", configDir); this.tokenFilePath = path.join(this.configDir, "auth.json"); this.logsDir = path.join(this.configDir, "logs"); this.auth = authClient; } /** * Set the auth client for token refresh operations */ setAuthClient(authClient) { this.auth = authClient; } /** * Ensure the config directory exists */ async ensureConfigDir() { try { await fs.access(this.configDir); } catch { await fs.mkdir(this.configDir, { recursive: true }); } } /** * Ensure the logs directory exists */ async ensureLogsDir() { try { await fs.access(this.logsDir); } catch { await fs.mkdir(this.logsDir, { recursive: true }); } } /** * Log error to file in logs directory */ async logError(error) { try { await this.ensureLogsDir(); const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] Token refresh error: ${error}\n`; const logFile = path.join(this.logsDir, "errors.log"); await fs.appendFile(logFile, logEntry); } catch (logError) { // Silent fail for logging errors to avoid infinite loops console.error("Failed to write to log file:", logError); } } /** * Store authentication tokens */ async storeTokens(tokens) { await this.ensureConfigDir(); const tokenData = JSON.stringify(tokens, null, 2); await fs.writeFile(this.tokenFilePath, tokenData, { mode: 0o600 }); // Restrict file permissions } /** * Refresh tokens using the stored refresh token */ async refreshTokens(currentTokens) { // If a refresh is already in progress, wait for it if (this.refreshPromise) { return this.refreshPromise; } if (!this.auth) { await this.logError("No auth client available for token refresh"); return null; } // Start the refresh and store the promise this.refreshPromise = this.performRefresh(currentTokens); try { const result = await this.refreshPromise; return result; } finally { // Clear the promise after completion this.refreshPromise = undefined; } } /** * Perform the actual token refresh */ async performRefresh(currentTokens) { try { // Use Supabase's refresh session method const { data, error } = await this.auth.refreshSession({ refresh_token: currentTokens.refreshToken, }); if (error || !data.session) { const errorMessage = error ? error.message : "No session data returned"; await this.logError(`Token refresh failed: ${errorMessage}`); // If refresh token is invalid, we should clear tokens to avoid repeated failed attempts if (error && (error.message.includes('Invalid Refresh Token') || error.message.includes('Refresh Token Not Found'))) { await this.logError("Invalid refresh token detected, clearing stored tokens"); await this.clearTokens(); } return null; } const session = data.session; const refreshedTokens = { accessToken: session.access_token, refreshToken: session.refresh_token, expiresAt: session.expires_at ? session.expires_at * 1000 : Date.now() + 3600000, // Default 1 hour if no expires_at user: { id: session.user.id, email: session.user.email || currentTokens.user.email, // Fallback to stored email }, }; // Store the refreshed tokens await this.storeTokens(refreshedTokens); return refreshedTokens; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await this.logError(`Token refresh exception: ${errorMessage}`); return null; } } /** * Attempt to refresh tokens on startup - doesn't clear tokens on failure */ async attemptStartupRefresh() { try { // Check if tokens file exists first try { await fs.access(this.tokenFilePath); } catch { // Token file doesn't exist, no need to refresh return false; } const tokenData = await fs.readFile(this.tokenFilePath, "utf-8"); const tokens = JSON.parse(tokenData); // Check if tokens are expired or close to expiry const now = Date.now(); const timeUntilExpiry = tokens.expiresAt - now; // If tokens expire within 10 minutes, try to refresh them if (timeUntilExpiry <= 10 * 60 * 1000) { const refreshedTokens = await this.refreshTokens(tokens); if (refreshedTokens === null) { // Refresh failed, but don't clear tokens in startup refresh // Let getTokens() handle clearing when actually requested return false; } return true; } return true; // Tokens are still valid } catch (error) { // Only log if it's not a file not found error (tokens already cleared) if (error instanceof Error && !error.message.includes('ENOENT')) { const errorMessage = error.message; await this.logError(`Failed to read tokens for startup refresh: ${errorMessage}`); } return false; } } /** * Retrieve stored authentication tokens with automatic refresh */ async getTokens() { try { const tokenData = await fs.readFile(this.tokenFilePath, "utf-8"); const tokens = JSON.parse(tokenData); // Check if tokens are expired const now = Date.now(); const timeUntilExpiry = tokens.expiresAt - now; // If tokens expire within 5 minutes, try to refresh them if (timeUntilExpiry <= 5 * 60 * 1000) { const refreshedTokens = await this.refreshTokens(tokens); if (refreshedTokens) { return refreshedTokens; } // If refresh failed, tokens might already be cleared by refreshTokens() // Check if tokens still exist before trying to clear them again try { await fs.access(this.tokenFilePath); await this.logError("Token refresh failed in getTokens, clearing tokens"); await this.clearTokens(); } catch { // Tokens already cleared, nothing to do await this.logError("Token refresh failed, tokens already cleared"); } return null; } return tokens; } catch (error) { // File doesn't exist or is corrupted return null; } } /** * Clear stored authentication tokens */ async clearTokens() { try { await fs.unlink(this.tokenFilePath); } catch { // File doesn't exist, nothing to clear } } /** * Check if valid tokens exist */ async hasValidTokens() { const tokens = await this.getTokens(); return tokens !== null; } /** * Get the stored user information */ async getStoredUser() { const tokens = await this.getTokens(); return tokens?.user || null; } }