Skip to main content
Glama
tokenManager.js5.36 kB
/** * Token Manager for YNAB MCP * Handles storage, retrieval, and refresh of OAuth tokens */ const fs = require('fs-extra'); const path = require('path'); const axios = require('axios'); const { logger } = require('../utils/logger'); const { TokenError } = require('../utils/errorHandler'); const config = require('../../config-example'); class TokenManager { constructor(options = {}) { this.configPath = options.configPath || path.join(process.cwd(), 'config', 'tokens.json'); this.tokens = {}; this.encryptionEnabled = options.encryptionEnabled || config.tokenStore.encrypt || false; // Create directory if it doesn't exist const dir = path.dirname(this.configPath); fs.ensureDirSync(dir); this.loadTokens(); } /** * Load tokens from storage */ loadTokens() { try { if (fs.existsSync(this.configPath)) { const data = fs.readFileSync(this.configPath, 'utf8'); this.tokens = JSON.parse(data); logger.info('Tokens loaded successfully'); } else { logger.info('No tokens file found, starting with empty tokens'); } } catch (error) { logger.error('Failed to load tokens', error); this.tokens = {}; } } /** * Save tokens to persistent storage */ saveTokens() { try { fs.writeFileSync(this.configPath, JSON.stringify(this.tokens, null, 2)); logger.info('Tokens saved successfully'); } catch (error) { logger.error('Failed to save tokens', error); throw new TokenError('Failed to save authentication data'); } } /** * Get token for a specific account * @param {string} email - Account identifier * @returns {object|null} - Token data or null if not found */ getToken(email) { return this.tokens[email]; } /** * Set token for a specific account * @param {string} email - Account identifier * @param {object} tokenData - Token data including access_token, refresh_token, etc. */ setToken(email, tokenData) { this.tokens[email] = { ...tokenData, expires_at: Date.now() + (tokenData.expires_in * 1000) }; this.saveTokens(); } /** * Check if token exists for an account * @param {string} email - Account identifier * @returns {boolean} - True if token exists */ hasToken(email) { return !!this.tokens[email]; } /** * Remove token for an account * @param {string} email - Account identifier */ removeToken(email) { if (this.tokens[email]) { delete this.tokens[email]; this.saveTokens(); logger.info(`Token removed for ${email}`); return true; } return false; } /** * List all configured accounts * @returns {Array} - Array of account objects with authentication status */ listAccounts() { return Object.keys(this.tokens).map(email => { const token = this.tokens[email]; const isExpired = token.expires_at < Date.now(); return { email, authenticated: true, hasRefreshToken: !!token.refresh_token, isExpired, expiresAt: new Date(token.expires_at).toISOString() }; }); } /** * Get fresh access token for an account, refreshing if needed * @param {string} email - Account identifier * @returns {Promise<string>} - Fresh access token */ async getFreshAccessToken(email) { const token = this.getToken(email); if (!token) { throw new TokenError(`No token found for account: ${email}`); } // Check if token is about to expire (within 5 minutes) if (token.expires_at - Date.now() < 300000) { logger.info(`Token for ${email} is expiring soon, refreshing`); const refreshedToken = await this.refreshToken(email, token.refresh_token); return refreshedToken.access_token; } return token.access_token; } /** * Refresh an expired token * @param {string} email - Account identifier * @param {string} refreshToken - Refresh token * @returns {Promise<object>} - New token data */ async refreshToken(email, refreshToken) { try { const tokenUrl = 'https://app.youneedabudget.com/oauth/token'; const response = await axios.post(tokenUrl, { client_id: config.oauth.clientId, client_secret: config.oauth.clientSecret, grant_type: 'refresh_token', refresh_token: refreshToken }); const tokenData = { access_token: response.data.access_token, refresh_token: response.data.refresh_token || refreshToken, // Use new refresh token if provided token_type: response.data.token_type, expires_in: response.data.expires_in }; this.setToken(email, tokenData); logger.info(`Token refreshed successfully for ${email}`); return tokenData; } catch (error) { logger.error(`Token refresh failed for ${email}`, error); // Remove invalid token if (error.response && (error.response.status === 400 || error.response.status === 401)) { this.removeToken(email); } throw new TokenError('Failed to refresh token, authentication required'); } } } // Create singleton instance const tokenManager = new TokenManager(); module.exports = { TokenManager, tokenManager };

Latest Blog Posts

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/mattweg/ynab-mcp'

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