@coretext-ai/qa-google-contacts-a58500d5-8331-4ce9-a140-d204a9fae815
Version:
MCP server with google-contacts integration
201 lines • 7.33 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
export class TokenManager {
constructor() {
this.tokenPath = path.resolve('~/.mcp/google-tokens/'.replace(/^~/, os.homedir()));
this.ensureTokenDirectory();
}
/**
* Store OAuth tokens securely
*/
async storeTokens(tokens, userId = 'default') {
const tokenFile = path.join(this.tokenPath, `${userId}.json`);
try {
// Try platform keychain first
const encryptedData = await this.encryptTokenData(tokens);
// Write with secure permissions
await fs.writeFile(tokenFile, encryptedData, { mode: 0o600 });
console.error(`[TOKEN_MANAGER] Tokens stored securely for user: ${userId}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to store tokens: ${errorMessage}`);
throw new Error(`Token storage failed: ${errorMessage}`);
}
}
/**
* Retrieve stored OAuth tokens
*/
async getTokens(userId = 'default') {
const tokenFile = path.join(this.tokenPath, `${userId}.json`);
if (!await fs.pathExists(tokenFile)) {
return null;
}
try {
const encryptedData = await fs.readFile(tokenFile, 'utf8');
return await this.decryptTokenData(encryptedData);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to retrieve tokens: ${errorMessage}`);
return null;
}
}
/**
* Delete stored tokens
*/
async deleteTokens(userId = 'default') {
const tokenFile = path.join(this.tokenPath, `${userId}.json`);
try {
if (await fs.pathExists(tokenFile)) {
await fs.remove(tokenFile);
console.error(`[TOKEN_MANAGER] Tokens deleted for user: ${userId}`);
}
// Also try to delete from keychain if used
await this.deleteFromKeychain(userId);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to delete tokens: ${errorMessage}`);
throw new Error(`Token deletion failed: ${errorMessage}`);
}
}
/**
* Encrypt token data using platform keychain or file-based encryption
*/
async encryptTokenData(tokens) {
try {
// Try platform keychain first
return await this.encryptWithKeychain(tokens);
}
catch (error) {
console.warn('Keychain not available, using file-based encryption');
return this.encryptTokenDataFile(tokens);
}
}
/**
* Decrypt token data from platform keychain or file
*/
async decryptTokenData(encryptedData) {
try {
const reference = JSON.parse(encryptedData);
if (reference.type === 'keychain') {
return await this.decryptFromKeychain(reference);
}
else {
return this.decryptTokenDataFile(encryptedData);
}
}
catch (error) {
// Fallback to file decryption
return this.decryptTokenDataFile(encryptedData);
}
}
/**
* Platform keychain encryption (macOS/Windows/Linux)
*/
async encryptWithKeychain(tokens) {
try {
const keytar = await import('keytar');
const serviceName = 'mcp-oauth-tokens';
const accountName = 'google-default';
const serializedData = JSON.stringify(tokens);
await keytar.setPassword(serviceName, accountName, serializedData);
// Return encrypted reference
return JSON.stringify({
type: 'keychain',
service: serviceName,
account: accountName
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Keychain encryption failed: ${errorMessage}`);
}
}
/**
* Platform keychain decryption
*/
async decryptFromKeychain(reference) {
try {
const keytar = await import('keytar');
const tokenData = await keytar.getPassword(reference.service, reference.account);
if (!tokenData) {
throw new Error('No data found in keychain');
}
return JSON.parse(tokenData);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Keychain decryption failed: ${errorMessage}`);
}
}
/**
* Delete from keychain
*/
async deleteFromKeychain(userId) {
try {
const keytar = await import('keytar');
const serviceName = 'mcp-oauth-tokens';
const accountName = `google-${userId}`;
await keytar.deletePassword(serviceName, accountName);
}
catch (error) {
// Ignore errors, keychain might not be available
}
}
/**
* Fallback file-based encryption
*/
encryptTokenDataFile(tokens) {
const algorithm = 'aes-256-gcm';
const key = this.getDerivedKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(JSON.stringify(tokens), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return JSON.stringify({
type: 'file',
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
data: encrypted
});
}
/**
* Fallback file-based decryption
*/
decryptTokenDataFile(encryptedData) {
const algorithm = 'aes-256-gcm';
const key = this.getDerivedKey();
const encrypted = JSON.parse(encryptedData);
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(encrypted.iv, 'hex'));
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
/**
* Derive encryption key from system-specific data
*/
getDerivedKey() {
const systemInfo = `${os.hostname()}-${os.userInfo().username}`;
return crypto.pbkdf2Sync('mcp-oauth', systemInfo, 100000, 32, 'sha512');
}
/**
* Ensure token directory exists with secure permissions
*/
async ensureTokenDirectory() {
try {
await fs.ensureDir(this.tokenPath, { mode: 0o700 });
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to create token directory: ${errorMessage}`);
throw new Error(`Token directory creation failed: ${errorMessage}`);
}
}
}
//# sourceMappingURL=token-manager.js.map