import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* Stored credentials for the MCP server
*/
export interface StoredCredentials {
accessToken: string;
refreshToken: string;
expiresAt: string; // ISO timestamp
email: string;
name?: string;
}
/**
* Get the path to the credentials file
*/
export function getCredentialsPath(): string {
const configDir = path.join(os.homedir(), '.fastmode');
return path.join(configDir, 'credentials.json');
}
/**
* Ensure the config directory exists
*/
function ensureConfigDir(): void {
const configDir = path.dirname(getCredentialsPath());
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
}
}
/**
* Load stored credentials from disk
*/
export function loadCredentials(): StoredCredentials | null {
const credentialsPath = getCredentialsPath();
try {
if (!fs.existsSync(credentialsPath)) {
return null;
}
const content = fs.readFileSync(credentialsPath, 'utf-8');
const credentials = JSON.parse(content) as StoredCredentials;
// Validate required fields
if (!credentials.accessToken || !credentials.refreshToken || !credentials.expiresAt || !credentials.email) {
return null;
}
return credentials;
} catch {
return null;
}
}
/**
* Save credentials to disk
*/
export function saveCredentials(credentials: StoredCredentials): void {
ensureConfigDir();
const credentialsPath = getCredentialsPath();
// Write with restricted permissions (owner read/write only)
fs.writeFileSync(
credentialsPath,
JSON.stringify(credentials, null, 2),
{ mode: 0o600 }
);
}
/**
* Delete stored credentials
*/
export function deleteCredentials(): void {
const credentialsPath = getCredentialsPath();
try {
if (fs.existsSync(credentialsPath)) {
fs.unlinkSync(credentialsPath);
}
} catch {
// Ignore errors
}
}
/**
* Check if credentials exist
*/
export function hasCredentials(): boolean {
return loadCredentials() !== null;
}
/**
* Check if the access token is expired or about to expire
*/
export function isTokenExpired(credentials: StoredCredentials, bufferMinutes: number = 5): boolean {
const expiresAt = new Date(credentials.expiresAt);
const bufferMs = bufferMinutes * 60 * 1000;
return Date.now() >= expiresAt.getTime() - bufferMs;
}
/**
* Get the API URL from environment or default
*/
export function getApiUrl(): string {
return process.env.FASTMODE_API_URL || 'https://api.fastmode.ai';
}
/**
* Refresh the access token using the refresh token
*/
export async function refreshAccessToken(credentials: StoredCredentials): Promise<StoredCredentials | null> {
const apiUrl = getApiUrl();
try {
const response = await fetch(`${apiUrl}/api/auth/device/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: credentials.refreshToken,
grant_type: 'refresh_token',
}),
});
if (!response.ok) {
// Refresh token is invalid or expired - need to re-authenticate
return null;
}
const data = await response.json();
if (!data.success || !data.data) {
return null;
}
const newCredentials: StoredCredentials = {
accessToken: data.data.access_token,
refreshToken: data.data.refresh_token,
expiresAt: new Date(Date.now() + data.data.expires_in * 1000).toISOString(),
email: data.data.email,
name: data.data.name,
};
// Save the new credentials
saveCredentials(newCredentials);
return newCredentials;
} catch {
return null;
}
}
/**
* Get valid credentials, refreshing if needed
* Returns null if no credentials or refresh fails
*/
export async function getValidCredentials(): Promise<StoredCredentials | null> {
const credentials = loadCredentials();
if (!credentials) {
return null;
}
// Check if token is expired or about to expire
if (isTokenExpired(credentials)) {
// Try to refresh
const newCredentials = await refreshAccessToken(credentials);
if (!newCredentials) {
// Refresh failed - credentials are invalid
deleteCredentials();
return null;
}
return newCredentials;
}
return credentials;
}
/**
* Get the current auth token (access token) if available and valid
*/
export async function getAuthToken(): Promise<string | null> {
const credentials = await getValidCredentials();
return credentials?.accessToken || null;
}