teams-mcp-server
Version:
Microsoft Teams MCP server with direct messaging support
121 lines (120 loc) • 4.19 kB
JavaScript
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;
}
}
}