@visionfi/server-cli
Version:
Command-line interface for VisionFI Server SDK
303 lines (302 loc) • 13.2 kB
JavaScript
/**
* Authentication commands for VisionFI CLI
* Copyright (c) 2024-2025 VisionFI. All Rights Reserved.
*/
import { Display } from '../utils/display.js';
import { loadConfig, saveConfig, createClient, getConfigPath } from '../utils/config.js';
import inquirer from 'inquirer';
import * as fs from 'fs';
/**
* Verify authentication with the API
*/
export async function verifyAuth() {
const spinner = Display.spinner('Verifying authentication...');
const config = loadConfig();
try {
const client = createClient();
const result = await client.authentication.verify();
spinner.succeed('Authentication verified');
if (result.authenticated) {
Display.success('Successfully authenticated with VisionFI API');
// Show current auth method if configured
if (config.authMethod === 'impersonate' && config.impersonateServiceAccount) {
Display.info(`Using impersonation: ${config.impersonateServiceAccount}`);
}
else if (config.serviceAccountPath) {
Display.info(`Using service account: ${config.serviceAccountPath}`);
}
else {
Display.info('Using Application Default Credentials');
}
}
else {
Display.warning('Authentication check returned false');
}
}
catch (error) {
spinner.fail('Authentication failed');
// Check for common authentication errors and provide helpful guidance
const errorMessage = error.message || '';
if (errorMessage.includes('Could not load the default credentials')) {
Display.error('Could not load Application Default Credentials');
Display.info('');
if (config.authMethod === 'impersonate') {
Display.info('For impersonation mode, you need to authenticate first:');
Display.info(' 1. Install gcloud CLI: https://cloud.google.com/sdk/docs/install');
Display.info(' 2. Run: gcloud auth application-default login');
Display.info(' 3. Ensure you have IAM role: roles/iam.serviceAccountTokenCreator');
Display.info(` 4. On service account: ${config.impersonateServiceAccount}`);
}
else {
Display.info('Please authenticate using one of these methods:');
Display.info(' 1. Run: gcloud auth application-default login');
Display.info(' 2. Set GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json');
Display.info(' 3. Use: visionfi auth configure');
}
}
else if (errorMessage.includes('Permission') || errorMessage.includes('403')) {
Display.error(errorMessage);
Display.info('');
if (config.authMethod === 'impersonate') {
Display.info('Make sure you have the required IAM permissions:');
Display.info(' - roles/iam.serviceAccountTokenCreator');
Display.info(` - On service account: ${config.impersonateServiceAccount}`);
Display.info('');
Display.info('To grant permissions, run:');
Display.info(` gcloud iam service-accounts add-iam-policy-binding \\`);
Display.info(` ${config.impersonateServiceAccount} \\`);
Display.info(` --member="user:YOUR_EMAIL@example.com" \\`);
Display.info(` --role="roles/iam.serviceAccountTokenCreator"`);
}
}
else {
Display.error(errorMessage);
}
process.exit(1);
}
}
/**
* Configure authentication
*/
export async function configureAuth(options) {
const config = loadConfig();
// If options are provided, use non-interactive mode
if (options?.method) {
if (options.method === 'file') {
if (!options.serviceAccountPath) {
Display.error('Service account path is required when using file method');
process.exit(1);
}
if (!fs.existsSync(options.serviceAccountPath)) {
Display.error(`File does not exist: ${options.serviceAccountPath}`);
process.exit(1);
}
config.authMethod = 'file';
config.serviceAccountPath = options.serviceAccountPath;
delete config.impersonateServiceAccount;
saveConfig(config);
Display.success('Authentication configured with service account file');
return;
}
else if (options.method === 'adc') {
// Clear any existing service account path
config.authMethod = 'adc';
delete config.serviceAccountPath;
delete config.impersonateServiceAccount;
saveConfig(config);
Display.success('Authentication configured to use Application Default Credentials');
Display.info('Make sure you have run: gcloud auth application-default login');
return;
}
else if (options.method === 'impersonate') {
if (!options.impersonateServiceAccount) {
Display.error('Service account email is required when using impersonate method');
Display.info('Usage: visionfi auth configure --method=impersonate --impersonate-service-account=EMAIL');
process.exit(1);
}
if (!options.impersonateServiceAccount.includes('@') || !options.impersonateServiceAccount.endsWith('.iam.gserviceaccount.com')) {
Display.error('Invalid service account email format');
Display.info('Expected format: account-name@project-id.iam.gserviceaccount.com');
process.exit(1);
}
config.authMethod = 'impersonate';
config.impersonateServiceAccount = options.impersonateServiceAccount;
delete config.serviceAccountPath;
saveConfig(config);
Display.success('Authentication configured for service account impersonation');
Display.info(`Impersonating: ${options.impersonateServiceAccount}`);
Display.info('Make sure you have:');
Display.info(' 1. Authenticated with: gcloud auth application-default login');
Display.info(' 2. IAM role: roles/iam.serviceAccountTokenCreator');
return;
}
else if (options.method === 'env') {
Display.info('Set one of the following environment variables:');
Display.keyValue('GOOGLE_APPLICATION_CREDENTIALS', '/path/to/service-account.json');
Display.keyValue('VISIONFI_IMPERSONATE_SERVICE_ACCOUNT', 'account@project.iam.gserviceaccount.com');
return;
}
else {
Display.error(`Invalid authentication method: ${options.method}`);
Display.info('Valid methods are: file, adc, impersonate, env');
process.exit(1);
}
}
// Interactive mode
const answers = await inquirer.prompt([
{
type: 'list',
name: 'authMethod',
message: 'Select authentication method:',
choices: [
{ name: 'Service Account JSON file', value: 'file' },
{ name: 'Application Default Credentials (ADC)', value: 'adc' },
{ name: 'Service Account Impersonation (for internal devs)', value: 'impersonate' },
{ name: 'Environment Variable', value: 'env' }
]
}
]);
if (answers.authMethod === 'file') {
const fileAnswer = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter path to service account JSON file:',
validate: (input) => {
if (!fs.existsSync(input)) {
return 'File does not exist';
}
return true;
}
}
]);
config.authMethod = 'file';
config.serviceAccountPath = fileAnswer.path;
delete config.impersonateServiceAccount;
saveConfig(config);
Display.success('Authentication configured with service account file');
}
else if (answers.authMethod === 'adc') {
// Clear any existing service account path
config.authMethod = 'adc';
delete config.serviceAccountPath;
delete config.impersonateServiceAccount;
saveConfig(config);
Display.success('Authentication configured to use Application Default Credentials');
Display.info('Make sure you have run: gcloud auth application-default login');
}
else if (answers.authMethod === 'impersonate') {
const impersonateAnswer = await inquirer.prompt([
{
type: 'input',
name: 'serviceAccount',
message: 'Enter service account email to impersonate:',
validate: (input) => {
if (!input.includes('@') || !input.endsWith('.iam.gserviceaccount.com')) {
return 'Must be a valid service account email (account@project.iam.gserviceaccount.com)';
}
return true;
}
}
]);
config.authMethod = 'impersonate';
config.impersonateServiceAccount = impersonateAnswer.serviceAccount;
delete config.serviceAccountPath;
saveConfig(config);
Display.success('Authentication configured for service account impersonation');
Display.info(`Impersonating: ${impersonateAnswer.serviceAccount}`);
Display.info('Make sure you have:');
Display.info(' 1. Authenticated with: gcloud auth application-default login');
Display.info(' 2. IAM role: roles/iam.serviceAccountTokenCreator');
}
else {
Display.info('Set one of the following environment variables:');
Display.keyValue('GOOGLE_APPLICATION_CREDENTIALS', '/path/to/service-account.json');
Display.keyValue('VISIONFI_IMPERSONATE_SERVICE_ACCOUNT', 'account@project.iam.gserviceaccount.com');
}
}
/**
* Get and display the JWT token
*/
export async function getToken() {
const spinner = Display.spinner('Retrieving authentication token...');
spinner.fail('getToken method not available in new SDK');
Display.error('ID tokens are automatically handled by the SDK');
process.exit(1);
}
/**
* Logout and clear authentication configuration
*/
export async function logout() {
const config = loadConfig();
const configPath = getConfigPath();
// Check if config exists
if (!fs.existsSync(configPath)) {
Display.warning('No configuration file found');
Display.info(`Config path: ${configPath}`);
return;
}
// Show what will be cleared
if (config.authMethod === 'impersonate' && config.impersonateServiceAccount) {
Display.info(`Current auth: Impersonating ${config.impersonateServiceAccount}`);
}
else if (config.serviceAccountPath) {
Display.info(`Current auth: Service account file ${config.serviceAccountPath}`);
}
else {
Display.info('Current auth: Application Default Credentials');
}
// Ask for confirmation
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to clear the authentication configuration?',
default: false
}
]);
if (!answers.confirm) {
Display.info('Logout cancelled');
return;
}
// Clear the config file
try {
fs.unlinkSync(configPath);
Display.success('Authentication configuration cleared');
Display.info('You will need to run "visionfi auth configure" to authenticate again');
}
catch (error) {
Display.error(`Failed to clear configuration: ${error.message}`);
process.exit(1);
}
}
/**
* Show current authentication configuration
*/
export async function showAuth() {
const config = loadConfig();
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
Display.warning('No configuration file found');
Display.info('Run "visionfi auth configure" to set up authentication');
return;
}
Display.info('Current Authentication Configuration:');
Display.info('');
if (config.authMethod === 'impersonate' && config.impersonateServiceAccount) {
Display.keyValue('Method', 'Service Account Impersonation');
Display.keyValue('Service Account', config.impersonateServiceAccount);
}
else if (config.serviceAccountPath) {
Display.keyValue('Method', 'Service Account File');
Display.keyValue('Path', config.serviceAccountPath);
}
else {
Display.keyValue('Method', 'Application Default Credentials');
}
if (config.apiUrl) {
Display.keyValue('API URL', config.apiUrl);
}
Display.info('');
Display.info(`Config file: ${configPath}`);
}