UNPKG

teams-mcp-server

Version:

Microsoft Teams MCP server with direct messaging support

121 lines (120 loc) 4.19 kB
import * as keytar from 'keytar'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as crypto from 'crypto'; import { debugLog } from '../utils/logger.js'; const SERVICE_NAME = 'teams-mcp-server'; const ACCOUNT_NAME = 'auth-token'; const FALLBACK_CACHE_DIR = path.join(process.cwd(), '.teams-mcp'); const FALLBACK_CACHE_FILE = path.join(FALLBACK_CACHE_DIR, 'auth-cache'); export class TokenCache { useKeytar = true; encryptionKey; constructor() { this.encryptionKey = crypto.scryptSync('teams-mcp-salt', 'salt', 32); } async saveToken(token) { const tokenData = JSON.stringify(token); debugLog('Saving token to cache...'); try { if (this.useKeytar) { await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, tokenData); debugLog('Token saved to keychain'); return; } } catch (error) { debugLog(`Keytar not available, falling back to encrypted file storage: ${error}`); this.useKeytar = false; } await this.saveToEncryptedFile(tokenData); debugLog('Token saved to encrypted file'); } async getToken() { let tokenData = null; debugLog('Attempting to retrieve token from cache...'); try { if (this.useKeytar) { tokenData = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME); if (tokenData) { debugLog('Token found in keychain'); } } } catch (error) { debugLog(`Keytar error: ${error}`); this.useKeytar = false; } if (!tokenData) { tokenData = await this.readFromEncryptedFile(); if (tokenData) { debugLog('Token found in encrypted file'); } } if (!tokenData) { debugLog('No token found in cache'); return null; } try { const token = JSON.parse(tokenData); token.expiresOn = new Date(token.expiresOn); if (token.expiresOn < new Date()) { await this.clearToken(); return null; } return token; } catch (error) { await this.clearToken(); return null; } } async clearToken() { try { if (this.useKeytar) { await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME); } } catch (error) { // Ignore keytar errors } try { await fs.unlink(FALLBACK_CACHE_FILE); } catch (error) { // File might not exist } } async saveToEncryptedFile(data) { try { await fs.mkdir(FALLBACK_CACHE_DIR, { recursive: true }); debugLog(`Created cache directory: ${FALLBACK_CACHE_DIR}`); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const combined = iv.toString('hex') + ':' + encrypted; await fs.writeFile(FALLBACK_CACHE_FILE, combined, 'utf8'); await fs.chmod(FALLBACK_CACHE_FILE, 0o600); debugLog(`Token saved to file: ${FALLBACK_CACHE_FILE}`); } catch (error) { debugLog(`Failed to save token to encrypted file: ${error}`); throw error; } } async readFromEncryptedFile() { try { const combined = await fs.readFile(FALLBACK_CACHE_FILE, 'utf8'); const [ivHex, encrypted] = combined.split(':'); const iv = Buffer.from(ivHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { return null; } } }