mcp-memory-libsql
by spences10
Verified
- src
- utils
import fs from 'fs/promises';
import path from 'path';
import { TokenData, GoogleApiError, Account } from '../types.js';
const ENV_PREFIX = 'GOOGLE_TOKEN_';
export class TokenManager {
private readonly credentialsDir: string;
private readonly envTokens: Map<string, TokenData>;
constructor() {
this.credentialsDir = process.env.CREDENTIALS_DIR || path.resolve('config', 'credentials');
this.envTokens = new Map();
this.loadEnvTokens();
}
private loadEnvTokens(): void {
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith(ENV_PREFIX) && value) {
try {
const email = key
.slice(ENV_PREFIX.length)
.toLowerCase()
.replace(/_/g, '.');
const tokenData = JSON.parse(
Buffer.from(value, 'base64').toString()
) as TokenData;
this.envTokens.set(email, tokenData);
} catch (error) {
console.warn(`Failed to parse token from env var ${key}:`, error);
}
}
}
}
private getTokenPath(email: string): string {
const sanitizedEmail = email.replace(/[@.]/g, '-');
return path.join(this.credentialsDir, `${sanitizedEmail}.token.json`);
}
private updateEnvToken(email: string, tokenData: TokenData): void {
const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
process.env[`${ENV_PREFIX}${safeEmail}`] = Buffer.from(
JSON.stringify(tokenData)
).toString('base64');
this.envTokens.set(email, tokenData);
}
async saveToken(email: string, tokenData: TokenData): Promise<void> {
try {
// Save to file system
const tokenPath = this.getTokenPath(email);
await fs.writeFile(tokenPath, JSON.stringify(tokenData, null, 2));
// Update environment variable
this.updateEnvToken(email, tokenData);
} catch (error) {
throw new GoogleApiError(
'Failed to save token',
'TOKEN_SAVE_ERROR',
'Please ensure the directory specified by CREDENTIALS_DIR is writable'
);
}
}
async loadToken(email: string): Promise<TokenData | null> {
// First try to load from environment variables
const envToken = this.envTokens.get(email);
if (envToken) {
return envToken;
}
// Fall back to file system
try {
const tokenPath = this.getTokenPath(email);
const data = await fs.readFile(tokenPath, 'utf-8');
const token = JSON.parse(data) as TokenData;
// Update environment variable for future use
this.updateEnvToken(email, token);
return token;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw new GoogleApiError(
'Failed to load token',
'TOKEN_LOAD_ERROR',
'Token file may be corrupted or inaccessible'
);
}
}
async deleteToken(email: string): Promise<void> {
try {
// Remove from file system
const tokenPath = this.getTokenPath(email);
await fs.unlink(tokenPath);
// Remove from environment
const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
delete process.env[`${ENV_PREFIX}${safeEmail}`];
this.envTokens.delete(email);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw new GoogleApiError(
'Failed to delete token',
'TOKEN_DELETE_ERROR',
'Please ensure you have permission to delete the token file in CREDENTIALS_DIR'
);
}
}
}
isTokenExpired(tokenData: TokenData): boolean {
// Consider token expired 5 minutes before actual expiry
const expiryBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
return Date.now() >= tokenData.expiry_date - expiryBuffer;
}
hasRequiredScopes(tokenData: TokenData, requiredScopes: string[]): boolean {
const tokenScopes = new Set(tokenData.scope.split(' '));
return requiredScopes.every(scope => tokenScopes.has(scope));
}
async getTokenStatus(email: string): Promise<Account['auth_status']> {
const token = await this.loadToken(email);
if (!token) {
return { has_token: false };
}
return {
has_token: true,
scopes: token.scope.split(' '),
expires: token.expiry_date
};
}
async validateToken(email: string, requiredScopes: string[]): Promise<{
valid: boolean;
token?: TokenData;
reason?: string;
}> {
const token = await this.loadToken(email);
if (!token) {
return { valid: false, reason: 'Token not found' };
}
if (this.isTokenExpired(token)) {
return { valid: false, token, reason: 'Token expired' };
}
if (!this.hasRequiredScopes(token, requiredScopes)) {
return { valid: false, reason: 'Insufficient scopes' };
}
return { valid: true, token };
}
}