@dinanathdash/envault 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/envault.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import boxen from 'boxen';
6
+ import { login } from '../src/commands/login.js';
7
+ import { init } from '../src/commands/init.js';
8
+ import { deploy } from '../src/commands/deploy.js';
9
+ import { pull } from '../src/commands/pull.js';
10
+
11
+ const program = new Command();
12
+
13
+ // Gradient Logo Helper
14
+ const showLogo = () => {
15
+ console.log(chalk.bold.hex('#10B981')(`
16
+ ███████╗███╗ ██╗██╗ ██╗ █████╗ ██╗ ██╗██╗ ████████╗
17
+ ██╔════╝████╗ ██║██║ ██║██╔══██╗██║ ██║██║ ╚══██╔══╝
18
+ █████╗ ██╔██╗ ██║██║ ██║███████║██║ ██║██║ ██║
19
+ ██╔══╝ ██║╚██╗██║╚██╗ ██╔╝██╔══██║██║ ██║██║ ██║
20
+ ███████╗██║ ╚████║ ╚████╔╝ ██║ ██║╚██████╔╝███████╗██║
21
+ ╚══════╝╚═╝ ╚═══╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝
22
+ `));
23
+ console.log(chalk.dim(' Secure Environment Variable Management\n'));
24
+ };
25
+
26
+ program
27
+ .name('envault')
28
+ .description('Cliff-side security for your environment variables')
29
+ .version('1.0.0')
30
+ .hook('preAction', (thisCommand) => {
31
+ // Show logo on all commands? Maybe too noisy.
32
+ // Let's show it only on help or specific ones.
33
+ // Or just a mini header.
34
+ });
35
+
36
+ program.command('login')
37
+ .description('Authenticate with Envault using Device Flow')
38
+ .action(async () => {
39
+ showLogo();
40
+ await login();
41
+ });
42
+
43
+ program.command('init')
44
+ .description('Initialize Envault in the current directory')
45
+ .action(async () => {
46
+ await init();
47
+ });
48
+
49
+ program.command('deploy')
50
+ .description('Deploy local .env to Envault (Encrypt & Push)')
51
+ .option('-p, --project <id>', 'Project ID')
52
+ .option('--dry-run', 'Show what would change without pushing')
53
+ .action(async (options) => {
54
+ await deploy(options);
55
+ });
56
+
57
+ program.command('pull')
58
+ .description('Pull secrets from Envault to local .env')
59
+ .option('-p, --project <id>', 'Project ID')
60
+ .option('-f, --force', 'Overwrite local .env without asking')
61
+ .action(async (options) => {
62
+ await pull(options);
63
+ });
64
+
65
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@dinanathdash/envault",
3
+ "version": "1.0.0",
4
+ "description": "Envault CLI - Securely manage your environment variables",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "envault": "./bin/envault.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "envault",
15
+ "secrets",
16
+ "cli"
17
+ ],
18
+ "author": "Envault",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "axios": "^1.6.0",
22
+ "boxen": "^7.1.1",
23
+ "chalk": "^5.3.0",
24
+ "commander": "^11.1.0",
25
+ "conf": "^12.0.0",
26
+ "dotenv": "^16.3.1",
27
+ "inquirer": "^9.2.11",
28
+ "ora": "^7.0.1",
29
+ "open": "^10.0.3"
30
+ }
31
+ }
@@ -0,0 +1,53 @@
1
+
2
+ import fs from 'fs';
3
+ import dotenv from 'dotenv';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { api, handleApiError } from '../lib/api.js';
7
+
8
+ function getProjectId(options) {
9
+ if (options.project) return options.project;
10
+ if (fs.existsSync('envault.json')) {
11
+ const config = JSON.parse(fs.readFileSync('envault.json', 'utf-8'));
12
+ return config.projectId;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export async function deploy(options) {
18
+ const projectId = getProjectId(options);
19
+ if (!projectId) {
20
+ console.error(chalk.red('Error: No project linked. Run "envault init" or use --project <id>'));
21
+ return;
22
+ }
23
+
24
+ const envPath = '.env';
25
+ if (!fs.existsSync(envPath)) {
26
+ console.error(chalk.red('Error: .env file not found.'));
27
+ return;
28
+ }
29
+
30
+ const envConfig = dotenv.parse(fs.readFileSync(envPath));
31
+ const secrets = Object.entries(envConfig).map(([key, value]) => ({ key, value }));
32
+
33
+ if (secrets.length === 0) {
34
+ console.log(chalk.yellow('No secrets found in .env'));
35
+ return;
36
+ }
37
+
38
+ if (options.dryRun) {
39
+ console.log(chalk.blue(`Dry Run: Would deploy ${secrets.length} secrets to project ${projectId}`));
40
+ secrets.forEach(s => console.log(`- ${s.key}`));
41
+ return;
42
+ }
43
+
44
+ const spinner = ora('Encrypting and deploying secrets...').start();
45
+
46
+ try {
47
+ const { data } = await api.post(`/projects/${projectId}/secrets`, { secrets });
48
+ spinner.succeed(chalk.green(`✔ Successfully deployed ${secrets.length} secrets!`));
49
+ } catch (error) {
50
+ spinner.fail('Deploy failed.');
51
+ console.error(chalk.red(handleApiError(error)));
52
+ }
53
+ }
@@ -0,0 +1,59 @@
1
+
2
+ import inquirer from 'inquirer';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { api, handleApiError } from '../lib/api.js';
8
+
9
+ export async function init() {
10
+ // Check config existing
11
+ if (fs.existsSync('envault.json')) {
12
+ console.log(chalk.yellow('envault.json already exists in this directory.'));
13
+ const { confirm } = await inquirer.prompt([{
14
+ type: 'confirm',
15
+ name: 'confirm',
16
+ message: 'Do you want to overwrite it?',
17
+ default: false
18
+ }]);
19
+ if (!confirm) return;
20
+ }
21
+
22
+ const spinner = ora('Fetching your projects...').start();
23
+ let projects = [];
24
+
25
+ // We can't use /projects directly if we implemented /cli/projects/...
26
+ // Wait, I implemented /projects (standard API)? No, I only implemented /api/cli/projects/[id]/secrets
27
+ // I need a list projects endpoint for the CLI to pick from!
28
+ // I missed `GET /api/cli/projects` in the plan implementation step 105.
29
+ // I should probably add it now or reusing the frontend logic if possible?
30
+ // Or just "Enter Project ID" fallback?
31
+ // "Interactive project picker" was promised. I need that endpoint.
32
+
33
+ // Quick fix: Add the endpoint after this file creation.
34
+
35
+ try {
36
+ const { data } = await api.get('/projects'); // Assuming exists or I will create it next.
37
+ projects = data.projects || data.data || []; // Adjust based on API response structure
38
+ spinner.stop();
39
+ } catch (error) {
40
+ spinner.fail('Failed to fetch projects.');
41
+ console.error(chalk.red(handleApiError(error)));
42
+ return;
43
+ }
44
+
45
+ if (projects.length === 0) {
46
+ console.log(chalk.yellow('No projects found. Create one in the dashboard first.'));
47
+ return;
48
+ }
49
+
50
+ const { projectId } = await inquirer.prompt([{
51
+ type: 'list',
52
+ name: 'projectId',
53
+ message: 'Select the project to link:',
54
+ choices: projects.map(p => ({ name: p.name, value: p.id }))
55
+ }]);
56
+
57
+ fs.writeFileSync('envault.json', JSON.stringify({ projectId }, null, 2));
58
+ console.log(chalk.green(`\n✔ Project linked! (ID: ${projectId})`));
59
+ }
@@ -0,0 +1,86 @@
1
+
2
+ import chalk from 'chalk';
3
+ import boxen from 'boxen';
4
+ import ora from 'ora';
5
+ import open from 'open';
6
+ import { api, handleApiError } from '../lib/api.js';
7
+ import { setToken } from '../lib/config.js';
8
+
9
+ export async function login() {
10
+ console.log(chalk.blue(' Starting Device Authentication Flow...\n'));
11
+
12
+ // 1. Request Code
13
+ const spinner = ora('Contacting Envault servers...').start();
14
+ let deviceCode, userCode, verificationUri, interval;
15
+
16
+ try {
17
+ const { data } = await api.post('/auth/device/code');
18
+ deviceCode = data.device_code;
19
+ userCode = data.user_code; // 8-char
20
+ verificationUri = data.verification_uri;
21
+ interval = data.interval || 2;
22
+ spinner.succeed('Device code generated.');
23
+ } catch (error) {
24
+ spinner.fail('Failed to initiate login.');
25
+ console.error(chalk.red('Error: ' + handleApiError(error)));
26
+ return;
27
+ }
28
+
29
+ // 2. Display Code
30
+ console.log('\nPlease visit: ' + chalk.cyan.underline(verificationUri));
31
+
32
+ console.log(boxen(chalk.green.bold(userCode), {
33
+ title: 'Authentication Code',
34
+ titleAlignment: 'center',
35
+ padding: 1,
36
+ margin: 1,
37
+ borderStyle: 'round',
38
+ borderColor: 'green'
39
+ }));
40
+
41
+ // Open browser automatically
42
+ try {
43
+ await open(verificationUri);
44
+ } catch (e) {
45
+ // Ignore if fails (headless env)
46
+ }
47
+
48
+ // 3. Poll for Token
49
+ spinner.text = 'Waiting for browser approval...';
50
+ spinner.start();
51
+
52
+ const poll = async () => {
53
+ while (true) {
54
+ try {
55
+ const { data } = await api.post('/auth/device/token', { device_code: deviceCode });
56
+ if (data.access_token) {
57
+ return data.access_token;
58
+ }
59
+ } catch (error) {
60
+ const errMsg = error.response?.data?.error;
61
+ if (errMsg === 'authorization_pending') {
62
+ // Continue waiting
63
+ } else if (errMsg === 'access_denied') {
64
+ throw new Error('Access denied by user.');
65
+ } else if (errMsg === 'expired_token') {
66
+ throw new Error('Code expired. Please try again.');
67
+ } else {
68
+ // Unknown error
69
+ // throw new Error(handleApiError(error));
70
+ }
71
+ }
72
+
73
+ // Wait interval
74
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
75
+ }
76
+ };
77
+
78
+ try {
79
+ const token = await poll();
80
+ setToken(token);
81
+ spinner.succeed(chalk.green('Successfully authenticated! Token saved.'));
82
+ } catch (error) {
83
+ spinner.fail('Authentication failed.');
84
+ console.error(chalk.red(error.message));
85
+ }
86
+ }
@@ -0,0 +1,56 @@
1
+
2
+ import fs from 'fs';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import inquirer from 'inquirer';
6
+ import { api, handleApiError } from '../lib/api.js';
7
+
8
+ function getProjectId(options) {
9
+ if (options.project) return options.project;
10
+ if (fs.existsSync('envault.json')) {
11
+ const config = JSON.parse(fs.readFileSync('envault.json', 'utf-8'));
12
+ return config.projectId;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export async function pull(options) {
18
+ const projectId = getProjectId(options);
19
+ if (!projectId) {
20
+ console.error(chalk.red('Error: No project linked. Run "envault init" or use --project <id>'));
21
+ return;
22
+ }
23
+
24
+ if (fs.existsSync('.env') && !options.force) {
25
+ const { confirm } = await inquirer.prompt([{
26
+ type: 'confirm',
27
+ name: 'confirm',
28
+ message: 'This will overwrite your current .env file. Continue?',
29
+ default: false
30
+ }]);
31
+ if (!confirm) return;
32
+ }
33
+
34
+ const spinner = ora('Fetching secrets...').start();
35
+
36
+ try {
37
+ const { data } = await api.get(`/projects/${projectId}/secrets`);
38
+ // data.secrets = [{key, value}]
39
+
40
+ if (!data.secrets || data.secrets.length === 0) {
41
+ spinner.info('No secrets found for this project.');
42
+ return;
43
+ }
44
+
45
+ const envContent = data.secrets
46
+ .map(s => `${s.key}="${s.value}"`) // Quote values to be safe
47
+ .join('\n');
48
+
49
+ fs.writeFileSync('.env', envContent);
50
+ spinner.succeed(chalk.green(`✔ Pulled ${data.secrets.length} secrets to .env`));
51
+
52
+ } catch (error) {
53
+ spinner.fail('Pull failed.');
54
+ console.error(chalk.red(handleApiError(error)));
55
+ }
56
+ }
package/src/lib/api.js ADDED
@@ -0,0 +1,25 @@
1
+
2
+ import axios from 'axios';
3
+ import { getToken, getApiUrl } from './config.js';
4
+
5
+ export const api = axios.create({
6
+ baseURL: getApiUrl(),
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ },
10
+ });
11
+
12
+ api.interceptors.request.use((config) => {
13
+ const token = getToken();
14
+ if (token) {
15
+ config.headers.Authorization = `Bearer ${token}`; // Note: Backend expects Bearer
16
+ }
17
+ return config;
18
+ });
19
+
20
+ export function handleApiError(error) {
21
+ if (error.response) {
22
+ return error.response.data.error || error.response.statusText;
23
+ }
24
+ return error.message;
25
+ }
@@ -0,0 +1,31 @@
1
+
2
+ import Conf from 'conf';
3
+
4
+ // Schema:
5
+ // {
6
+ // "auth": {
7
+ // "token": "..."
8
+ // }
9
+ // }
10
+
11
+ const config = new Conf({
12
+ projectName: 'envault-cli',
13
+ projectSuffix: ''
14
+ });
15
+
16
+ export function getToken() {
17
+ return config.get('auth.token');
18
+ }
19
+
20
+ export function setToken(token) {
21
+ config.set('auth.token', token);
22
+ }
23
+
24
+ export function clearToken() {
25
+ config.delete('auth.token');
26
+ }
27
+
28
+ export function getApiUrl() {
29
+ // Default to localhost for dev, but should check env
30
+ return process.env.ENVAULT_API_URL || 'http://localhost:3000/api/cli';
31
+ }