import { randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';
const scryptAsync = promisify(scrypt);
export interface StoredTokens {
access_token: string;
refresh_token: string;
expires_at: number;
scope: string;
user_id?: string;
created_at: string;
updated_at: string;
}
export interface TokenStoreConfig {
encryptionKey: string;
ttlMs?: number;
}
export class EncryptedTokenStore {
private key: Buffer;
private ttlMs: number;
constructor(config: TokenStoreConfig) {
this.key = Buffer.from(config.encryptionKey, 'base64');
this.ttlMs = config.ttlMs || 7 * 24 * 60 * 60 * 1000;
}
private async encryptTokens(tokens: StoredTokens): Promise<string> {
const iv = randomBytes(16);
const cipher = require('crypto').createCipher('aes-256-gcm', this.key);
cipher.setAAD(Buffer.from('mcp-router-tokens'));
let encrypted = cipher.update(JSON.stringify(tokens), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}
private async decryptTokens(encryptedData: string): Promise<StoredTokens> {
const parts = encryptedData.split(':');
if (parts.length !== 3) {
throw new Error('Invalid encrypted token format');
}
const iv = Buffer.from(parts[0], 'hex');
const authTag = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const decipher = require('crypto').createDecipher('aes-256-gcm', this.key);
decipher.setAAD(Buffer.from('mcp-router-tokens'));
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
async storeTokens(userId: string, tokens: any): Promise<void> {
const now = new Date().toISOString();
const storedTokens: StoredTokens = {
...tokens,
user_id: userId,
created_at: now,
updated_at: now
};
const encrypted = await this.encryptTokens(storedTokens);
this.memoryStore.set(userId, encrypted);
}
async getTokens(userId: string): Promise<StoredTokens | null> {
const encrypted = this.memoryStore.get(userId);
if (!encrypted) {
return null;
}
try {
const tokens = await this.decryptTokens(encrypted);
if (Date.now() > tokens.expires_at) {
this.memoryStore.delete(userId);
return null;
}
return tokens;
} catch (error) {
this.memoryStore.delete(userId);
return null;
}
}
async updateTokens(userId: string, tokens: Partial<any>): Promise<void> {
const existing = await this.getTokens(userId);
if (!existing) {
throw new Error('No existing tokens found for user');
}
const updatedTokens: StoredTokens = {
...existing,
...tokens,
updated_at: new Date().toISOString()
};
const encrypted = await this.encryptTokens(updatedTokens);
this.memoryStore.set(userId, encrypted);
}
async deleteTokens(userId: string): Promise<void> {
this.memoryStore.delete(userId);
}
async hasValidTokens(userId: string): Promise<boolean> {
const tokens = await this.getTokens(userId);
return tokens !== null && !this.isTokenExpired(tokens);
}
private isTokenExpired(tokens: StoredTokens): boolean {
return Date.now() >= tokens.expires_at;
}
async getRefreshToken(userId: string): Promise<string | null> {
const tokens = await this.getTokens(userId);
return tokens?.refresh_token || null;
}
async updateAccessToken(
userId: string,
accessToken: string,
expiresAt: number
): Promise<void> {
await this.updateTokens(userId, {
access_token: accessToken,
expires_at: expiresAt
});
}
private memoryStore = new Map<string, string>();
}
export function createTokenStore(config: TokenStoreConfig): EncryptedTokenStore {
return new EncryptedTokenStore(config);
}
export function generateEncryptionKey(): string {
return randomBytes(32).toString('base64');
}