@akshxy/envgit 0.4.2 → 0.5.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/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g @akshxy/envgit
14
14
 
15
15
  - Secrets are encrypted with **AES-256-GCM** and live in `.envgit/` — safe to commit
16
16
  - The key lives on **your machine only** (`~/.config/envgit/keys/`) — never touches the repo
17
- - Teammates just run `envgit keygen --set <key>` after cloning no manual file management
17
+ - Onboard a teammate in one command: `envgit share` send them the link they run `envgit join`
18
18
  - `envgit unpack dev` writes a clean, **beautifully formatted `.env`** grouped by service
19
19
 
20
20
  ---
@@ -49,18 +49,23 @@ envgit init
49
49
  envgit set DB_URL=postgres://... OPENAI_API_KEY=sk-...
50
50
  git add .envgit/ && git commit -m "chore: encrypted env"
51
51
 
52
- envgit keygen --show
53
- # Prints your key send it to teammates via 1Password, Bitwarden, etc.
52
+ envgit share
53
+ # Key encrypted and uploaded. Link expires in 24 hours, usable once.
54
+ #
55
+ # envgit join abc123... --code Xk9mP2...==
56
+ #
57
+ # Send that one line to your teammate — nothing else needed.
54
58
 
55
59
  # ── Developer B (teammate, after cloning) ─────────────────
56
- envgit keygen --set <key-from-teammate>
57
- # Key is saved to ~/.config/envgit/keys/<project-id>.key automatically
58
- # No file paths to think about — it just works
60
+ envgit join abc123... --code Xk9mP2...==
61
+ # Key saved to ~/.config/envgit/keys/<project-id>.key
59
62
 
60
63
  envgit verify # confirm the key works
61
64
  envgit unpack dev # writes .env
62
65
  ```
63
66
 
67
+ The relay is **cryptographically blind** — it stores only AES-256-GCM ciphertext and never sees the passphrase. The link is deleted the moment your teammate uses it.
68
+
64
69
  ---
65
70
 
66
71
  ## Formatted `.env` output
@@ -127,8 +132,10 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
127
132
  |---------|-------------|
128
133
  | `envgit init` | Initialize project, generate key, save to `~/.config/envgit/keys/` |
129
134
  | `envgit keygen` | Generate a new key for the current project |
130
- | `envgit keygen --show` | Print current key to share with teammates |
131
- | `envgit keygen --set <key>` | Save a teammate's key for the current project |
135
+ | `envgit keygen --show` | Print current key |
136
+ | `envgit keygen --set <key>` | Save a key for the current project |
137
+ | `envgit share` | Upload encrypted key to a one-time relay link |
138
+ | `envgit join <token> --code <passphrase>` | Download and save a key shared via `envgit share` |
132
139
  | `envgit rotate-key` | Generate new key and re-encrypt all environments |
133
140
  | `envgit verify` | Confirm all environments decrypt with the current key |
134
141
 
@@ -216,3 +223,6 @@ ENVGIT_KEY=$(cat ~/.config/envgit/keys/<id>.key) envgit run -- node server.js
216
223
  - **File permissions enforced** — key files are locked to `0600`, errors if too permissive
217
224
  - **Key bytes zeroized** from memory immediately after use
218
225
  - **No plaintext ever written** except when you explicitly run `envgit unpack`
226
+ - **Relay is blind** — `envgit share` encrypts your key with a one-time passphrase before upload. The relay stores only ciphertext and never sees the passphrase. Even a full relay compromise leaks nothing.
227
+ - **One-time links** — tokens are deleted on first use via a strongly consistent Durable Object. Replay attacks are impossible.
228
+ - **24-hour TTL** — unclaimed tokens are automatically destroyed
package/bin/envgit.js CHANGED
@@ -18,6 +18,8 @@ import { envs } from '../src/commands/envs.js';
18
18
  import { exportEnv } from '../src/commands/export.js';
19
19
  import { verify } from '../src/commands/verify.js';
20
20
  import { rotateKey } from '../src/commands/rotate-key.js';
21
+ import { share } from '../src/commands/share.js';
22
+ import { join } from '../src/commands/join.js';
21
23
 
22
24
  program
23
25
  .name('envgit')
@@ -137,4 +139,15 @@ program
137
139
  .description('Generate a new key and re-encrypt all environments')
138
140
  .action(rotateKey);
139
141
 
142
+ program
143
+ .command('share')
144
+ .description('Encrypt your key and upload it to a one-time link — send the output to a teammate')
145
+ .action(share);
146
+
147
+ program
148
+ .command('join <token>')
149
+ .description('Download and save a key from a link generated by envgit share')
150
+ .requiredOption('--code <passphrase>', 'passphrase printed by envgit share')
151
+ .action(join);
152
+
140
153
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akshxy/envgit",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Encrypted per-project environment variable manager",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,50 @@
1
+ import { findProjectRoot, saveKey } from '../keystore.js';
2
+ import { decrypt } from '../crypto.js';
3
+ import { ok, fatal, bold, dim } from '../ui.js';
4
+
5
+ const RELAY = process.env.ENVGIT_RELAY ?? 'https://envgit-relay.akku41809.workers.dev';
6
+
7
+ export async function join(token, options) {
8
+ if (!token) fatal('Token required. Usage: envgit join <token> --code <passphrase>');
9
+ if (!options.code) fatal('Passphrase required. Usage: envgit join <token> --code <passphrase>');
10
+
11
+ const projectRoot = findProjectRoot();
12
+ if (!projectRoot) fatal('No envgit project found. Clone the repo and run envgit init first, or just be inside the project directory.');
13
+
14
+ let blob;
15
+ try {
16
+ const res = await fetch(`${RELAY}/join/${token}`);
17
+
18
+ if (res.status === 404) fatal('Token not found or already used. Ask your teammate to run envgit share again.');
19
+ if (res.status === 410) fatal('Token has expired. Ask your teammate to run envgit share again.');
20
+ if (!res.ok) fatal(`Relay error: ${res.status}`);
21
+
22
+ ({ blob } = await res.json());
23
+ } catch (e) {
24
+ if (e.cause?.code === 'ENOTFOUND') fatal('Cannot reach relay — check your connection.');
25
+ fatal(`Join failed: ${e.message}`);
26
+ }
27
+
28
+ let key;
29
+ try {
30
+ key = decrypt(blob, options.code);
31
+ } catch {
32
+ fatal('Decryption failed — wrong --code, or the blob was tampered with.');
33
+ }
34
+
35
+ // Validate it looks like a real key before saving
36
+ const decoded = Buffer.from(key, 'base64');
37
+ if (decoded.length !== 32) {
38
+ fatal('Decrypted value is not a valid envgit key. Wrong --code?');
39
+ }
40
+
41
+ const keyPath = saveKey(projectRoot, key);
42
+
43
+ console.log('');
44
+ ok('Key saved.');
45
+ console.log(dim(` Stored at: ${keyPath}`));
46
+ console.log('');
47
+ console.log(dim('Run `envgit verify` to confirm it works.'));
48
+ console.log(dim('Run `envgit unpack dev` to write your .env file.'));
49
+ console.log('');
50
+ }
@@ -0,0 +1,48 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { findProjectRoot, loadKey } from '../keystore.js';
3
+ import { encrypt, generateKey } from '../crypto.js';
4
+ import { ok, fatal, bold, dim } from '../ui.js';
5
+
6
+ const RELAY = process.env.ENVGIT_RELAY ?? 'https://envgit-relay.akku41809.workers.dev';
7
+
8
+ export async function share() {
9
+ const projectRoot = findProjectRoot();
10
+ if (!projectRoot) fatal('No envgit project found. Run envgit init first.');
11
+
12
+ let key;
13
+ try { key = loadKey(projectRoot); }
14
+ catch (e) { fatal(e.message); }
15
+
16
+ // Generate a one-time passphrase — used to encrypt the key before it leaves
17
+ // the machine. The relay stores only ciphertext; it never sees this passphrase.
18
+ const passphrase = generateKey(); // 32 bytes of OS randomness, base64-encoded
19
+ const blob = encrypt(key, passphrase);
20
+
21
+ let token;
22
+ try {
23
+ const res = await fetch(`${RELAY}/share`, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ blob }),
27
+ });
28
+
29
+ if (res.status === 429) fatal('Rate limit hit — try again in an hour.');
30
+ if (!res.ok) fatal(`Relay error: ${res.status}`);
31
+
32
+ ({ token } = await res.json());
33
+ } catch (e) {
34
+ if (e.cause?.code === 'ENOTFOUND') fatal('Cannot reach relay — check your connection.');
35
+ fatal(`Share failed: ${e.message}`);
36
+ }
37
+
38
+ console.log('');
39
+ ok('Key encrypted and uploaded. Link expires in 24 hours, usable once.');
40
+ console.log('');
41
+ console.log(bold('Send this to your teammate:'));
42
+ console.log('');
43
+ console.log(` envgit join ${token} --code ${passphrase}`);
44
+ console.log('');
45
+ console.log(dim('They can run it directly in their terminal after cloning.'));
46
+ console.log(dim('The link is deleted the moment they use it.'));
47
+ console.log('');
48
+ }