autopv-cli
Version:
AutoPrivacy DSAR evidence-pack generator - Automated GDPR compliance for SaaS companies
289 lines (288 loc) âĸ 11.8 kB
JavaScript
import { Command, Flags } from '@oclif/core';
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'crypto';
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import * as readline from 'readline';
export default class Login extends Command {
static description = 'Configure API keys and secrets for AutoPrivacy CLI';
static examples = [
'$ autopv login',
'$ autopv login --github-token ghp_your_token_here',
'$ autopv login --reset'
];
static flags = {
'github-token': Flags.string({
char: 'g',
description: 'GitHub Personal Access Token (repo:read, admin:org scopes)'
}),
'stripe-key': Flags.string({
char: 's',
description: 'Stripe Secret Key (optional, for payment data export)'
}),
'openai-key': Flags.string({
char: 'o',
description: 'OpenAI API Key (for GDPR classification)'
}),
'archive-password': Flags.string({
char: 'p',
description: 'Password for encrypted evidence archives'
}),
reset: Flags.boolean({
char: 'r',
description: 'Reset all stored credentials'
}),
show: Flags.boolean({
description: 'Show current configuration (masked)'
})
};
configPath = join(homedir(), '.autopv', 'config.json');
encryptionKey = 'autopv-cli-encryption-key-v1'; // In production, use better key derivation
async run() {
const { flags } = await this.parse(Login);
// Ensure config directory exists
const configDir = dirname(this.configPath);
if (!existsSync(configDir)) {
mkdirSync(configDir, { recursive: true });
}
if (flags.reset) {
return this.resetConfig();
}
if (flags.show) {
return this.showConfig();
}
// Interactive or flag-based configuration
await this.configureSecrets(flags);
}
async configureSecrets(flags) {
this.log('đ AutoPrivacy CLI - Secrets Configuration');
this.log('');
let config = this.loadConfig();
// GitHub Token
if (flags['github-token']) {
config.githubToken = flags['github-token'];
this.log('â
GitHub token updated');
}
else if (!config.githubToken) {
this.log('đ GitHub Personal Access Token Setup:');
this.log(' 1. Go to: https://github.com/settings/tokens');
this.log(' 2. Click "Generate new token (classic)"');
this.log(' 3. Select scopes: repo:read, admin:org');
this.log(' 4. Copy the token and paste below');
this.log('');
const githubToken = await this.promptSecret('Enter GitHub token (ghp_...): ');
if (githubToken) {
config.githubToken = githubToken;
this.log('â
GitHub token saved');
}
}
// OpenAI API Key
if (flags['openai-key']) {
config.openaiApiKey = flags['openai-key'];
this.log('â
OpenAI API key updated');
}
else if (!config.openaiApiKey) {
this.log('');
this.log('đ¤ OpenAI API Key Setup (for GDPR classification):');
this.log(' 1. Go to: https://platform.openai.com/api-keys');
this.log(' 2. Click "Create new secret key"');
this.log(' 3. Copy the key and paste below');
this.log('');
const openaiKey = await this.promptSecret('Enter OpenAI API key (sk-...): ');
if (openaiKey) {
config.openaiApiKey = openaiKey;
this.log('â
OpenAI API key saved');
}
}
// Stripe Secret Key (optional)
if (flags['stripe-key']) {
config.stripeSecretKey = flags['stripe-key'];
this.log('â
Stripe secret key updated');
}
else if (!config.stripeSecretKey) {
this.log('');
this.log('đŗ Stripe Secret Key Setup (optional):');
this.log(' âĸ Skip if you don\'t use Stripe for payments');
this.log(' âĸ Go to: https://dashboard.stripe.com/apikeys');
this.log(' âĸ Copy "Secret key" and paste below');
this.log('');
const stripeKey = await this.promptSecret('Enter Stripe secret key (sk_...) or press Enter to skip: ', true);
if (stripeKey) {
config.stripeSecretKey = stripeKey;
this.log('â
Stripe secret key saved');
}
else {
this.log('âī¸ Skipped Stripe configuration');
}
}
// Archive Password
if (flags['archive-password']) {
config.archivePassword = flags['archive-password'];
this.log('â
Archive password updated');
}
else if (!config.archivePassword) {
this.log('');
this.log('đ Archive Password Setup:');
this.log(' âĸ Used to encrypt evidence pack archives');
this.log(' âĸ Share this password with data subjects for file extraction');
this.log('');
const archivePassword = await this.promptSecret('Enter archive password: ');
if (archivePassword) {
config.archivePassword = archivePassword;
this.log('â
Archive password saved');
}
}
// Save encrypted configuration
config.lastUpdated = new Date().toISOString();
if (!config.createdAt) {
config.createdAt = config.lastUpdated;
}
this.saveConfig(config);
this.log('');
this.log('đ Configuration complete!');
this.log('');
this.log('đ Config saved to:', this.configPath);
this.log('đ All secrets are encrypted at rest');
this.log('');
this.log('Next steps:');
this.log(' autopv generate -e user@company.com -g github-org');
this.log('');
}
async promptSecret(prompt, optional = false) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
// Hide input for secrets
const stdin = process.stdin;
stdin.setRawMode(true);
let input = '';
process.stdout.write(prompt);
stdin.on('data', (data) => {
const char = data.toString();
if (char === '\r' || char === '\n') {
stdin.setRawMode(false);
process.stdout.write('\n');
rl.close();
resolve(input.trim());
}
else if (char === '\u0003') { // Ctrl+C
process.exit(0);
}
else if (char === '\u007f') { // Backspace
if (input.length > 0) {
input = input.slice(0, -1);
process.stdout.write('\b \b');
}
}
else {
input += char;
process.stdout.write('*');
}
});
});
}
loadConfig() {
if (!existsSync(this.configPath)) {
return {
createdAt: new Date().toISOString(),
lastUpdated: new Date().toISOString()
};
}
try {
const encryptedData = readFileSync(this.configPath, 'utf8');
const decrypted = this.decrypt(encryptedData);
return JSON.parse(decrypted);
}
catch (error) {
this.warn('Failed to load config, creating new one');
return {
createdAt: new Date().toISOString(),
lastUpdated: new Date().toISOString()
};
}
}
saveConfig(config) {
const encrypted = this.encrypt(JSON.stringify(config, null, 2));
writeFileSync(this.configPath, encrypted, { mode: 0o600 }); // Restrict file permissions
}
encrypt(text) {
const algorithm = 'aes-256-cbc';
const key = pbkdf2Sync(this.encryptionKey, 'autopv-salt', 10000, 32, 'sha256');
const iv = randomBytes(16);
const cipher = createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
decrypt(encryptedText) {
const algorithm = 'aes-256-cbc';
const key = pbkdf2Sync(this.encryptionKey, 'autopv-salt', 10000, 32, 'sha256');
const parts = encryptedText.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
showConfig() {
const config = this.loadConfig();
this.log('đ AutoPrivacy CLI Configuration');
this.log('');
this.log('GitHub Token:', config.githubToken ? 'â
Configured (ghp_***...)' : 'â Not configured');
this.log('OpenAI API Key:', config.openaiApiKey ? 'â
Configured (sk-***...)' : 'â Not configured');
this.log('Stripe Secret Key:', config.stripeSecretKey ? 'â
Configured (sk_***...)' : 'âī¸ Not configured (optional)');
this.log('Archive Password:', config.archivePassword ? 'â
Configured' : 'â Not configured');
this.log('');
this.log('Created:', config.createdAt);
this.log('Last Updated:', config.lastUpdated);
this.log('Config Path:', this.configPath);
}
resetConfig() {
if (existsSync(this.configPath)) {
const fs = require('fs');
fs.unlinkSync(this.configPath);
this.log('â
Configuration reset - all secrets cleared');
}
else {
this.log('âšī¸ No configuration found to reset');
}
}
/**
* Export configuration as environment variables for the generate command
*/
static loadConfigAsEnv() {
const configPath = join(homedir(), '.autopv', 'config.json');
if (!existsSync(configPath)) {
return {};
}
try {
const encryptionKey = 'autopv-cli-encryption-key-v1';
const encryptedData = readFileSync(configPath, 'utf8');
const algorithm = 'aes-256-cbc';
const key = pbkdf2Sync(encryptionKey, 'autopv-salt', 10000, 32, 'sha256');
const parts = encryptedData.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
const config = JSON.parse(decrypted);
const env = {};
if (config.githubToken)
env.GITHUB_TOKEN = config.githubToken;
if (config.openaiApiKey)
env.OPENAI_API_KEY = config.openaiApiKey;
if (config.stripeSecretKey)
env.STRIPE_SECRET_KEY = config.stripeSecretKey;
if (config.archivePassword)
env.ARCHIVE_PW = config.archivePassword;
return env;
}
catch (error) {
return {};
}
}
}