sharenv 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/.env.example +5 -0
- package/README.md +116 -0
- package/bin/sharenv.js +385 -0
- package/package.json +25 -0
- package/sharenv-1.0.0.tgz +0 -0
package/.env.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# envcrypt CLI
|
|
2
|
+
|
|
3
|
+
A zero-dependency Node.js CLI that **encrypts your `.env` file locally** and shares it securely via the [EnvShare](https://github.com) backend.
|
|
4
|
+
|
|
5
|
+
> 🔒 Your passkey **never leaves your machine**. Only the AES-256-GCM ciphertext is stored on the server.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install (global)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd cli
|
|
13
|
+
npm link
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
After linking, `envcrypt` is available everywhere in your terminal.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### `envcrypt set [path]`
|
|
23
|
+
|
|
24
|
+
Encrypts a `.env` file and uploads it. Prints a **Share ID** for the recipient.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Encrypt .env in the current folder
|
|
28
|
+
envcrypt set
|
|
29
|
+
|
|
30
|
+
# Encrypt .env from a specific project
|
|
31
|
+
envcrypt set C:/projects/myapp
|
|
32
|
+
envcrypt set /home/user/backend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You'll be prompted for:
|
|
36
|
+
- **Passkey** (confirmed twice) — stay at 8+ chars
|
|
37
|
+
- **TTL** in minutes (default 1440 = 24 h, max 10080 = 7 days)
|
|
38
|
+
- **Max reads** (default 10, max 100) — link auto-deletes after this many reads
|
|
39
|
+
|
|
40
|
+
Output example:
|
|
41
|
+
```
|
|
42
|
+
✓ Encrypted locally
|
|
43
|
+
✓ Uploaded to backend
|
|
44
|
+
|
|
45
|
+
Share ID
|
|
46
|
+
a1b2c3d4e5f6...
|
|
47
|
+
|
|
48
|
+
Recipient command
|
|
49
|
+
envcrypt get a1b2c3d4e5f6...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### `envcrypt get <share-id> [output-path]`
|
|
55
|
+
|
|
56
|
+
Fetches and decrypts a `.env`. Writes it to the specified folder (or current directory).
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Write .env to current folder
|
|
60
|
+
envcrypt get a1b2c3d4e5f6
|
|
61
|
+
|
|
62
|
+
# Write .env to a specific project folder
|
|
63
|
+
envcrypt get a1b2c3d4e5f6 C:/projects/myapp
|
|
64
|
+
envcrypt get a1b2c3d4e5f6 ./backend
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You'll be prompted for the passkey. If wrong, decryption fails with an error — nothing is written.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `envcrypt info <share-id>`
|
|
72
|
+
|
|
73
|
+
Peek at a share's metadata without consuming a read.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
envcrypt info a1b2c3d4e5f6
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Backend URL
|
|
82
|
+
|
|
83
|
+
By default the CLI uses `http://localhost:3001/api`.
|
|
84
|
+
|
|
85
|
+
To point it at your deployed backend:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Windows PowerShell
|
|
89
|
+
$env:ENVCRYPT_API="https://your-backend.onrender.com/api"
|
|
90
|
+
envcrypt set
|
|
91
|
+
|
|
92
|
+
# Linux / macOS
|
|
93
|
+
ENVCRYPT_API=https://your-backend.onrender.com/api envcrypt set
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or set it permanently in your system environment variables.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Security
|
|
101
|
+
|
|
102
|
+
| Property | Value |
|
|
103
|
+
|---|---|
|
|
104
|
+
| Encryption | AES-256-GCM |
|
|
105
|
+
| Key derivation | PBKDF2-SHA256, 310 000 iterations |
|
|
106
|
+
| Salt / IV | Random per upload (16 / 12 bytes) |
|
|
107
|
+
| Compatibility | Works with the EnvShare web UI |
|
|
108
|
+
| Dependencies | **Zero** — uses Node.js built-ins only |
|
|
109
|
+
|
|
110
|
+
Blobs created in the browser can be retrieved from the CLI and vice versa.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Requirements
|
|
115
|
+
|
|
116
|
+
- Node.js ≥ 18
|
package/bin/sharenv.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* envcrypt — Encrypt & share .env files via your EnvShare backend.
|
|
4
|
+
*
|
|
5
|
+
* Crypto is 100% compatible with the EnvShare web frontend:
|
|
6
|
+
* - PBKDF2 (310,000 iterations, SHA-256) for key derivation
|
|
7
|
+
* - AES-256-GCM for encryption
|
|
8
|
+
* - Base64 for iv / salt / ciphertext encoding
|
|
9
|
+
*
|
|
10
|
+
* Your passkey NEVER leaves this machine — only the ciphertext is uploaded.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "crypto";
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
import { createInterface } from "readline";
|
|
17
|
+
|
|
18
|
+
// ─── CONFIG ────────────────────────────────────────────────────────────────────
|
|
19
|
+
const API_BASE = process.env.ENVCRYPT_API || "http://localhost:3001/api";
|
|
20
|
+
|
|
21
|
+
// ─── ANSI COLOURS ─────────────────────────────────────────────────────────────
|
|
22
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
23
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
24
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
25
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
26
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
27
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
28
|
+
|
|
29
|
+
// ─── PROMPT HELPERS ────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Normal visible prompt via readline. */
|
|
32
|
+
function ask(question) {
|
|
33
|
+
return new Promise((res) => {
|
|
34
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
rl.question(question, (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
res(answer.trim());
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hidden password prompt using raw mode stdin.
|
|
44
|
+
* Reads char by char, prints * for each character, handles backspace.
|
|
45
|
+
* Does NOT use readline at all — avoids the Windows readline conflicts.
|
|
46
|
+
*/
|
|
47
|
+
function askPassword(prompt) {
|
|
48
|
+
return new Promise((res) => {
|
|
49
|
+
process.stdout.write(prompt);
|
|
50
|
+
let password = "";
|
|
51
|
+
|
|
52
|
+
process.stdin.setRawMode(true);
|
|
53
|
+
process.stdin.resume();
|
|
54
|
+
process.stdin.setEncoding("utf8");
|
|
55
|
+
|
|
56
|
+
const onData = (key) => {
|
|
57
|
+
// Ctrl+C
|
|
58
|
+
if (key === "\u0003") {
|
|
59
|
+
process.stdin.setRawMode(false);
|
|
60
|
+
process.stdin.pause();
|
|
61
|
+
process.stdin.removeListener("data", onData);
|
|
62
|
+
process.stdout.write("\n");
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
// Enter
|
|
66
|
+
if (key === "\r" || key === "\n") {
|
|
67
|
+
process.stdin.setRawMode(false);
|
|
68
|
+
process.stdin.pause();
|
|
69
|
+
process.stdin.removeListener("data", onData);
|
|
70
|
+
process.stdout.write("\n");
|
|
71
|
+
res(password);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Backspace
|
|
75
|
+
if (key === "\b" || key === "\x7f") {
|
|
76
|
+
if (password.length > 0) {
|
|
77
|
+
password = password.slice(0, -1);
|
|
78
|
+
process.stdout.write("\b \b"); // erase last *
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Normal character
|
|
83
|
+
password += key;
|
|
84
|
+
process.stdout.write("*");
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
process.stdin.on("data", onData);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── CRYPTO (matches frontend WebCrypto exactly) ──────────────────────────────
|
|
92
|
+
|
|
93
|
+
function deriveKey(password, saltBuf) {
|
|
94
|
+
return pbkdf2Sync(password, saltBuf, 310_000, 32, "sha256");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function encryptEnv(plaintext, password) {
|
|
98
|
+
const saltBuf = randomBytes(16);
|
|
99
|
+
const ivBuf = randomBytes(12);
|
|
100
|
+
const key = deriveKey(password, saltBuf);
|
|
101
|
+
|
|
102
|
+
const cipher = createCipheriv("aes-256-gcm", key, ivBuf);
|
|
103
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
104
|
+
const authTag = cipher.getAuthTag();
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
encryptedData: Buffer.concat([enc, authTag]).toString("base64"),
|
|
108
|
+
iv: ivBuf.toString("base64"),
|
|
109
|
+
salt: saltBuf.toString("base64"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function decryptEnv(encryptedDataB64, ivB64, saltB64, password) {
|
|
114
|
+
const saltBuf = Buffer.from(saltB64, "base64");
|
|
115
|
+
const ivBuf = Buffer.from(ivB64, "base64");
|
|
116
|
+
const combined = Buffer.from(encryptedDataB64, "base64");
|
|
117
|
+
const key = deriveKey(password, saltBuf);
|
|
118
|
+
|
|
119
|
+
const authTag = combined.slice(combined.length - 16);
|
|
120
|
+
const encBuf = combined.slice(0, combined.length - 16);
|
|
121
|
+
|
|
122
|
+
const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
123
|
+
decipher.setAuthTag(authTag);
|
|
124
|
+
return Buffer.concat([decipher.update(encBuf), decipher.final()]).toString("utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── API ───────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function apiPost(path, body) {
|
|
130
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
});
|
|
135
|
+
const json = await res.json();
|
|
136
|
+
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
|
|
137
|
+
return json;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function apiGet(path) {
|
|
141
|
+
const res = await fetch(`${API_BASE}${path}`);
|
|
142
|
+
const json = await res.json();
|
|
143
|
+
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
|
|
144
|
+
return json;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── RESOLVE .env PATH ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function resolveEnvPath(arg) {
|
|
150
|
+
if (!arg) {
|
|
151
|
+
const p = join(process.cwd(), ".env");
|
|
152
|
+
if (!existsSync(p)) {
|
|
153
|
+
console.error(red(`\n ✗ No .env found in current directory.`));
|
|
154
|
+
console.error(dim(` Run inside a project folder or give a path:`));
|
|
155
|
+
console.error(dim(` envcrypt set C:/projects/myapp\n`));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
return p;
|
|
159
|
+
}
|
|
160
|
+
const given = resolve(arg);
|
|
161
|
+
if (given.endsWith(".env") && existsSync(given)) return given;
|
|
162
|
+
const candidate = join(given, ".env");
|
|
163
|
+
if (!existsSync(candidate)) {
|
|
164
|
+
console.error(red(`\n ✗ No .env found at: ${candidate}\n`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── COMMAND: set ─────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
async function cmdSet(args) {
|
|
173
|
+
console.log(`\n ${bold(cyan("🔐 envcrypt set"))}\n`);
|
|
174
|
+
|
|
175
|
+
const envPath = resolveEnvPath(args[0]);
|
|
176
|
+
console.log(dim(` Source: ${envPath}\n`));
|
|
177
|
+
|
|
178
|
+
const envContent = readFileSync(envPath, "utf8");
|
|
179
|
+
if (!envContent.trim()) {
|
|
180
|
+
console.error(yellow(" ⚠ .env file is empty — nothing to upload.\n"));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Step 1: Passkey (hidden with ***) ──
|
|
185
|
+
const passkey = await askPassword(bold(" Enter passkey: "));
|
|
186
|
+
if (!passkey || passkey.length < 4) {
|
|
187
|
+
console.error(red("\n ✗ Passkey must be at least 4 characters.\n"));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const passkey2 = await askPassword(bold(" Confirm passkey: "));
|
|
192
|
+
if (passkey !== passkey2) {
|
|
193
|
+
console.error(red("\n ✗ Passkeys do not match.\n"));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Step 2: TTL & max reads ──
|
|
198
|
+
const ttlInput = await ask(dim(" TTL in minutes [default 1440 = 24h]: "));
|
|
199
|
+
const readsInput = await ask(dim(" Max reads [default 10]: "));
|
|
200
|
+
|
|
201
|
+
const ttlMinutes = (ttlInput && !isNaN(ttlInput)) ? parseInt(ttlInput) : 1440;
|
|
202
|
+
const maxReads = (readsInput && !isNaN(readsInput)) ? parseInt(readsInput) : 10;
|
|
203
|
+
|
|
204
|
+
// ── Step 3: Encrypt locally ──
|
|
205
|
+
console.log(dim("\n Encrypting locally..."));
|
|
206
|
+
let encResult;
|
|
207
|
+
try {
|
|
208
|
+
encResult = encryptEnv(envContent, passkey);
|
|
209
|
+
console.log(green(" ✓ Encrypted (passkey stays on your machine)"));
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error(red(` ✗ Encryption error: ${e.message}`));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Step 4: Upload ──
|
|
216
|
+
console.log(dim(" Uploading encrypted blob..."));
|
|
217
|
+
let result;
|
|
218
|
+
try {
|
|
219
|
+
result = await apiPost("/store", { ...encResult, maxReads, ttlMinutes });
|
|
220
|
+
console.log(green(" ✓ Uploaded to backend"));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(red(`\n ✗ Upload failed: ${e.message}`));
|
|
223
|
+
console.error(dim(` Is the backend running? API: ${API_BASE}\n`));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Result ──
|
|
228
|
+
console.log(`
|
|
229
|
+
${bold(green("✓ Done!"))}
|
|
230
|
+
|
|
231
|
+
${bold("Share ID:")}
|
|
232
|
+
${cyan(result.id)}
|
|
233
|
+
|
|
234
|
+
${bold("Recipient runs:")}
|
|
235
|
+
${yellow("envcrypt get " + result.id)}
|
|
236
|
+
|
|
237
|
+
${dim(`TTL: ${result.ttlMinutes} min · Max reads: ${result.maxReads}`)}
|
|
238
|
+
${dim("Share your passkey separately — NOT in the same message as the ID.")}
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── COMMAND: get ─────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
async function cmdGet(args) {
|
|
245
|
+
console.log(`\n ${bold(cyan("📥 envcrypt get"))}\n`);
|
|
246
|
+
|
|
247
|
+
const shareId = args[0];
|
|
248
|
+
if (!shareId) {
|
|
249
|
+
console.error(red(" ✗ Usage: envcrypt get <share-id> [output-path]\n"));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const outDir = args[1] ? resolve(args[1]) : process.cwd();
|
|
254
|
+
const outFile = join(outDir, ".env");
|
|
255
|
+
|
|
256
|
+
if (existsSync(outFile)) {
|
|
257
|
+
const ans = await ask(yellow(` ⚠ .env already exists at ${outFile}\n Overwrite? (y/N): `));
|
|
258
|
+
if (!ans.toLowerCase().startsWith("y")) {
|
|
259
|
+
console.log(dim("\n Aborted.\n"));
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Passkey (hidden with ***) ──
|
|
265
|
+
const passkey = await askPassword(bold(" Enter passkey: "));
|
|
266
|
+
if (!passkey) {
|
|
267
|
+
console.error(red(" ✗ Passkey cannot be empty.\n"));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Fetch ──
|
|
272
|
+
console.log(dim("\n Fetching encrypted blob..."));
|
|
273
|
+
let data;
|
|
274
|
+
try {
|
|
275
|
+
data = await apiGet(`/retrieve/${shareId}`);
|
|
276
|
+
console.log(green(" ✓ Fetched"));
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.error(red(` ✗ Fetch failed: ${e.message}`));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Decrypt ──
|
|
283
|
+
console.log(dim(" Decrypting locally..."));
|
|
284
|
+
let envContent;
|
|
285
|
+
try {
|
|
286
|
+
envContent = decryptEnv(data.encryptedData, data.iv, data.salt, passkey);
|
|
287
|
+
console.log(green(" ✓ Decrypted"));
|
|
288
|
+
} catch {
|
|
289
|
+
console.error(red(" ✗ Decryption failed — wrong passkey or corrupted data."));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Write ──
|
|
294
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
295
|
+
writeFileSync(outFile, envContent, "utf8");
|
|
296
|
+
|
|
297
|
+
const varCount = envContent
|
|
298
|
+
.split("\n")
|
|
299
|
+
.filter((l) => l.trim() && !l.startsWith("#") && l.includes("="))
|
|
300
|
+
.length;
|
|
301
|
+
|
|
302
|
+
console.log(`
|
|
303
|
+
${bold(green("✓ .env created!"))}
|
|
304
|
+
|
|
305
|
+
${dim("Location : ")} ${outFile}
|
|
306
|
+
${dim("Variables: ")} ${varCount}
|
|
307
|
+
${dim("Reads : ")} ${data.readsUsed} / ${data.maxReads}
|
|
308
|
+
`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── COMMAND: info ────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async function cmdInfo(args) {
|
|
314
|
+
const shareId = args[0];
|
|
315
|
+
if (!shareId) {
|
|
316
|
+
console.error(red(" ✗ Usage: envcrypt info <share-id>\n"));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
let data;
|
|
320
|
+
try {
|
|
321
|
+
data = await apiGet(`/info/${shareId}`);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
console.error(red(`\n ✗ ${e.message}\n`));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
console.log(`
|
|
327
|
+
${bold(cyan("📊 Share Info"))}
|
|
328
|
+
|
|
329
|
+
${dim("ID: ")} ${cyan(shareId)}
|
|
330
|
+
${dim("Reads left: ")} ${bold(String(data.readsLeft))}
|
|
331
|
+
${dim("Max reads: ")} ${data.maxReads}
|
|
332
|
+
${dim("TTL left: ")} ${Math.ceil(data.ttlSeconds / 60)} min
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ─── HELP ─────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function printHelp() {
|
|
339
|
+
console.log(`
|
|
340
|
+
${bold(cyan("sharenv"))} — Encrypt & share .env files securely
|
|
341
|
+
|
|
342
|
+
${bold("Usage:")}
|
|
343
|
+
${yellow("sharenv set")} ${dim("[path]")} Encrypt .env and upload
|
|
344
|
+
${yellow("sharenv get")} ${dim("<id> [path]")} Download & decrypt a .env
|
|
345
|
+
${yellow("sharenv info")} ${dim("<id>")} Check share status
|
|
346
|
+
|
|
347
|
+
${bold("Examples:")}
|
|
348
|
+
${green("sharenv set")} # encrypts .env in current folder
|
|
349
|
+
${green("sharenv set C:/projects/myapp")} # encrypts .env from that project
|
|
350
|
+
${green("sharenv get abc123def456")} # write .env to current folder
|
|
351
|
+
${green("sharenv get abc123 ./myapp")} # write .env to ./myapp folder
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
${bold("Security:")}
|
|
355
|
+
• AES-256-GCM + PBKDF2-SHA256 (310k iterations)
|
|
356
|
+
• Compatible with senvsecure web frontend
|
|
357
|
+
• Your passkey ${bold("never")} leaves this machine
|
|
358
|
+
`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
const [,, cmd, ...rest] = process.argv;
|
|
364
|
+
|
|
365
|
+
switch (cmd) {
|
|
366
|
+
case "set":
|
|
367
|
+
cmdSet(rest).catch((e) => { console.error(red(`\n ✗ ${e.message}\n`)); process.exit(1); });
|
|
368
|
+
break;
|
|
369
|
+
case "get":
|
|
370
|
+
cmdGet(rest).catch((e) => { console.error(red(`\n ✗ ${e.message}\n`)); process.exit(1); });
|
|
371
|
+
break;
|
|
372
|
+
case "info":
|
|
373
|
+
cmdInfo(rest).catch((e) => { console.error(red(`\n ✗ ${e.message}\n`)); process.exit(1); });
|
|
374
|
+
break;
|
|
375
|
+
case "--help":
|
|
376
|
+
case "-h":
|
|
377
|
+
case "help":
|
|
378
|
+
case undefined:
|
|
379
|
+
printHelp();
|
|
380
|
+
break;
|
|
381
|
+
default:
|
|
382
|
+
console.error(red(`\n ✗ Unknown command: ${cmd}`));
|
|
383
|
+
printHelp();
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharenv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Encrypt & share .env files securely via the EnvShare backend",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sharenv": "./bin/sharenv.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/sharenv.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"env",
|
|
14
|
+
"sharenv",
|
|
15
|
+
"cli",
|
|
16
|
+
"dotenv",
|
|
17
|
+
"share",
|
|
18
|
+
"aes",
|
|
19
|
+
"pbkdf2"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
Binary file
|