@akshxy/envgit 0.2.0 → 0.3.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/package.json +1 -1
- package/src/commands/init.js +15 -8
- package/src/commands/keygen.js +21 -19
- package/src/commands/status.js +14 -6
- package/src/keystore.js +54 -19
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
3
4
|
import { generateKey } from '../crypto.js';
|
|
4
|
-
import { saveKey } from '../keystore.js';
|
|
5
|
+
import { saveKey, globalKeyPath } from '../keystore.js';
|
|
5
6
|
import { saveConfig, getEnvctlDir } from '../config.js';
|
|
6
7
|
import { writeEncEnv } from '../enc.js';
|
|
7
|
-
import { ok,
|
|
8
|
+
import { ok, fatal, bold, dim, label } from '../ui.js';
|
|
8
9
|
|
|
9
10
|
export async function init(options) {
|
|
10
11
|
const projectRoot = process.cwd();
|
|
@@ -15,18 +16,21 @@ export async function init(options) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const defaultEnv = options.env;
|
|
19
|
+
const keyId = randomUUID();
|
|
18
20
|
|
|
19
21
|
mkdirSync(envgitDir, { recursive: true });
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
saveKey(projectRoot, key);
|
|
23
|
-
|
|
23
|
+
// Save config first (saveKey needs key_id from config)
|
|
24
24
|
saveConfig(projectRoot, {
|
|
25
25
|
version: 1,
|
|
26
26
|
default_env: defaultEnv,
|
|
27
27
|
envs: [defaultEnv],
|
|
28
|
+
key_id: keyId,
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
const key = generateKey();
|
|
32
|
+
const keyPath = saveKey(projectRoot, key, keyId);
|
|
33
|
+
|
|
30
34
|
writeEncEnv(projectRoot, defaultEnv, key, {});
|
|
31
35
|
|
|
32
36
|
updateGitignore(projectRoot);
|
|
@@ -35,16 +39,19 @@ export async function init(options) {
|
|
|
35
39
|
console.log(bold('envgit initialized'));
|
|
36
40
|
console.log('');
|
|
37
41
|
ok(`Default environment: ${label(defaultEnv)}`);
|
|
38
|
-
ok(
|
|
42
|
+
ok(`Key stored at ${dim(keyPath)}`);
|
|
39
43
|
console.log('');
|
|
40
|
-
console.log(dim('Keep .envgit.key secret and do not commit it.'));
|
|
41
44
|
console.log(dim('Commit .envgit/ to share encrypted environments with your team.'));
|
|
45
|
+
console.log(dim('Your key never touches the repo — it lives only on your machine.'));
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(`Share your key with teammates: ${bold('envgit keygen --show')}`);
|
|
48
|
+
console.log(`Teammates save it with: ${bold('envgit keygen --set <key>')}`);
|
|
42
49
|
console.log('');
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function updateGitignore(projectRoot) {
|
|
46
53
|
const gitignorePath = join(projectRoot, '.gitignore');
|
|
47
|
-
const entries = ['.env'
|
|
54
|
+
const entries = ['.env'];
|
|
48
55
|
|
|
49
56
|
let existing = '';
|
|
50
57
|
if (existsSync(gitignorePath)) {
|
package/src/commands/keygen.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { generateKey } from '../crypto.js';
|
|
2
|
-
import { findProjectRoot, saveKey, loadKey } from '../keystore.js';
|
|
3
|
-
import {
|
|
2
|
+
import { findProjectRoot, saveKey, loadKey, globalKeyPath } from '../keystore.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { ok, warn, fatal, bold, dim } from '../ui.js';
|
|
4
5
|
|
|
5
6
|
export async function keygen(options) {
|
|
6
7
|
const projectRoot = findProjectRoot();
|
|
7
8
|
|
|
8
9
|
if (options.show) {
|
|
9
10
|
if (!projectRoot) {
|
|
10
|
-
|
|
11
|
-
process.exit(1);
|
|
11
|
+
fatal('No envgit project found — cannot show key.');
|
|
12
12
|
}
|
|
13
13
|
let key;
|
|
14
14
|
try {
|
|
15
15
|
key = loadKey(projectRoot);
|
|
16
16
|
} catch (e) {
|
|
17
|
-
|
|
18
|
-
process.exit(1);
|
|
17
|
+
fatal(e.message);
|
|
19
18
|
}
|
|
20
19
|
const hint = key.slice(0, 8);
|
|
21
20
|
console.log('');
|
|
@@ -23,8 +22,8 @@ export async function keygen(options) {
|
|
|
23
22
|
console.log(` ${key}`);
|
|
24
23
|
console.log('');
|
|
25
24
|
console.log(dim(`Hint (first 8 chars): ${hint}`));
|
|
26
|
-
console.log(dim('Share
|
|
27
|
-
console.log(dim('
|
|
25
|
+
console.log(dim('Share via a secure channel (not git, not chat).'));
|
|
26
|
+
console.log(dim('Teammate saves it with: envgit keygen --set <key>'));
|
|
28
27
|
console.log('');
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
@@ -33,16 +32,18 @@ export async function keygen(options) {
|
|
|
33
32
|
const key = options.set;
|
|
34
33
|
const decoded = Buffer.from(key, 'base64');
|
|
35
34
|
if (decoded.length !== 32) {
|
|
36
|
-
|
|
37
|
-
process.exit(1);
|
|
35
|
+
fatal(`Invalid key — must decode to exactly 32 bytes (got ${decoded.length}). Generate one with: envgit keygen`);
|
|
38
36
|
}
|
|
39
37
|
if (!projectRoot) {
|
|
40
|
-
|
|
41
|
-
process.exit(1);
|
|
38
|
+
fatal('No envgit project found. Run envgit init first, or clone a repo that uses envgit.');
|
|
42
39
|
}
|
|
43
|
-
saveKey(projectRoot, key);
|
|
44
|
-
ok(`Key saved
|
|
45
|
-
console.log(dim(`
|
|
40
|
+
const keyPath = saveKey(projectRoot, key);
|
|
41
|
+
ok(`Key saved for this project`);
|
|
42
|
+
console.log(dim(` Stored at: ${keyPath}`));
|
|
43
|
+
console.log(dim(` Hint: ${key.slice(0, 8)}`));
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(dim('Run `envgit verify` to confirm it works.'));
|
|
46
|
+
console.log('');
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -51,8 +52,9 @@ export async function keygen(options) {
|
|
|
51
52
|
const hint = key.slice(0, 8);
|
|
52
53
|
|
|
53
54
|
if (projectRoot) {
|
|
54
|
-
saveKey(projectRoot, key);
|
|
55
|
-
ok(
|
|
55
|
+
const keyPath = saveKey(projectRoot, key);
|
|
56
|
+
ok(`New key generated`);
|
|
57
|
+
console.log(dim(` Stored at: ${keyPath}`));
|
|
56
58
|
} else {
|
|
57
59
|
console.log('');
|
|
58
60
|
console.log(bold('Generated key (no project found — not saved):'));
|
|
@@ -63,7 +65,7 @@ export async function keygen(options) {
|
|
|
63
65
|
console.log(` ${key}`);
|
|
64
66
|
console.log('');
|
|
65
67
|
console.log(dim(`Hint (first 8 chars): ${hint}`));
|
|
66
|
-
console.log(dim('Share
|
|
67
|
-
console.log(dim('
|
|
68
|
+
console.log(dim('Share via a secure channel (not git, not chat).'));
|
|
69
|
+
console.log(dim('Teammate saves it with: envgit keygen --set <key>'));
|
|
68
70
|
console.log('');
|
|
69
71
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { requireProjectRoot } from '../keystore.js';
|
|
3
|
+
import { requireProjectRoot, globalKeyPath } from '../keystore.js';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { getCurrentEnv } from '../state.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
@@ -11,11 +11,19 @@ export async function status() {
|
|
|
11
11
|
const config = loadConfig(projectRoot);
|
|
12
12
|
const current = getCurrentEnv(projectRoot);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
let keySource;
|
|
15
|
+
if (process.env.ENVGIT_KEY) {
|
|
16
|
+
keySource = chalk.green('ENVGIT_KEY') + dim(' (env var)');
|
|
17
|
+
} else if (config.key_id) {
|
|
18
|
+
const keyPath = globalKeyPath(config.key_id);
|
|
19
|
+
keySource = existsSync(keyPath)
|
|
20
|
+
? chalk.green('~/.config/envgit/keys/') + dim(config.key_id.slice(0, 8) + '…')
|
|
21
|
+
: chalk.red('not found on this machine') + dim(' — run: envgit keygen --set <key>');
|
|
22
|
+
} else {
|
|
23
|
+
// Legacy
|
|
24
|
+
const legacyPath = join(projectRoot, '.envgit.key');
|
|
25
|
+
keySource = existsSync(legacyPath) ? dim('.envgit.key (legacy)') : chalk.red('(not found)');
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
const dotenvExists = existsSync(join(projectRoot, '.env'));
|
|
21
29
|
|
package/src/keystore.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, chmodSync, existsSync, statSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, chmodSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
3
5
|
|
|
4
|
-
const KEY_FILE = '.envgit.key';
|
|
5
6
|
const ENV_VAR = 'ENVGIT_KEY';
|
|
6
7
|
const SECURE_MODE = 0o600;
|
|
7
8
|
|
|
9
|
+
function globalKeysDir() {
|
|
10
|
+
return join(homedir(), '.config', 'envgit', 'keys');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function globalKeyPath(keyId) {
|
|
14
|
+
return join(globalKeysDir(), `${keyId}.key`);
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
9
18
|
let dir = startDir;
|
|
10
19
|
while (true) {
|
|
11
20
|
if (existsSync(join(dir, '.envgit'))) return dir;
|
|
12
21
|
const parent = dirname(dir);
|
|
13
|
-
if (parent === dir) return null;
|
|
22
|
+
if (parent === dir) return null;
|
|
14
23
|
dir = parent;
|
|
15
24
|
}
|
|
16
25
|
}
|
|
@@ -26,27 +35,53 @@ export function requireProjectRoot() {
|
|
|
26
35
|
|
|
27
36
|
export function loadKey(projectRoot) {
|
|
28
37
|
if (process.env[ENV_VAR]) return process.env[ENV_VAR];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
process.exit(1);
|
|
38
|
+
|
|
39
|
+
const config = loadConfig(projectRoot);
|
|
40
|
+
|
|
41
|
+
if (config.key_id) {
|
|
42
|
+
const keyPath = globalKeyPath(config.key_id);
|
|
43
|
+
if (existsSync(keyPath)) {
|
|
44
|
+
enforcePermissions(keyPath);
|
|
45
|
+
return readFileSync(keyPath, 'utf8').trim();
|
|
38
46
|
}
|
|
39
|
-
|
|
47
|
+
// Project found, but this machine doesn't have the key yet
|
|
48
|
+
console.error(
|
|
49
|
+
`No key found for this project. Ask your team for the key, then run:\n envgit keygen --set <key>`
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
|
|
54
|
+
// Legacy fallback: .envgit.key in project root
|
|
55
|
+
const legacyPath = join(projectRoot, '.envgit.key');
|
|
56
|
+
if (existsSync(legacyPath)) {
|
|
57
|
+
enforcePermissions(legacyPath);
|
|
58
|
+
return readFileSync(legacyPath, 'utf8').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.error(
|
|
62
|
+
`No key found for this project. Ask your team for the key, then run:\n envgit keygen --set <key>`
|
|
43
63
|
);
|
|
64
|
+
process.exit(1);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
export function saveKey(projectRoot, key) {
|
|
47
|
-
const
|
|
67
|
+
export function saveKey(projectRoot, key, keyId) {
|
|
68
|
+
const dir = globalKeysDir();
|
|
69
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
|
|
71
|
+
const id = keyId ?? loadConfig(projectRoot).key_id;
|
|
72
|
+
const keyPath = globalKeyPath(id);
|
|
48
73
|
writeFileSync(keyPath, key + '\n', { mode: SECURE_MODE });
|
|
49
|
-
// Explicitly tighten permissions in case the file already existed —
|
|
50
|
-
// writeFileSync's mode option only applies when creating a new file.
|
|
51
74
|
chmodSync(keyPath, SECURE_MODE);
|
|
75
|
+
return keyPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function enforcePermissions(keyPath) {
|
|
79
|
+
const stat = statSync(keyPath);
|
|
80
|
+
const mode = stat.mode & 0o777;
|
|
81
|
+
if (mode & 0o077) {
|
|
82
|
+
console.error(
|
|
83
|
+
`Error: key file has insecure permissions (${mode.toString(8)}). Run: chmod 600 ${keyPath}`
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
52
87
|
}
|