import fs from 'fs/promises';
import path from 'path';
import os from 'os';
/**
* トークン情報
*/
export interface TokenInfo {
token: string;
baseUrl: string;
email: string;
expiresAt?: number; // タイムスタンプ (ミリ秒)
}
/**
* トークンストレージ
* ローカルファイルシステムにトークンを安全に保存・読み込み
*/
export class TokenStorage {
private tokenDir: string;
private tokenFile: string;
constructor() {
// ~/.verge-mcp/ ディレクトリにトークンを保存
this.tokenDir = path.join(os.homedir(), '.verge-mcp');
this.tokenFile = path.join(this.tokenDir, 'token.json');
}
/**
* トークンを保存
*/
async saveToken(tokenInfo: TokenInfo): Promise<void> {
try {
// ディレクトリが存在しない場合は作成
await fs.mkdir(this.tokenDir, { recursive: true, mode: 0o700 });
// トークン情報をJSON形式で保存
await fs.writeFile(
this.tokenFile,
JSON.stringify(tokenInfo, null, 2),
{ mode: 0o600 } // 所有者のみ読み書き可能
);
} catch (error) {
throw new Error(
`Failed to save token: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* 保存されたトークンを読み込み
*/
async loadToken(): Promise<TokenInfo | null> {
try {
const data = await fs.readFile(this.tokenFile, 'utf-8');
const tokenInfo: TokenInfo = JSON.parse(data);
// 有効期限チェック
if (tokenInfo.expiresAt && tokenInfo.expiresAt < Date.now()) {
// トークンが期限切れの場合は削除
await this.deleteToken();
return null;
}
return tokenInfo;
} catch (error) {
// ファイルが存在しない場合はnullを返す
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw new Error(
`Failed to load token: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* 保存されたトークンを削除
*/
async deleteToken(): Promise<void> {
try {
await fs.unlink(this.tokenFile);
} catch (error) {
// ファイルが存在しない場合は無視
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw new Error(
`Failed to delete token: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
}
/**
* トークンが保存されているかチェック
*/
async hasToken(): Promise<boolean> {
try {
await fs.access(this.tokenFile);
return true;
} catch {
return false;
}
}
}