Skip to main content
Glama
by mohalmah
tokenManager.js7.02 kB
/** * OAuth Token Manager for secure token storage and management * Handles access token refresh and secure storage of refresh tokens */ import fs from 'fs'; import path from 'path'; import os from 'os'; export class TokenManager { constructor() { this.tokenDir = this.getTokenDirectory(); this.tokenFile = path.join(this.tokenDir, 'tokens.json'); this.ensureTokenDirectory(); } /** * Get platform-specific directory for token storage * @returns {string} Token directory path */ getTokenDirectory() { const platform = os.platform(); const homedir = os.homedir(); switch (platform) { case 'win32': return path.join(homedir, 'AppData', 'Roaming', 'google-apps-script-mcp'); case 'darwin': return path.join(homedir, 'Library', 'Application Support', 'google-apps-script-mcp'); default: return path.join(homedir, '.config', 'google-apps-script-mcp'); } } /** * Ensure token directory exists */ ensureTokenDirectory() { if (!fs.existsSync(this.tokenDir)) { fs.mkdirSync(this.tokenDir, { recursive: true }); console.log(`📁 Created token directory: ${this.tokenDir}`); } } /** * Save OAuth tokens securely * @param {Object} tokens - OAuth tokens object */ saveTokens(tokens) { const tokenData = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: Date.now() + (tokens.expires_in * 1000), token_type: tokens.token_type || 'Bearer', scope: tokens.scope, saved_at: new Date().toISOString() }; try { fs.writeFileSync(this.tokenFile, JSON.stringify(tokenData, null, 2), { mode: 0o600 }); console.log(`💾 Tokens saved securely to: ${this.tokenFile}`); console.log(`🔒 File permissions: 600 (owner read/write only)`); } catch (error) { console.error('❌ Failed to save tokens:', error.message); throw new Error(`Failed to save tokens: ${error.message}`); } } /** * Load stored OAuth tokens * @returns {Object|null} Stored tokens or null if not found */ loadTokens() { if (!fs.existsSync(this.tokenFile)) { console.log('📝 No stored tokens found'); return null; } try { const tokenData = JSON.parse(fs.readFileSync(this.tokenFile, 'utf8')); console.log(`📖 Loaded tokens from: ${this.tokenFile}`); console.log(`💾 Tokens saved at: ${tokenData.saved_at || 'Unknown'}`); return tokenData; } catch (error) { console.error('❌ Failed to load tokens:', error.message); return null; } } /** * Check if current access token is expired or will expire soon * @param {Object} tokens - Token object * @returns {boolean} True if token is expired or will expire within 1 minute */ isTokenExpired(tokens) { if (!tokens || !tokens.expires_at) { return true; } // Consider token expired if it expires within the next minute const bufferTime = 60000; // 1 minute buffer const isExpired = Date.now() >= (tokens.expires_at - bufferTime); if (isExpired) { console.log('⏰ Access token is expired or will expire soon'); } else { const expiresIn = Math.round((tokens.expires_at - Date.now()) / 1000 / 60); console.log(`⏰ Access token expires in ${expiresIn} minutes`); } return isExpired; } /** * Refresh access token using stored refresh token * @param {string} clientId - OAuth client ID * @param {string} clientSecret - OAuth client secret * @returns {Promise<Object>} New tokens */ async refreshAccessToken(clientId, clientSecret) { const tokens = this.loadTokens(); if (!tokens || !tokens.refresh_token) { throw new Error('No refresh token available. Please run OAuth setup again: node oauth-setup.js'); } console.log('🔄 Refreshing access token...'); try { const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, refresh_token: tokens.refresh_token, grant_type: 'refresh_token' }) }); if (!response.ok) { const errorText = await response.text(); console.error('❌ Token refresh failed:', response.status, errorText); throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`); } const newTokens = await response.json(); // Keep the existing refresh token if not provided in response if (!newTokens.refresh_token) { newTokens.refresh_token = tokens.refresh_token; } // Save the refreshed tokens this.saveTokens(newTokens); console.log('✅ Access token refreshed successfully'); return newTokens; } catch (error) { console.error('❌ Failed to refresh access token:', error.message); throw error; } } /** * Get a valid access token, refreshing if necessary * @param {string} clientId - OAuth client ID * @param {string} clientSecret - OAuth client secret * @returns {Promise<string>} Valid access token */ async getValidAccessToken(clientId, clientSecret) { let tokens = this.loadTokens(); if (!tokens) { throw new Error('No tokens found. Please run OAuth setup first: node oauth-setup.js'); } if (this.isTokenExpired(tokens)) { console.log('🔄 Token expired, refreshing...'); tokens = await this.refreshAccessToken(clientId, clientSecret); } else { console.log('✅ Using existing valid access token'); } return tokens.access_token; } /** * Check if tokens are stored and available * @returns {boolean} True if refresh token is available */ hasStoredTokens() { const tokens = this.loadTokens(); return tokens && tokens.refresh_token; } /** * Clear stored tokens (for logout/reset) */ clearTokens() { if (fs.existsSync(this.tokenFile)) { fs.unlinkSync(this.tokenFile); console.log('🗑️ Stored tokens cleared'); } } /** * Get token storage information * @returns {Object} Token storage info */ getTokenInfo() { const tokens = this.loadTokens(); if (!tokens) { return { hasTokens: false, location: this.tokenFile, status: 'No tokens stored' }; } const isExpired = this.isTokenExpired(tokens); const expiresAt = new Date(tokens.expires_at); return { hasTokens: true, location: this.tokenFile, savedAt: tokens.saved_at, expiresAt: expiresAt.toISOString(), isExpired, scope: tokens.scope, status: isExpired ? 'Token expired' : 'Token valid' }; } } // Export using named export (already done at class declaration)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mohalmah/google-appscript-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server