Skip to main content
Glama
token-store.ts6.73 kB
/** * FreshBooks OAuth2 Token Storage * * Provides secure encrypted file storage and environment-based storage for OAuth tokens. */ import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto'; import { promises as fs } from 'fs'; import { promisify } from 'util'; import { dirname } from 'path'; import { TokenData, TokenStore } from './types.js'; const scryptAsync = promisify(scrypt); /** * Encrypted file-based token storage using AES-256-GCM * * Tokens are encrypted at rest using a key derived from: * - Machine-specific identifier (username + platform) * - Optional password from environment variable * * This provides defense-in-depth: even if the token file is accessed, * it cannot be decrypted without the correct machine context. */ export class EncryptedFileTokenStore implements TokenStore { private filePath: string; private encryptionKey: Buffer | null = null; constructor(filePath: string) { this.filePath = filePath; } /** * Derive encryption key from machine-specific data */ private async getKey(): Promise<Buffer> { if (this.encryptionKey) { return this.encryptionKey; } // Use machine-specific salt const machineId = this.getMachineId(); // Optional password from environment, or default const password = process.env.FRESHBOOKS_TOKEN_PASSWORD || 'freshbooks-mcp-default-key'; // Derive 32-byte key using scrypt this.encryptionKey = (await scryptAsync(password, machineId, 32)) as Buffer; return this.encryptionKey; } /** * Generate machine-specific identifier for key derivation */ private getMachineId(): string { // Combine platform-specific data for unique salt const username = process.env.USERNAME || process.env.USER || 'unknown'; const platform = process.platform; const hostname = process.env.COMPUTERNAME || process.env.HOSTNAME || 'unknown'; return `${username}-${platform}-${hostname}-freshbooks-mcp`; } /** * Save encrypted token data to file */ async save(token: TokenData): Promise<void> { const key = await this.getKey(); // Generate random IV for this encryption const iv = randomBytes(16); // Create cipher const cipher = createCipheriv('aes-256-gcm', key, iv); // Encrypt token data const data = JSON.stringify(token); const encrypted = Buffer.concat([ cipher.update(data, 'utf8'), cipher.final(), ]); // Get authentication tag const authTag = cipher.getAuthTag(); // Store IV, auth tag, and encrypted data const stored = { iv: iv.toString('hex'), authTag: authTag.toString('hex'), data: encrypted.toString('hex'), }; // Ensure directory exists await fs.mkdir(dirname(this.filePath), { recursive: true }); // Write to file with restricted permissions await fs.writeFile(this.filePath, JSON.stringify(stored), { encoding: 'utf8', mode: 0o600, // Owner read/write only }); } /** * Retrieve and decrypt token data from file */ async get(): Promise<TokenData | null> { try { // Read encrypted file const content = await fs.readFile(this.filePath, 'utf8'); const stored = JSON.parse(content); // Get decryption key const key = await this.getKey(); // Parse stored components const iv = Buffer.from(stored.iv, 'hex'); const authTag = Buffer.from(stored.authTag, 'hex'); const encrypted = Buffer.from(stored.data, 'hex'); // Create decipher const decipher = createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(authTag); // Decrypt data const decrypted = Buffer.concat([ decipher.update(encrypted), decipher.final(), ]); // Parse and return token data return JSON.parse(decrypted.toString('utf8')); } catch (error) { // File doesn't exist, is corrupted, or decryption failed // Return null to indicate no valid token return null; } } /** * Clear all stored tokens */ async clear(): Promise<void> { try { await fs.unlink(this.filePath); } catch { // File may not exist, ignore error } // Clear cached encryption key this.encryptionKey = null; } } /** * Environment-based token storage for CI/testing * * Reads tokens from environment variables: * - FRESHBOOKS_ACCESS_TOKEN (required) * - FRESHBOOKS_REFRESH_TOKEN (optional) * - FRESHBOOKS_TOKEN_EXPIRES (optional, unix timestamp) * - FRESHBOOKS_ACCOUNT_ID (optional) * - FRESHBOOKS_BUSINESS_ID (optional) * * This store is read-only and cannot persist token changes. */ export class EnvTokenStore implements TokenStore { /** * Retrieve token from environment variables */ async get(): Promise<TokenData | null> { const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN; if (!accessToken) { return null; } const refreshToken = process.env.FRESHBOOKS_REFRESH_TOKEN || ''; const expiresAt = parseInt(process.env.FRESHBOOKS_TOKEN_EXPIRES || '0', 10); const accountId = process.env.FRESHBOOKS_ACCOUNT_ID; const businessId = process.env.FRESHBOOKS_BUSINESS_ID ? parseInt(process.env.FRESHBOOKS_BUSINESS_ID, 10) : undefined; const tokenData: TokenData = { accessToken, refreshToken, expiresAt, tokenType: 'Bearer', }; if (accountId) { tokenData.accountId = accountId; } if (businessId !== undefined) { tokenData.businessId = businessId; } return tokenData; } /** * Save operation not supported for environment store */ async save(_token: TokenData): Promise<void> { // Environment store is read-only // Tokens should be updated via environment variable configuration console.error( 'Warning: EnvTokenStore is read-only. Cannot persist token updates. ' + 'Update FRESHBOOKS_ACCESS_TOKEN and related environment variables manually.' ); } /** * Clear operation not supported for environment store */ async clear(): Promise<void> { // Cannot clear environment variables console.error( 'Warning: EnvTokenStore is read-only. Cannot clear environment variables.' ); } } /** * In-memory token storage for testing * * Stores tokens in memory only. Tokens are lost when process exits. * Useful for unit tests and temporary operations. */ export class InMemoryTokenStore implements TokenStore { private token: TokenData | null = null; async get(): Promise<TokenData | null> { return this.token; } async save(token: TokenData): Promise<void> { this.token = token; } async clear(): Promise<void> { this.token = null; } }

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/Good-Samaritan-Software-LLC/freshbooks-mcp'

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