import { input, search } from '@inquirer/prompts'; import { addLog, ErrorLog, InfoLog, SuccessLog } from '@mintlify/previewing'; import chalk from 'chalk'; import { Box, Text } from 'ink'; import open from 'open'; import { calculatePKCECodeChallenge, randomNonce, randomPKCECodeVerifier, randomState, } from 'openid-client'; import { startCallbackServer } from './callbackServer.js'; import { setConfigValue } from './config.js'; import { DASHBOARD_URL, TOKEN_ENDPOINT } from './constants.js'; import { getEmailFromIdToken } from './idToken.js'; import { storeCredentials } from './keyring.js'; import { getCliSubdomains } from './status.js'; import { trackLoginAttempt, trackLoginFailed, trackLoginSuccess } from './telemetry/track.js'; export interface TokenResponse { access_token: string; id_token: string; refresh_token: string; token_type: 'bearer'; expires_in: number; request_id: string; status_code: number; } export async function login(): Promise { const codeVerifier = randomPKCECodeVerifier(); const codeChallenge = await calculatePKCECodeChallenge(codeVerifier); const nonce = randomNonce(); const clientState = randomState(); const state = Buffer.from(JSON.stringify({ nonce, clientState })).toString('base64url'); const authorizeUrl = new URL('/api/cli/oauth/authorize', DASHBOARD_URL); authorizeUrl.searchParams.set('state', state); authorizeUrl.searchParams.set('code_challenge', codeChallenge); const url = authorizeUrl.toString(); void trackLoginAttempt(); const { codePromise, close: closeServer } = await startCallbackServer(); addLog( ◆ A browser window will open for Mintlify authentication If your browser doesn't open automatically, copy this URL: {url} ); open(url).catch(() => {}); addLog( ╭─ Paste the authorization code from your browser │ ); // Let ink finish rendering before inquirer takes over stdout await new Promise((resolve) => setTimeout(resolve, 50)); const inputPromise = input({ message: '█', theme: { prefix: chalk.dim(' │'), style: { answer: (text: string) => chalk.cyan(text), }, }, }); let code: string; try { code = await Promise.race([codePromise, inputPromise]); } catch { closeServer(); inputPromise.cancel(); addLog(); return; } closeServer(); inputPromise.cancel(); const res = await fetch(TOKEN_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: codeVerifier, }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { const reason = body.error_message ?? body.error ?? 'unknown error'; void trackLoginFailed(reason); addLog(); return; } const token = body as TokenResponse; await storeCredentials( token.access_token, token.refresh_token, getEmailFromIdToken(token.id_token) ?? null ); void trackLoginSuccess(); addLog(); await promptSubdomainSelection(token.access_token); } function isPromptCancellationError(error: unknown): boolean { return error instanceof Error && error.name === 'ExitPromptError'; } async function promptSubdomainSelection(accessToken: string): Promise { const subdomains = await getCliSubdomains(accessToken); if (subdomains.length === 0) return; if (subdomains.length === 1) { await setConfigValue('subdomain', subdomains[0]!); addLog(); return; } await new Promise((resolve) => setTimeout(resolve, 50)); let chosen: string; try { chosen = await search({ message: 'Select a default project', source: (term) => { const results = term ? subdomains.filter((s) => s.toLowerCase().includes(term.toLowerCase())) : subdomains; return results.map((s) => ({ name: s, value: s })); }, }); } catch (error) { if (isPromptCancellationError(error)) { addLog( ')}`} /> ); return; } throw error; } await setConfigValue('subdomain', chosen); addLog(); }