import { exec } from 'child_process';
import { getApiUrl, saveCredentials, StoredCredentials } from './credentials';
/**
* Device authorization response from the API
*/
interface DeviceAuthResponse {
device_code: string;
user_code: string;
verification_uri: string;
verification_uri_complete: string;
expires_in: number;
interval: number;
}
/**
* Token response from the API
*/
interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
email: string;
name?: string;
}
/**
* Open a URL in the default browser
*/
function openBrowser(url: string): Promise<void> {
return new Promise((resolve) => {
// Determine the command based on platform
let command: string;
switch (process.platform) {
case 'darwin':
command = `open "${url}"`;
break;
case 'win32':
command = `start "" "${url}"`;
break;
default:
// Linux and others
command = `xdg-open "${url}"`;
}
exec(command, (error) => {
if (error) {
// Don't fail if browser can't open - user can manually navigate
resolve();
} else {
resolve();
}
});
});
}
/**
* Start the device authorization flow
* Returns a message describing the flow status
*/
export async function startDeviceFlow(): Promise<string> {
const apiUrl = getApiUrl();
// Step 1: Request device authorization
let authResponse: DeviceAuthResponse;
try {
const response = await fetch(`${apiUrl}/api/auth/device/authorize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientName: 'FastMode MCP',
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
return `# Authentication Error
Failed to start device authorization: ${error.error || response.statusText}
Please check:
1. Your network connection
2. The API URL is correct (${apiUrl})
`;
}
const data = await response.json();
authResponse = data.data;
} catch (error) {
return `# Network Error
Unable to connect to FastMode API.
**API URL:** ${apiUrl}
**Error:** ${error instanceof Error ? error.message : 'Unknown error'}
Please check your network connection and try again.
`;
}
// Step 2: Open browser for user authorization
try {
await openBrowser(authResponse.verification_uri_complete);
} catch {
// Browser failed to open - user will need to navigate manually
}
// Step 3: Poll for authorization
const pollInterval = (authResponse.interval || 5) * 1000; // seconds to ms
const expiresAt = Date.now() + authResponse.expires_in * 1000;
// Log instructions to stderr for user visibility
console.error(`
# Device Authorization
A browser window should open automatically.
If it doesn't, please visit:
${authResponse.verification_uri}
And enter this code: ${authResponse.user_code}
**Don't have a Fast Mode account?**
Sign up at https://fastmode.ai first, then return here.
Waiting for authorization...
`);
// Start polling
const pollResult = await pollForToken(
apiUrl,
authResponse.device_code,
pollInterval,
expiresAt
);
if (pollResult.success && pollResult.credentials) {
// Save credentials
saveCredentials(pollResult.credentials);
return `# Authentication Successful
Logged in as: **${pollResult.credentials.email}**${pollResult.credentials.name ? ` (${pollResult.credentials.name})` : ''}
Credentials saved to ~/.fastmode/credentials.json
You can now use FastMode MCP tools.
`;
} else {
return `# Authentication Failed
${pollResult.error || 'Authorization timed out or was denied.'}
**Don't have an account?**
Sign up at https://fastmode.ai and then try again.
**To retry:** Call any authenticated tool like \`list_projects\` to start a new auth flow.
`;
}
}
/**
* Poll the token endpoint until authorization is complete or timeout
*/
async function pollForToken(
apiUrl: string,
deviceCode: string,
interval: number,
expiresAt: number
): Promise<{ success: boolean; credentials?: StoredCredentials; error?: string }> {
while (Date.now() < expiresAt) {
// Wait for the polling interval
await new Promise(resolve => setTimeout(resolve, interval));
try {
const response = await fetch(`${apiUrl}/api/auth/device/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
device_code: deviceCode,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
}),
});
const data = await response.json();
if (response.ok && data.success && data.data) {
// Authorization successful!
const tokenData: TokenResponse = data.data;
const credentials: StoredCredentials = {
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(),
email: tokenData.email,
name: tokenData.name,
};
return { success: true, credentials };
}
// Check for specific error codes
if (data.error === 'authorization_pending') {
// User hasn't authorized yet - keep polling
continue;
}
if (data.error === 'slow_down') {
// Server is asking us to slow down
interval = Math.min(interval * 2, 30000); // Max 30 seconds
continue;
}
if (data.error === 'expired_token') {
return { success: false, error: 'The authorization request expired. Please try again.' };
}
if (data.error === 'access_denied') {
return { success: false, error: 'Authorization was denied. Please try again.' };
}
// Unknown error - keep polling
} catch {
// Network error - keep trying
}
}
return { success: false, error: 'Authorization timed out. Please try again.' };
}
/**
* Check if device flow authentication is needed and perform it if so
* Returns the authentication result message
*/
export async function ensureAuthenticated(): Promise<{ authenticated: boolean; message: string }> {
// Import here to avoid circular dependency
const { getValidCredentials } = await import('./credentials');
const { waitForAuth } = await import('./auth-state');
// Wait for any startup auth that might be in progress
await waitForAuth();
const credentials = await getValidCredentials();
if (credentials) {
return {
authenticated: true,
message: `Authenticated as ${credentials.email}`,
};
}
// Need to authenticate
const result = await startDeviceFlow();
// Check if authentication was successful
const newCredentials = await getValidCredentials();
return {
authenticated: !!newCredentials,
message: result,
};
}