@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 +65 -0
- package/package.json +31 -0
- package/src/commands/deploy.js +53 -0
- package/src/commands/init.js +59 -0
- package/src/commands/login.js +86 -0
- package/src/commands/pull.js +56 -0
- package/src/lib/api.js +25 -0
- package/src/lib/config.js +31 -0
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
|
+
}
|