/** * CLI Authentication Integration * * Store and load authentication tokens for the CLI. * Uses secure storage in ~/.postgres.do/config.json * * @module cli/cli-auth */ import { loadConfig, saveConfig, getApiUrl } from './config.js' import { validateToken, type TokenValidationResult } from '../auth.js' /** * Authentication token info */ export interface AuthToken { token: string expiresAt?: Date refreshToken?: string user?: { id: string email: string name?: string } } /** * Login result */ export interface LoginResult { success: boolean token?: string user?: { id: string email: string name?: string } error?: string } /** * Store authentication token for an API URL * * @param token - The authentication token * @param apiUrl - The API URL (defaults to configured URL) * @param expiresAt - Optional expiration date * @param refreshToken - Optional refresh token */ export function storeToken( token: string, apiUrl?: string, expiresAt?: Date, refreshToken?: string ): void { const config = loadConfig() const url = apiUrl || getApiUrl() config.auth = config.auth || {} const authEntry: { token: string; expiresAt?: string; refreshToken?: string } = { token } if (expiresAt) authEntry.expiresAt = expiresAt.toISOString() if (refreshToken) authEntry.refreshToken = refreshToken config.auth[url] = authEntry saveConfig(config) } /** * Load authentication token for an API URL * * @param apiUrl - The API URL (defaults to configured URL) * @returns The stored token info or null if not found */ export function loadToken(apiUrl?: string): AuthToken | null { const config = loadConfig() const url = apiUrl || getApiUrl() const authInfo = config.auth?.[url] if (!authInfo) return null const result: AuthToken = { token: authInfo.token } if (authInfo.expiresAt) result.expiresAt = new Date(authInfo.expiresAt) if (authInfo.refreshToken) result.refreshToken = authInfo.refreshToken return result } /** * Remove authentication token for an API URL * * @param apiUrl - The API URL (defaults to configured URL) */ export function removeToken(apiUrl?: string): void { const config = loadConfig() const url = apiUrl || getApiUrl() if (config.auth?.[url]) { delete config.auth[url] saveConfig(config) } } /** * Check if a token is stored and not expired * * @param apiUrl - The API URL (defaults to configured URL) * @returns True if a valid token is stored */ export function hasValidToken(apiUrl?: string): boolean { const auth = loadToken(apiUrl) if (!auth) return false // Check if expired if (auth.expiresAt && auth.expiresAt < new Date()) { return false } return true } /** * Get the current authentication token * * Checks for token in this order: * 1. POSTGRES_DO_API_KEY environment variable * 2. Stored token in config * * @param apiUrl - The API URL (defaults to configured URL) * @returns The token or null if not found */ export function getToken(apiUrl?: string): string | null { // Check environment variable first if (process.env['POSTGRES_DO_API_KEY']) { return process.env['POSTGRES_DO_API_KEY'] } // Check stored token const auth = loadToken(apiUrl) if (auth && hasValidToken(apiUrl)) { return auth.token } return null } /** * Ensure we have a valid token, throwing if not * * @param apiUrl - The API URL (defaults to configured URL) * @returns The valid token * @throws Error if no valid token is available */ export function requireToken(apiUrl?: string): string { const token = getToken(apiUrl) if (!token) { throw new Error( 'Authentication required. Run `postgres.do login` or set POSTGRES_DO_API_KEY environment variable.' ) } return token } /** * Validate and refresh the current token * * @param apiUrl - The API URL (defaults to configured URL) * @returns Validation result */ export async function validateCurrentToken(apiUrl?: string): Promise { const token = getToken(apiUrl) if (!token) { return { valid: false, error: 'No token available', } } const url = apiUrl || getApiUrl() const oauthUrl = url.replace('api.postgres.do', 'oauth.do') return validateToken(token, { oauthUrl }) } /** * Login with credentials (for non-interactive use) * * @param email - User email * @param password - User password * @param apiUrl - The API URL (defaults to configured URL) * @returns Login result */ export async function loginWithCredentials( email: string, password: string, apiUrl?: string ): Promise { const url = apiUrl || getApiUrl() const oauthUrl = url.replace('api.postgres.do', 'oauth.do') try { const response = await fetch(`${oauthUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }), }) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Login failed' })) return { success: false, error: (error as { message?: string }).message || 'Login failed', } } const data = await response.json() as { token: string expiresAt?: string refreshToken?: string user?: { id: string; email: string; name?: string } } // Store the token storeToken( data.token, url, data.expiresAt ? new Date(data.expiresAt) : undefined, data.refreshToken ) const result: LoginResult = { success: true, token: data.token, } if (data.user) result.user = data.user return result } catch (error) { return { success: false, error: `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`, } } } /** * Login with API key * * @param apiKey - The API key * @param apiUrl - The API URL (defaults to configured URL) * @returns Login result */ export async function loginWithApiKey(apiKey: string, apiUrl?: string): Promise { const url = apiUrl || getApiUrl() // Validate the API key const validation = await validateToken(apiKey, { oauthUrl: url.replace('api.postgres.do', 'oauth.do'), }) if (!validation.valid) { return { success: false, error: validation.error || 'Invalid API key', } } // Store the token storeToken(apiKey, url, validation.expiresAt) const result: LoginResult = { success: true, token: apiKey, } if (validation.user) result.user = validation.user return result } /** * Logout (remove stored token) * * @param apiUrl - The API URL (defaults to configured URL) */ export function logout(apiUrl?: string): void { removeToken(apiUrl) } /** * Get current user info if logged in * * @param apiUrl - The API URL (defaults to configured URL) * @returns User info or null if not logged in */ export async function getCurrentUser(apiUrl?: string): Promise<{ id: string email: string name?: string } | null> { const validation = await validateCurrentToken(apiUrl) return validation.valid && validation.user ? validation.user : null } /** * Check if user is logged in * * @param apiUrl - The API URL (defaults to configured URL) * @returns True if logged in */ export function isLoggedIn(apiUrl?: string): boolean { return hasValidToken(apiUrl) }