keyvault-cli
Version:
Secure API key management CLI tool
2,205 lines • 92.5 kB
JavaScript
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { CodeScanner } from './scanner.js';
import { CLIAuth } from './auth.js';
// Get package version dynamically
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const program = new Command();
// ASCII Logo
const logo = `
╦╔═╔═╗╦ ╦╦ ╦╔═╗╦ ╦╦ ╔╦╗
╠╩╗║╣ ╚╦╝╚╗╔╝╠═╣║ ║║ ║
╩ ╩╚═╝ ╩ ╚╝ ╩ ╩╚═╝╩═╝╩
`;
program
.name('keyvault')
.description(`${chalk.cyan(logo)}\nKeyVault CLI - Secure API key management for developers`)
.version(packageJson.version);
// Scan command
program
.command('scan')
.description('Scan codebase for exposed API keys and secrets')
.option('-d, --directory <path>', 'Directory to scan', process.cwd())
.option('-o, --output <file>', 'Output file for results')
.option('-f, --format <format>', 'Output format (json|text)', 'text')
.action(async (options) => {
console.log(chalk.blue('🔍 Scanning for API keys and secrets...'));
const scanner = new CodeScanner();
const results = await scanner.scanDirectory(options.directory);
if (options.format === 'json') {
const output = JSON.stringify(results, null, 2);
if (options.output) {
await fs.promises.writeFile(options.output, output);
console.log(chalk.green(`Results saved to ${options.output}`));
}
else {
console.log(output);
}
}
else {
const report = scanner.generateReport(results);
if (options.output) {
await fs.promises.writeFile(options.output, report);
console.log(chalk.green(`Report saved to ${options.output}`));
}
else {
console.log(report);
}
}
if (results.length > 0) {
console.log(chalk.yellow('\n⚠️ Security issues found! Please review and secure your API keys.'));
process.exit(1);
}
else {
console.log(chalk.green('\n✅ No exposed API keys found in your codebase.'));
}
});
// Helper functions for init
async function initPersonalKeys() {
console.log(chalk.blue('🔐 Personal KeyVault Setup'));
console.log(chalk.gray('This will generate your personal encryption keys from a master passphrase.'));
console.log(chalk.gray('Your passphrase is the ONLY way to access your encrypted keys.'));
console.log(chalk.yellow('⚠️ Keep your passphrase safe - it cannot be recovered if lost!'));
const { KeyVaultCrypto } = await import('./crypto.js');
const { passphrase, confirmPassphrase } = await inquirer.prompt([
{
type: 'password',
name: 'passphrase',
message: 'Enter your master passphrase:',
mask: '*',
validate: (input) => {
const validation = KeyVaultCrypto.validatePassphrase(input);
return validation.valid || validation.message || 'Invalid passphrase';
}
},
{
type: 'password',
name: 'confirmPassphrase',
message: 'Confirm your master passphrase:',
mask: '*',
validate: (input, answers) => input === answers.passphrase || 'Passphrases do not match'
}
]);
console.log(chalk.blue('\n🔑 Generating your personal encryption keys...'));
try {
// Generate deterministic key pair
const keyPair = KeyVaultCrypto.generateKeyPair(passphrase);
console.log(chalk.green('✅ Personal keys generated successfully!'));
console.log(chalk.gray(`Address: ${keyPair.address}`));
console.log(chalk.gray(`Public Key: ${KeyVaultCrypto.maskKey(keyPair.publicKey)}`));
// Save personal keys to ~/.keyvault/config.json
const personalKeys = {
address: keyPair.address,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
createdAt: new Date().toISOString()
};
await CLIAuth.savePersonalKeys(personalKeys);
console.log(chalk.green(`🔐 Personal keys saved to ~/.keyvault/config.json`));
console.log(chalk.gray('Keys are stored in your home directory for global access'));
console.log(chalk.gray('\nYour private key is stored locally and will never be sent to the server.'));
console.log(chalk.gray('Keep this file safe - it contains your encryption keys.'));
// Keys are stored locally only, no server upload needed
console.log(chalk.blue('\n✅ Personal setup complete! Your keys are stored locally.'));
console.log(chalk.gray('Your private key never leaves this device for maximum security.'));
}
catch (error) {
console.log(chalk.red('❌ Personal key generation failed:'), error instanceof Error ? error.message : 'Unknown error');
}
}
async function createTeamAndInitKeys(teamName) {
try {
// First create the team on the server
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Not logged in. Please login first.'));
return;
}
console.log(chalk.blue(`🏢 Creating team "${teamName}" on server...`));
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/teams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
},
body: JSON.stringify({ name: teamName })
});
if (!response.ok) {
const errorData = await response.json();
console.log(chalk.red(`❌ Failed to create team: ${errorData.message}`));
return;
}
console.log(chalk.green(`✅ Team "${teamName}" created successfully!`));
// Now init the encryption keys
await initTeamKeys(teamName);
}
catch (error) {
console.error(chalk.red('❌ Error creating team:'), error);
}
}
async function initTeamKeys(teamName) {
console.log(chalk.blue(`🏢 Team KeyVault Setup: ${teamName}`));
console.log(chalk.gray('This will generate team-specific encryption keys from a team passphrase.'));
console.log(chalk.gray('Team members with the same passphrase will generate identical keys.'));
console.log(chalk.yellow('⚠️ Keep the team passphrase safe and share it securely with team members!'));
const { KeyVaultCrypto } = await import('./crypto.js');
const { passphrase, confirmPassphrase } = await inquirer.prompt([
{
type: 'password',
name: 'passphrase',
message: `Enter team passphrase for "${teamName}":`,
mask: '*',
validate: (input) => {
const validation = KeyVaultCrypto.validatePassphrase(input);
return validation.valid || validation.message || 'Invalid passphrase';
}
},
{
type: 'password',
name: 'confirmPassphrase',
message: 'Confirm team passphrase:',
mask: '*',
validate: (input, answers) => input === answers.passphrase || 'Passphrases do not match'
}
]);
console.log(chalk.blue(`\n🔑 Generating team encryption keys for "${teamName}"...`));
try {
// Generate deterministic team key pair using team name + passphrase
const teamSeed = `team:${teamName}:${passphrase}`;
const keyPair = KeyVaultCrypto.generateKeyPair(teamSeed);
console.log(chalk.green(`✅ Team keys for "${teamName}" generated successfully!`));
console.log(chalk.gray(`Team Address: ${keyPair.address}`));
console.log(chalk.gray(`Team Public Key: ${KeyVaultCrypto.maskKey(keyPair.publicKey)}`));
// Save team keys to ~/.keyvault/config.json
const teamKeys = {
address: keyPair.address,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
createdAt: new Date().toISOString()
};
await CLIAuth.saveTeamKeys(teamName, teamKeys);
console.log(chalk.green(`🔐 Team keys saved to ~/.keyvault/config.json`));
console.log(chalk.gray('Keys are stored in your home directory for global access'));
console.log(chalk.gray(`\nTeam "${teamName}" private key is stored locally.`));
console.log(chalk.gray('Share the team passphrase securely with team members.'));
console.log(chalk.blue(`\n✅ Team "${teamName}" setup complete!`));
console.log(chalk.gray('All team members with the same passphrase will generate identical keys.'));
}
catch (error) {
console.log(chalk.red(`❌ Team key generation failed for "${teamName}":`, error instanceof Error ? error.message : 'Unknown error'));
}
}
// Init commands
const initCommand = program
.command('init')
.description('Initialize KeyVault encryption keys');
// Main init command - ask user to choose type
initCommand
.action(async () => {
console.log(chalk.blue('🔐 KeyVault Initialization'));
console.log(chalk.gray('Choose the type of encryption keys to generate:'));
const { keyType } = await inquirer.prompt([
{
type: 'list',
name: 'keyType',
message: 'What type of keys do you want to generate?',
choices: [
{
name: '👤 Personal Keys - For your individual API keys',
value: 'personal'
},
{
name: '🏢 Team Keys - For shared team API keys',
value: 'team'
}
]
}
]);
if (keyType === 'personal') {
// Call personal init directly
await initPersonalKeys();
}
else {
// Team init - first check if user is logged in and has teams
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.yellow('💡 To see your teams, please login first: keyvault login'));
console.log(chalk.gray('You can still create team keys manually by entering a team name.'));
const { teamName } = await inquirer.prompt([
{
type: 'input',
name: 'teamName',
message: 'Enter team name:',
validate: (input) => {
if (!input || typeof input !== 'string') {
return 'Team name is required';
}
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(input)) {
return 'Team names can only contain letters, numbers, hyphens, underscores, and dots';
}
if (input.length < 2 || input.length > 50) {
return 'Team name must be between 2 and 50 characters';
}
return true;
}
}
]);
await initTeamKeys(teamName);
return;
}
// User is logged in, fetch their teams
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const teams = data.teams || data;
if (teams.length === 0) {
console.log(chalk.yellow('📝 You have no teams yet. Let\'s create a new team and generate encryption keys for it.'));
const { teamName } = await inquirer.prompt([
{
type: 'input',
name: 'teamName',
message: 'Enter team name:',
validate: (input) => {
if (!input || typeof input !== 'string') {
return 'Team name is required';
}
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(input)) {
return 'Team names can only contain letters, numbers, hyphens, underscores, and dots';
}
if (input.length < 2 || input.length > 50) {
return 'Team name must be between 2 and 50 characters';
}
return true;
}
}
]);
// Create the team first, then init keys
await createTeamAndInitKeys(teamName);
}
else {
// User has teams, let them choose
const choices = teams.map((team, index) => ({
name: `${team.name} (ID: ${team.id})`,
value: team.name
}));
choices.push({
name: '➕ Create keys for a different team',
value: 'NEW_TEAM'
});
const { selectedTeam } = await inquirer.prompt([
{
type: 'list',
name: 'selectedTeam',
message: 'Select a team to generate encryption keys for:',
choices: choices
}
]);
if (selectedTeam === 'NEW_TEAM') {
const { teamName } = await inquirer.prompt([
{
type: 'input',
name: 'teamName',
message: 'Enter team name:',
validate: (input) => {
if (!input || typeof input !== 'string') {
return 'Team name is required';
}
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(input)) {
return 'Team names can only contain letters, numbers, hyphens, underscores, and dots';
}
if (input.length < 2 || input.length > 50) {
return 'Team name must be between 2 and 50 characters';
}
return true;
}
}
]);
// Create the team first, then init keys
await createTeamAndInitKeys(teamName);
}
else {
await initTeamKeys(selectedTeam);
}
}
}
else {
// API call failed, fallback to manual input
console.log(chalk.yellow('⚠️ Unable to fetch your teams. You can still create team keys manually.'));
const { teamName } = await inquirer.prompt([
{
type: 'input',
name: 'teamName',
message: 'Enter team name:',
validate: (input) => {
if (!input || typeof input !== 'string') {
return 'Team name is required';
}
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(input)) {
return 'Team names can only contain letters, numbers, hyphens, underscores, and dots';
}
if (input.length < 2 || input.length > 50) {
return 'Team name must be between 2 and 50 characters';
}
return true;
}
}
]);
await initTeamKeys(teamName);
}
}
catch (error) {
console.log(chalk.yellow('⚠️ Unable to fetch your teams. You can still create team keys manually.'));
const { teamName } = await inquirer.prompt([
{
type: 'input',
name: 'teamName',
message: 'Enter team name:',
validate: (input) => {
if (!input || typeof input !== 'string') {
return 'Team name is required';
}
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(input)) {
return 'Team names can only contain letters, numbers, hyphens, underscores, and dots';
}
if (input.length < 2 || input.length > 50) {
return 'Team name must be between 2 and 50 characters';
}
return true;
}
}
]);
await initTeamKeys(teamName);
}
}
});
initCommand
.command('personal')
.alias('user')
.description('Initialize personal encryption keys')
.action(async () => {
await initPersonalKeys();
});
initCommand
.command('team <teamName>')
.description('Initialize team-specific encryption keys (team name must be URL-safe)')
.action(async (teamName) => {
// Validate team name
if (!teamName || typeof teamName !== 'string') {
console.log(chalk.red('❌ Team name is required'));
return;
}
// Team name validation: only alphanumeric, hyphens, underscores, dots
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(teamName)) {
console.log(chalk.red('❌ Invalid team name format'));
console.log(chalk.gray('Team names can only contain:'));
console.log(chalk.gray(' • Letters (a-z, A-Z)'));
console.log(chalk.gray(' • Numbers (0-9)'));
console.log(chalk.gray(' • Hyphens (-)'));
console.log(chalk.gray(' • Underscores (_)'));
console.log(chalk.gray(' • Dots (.)'));
console.log(chalk.gray('Examples: frontend-team, backend_api, team.mobile'));
return;
}
// Check length
if (teamName.length < 2 || teamName.length > 50) {
console.log(chalk.red('❌ Team name must be between 2 and 50 characters'));
return;
}
await initTeamKeys(teamName);
});
// Backward compatibility: keep the old 'init' command working
program
.command('init-legacy')
.description('Initialize KeyVault with your master passphrase (legacy)')
.option('--generate-keys', 'Generate new key pair from passphrase')
.action(async (options) => {
console.log(chalk.yellow('⚠️ This is the legacy init command. Consider using "keyvault init personal" instead.'));
console.log(chalk.blue('🔐 KeyVault Setup'));
console.log(chalk.gray('This will generate your personal encryption keys from a master passphrase.'));
console.log(chalk.gray('Your passphrase is the ONLY way to access your encrypted keys.'));
console.log(chalk.yellow('⚠️ Keep your passphrase safe - it cannot be recovered if lost!'));
const { KeyVaultCrypto } = await import('./crypto.js');
const { passphrase, confirmPassphrase } = await inquirer.prompt([
{
type: 'password',
name: 'passphrase',
message: 'Enter your master passphrase:',
mask: '*',
validate: (input) => {
const validation = KeyVaultCrypto.validatePassphrase(input);
return validation.valid || validation.message || 'Invalid passphrase';
}
},
{
type: 'password',
name: 'confirmPassphrase',
message: 'Confirm your master passphrase:',
mask: '*',
validate: (input, answers) => input === answers.passphrase || 'Passphrases do not match'
}
]);
console.log(chalk.blue('\n🔑 Generating your encryption keys...'));
try {
// Generate deterministic key pair
const keyPair = KeyVaultCrypto.generateKeyPair(passphrase);
console.log(chalk.green('✅ Keys generated successfully!'));
console.log(chalk.gray(`Address: ${keyPair.address}`));
console.log(chalk.gray(`Public Key: ${KeyVaultCrypto.maskKey(keyPair.publicKey)}`));
// Save keys to ~/.keyvault/config.json (legacy support)
const personalKeys = {
address: keyPair.address,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
createdAt: new Date().toISOString()
};
await CLIAuth.savePersonalKeys(personalKeys);
console.log(chalk.green(`🔐 Key information saved to ~/.keyvault/config.json`));
console.log(chalk.gray('Keys are stored in your home directory for global access'));
console.log(chalk.gray('\nYour private key is stored locally and will never be sent to the server.'));
console.log(chalk.gray('Keep this file safe - it contains your encryption keys.'));
// Keys are stored locally only, no server upload needed
console.log(chalk.blue('\n✅ Setup complete! Your keys are stored locally.'));
console.log(chalk.gray('Your private key never leaves this device for maximum security.'));
}
catch (error) {
console.log(chalk.red('❌ Key generation failed:'), error instanceof Error ? error.message : 'Unknown error');
}
});
// Check command
program
.command('check')
.description('Check if current directory contains any exposed secrets')
.action(async () => {
const scanner = new CodeScanner();
const results = await scanner.scanDirectory(process.cwd());
if (results.length > 0) {
console.log(chalk.red(`❌ Found ${results.length} exposed secrets!`));
const highSeverity = results.filter((r) => r.severity === 'high');
if (highSeverity.length > 0) {
console.log(chalk.red(`🔴 ${highSeverity.length} high-severity issues require immediate attention`));
}
process.exit(1);
}
else {
console.log(chalk.green('✅ No exposed secrets found'));
}
});
// Auth commands
program
.command('login')
.description('Login to KeyVault')
.action(async () => {
await CLIAuth.login();
});
program
.command('register')
.alias('signup')
.description('Create a new KeyVault account')
.action(async () => {
console.log(chalk.blue('🚀 KeyVault Registration'));
const apiUrl = 'https://1pass.vercel.app';
const { email, password, confirmPassword } = await inquirer.prompt([
{
type: 'input',
name: 'email',
message: 'Email:',
validate: (input) => input.includes('@') || 'Please enter a valid email'
},
{
type: 'password',
name: 'password',
message: 'Password:',
mask: '*',
validate: (input) => input.length >= 6 || 'Password must be at least 6 characters'
},
{
type: 'password',
name: 'confirmPassword',
message: 'Confirm Password:',
mask: '*',
validate: (input, answers) => input === answers.password || 'Passwords do not match'
}
]);
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${apiUrl}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Registration failed');
}
const data = await response.json();
await CLIAuth.saveConfig({
apiUrl,
token: data.token,
email
});
console.log(chalk.green('✅ Registration successful!'));
console.log(chalk.gray(`Logged in as: ${email}`));
}
catch (error) {
console.log(chalk.red('❌ Registration failed:'), error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
}
});
program
.command('logout')
.description('Logout from KeyVault')
.action(async () => {
await CLIAuth.logout();
});
program
.command('whoami')
.description('Show current user info')
.action(async () => {
await CLIAuth.whoami();
});
program
.command('forgot-password')
.description('Reset your password')
.action(async () => {
await CLIAuth.forgotPassword();
});
program
.command('reset-password')
.description('Reset password with token')
.option('-t, --token <token>', 'Reset token from email')
.action(async (options) => {
const apiUrl = 'https://1pass.vercel.app';
await CLIAuth.resetPassword(apiUrl, options.token);
});
// Team management commands
const teamCommand = program
.command('team')
.description('Team management commands');
teamCommand
.command('create <name>')
.description('Create a new team (name must be URL-safe)')
.action(async (name) => {
// Validate team name
if (!name || typeof name !== 'string') {
console.log(chalk.red('❌ Team name is required'));
return;
}
// Team name validation: only alphanumeric, hyphens, underscores, dots
const teamNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!teamNameRegex.test(name)) {
console.log(chalk.red('❌ Invalid team name format'));
console.log(chalk.gray('Team names can only contain:'));
console.log(chalk.gray(' • Letters (a-z, A-Z)'));
console.log(chalk.gray(' • Numbers (0-9)'));
console.log(chalk.gray(' • Hyphens (-)'));
console.log(chalk.gray(' • Underscores (_)'));
console.log(chalk.gray(' • Dots (.)'));
console.log(chalk.gray('Examples: frontend-team, backend_api, team.mobile'));
return;
}
// Check length
if (name.length < 2 || name.length > 50) {
console.log(chalk.red('❌ Team name must be between 2 and 50 characters'));
return;
}
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/teams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
},
body: JSON.stringify({ name })
});
if (response.ok) {
const data = await response.json();
console.log(chalk.green(`✅ Team "${name}" created successfully`));
console.log(chalk.gray(`Team ID: ${data.team.id}`));
console.log(chalk.blue(`💡 Generate team encryption keys with: keyvault init team ${name}`));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to create team: ${error}`));
}
});
teamCommand
.command('list')
.alias('ls')
.description('List your teams')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const teams = data.teams || data;
if (teams.length === 0) {
console.log(chalk.yellow('📝 No teams found. Create one with: keyvault team create <name>'));
}
else {
console.log(chalk.blue('👥 Your Teams:'));
console.log('');
teams.forEach((team, index) => {
const isActive = config.activeTeamId === team.id;
const activeIndicator = isActive ? chalk.green('●') : chalk.gray('○');
console.log(`${activeIndicator} ${chalk.white(`${index + 1}. ${team.name}`)}`);
console.log(chalk.gray(` ID: ${team.id}`));
console.log(chalk.gray(` Created: ${new Date(team.createdAt).toLocaleDateString()}`));
if (isActive) {
console.log(chalk.green(' ✅ Currently active'));
}
console.log('');
});
console.log(chalk.gray('💡 Switch to a team: keyvault team switch <number|id>'));
console.log(chalk.gray(' Example: keyvault team switch 1'));
if (teams[0]?.id) {
console.log(chalk.gray(' Example: keyvault team switch ' + teams[0].id));
}
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch teams: ${error}`));
}
});
teamCommand
.command('invite [email]')
.description('Invite a user to your active team')
.option('-e, --email <email>', 'User email to invite')
.option('-r, --role <role>', 'User role (admin|developer|read-only)', 'developer')
.action(async (emailParam, options) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
// Check if user has switched to a team
if (!config.activeTeamId) {
console.log(chalk.red('❌ No active team selected. Please switch to a team first:'));
console.log(chalk.gray(' 1. List your teams: keyvault team list'));
console.log(chalk.gray(' 2. Switch to a team: keyvault team switch <team-id>'));
return;
}
let email = emailParam || options.email;
let role = options.role;
// Interactive prompts for missing options
if (!email) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'email',
message: 'Email address to invite:',
validate: (input) => input.includes('@') || 'Please enter a valid email'
},
{
type: 'list',
name: 'role',
message: 'User role:',
choices: [
{ name: 'Developer - Can view and manage keys', value: 'developer' },
{ name: 'Admin - Full team management access', value: 'admin' },
{ name: 'Read-only - View access only', value: 'read-only' }
],
default: 'developer'
}
]);
email = answers.email;
role = answers.role;
}
try {
const fetch = (await import('node-fetch')).default;
// First get team name for confirmation
const teamResponse = await fetch(`${config.apiUrl}/api/teams/${config.activeTeamId}`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (!teamResponse.ok) {
console.log(chalk.red('❌ Active team not found. Please switch to a valid team.'));
return;
}
const teamData = await teamResponse.json();
const team = teamData.team || teamData;
const response = await fetch(`${config.apiUrl}/api/teams/${config.activeTeamId}/invite`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
},
body: JSON.stringify({ email, role })
});
if (response.ok) {
console.log(chalk.green(`✅ Invitation sent to ${email}`));
console.log(chalk.gray(`Team: ${team.name}`));
console.log(chalk.gray(`Role: ${role}`));
console.log(chalk.gray('They will receive an email with instructions to join the team.'));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to send invitation: ${error}`));
}
});
teamCommand
.command('switch <teamIdentifier>')
.description('Switch to a different team context (by number or ID)')
.action(async (teamIdentifier) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
// First get all teams
const teamsResponse = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (!teamsResponse.ok) {
console.log(chalk.red('❌ Failed to fetch teams'));
return;
}
const teamsData = await teamsResponse.json();
const teams = teamsData.teams || teamsData;
if (teams.length === 0) {
console.log(chalk.yellow('📝 No teams found. Create one with: keyvault team create <name>'));
return;
}
let targetTeam;
// Check if it's a number (index)
const teamIndex = parseInt(teamIdentifier);
if (!isNaN(teamIndex) && teamIndex >= 1 && teamIndex <= teams.length) {
targetTeam = teams[teamIndex - 1];
}
else {
// Try to find by ID
targetTeam = teams.find((team) => team.id === teamIdentifier);
}
if (!targetTeam) {
console.log(chalk.red(`❌ Team "${teamIdentifier}" not found`));
console.log(chalk.gray('Available teams:'));
teams.forEach((team, index) => {
console.log(chalk.gray(` ${index + 1}. ${team.name} (${team.id})`));
});
return;
}
// Verify team access
const response = await fetch(`${config.apiUrl}/api/teams/${targetTeam.id}`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
// Update config with active team
const updatedConfig = {
...config,
activeTeamId: targetTeam.id
};
await CLIAuth.saveConfig(updatedConfig);
console.log(chalk.green(`✅ Switched to team: ${targetTeam.name}`));
console.log(chalk.gray(`Team ID: ${targetTeam.id}`));
console.log(chalk.gray('All subsequent operations will use this team context.'));
console.log(chalk.gray('Now you can use: keyvault team invite -e user@example.com'));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error || 'Team not found or access denied'}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to switch team: ${error}`));
}
});
teamCommand
.command('deactivate')
.alias('exit')
.description('Exit team context and return to personal mode')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
if (!config.activeTeamId) {
console.log(chalk.yellow('ℹ️ No active team to deactivate. Already in personal mode.'));
return;
}
try {
// Get team name before removing it
const fetch = (await import('node-fetch')).default;
const teamResponse = await fetch(`${config.apiUrl}/api/teams/${config.activeTeamId}`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
let teamName = 'Unknown Team';
if (teamResponse.ok) {
const teamData = await teamResponse.json();
const team = teamData.team || teamData;
teamName = team.name;
}
// Remove active team from config
const updatedConfig = {
...config,
activeTeamId: undefined
};
await CLIAuth.saveConfig(updatedConfig);
console.log(chalk.green(`✅ Exited team context: ${teamName}`));
console.log(chalk.gray('Now in personal mode. All operations will affect your personal keys.'));
console.log(chalk.gray('To switch back to a team: keyvault team switch <team-id>'));
}
catch (error) {
// Even if API call fails, still remove from config
const updatedConfig = {
...config,
activeTeamId: undefined
};
await CLIAuth.saveConfig(updatedConfig);
console.log(chalk.green('✅ Exited team context'));
console.log(chalk.gray('Now in personal mode.'));
}
});
teamCommand
.command('members <teamId>')
.description('List team members')
.action(async (teamId) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/teams/${teamId}/members`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const members = data.members || [];
if (members.length === 0) {
console.log(chalk.yellow('👥 No team members found.'));
}
else {
console.log(chalk.blue(`👥 Team Members (${members.length}):`));
members.forEach((member, index) => {
console.log(chalk.white(`${index + 1}. ${member.email || member.userId}`));
console.log(chalk.gray(` Role: ${member.role}`));
console.log(chalk.gray(` Joined: ${new Date(member.joinedAt).toLocaleDateString()}`));
if (member.isOwner) {
console.log(chalk.yellow(` 👑 Team Owner`));
}
console.log('');
});
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch team members: ${error}`));
}
});
teamCommand
.command('invitations')
.alias('invites')
.description('List your pending team invitations')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/user/invitations`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const invitations = data.invitations || [];
if (invitations.length === 0) {
console.log(chalk.gray('📬 No pending invitations found.'));
}
else {
console.log(chalk.blue(`📬 Your Team Invitations (${invitations.length}):`));
console.log('');
invitations.forEach((invitation, index) => {
const status = invitation.status || 'pending';
const statusIcon = status === 'pending' ? '⏳' : status === 'accepted' ? '✅' : '❌';
const expiresAt = invitation.expiresAt ? new Date(invitation.expiresAt) : null;
const isExpired = expiresAt ? expiresAt < new Date() : false;
console.log(`${index + 1}. ${statusIcon} ${chalk.white(invitation.teamName)}`);
console.log(` ${chalk.gray('From:')} ${invitation.inviterEmail}`);
console.log(` ${chalk.gray('Role:')} ${invitation.role}`);
console.log(` ${chalk.gray('Status:')} ${status}`);
if (invitation.message) {
console.log(` ${chalk.gray('Message:')} ${invitation.message}`);
}
if (expiresAt) {
if (isExpired) {
console.log(` ${chalk.red('⚠️ Expired:')} ${expiresAt.toLocaleDateString()}`);
}
else {
console.log(` ${chalk.gray('Expires:')} ${expiresAt.toLocaleDateString()}`);
}
}
else {
console.log(` ${chalk.gray('Expires:')} Not set`);
}
if (status === 'pending' && !isExpired) {
console.log(` ${chalk.green('💡 Accept:')} keyvault team accept ${invitation.id}`);
}
console.log('');
});
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch invitations: ${error}`));
}
});
teamCommand
.command('accept <invitationId>')
.description('Accept a team invitation')
.action(async (invitationId) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
// First get invitation details
const inviteResponse = await fetch(`${config.apiUrl}/api/user/invitations`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (!inviteResponse.ok) {
console.log(chalk.red('❌ Failed to fetch invitations'));
return;
}
const inviteData = await inviteResponse.json();
const invitations = inviteData.invitations || [];
const invitation = invitations.find((inv) => inv.id === invitationId);
if (!invitation) {
console.log(chalk.red(`❌ Invitation ${invitationId} not found`));
console.log(chalk.gray('Use "keyvault team invitations" to see pending invitations'));
return;
}
if (invitation.status !== 'pending') {
console.log(chalk.yellow(`⚠️ Invitation already ${invitation.status}`));
return;
}
const expiresAt = new Date(invitation.expiresAt);
if (expiresAt < new Date()) {
console.log(chalk.red('❌ This invitation has expired'));
return;
}
// Show invitation details and confirm
console.log(chalk.blue('📋 Team Invitation Details:'));
console.log(` ${chalk.white('Team:')} ${invitation.teamName}`);
console.log(` ${chalk.white('From:')} ${invitation.inviterEmail}`);
console.log(` ${chalk.white('Role:')} ${invitation.role}`);
if (invitation.message) {
console.log(` ${chalk.white('Message:')} ${invitation.message}`);
}
console.log('');
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Accept invitation to join "${invitation.teamName}"?`,
default: true
}
]);
if (!confirm) {
console.log(chalk.gray('Invitation not accepted.'));
return;
}
// Accept the invitation
const response = await fetch(`${config.apiUrl}/api/team-invitations/${invitationId}/accept`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
console.log(chalk.green(`✅ Successfully joined team: ${invitation.teamName}`));
console.log(chalk.gray(`Your role: ${invitation.role}`));
console.log(chalk.gray('You can now switch to this team: keyvault team switch <team-name>'));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to accept invitation: ${error}`));
}
});
teamCommand
.command('decline <invitationId>')
.description('Decline a team invitation')
.action(async (invitationId) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
// First get invitation details
const inviteResponse = await fetch(`${config.apiUrl}/api/user/invitations`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (!inviteResponse.ok) {
console.log(chalk.red('❌ Failed to fetch invitations'));
return;
}
const inviteData = await inviteResponse.json();
const invitations = inviteData.invitations || [];
const invitation = invitations.find((inv) => inv.id === invitationId);
if (!invitation) {
console.log(chalk.red(`❌ Invitation ${invitationId} not found`));
console.log(chalk.gray('Use "keyvault team invitations" to see pending invitations'));
return;
}
if (invitation.status !== 'pending') {
console.log(chalk.yellow(`⚠️ Invitation already ${invitation.status}`));
return;
}
// Show invitation details and confirm
console.log(chalk.blue('📋 Team Invitation Details:'));
console.log(` ${chalk.white('Team:')} ${invitation.teamName}`);
console.log(` ${chalk.white('From:')} ${invitation.inviterEmail}`);
console.log('');
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Decline invitation to join "${invitation.teamName}"?`,
default: false
}
]);
if (!confirm) {
console.log(chalk.gray('Invitation not declined.'));
return;
}
// Decline the invitation
const response = await fetch(`${config.apiUrl}/api/team-invitations/${invitationId}/decline`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
console.log(chalk.yellow(`📋 Declined invitation to: ${invitation.teamName}`));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to decline invitation: ${error}`));
}
});
// Key management commands
const keyCommand = program
.command('key')
.description('API key management commands');
keyCommand
.command('add')
.description('Add a new API key (uses active team if available)')
.option('-n, --name <name>', 'Key name')
.option('-s, --service <service>', 'Service name')
.option('-v, --value <value>', 'Key value')
.option('-t, --team <teamId>', 'Team ID (optional)')
.action(async (options) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
let keyData = {};
// Determine team context - use provided team, or ask user to choose
let targetTeamId;
if (options.team !== undefined) {
// Team explicitly specified (including empty string for personal)
targetTeamId = options.team || undefined;
}
else {
// No team specified - check if user has encryption keys to determine options
const personalKeys = await CLIAuth.getPersonalKeys();
const teamKeysAll = await CLIAuth.getTeamKeys();
const teamNames = teamKeysAll ? Object.keys(teamKeysAll) : [];
if (!personalKeys && teamNames.length === 0) {
console.log(chalk.red('❌ No encryption keys found. Run "keyvault init personal" or "keyvault init team <n>" first.'));
return;
}
let choices = [];
// Add personal option if available
if (personalKeys) {
choices.push({
name: '👤 Personal Key - Just for you',
value: 'personal'
});
}
// Add team options if available
if (teamNames.length > 0) {
for (const teamName of teamNames) {
choices.push({
name: `🏢 Team: ${teamName} - Shared with team members`,
value: teamName
});
}
}
if (choices.length === 1) {
// Only one option available
const choice = choices[0];
if (choice.value === 'personal') {
targetTeamId = undefined;
console.log(chalk.cyan('🔐 Using personal encryption (only option available)'));
}
else {
targetTeamId = choice.value; // This is the team name
console.log(chalk.cyan(`🔐 Using team encryption for "${choice.value}" (only option available)`));
}
}
else {
// Multiple options - let user choose
const { keyType } = await inquirer.prompt([
{
type: 'list',
name: 'keyType',
message: 'Where do you want to store this API key?',
choices: choices
}
]);
if (keyType === 'personal') {
targetTeamId = undefined;
}
else {
targetTeamId = keyType; // This is the team name
}
}
}
// Show context info
if (targetTeamId) {
if (options.team) {
console.log(chalk.cyan(`🔗 Creating team key for: ${targetTeamId}`));
}
else {
console.log(chalk.cyan(`🔗 Creating team key for: ${targetTeamId}`));
}
}
else if (options.team === "") {
console.log(chalk.cyan('👤 Creating personal key (team context overridden)'));
}
else {
console.log(chalk.cyan('👤 Creating personal key'));
}
// Interactive prompts for missing options
if (!options.name || !options.service || !options.value) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Key name:',
default: options.name,
when: !options.name,
validate: (input) => {
if (!input || input.trim().length === 0) {
return 'Key name is required';
}
if (input.includes(' ')) {
return 'Key name cannot contain spaces. Use hyphens or underscores instead.';
}
if (input.length > 50) {
return 'Key name must be 50 characters or less';
}
return true;
}
},
{
type: 'input',
name: 'service',
message: 'Service name:',
default: options.service,
when: !options.service,
validate: (input) => input.trim().length > 0 || 'Service name is required'
},
{
type: 'password',
name: 'value',
message: 'Key value:',
default: options.value,
when: !options.value,
mask: '*',
validate: (input) => input.trim().length > 0 || 'Key value is required'
}
]);
keyData = {
name: options.name || answers.name,
service: options.service || answers.service,
value: options.value || answers.value,
teamId: targetTeamId
};
}
else {
keyData = {
name: options.name,
service: options.service,
value: options.value,
teamId: targetTeamId
};
}
try {
// Encrypt the key value on client side before sending
const personalKeys = await CLIAuth.getPersonalKeys();
const teamKeysAll = await CLIAuth.getTeamKeys();
if (!personalKeys && !teamKeysAll) {
console.log(chalk.red('❌ No encryption keys found. Run "keyvault init personal" or "keyvault init team <n>" first.'));
return;
}
const { KeyVaultCrypto } = await import('./crypto.js');
let encryptionKey;
let encryptionType;
let actualTeamId; // For server storage
if (targetTeamId) {
// Team key - targetTeamId is the team name
const teamName = targetTeamId;
// Check if team keys exist
const teamKeys = await CLIAuth.getTeamKeys(teamName);
if (teamKeys) {
encryptionKey = teamKeys.publicKey;
encryptionType = `team "${teamName}"`;
console.log(chalk.blue(`🔐 Using team encryption key for "${teamName}"`));
// Get actual team ID from server for storage
try {
const fetch = (await import('node-fetch')).default;
const teamsResponse = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamsResponse.ok) {
const teamsData = await teamsResponse.json();
const teams = teamsData.teams || teamsData;
const team = teams.find((t) => t.name === teamName);
if (team) {
actualTeamId = team.id;
}
else {
console.log(chalk.red(`❌ Team "${teamName}" not found on server. Please ensure you're a member of this team.`));
return;
}
}
else {
console.log(chalk.red('❌ Failed to fetch team information from server.'));
return;
}
}
catch (teamError) {
console.log(chalk.red('❌ Failed to fetch team information from server.'));
return;
}
}
else {
console.log(chalk.red(`❌ No team encryption keys found for "${teamName}". Run "keyvault init team ${teamName}" first.`));
return;
}
}
else {
// Personal key
if (personalKeys) {
encryptionKey = personalKeys.publicKey;
encryptionType = 'personal';
console.log(chalk.blue('🔐 Using personal encryption key'));
actualTeamId = undefined;
}
else {
console.log(chalk.red('❌ No personal encryption keys found. Run "keyvault init personal" first.'));
return;
}
}
// Encrypt the key value with appropriate encryption key
const encryptedValue = KeyVaultCrypto.encrypt(keyData.value, encryptionKey);
// Send encrypted data to server
const encryptedKeyData = {
name: keyData.name,
service: keyData.service,
encryptedValue: encryptedValue, // Send encrypted value instead of plain text
teamId: actualTeamId // Use actual team ID from server, not team name
};
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
},
body: JSON.stringify(encryptedKeyData)
});
if (response.ok) {
const data = await response.json();
console.log(chalk.green(`✅ API key "${keyData.name}" added successfully`));
console.log(chalk.gray(`Key ID: ${data.key.id}`));
console.log(chalk.gray(`Service: ${keyData.service}`));
if (actualTeamId) {
console.log(chalk.gray(`Team: ${targetTeamId} (ID: ${actualTeamId})`));
}
else {
console.log(chalk.gray('Type: Personal'));
}
console.log(chalk.green(`🔒 Key encrypted with ${encryptionType} key and stored securely`));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to add key: ${error}`));
}
});
keyCommand
.command('list')
.alias('ls')
.description('List your personal API keys')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const allKeys = data.keys || data;
// Filter to only personal keys (exclude team keys)
const keys = allKeys.filter((key) => !key.teamId);
if (keys.length === 0) {
console.log(chalk.yellow('🔑 No personal API keys found. Add one with: keyvault key add'));
}
else {
console.log(chalk.blue('🔑 Your Personal API Keys:'));
keys.forEach((key, index) => {
console.log(chalk.white(`${index + 1}. ${key.name}`));
console.log(chalk.gray(` Service: ${key.service}`));
console.log(chalk.gray(` ID: ${key.id}`));
console.log(chalk.gray(` Created: ${new Date(key.createdAt).toLocaleDateString()}`));
if (key.lastUsed) {
console.log(chalk.gray(` Last Used: ${new Date(key.lastUsed).toLocaleDateString()}`));
}
console.log('');
});
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch keys: ${error}`));
}
});
keyCommand
.command('list-team [teamId]')
.alias('lst')
.description('List team API keys (uses active team if no ID provided)')
.action(async (teamId) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
// Use provided teamId or fall back to active team
const targetTeamId = teamId || config.activeTeamId;
if (!targetTeamId) {
console.log(chalk.red('❌ No team specified and no active team selected.'));
console.log(chalk.gray('Options:'));
console.log(chalk.gray(' 1. Specify team ID: keyvault key list-team <team-id>'));
console.log(chalk.gray(' 2. Switch to a team: keyvault team switch <team-id>'));
console.log(chalk.gray(' 3. List your teams: keyvault team list'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
// Get team name for display
const teamResponse = await fetch(`${config.apiUrl}/api/teams/${targetTeamId}`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
let teamName = 'Unknown Team';
if (teamResponse.ok) {
const teamData = await teamResponse.json();
const team = teamData.team || teamData;
teamName = team.name;
}
const response = await fetch(`${config.apiUrl}/api/teams/${targetTeamId}/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const keys = data.keys || data;
if (keys.length === 0) {
console.log(chalk.yellow(`🔑 No team API keys found for "${teamName}"`));
console.log(chalk.gray('Add team keys through the web interface or create keys with teamId'));
}
else {
console.log(chalk.blue(`🔑 Team "${teamName}" API Keys:`));
keys.forEach((key, index) => {
console.log(chalk.white(`${index + 1}. ${key.name}`));
console.log(chalk.gray(` Service: ${key.service}`));
console.log(chalk.gray(` ID: ${key.id}`));
console.log(chalk.gray(` Created: ${new Date(key.createdAt).toLocaleDateString()}`));
if (key.lastUsed) {
console.log(chalk.gray(` Last Used: ${new Date(key.lastUsed).toLocaleDateString()}`));
}
console.log('');
});
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch team keys: ${error}`));
}
});
keyCommand
.command('list-all')
.alias('la')
.description('List both personal and team keys')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
console.log(chalk.blue('🔑 All Your API Keys:'));
console.log('');
// Personal keys
const personalResponse = await fetch(`${config.apiUrl}/api/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (personalResponse.ok) {
const personalData = await personalResponse.json();
const allPersonalKeys = personalData.keys || personalData;
// Filter to only personal keys (exclude team keys)
const personalKeys = allPersonalKeys.filter((key) => !key.teamId);
console.log(chalk.cyan('📝 Personal Keys:'));
if (personalKeys.length === 0) {
console.log(chalk.gray(' No personal keys found'));
}
else {
personalKeys.forEach((key, index) => {
console.log(chalk.white(` ${index + 1}. ${key.name}`));
console.log(chalk.gray(` Service: ${key.service}`));
console.log(chalk.gray(` ID: ${key.id}`));
console.log('');
});
}
}
// Team keys (get all teams first)
const teamsResponse = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamsResponse.ok) {
const teamsData = await teamsResponse.json();
const teams = teamsData.teams || teamsData;
for (const team of teams) {
const teamKeysResponse = await fetch(`${config.apiUrl}/api/teams/${team.id}/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamKeysResponse.ok) {
const teamKeysData = await teamKeysResponse.json();
const teamKeys = teamKeysData.keys || teamKeysData;
console.log(chalk.cyan(`👥 Team "${team.name}" Keys:`));
if (teamKeys.length === 0) {
console.log(chalk.gray(' No team keys found'));
}
else {
teamKeys.forEach((key, index) => {
console.log(chalk.white(` ${index + 1}. ${key.name}`));
console.log(chalk.gray(` Service: ${key.service}`));
console.log(chalk.gray(` ID: ${key.id}`));
console.log('');
});
}
}
}
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch keys: ${error}`));
}
});
keyCommand
.command('get [keyIdentifier]')
.description('Get API key value (by ID or name)')
.option('-n, --name <name>', 'Key name to get')
.option('-t, --team', 'Get from team keys')
.option('-p, --personal', 'Get from personal keys')
.action(async (keyIdentifier, options) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
let keyName = keyIdentifier || options.name;
// Check conflicting flags
if (options.team && options.personal) {
console.log(chalk.red('❌ Cannot use both --team and --personal flags together'));
return;
}
let targetKey;
if (!keyName) {
// Get personal keys
const personalResponse = await fetch(`${config.apiUrl}/api/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
let personalKeys = [];
if (personalResponse.ok) {
const personalData = await personalResponse.json();
const allKeys = personalData.keys || personalData;
// Filter to only personal keys (exclude team keys)
personalKeys = allKeys.filter((key) => !key.teamId);
}
// Get team keys
let teamKeys = [];
const teamsResponse = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamsResponse.ok) {
const teamsData = await teamsResponse.json();
const teams = teamsData.teams || teamsData;
for (const team of teams) {
const teamKeysResponse = await fetch(`${config.apiUrl}/api/teams/${team.id}/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamKeysResponse.ok) {
const teamKeysData = await teamKeysResponse.json();
const keys = teamKeysData.keys || teamKeysData;
keys.forEach((key) => {
teamKeys.push({
...key,
teamName: team.name,
teamId: team.id
});
});
}
}
}
// Filter based on flags
let availableKeys = [];
if (options.personal) {
availableKeys = personalKeys.map((key) => ({
...key,
displayName: `👤 ${key.name} (${key.service})`,
type: 'personal'
}));
}
else if (options.team) {
availableKeys = teamKeys.map((key) => ({
...key,
displayName: `🏢 [${key.teamName}] ${key.name} (${key.service})`,
type: 'team'
}));
}
else {
// Show both with clear separation
availableKeys = [
...personalKeys.map((key) => ({
...key,
displayName: `👤 ${key.name} (${key.service})`,
type: 'personal'
})),
...teamKeys.map((key) => ({
...key,
displayName: `🏢 [${key.teamName}] ${key.name} (${key.service})`,
type: 'team'
}))
];
}
if (availableKeys.length === 0) {
console.log(chalk.yellow('🔑 No keys found'));
return;
}
const { selectedKey } = await inquirer.prompt([
{
type: 'list',
name: 'selectedKey',
message: 'Select a key to retrieve:',
choices: availableKeys.map((key) => ({
name: key.displayName,
value: key
}))
}
]);
// Use the selected key directly instead of searching again
targetKey = selectedKey;
}
else {
// User provided key name, need to search for it
// Find the key from all available keys
const personalResponse = await fetch(`${config.apiUrl}/api/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
let allKeys = [];
if (personalResponse.ok) {
const personalData = await personalResponse.json();
const personalKeys = personalData.keys || personalData;
allKeys = personalKeys.filter((key) => !key.teamId); // Personal keys only
}
// Get team keys as well
const teamsResponse = await fetch(`${config.apiUrl}/api/teams`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamsResponse.ok) {
const teamsData = await teamsResponse.json();
const teams = teamsData.teams || teamsData;
for (const team of teams) {
const teamKeysResponse = await fetch(`${config.apiUrl}/api/teams/${team.id}/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (teamKeysResponse.ok) {
const teamKeysData = await teamKeysResponse.json();
const keys = teamKeysData.keys || teamKeysData;
keys.forEach((key) => {
allKeys.push({
...key,
teamName: team.name,
teamId: team.id
});
});
}
}
}
targetKey = allKeys.find((key) => key.name === keyName || key.id === keyName);
}
if (!targetKey) {
console.log(chalk.red(`❌ Key "${keyName}" not found`));
return;
}
// Check flag constraints
if (options.personal && targetKey.teamId) {
console.log(chalk.red(`❌ Key "${keyName}" is a team key, not personal. Use --team flag or remove --personal flag.`));
return;
}
if (options.team && !targetKey.teamId) {
console.log(chalk.red(`❌ Key "${keyName}" is a personal key, not team. Use --personal flag or remove --team flag.`));
return;
}
// Get the key details
const response = await fetch(`${config.apiUrl}/api/keys/${targetKey.id}`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
console.log(chalk.blue(`🔑 Key: ${data.name}`));
console.log(chalk.gray(`Service: ${data.service}`));
// Check if we have client-side encryption data or old system
if (data.encryptedValue) {
// New system: Decrypt on client-side
try {
// Load keys for decryption from ~/.keyvault/config.json
const personalKeys = await CLIAuth.getPersonalKeys();
const teamKeysAll = await CLIAuth.getTeamKeys();
if (!personalKeys && !teamKeysAll) {
console.log(chalk.red('❌ No encryption keys found. Run "keyvault init personal" or "keyvault init team <n>" first.'));
return;
}
const { KeyVaultCrypto } = await import('./crypto.js');
let decryptionKey;
let decryptionType = 'unknown';
// Determine if this is a team key or personal key
// Use targetKey.teamId (from selection) instead of data.teamId (from API response)
if (targetKey.teamId) {
// This is a team key - find the team name first
const teamName = targetKey.teamName;
const teamKeys = await CLIAuth.getTeamKeys(teamName);
if (teamKeys) {
decryptionKey = teamKeys.privateKey;
decryptionType = `team "${teamName}"`;
console.log(chalk.blue(`🔓 Decrypting with team key for "${teamName}"`));
}
else {
console.log(chalk.red(`❌ No team decryption keys found for "${teamName}". Run "keyvault init team ${teamName}" first.`));
console.log(chalk.gray('Note: You need the team passphrase to decrypt team keys.'));
return;
}
}
else {
// This is a personal key
if (personalKeys) {
decryptionKey = personalKeys.privateKey;
decryptionType = 'personal';
console.log(chalk.blue('🔓 Decrypting with personal key'));
}
else {
console.log(chalk.red('❌ No personal decryption keys found. Run "keyvault init personal" first.'));
console.log(chalk.gray('Note: Legacy key format detected. Please regenerate with "keyvault init personal"'));
return;
}
}
// Decrypt using appropriate private key
const decryptedValue = KeyVaultCrypto.decrypt(data.encryptedValue, decryptionKey);
console.log(chalk.yellow(`Value: ${decryptedValue}`));
console.log(chalk.gray(`Decrypted with: ${decryptionType} key`));
}
catch (decryptError) {
console.log(chalk.red('❌ Failed to decrypt key. Wrong private key or corrupted data.'));
console.log(chalk.gray('Make sure you have the correct encryption keys for this key type.'));
}
}
else if (data.value) {
// Old system: Already decrypted by server
console.log(chalk.yellow(`Value: ${data.value}`));
console.log(chalk.gray('Decrypted with: server-side (legacy)'));
}
else {
console.log(chalk.red('❌ No value data received from server.'));
}
console.log(chalk.gray(`ID: ${data.id}`));
console.log(chalk.gray(`Created: ${new Date(data.createdAt).toLocaleDateString()}`));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to get key: ${error}`));
}
});
keyCommand
.command('delete <keyIdentifier>')
.description('Delete an API key (by ID or name)')
.action(async (keyIdentifier) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
// First, get all keys to find by name if needed
const listResponse = await fetch(`${config.apiUrl}/api/keys`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (!listResponse.ok) {
console.log(chalk.red('❌ Failed to fetch keys'));
return;
}
const listData = await listResponse.json();
const keys = listData.keys || listData;
// Try to find key by name first, then by ID
let targetKey = keys.find((key) => key.name === keyIdentifier);
if (!targetKey) {
targetKey = keys.find((key) => key.id === keyIdentifier);
}
if (!targetKey) {
console.log(chalk.red(`❌ Key "${keyIdentifier}" not found`));
console.log(chalk.gray('Available keys:'));
keys.forEach((key, index) => {
console.log(chalk.gray(` ${index + 1}. ${key.name} (${key.id})`));
});
return;
}
// Show key info and confirm deletion
console.log(chalk.yellow(`About to delete key: ${targetKey.name} (${targetKey.service})`));
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to delete this API key?',
default: false
}
]);
if (!confirm) {
console.log(chalk.gray('Deletion cancelled.'));
return;
}
const response = await fetch(`${config.apiUrl}/api/keys/${targetKey.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
console.log(chalk.green(`✅ API key "${targetKey.name}" deleted successfully`));
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to delete key: ${error}`));
}
});
// Endpoint management commands
const endpointCommand = program
.command('endpoint')
.description('API endpoint management commands');
endpointCommand
.command('add')
.description('Add a new API endpoint')
.option('-n, --name <name>', 'Endpoint name')
.option('-u, --url <url>', 'Endpoint URL')
.option('-m, --method <method>', 'HTTP method', 'GET')
.option('-d, --description <description>', 'Endpoint description')
.option('-t, --team <teamId>', 'Team ID (optional)')
.action(async (options) => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
let endpointData = {};
// Interactive prompts for missing options
if (!options.name || !options.url) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Endpoint name:',
default: options.name,
when: !options.name,
validate: (input) => input.trim().length > 0 || 'Endpoint name is required'
},
{
type: 'input',
name: 'url',
message: 'Endpoint URL:',
default: options.url,
when: !options.url,
validate: (input) => input.trim().length > 0 || 'Endpoint URL is required'
},
{
type: 'list',
name: 'method',
message: 'HTTP method:',
choices: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
default: options.method || 'GET',
when: !options.method
},
{
type: 'input',
name: 'description',
message: 'Description (optional):',
default: options.description || '',
when: !options.description
},
{
type: 'input',
name: 'team',
message: 'Team ID (optional, press enter to skip):',
default: options.team || '',
when: !options.team
}
]);
endpointData = {
name: options.name || answers.name,
url: options.url || answers.url,
method: options.method || answers.method,
description: options.description || answers.description || undefined,
teamId: options.team || answers.team || undefined
};
}
else {
endpointData = {
name: options.name,
url: options.url,
method: options.method || 'GET',
description: options.description || undefined,
teamId: options.team || undefined
};
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/endpoints`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.token}`
},
body: JSON.stringify(endpointData)
});
if (response.ok) {
const data = await response.json();
console.log(chalk.green(`✅ Endpoint "${endpointData.name}" added successfully`));
console.log(chalk.gray(`Endpoint ID: ${data.id}`));
console.log(chalk.gray(`URL: ${data.url}`));
console.log(chalk.gray(`Method: ${data.method}`));
if (data.teamId) {
console.log(chalk.gray(`Team: ${data.teamId}`));
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to add endpoint: ${error}`));
}
});
endpointCommand
.command('list')
.alias('ls')
.description('List your API endpoints')
.action(async () => {
const config = await CLIAuth.getConfig();
if (!config?.token) {
console.log(chalk.red('❌ Please login first: keyvault login'));
return;
}
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch(`${config.apiUrl}/api/endpoints`, {
headers: {
'Authorization': `Bearer ${config.token}`
}
});
if (response.ok) {
const data = await response.json();
const endpoints = data.endpoints || data;
if (endpoints.length === 0) {
console.log(chalk.yellow('🌐 No endpoints found. Add one with: keyvault endpoint add'));
}
else {
console.log(chalk.blue('🌐 Your API Endpoints:'));
endpoints.forEach((endpoint, index) => {
console.log(chalk.white(`${index + 1}. ${endpoint.name}`));
console.log(chalk.gray(` URL: ${endpoint.url}`));
console.log(chalk.gray(` Method: ${endpoint.method}`));
console.log(chalk.gray(` ID: ${endpoint.id}`));
if (endpoint.description) {
console.log(chalk.gray(` Description: ${endpoint.description}`));
}
console.log(chalk.gray(` Created: ${new Date(endpoint.createdAt).toLocaleDateString()}`));
console.log('');
});
}
}
else {
const error = await response.json();
console.log(chalk.red(`❌ Error: ${error.error}`));
}
}
catch (error) {
console.log(chalk.red(`❌ Failed to fetch endpoints: ${error}`));
}
});
// Upgrade command
program
.command('upgrade')
.description('Check for and install latest version of KeyVault CLI')
.option('--check-only', 'Only check for updates without installing')
.action(async (options) => {
console.log(chalk.blue('🔄 Checking for KeyVault CLI updates...'));
try {
const fetch = (await import('node-fetch')).default;
const { spawn } = await import('child_process');
// Get current version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
// Check NPM registry for latest version
const response = await fetch('https://registry.npmjs.org/keyvault-cli/latest');
if (!response.ok) {
console.log(chalk.red('❌ Failed to check for updates. Please try again later.'));
return;
}
const packageInfo = await response.json();
const latestVersion = packageInfo.version;
console.log(chalk.gray(`Current version: ${currentVersion}`));
console.log(chalk.gray(`Latest version: ${latestVersion}`));
// Simple version comparison (assuming semantic versioning)
const parseVersion = (version) => {
return version.split('.').map(Number);
};
const currentParts = parseVersion(currentVersion);
const latestParts = parseVersion(latestVersion);
let isNewer = false;
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const current = currentParts[i] || 0;
const latest = latestParts[i] || 0;
if (latest > current) {
isNewer = true;
break;
}
else if (latest < current) {
break;
}
}
if (!isNewer) {
console.log(chalk.green('✅ You are already using the latest version!'));
return;
}
console.log(chalk.yellow(`📦 New version available: ${latestVersion}`));
if (options.checkOnly) {
console.log(chalk.gray('Run "keyvault upgrade" to install the latest version.'));
return;
}
// Ask user for confirmation
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Do you want to upgrade to version ${latestVersion}?`,
default: true
}
]);
if (!confirm) {
console.log(chalk.gray('Upgrade cancelled.'));
return;
}
console.log(chalk.blue('📦 Installing latest version...'));
console.log(chalk.gray('This may take a moment...'));
// Install latest version
const installProcess = spawn('npm', ['install', '-g', 'keyvault-cli@latest'], {
stdio: 'inherit',
shell: true
});
installProcess.on('close', (code) => {
if (code === 0) {
console.log(chalk.green(`✅ Successfully upgraded to version ${latestVersion}!`));
console.log(chalk.gray('You may need to restart your terminal for changes to take effect.'));
}
else {
console.log(chalk.red('❌ Upgrade failed. Please try running the command manually:'));
console.log(chalk.gray('npm install -g keyvault-cli@latest'));
}
});
installProcess.on('error', (error) => {
console.log(chalk.red('❌ Upgrade failed:'), error.message);
console.log(chalk.gray('Please try running the command manually:'));
console.log(chalk.gray('npm install -g keyvault-cli@latest'));
});
}
catch (error) {
console.log(chalk.red('❌ Failed to check for updates:'), error instanceof Error ? error.message : 'Unknown error');
console.log(chalk.gray('You can manually update with: npm install -g keyvault-cli@latest'));
}
});
// Help command enhancement
program.configureHelp({
sortSubcommands: true,
subcommandTerm: (cmd) => cmd.name() + ' ' + cmd.usage()
});
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
process.exit(0);
}
// Parse arguments
program.parse();