google-calendar-mcp
by nspady
import { OAuth2Client, Credentials } from 'google-auth-library';
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';
export class TokenManager {
private oauth2Client: OAuth2Client;
private tokenPath: string;
private refreshTimer: NodeJS.Timeout | null = null;
private maxRetries = 5;
private refreshThreshold = 0.8; // Refresh at 80% of token lifetime
constructor(oauth2Client: OAuth2Client) {
this.oauth2Client = oauth2Client;
this.tokenPath = this.getSecureTokenPath();
this.setupTokenRefresh();
}
private getSecureTokenPath(): string {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
return path.join(__dirname, '../.gcp-saved-tokens.json');
}
private calculateJitter(retryCount: number): number {
const baseDelay = 1000; // 1 second
const maxJitter = 1000; // 1 second
const exponentialDelay = baseDelay * Math.pow(2, retryCount);
const jitter = Math.random() * maxJitter;
return exponentialDelay + jitter;
}
private async setupTokenRefresh(): Promise<void> {
const credentials = this.oauth2Client.credentials;
if (!credentials.expiry_date) return;
const now = Date.now();
const timeUntilExpiry = credentials.expiry_date - now;
const refreshTime = timeUntilExpiry * (1 - this.refreshThreshold);
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(
() => this.refreshToken(),
Math.max(0, refreshTime)
);
}
private async refreshToken(retryCount = 0): Promise<void> {
try {
const response = await this.oauth2Client.refreshAccessToken();
const newTokens = response.credentials;
if (!newTokens.access_token) {
throw new Error('Received invalid tokens during refresh');
}
await this.saveTokens(newTokens);
this.setupTokenRefresh();
} catch (error) {
console.error(`Token refresh attempt ${retryCount + 1} failed:`, error);
if (retryCount < this.maxRetries) {
const delay = this.calculateJitter(retryCount);
setTimeout(() => this.refreshToken(retryCount + 1), delay);
} else {
console.error('Token refresh failed after maximum retries');
}
}
}
public async loadSavedTokens(): Promise<boolean> {
try {
const tokens = JSON.parse(await fs.readFile(this.tokenPath, 'utf-8'));
if (!tokens || typeof tokens !== 'object') {
console.error('Invalid token format');
return false;
}
this.oauth2Client.setCredentials(tokens);
this.setupTokenRefresh();
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Error loading tokens:', error);
}
return false;
}
}
public async saveTokens(tokens: Credentials): Promise<void> {
try {
await fs.writeFile(
this.tokenPath,
JSON.stringify(tokens, null, 2),
{ mode: 0o600 }
);
this.oauth2Client.setCredentials(tokens);
this.setupTokenRefresh();
} catch (error) {
console.error('Error saving tokens:', error);
throw error;
}
}
public async validateTokens(): Promise<boolean> {
const credentials = this.oauth2Client.credentials;
if (!credentials.access_token) return false;
if (credentials.expiry_date) {
const now = Date.now();
if (now >= credentials.expiry_date) {
try {
await this.refreshToken();
return true;
} catch {
return false;
}
}
}
return true;
}
public clearTokens(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
}