sourcewizard
Version:
SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration
234 lines (233 loc) • 8.38 kB
JavaScript
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;
}
}