lazy-vault 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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ghost
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Lazy Vault
2
+
3
+ ![npm version](https://img.shields.io/npm/v/lazy-vault?color=blue)
4
+ ![license](https://img.shields.io/badge/license-MIT-green)
5
+ ![status](https://img.shields.io/badge/status-stable-brightgreen)
6
+
7
+ **Stop leaking secrets & Start committing safely.**
8
+
9
+ `lazy-vault` is a simple CLI tool that lets you **encrypt `.env` files**, commit them to Git safely, and **sync secrets across machines** using a password you control.
10
+
11
+ No cloud. No accounts. No lock-in.
12
+
13
+ ---
14
+
15
+ ## Why Lazy Vault?
16
+
17
+ Environment variables are:
18
+
19
+ - Critical
20
+ - Sensitive
21
+ - Painful to share across machines and teams
22
+
23
+ `.env` files don’t belong in Git — but **encrypted `.env` files do**.
24
+
25
+ `lazy-vault` solves this by giving you a **password-based, zero-trust workflow**.
26
+
27
+ ---
28
+
29
+ ## Core Features
30
+
31
+ - **Strong Encryption**
32
+ Uses modern, authenticated encryption with a password-derived key.
33
+
34
+ - **Git-Friendly**
35
+ Encrypt once → commit `.env.enc` → safely sync anywhere.
36
+
37
+ - **Simple CLI Workflow**
38
+ Two commands. No config files. No magic.
39
+
40
+ - **Merge-Aware Syncing**
41
+ Remote secrets override conflicts, local-only keys are preserved.
42
+
43
+ - **Safe by Default**
44
+ Automatically adds `.env` to `.gitignore`.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ npm install -g lazy-vault
52
+ ```
53
+
54
+ Or use without installing:
55
+
56
+ ```bash
57
+ npx lazy-vault
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ ### Encrypt Your `.env`
65
+
66
+ ```bash
67
+ lazy-vault lock
68
+ ```
69
+
70
+ You will be prompted for a password.
71
+
72
+ This will:
73
+
74
+ - Encrypt `.env` → `.env.enc`
75
+ - Add `.env` to `.gitignore`
76
+ - Leave `.env.enc` safe to commit
77
+
78
+ ```bash
79
+ git add .env.enc
80
+ git commit -m "Add encrypted env"
81
+ ```
82
+
83
+ ---
84
+
85
+ ### Sync on Another Machine
86
+
87
+ ```bash
88
+ lazy-vault sync
89
+ ```
90
+
91
+ - Enter the same password
92
+ - `.env` will be created or merged automatically
93
+
94
+ ---
95
+
96
+ ## How It Works (High Level)
97
+
98
+ 1. Your password is converted into a cryptographic key using a memory-hard algorithm
99
+ 2. `.env` is encrypted using authenticated encryption
100
+ 3. The encrypted file (`.env.enc`) contains:
101
+ - A random salt
102
+ - A random IV
103
+ - An authentication tag
104
+ - The encrypted payload
105
+
106
+ 4. On sync, the file is decrypted and merged safely
107
+
108
+ **Your password is never stored. Ever.**
109
+
110
+ ---
111
+
112
+ ## Security Model
113
+
114
+ - **Zero-Knowledge**:
115
+ `lazy-vault` cannot recover your password.
116
+
117
+ - **Authenticated Encryption**:
118
+ Tampered or corrupted files will fail to decrypt.
119
+
120
+ - **Local-Only Secrets**:
121
+ All encryption happens on your machine.
122
+
123
+ **Important**
124
+ If you lose your password, your secrets cannot be recovered.
125
+
126
+ ---
127
+
128
+ ## Supported `.env` Format
129
+
130
+ - Simple `KEY=value` pairs
131
+ - Comments (`#`) are ignored
132
+ - Quotes are supported
133
+
134
+ ```env
135
+ DATABASE_URL="postgres://localhost/db"
136
+ API_KEY=super-secret
137
+ ```
138
+
139
+ Advanced `.env` features (multiline values, shell expansion) are intentionally not supported.
140
+
141
+ ---
142
+
143
+ ## Commands
144
+
145
+ ### `lazy-vault lock`
146
+
147
+ Encrypts `.env` into `.env.enc`.
148
+
149
+ ```bash
150
+ lazy-vault lock
151
+ ```
152
+
153
+ ---
154
+
155
+ ### `lazy-vault sync`
156
+
157
+ Decrypts `.env.enc` and merges it into `.env`.
158
+
159
+ ```bash
160
+ lazy-vault sync
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Contributing
166
+
167
+ Contributions are welcome.
168
+
169
+ 1. Fork the repo
170
+ 2. Create a feature branch
171
+ 3. Open a pull request
172
+
173
+ Security-related issues should be reported responsibly.
174
+
175
+ ---
176
+
177
+ ## 📄 License
178
+
179
+ MIT License © ghost
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/bin.ts
4
+ import { Command as Command3 } from "commander";
5
+
6
+ // src/cli/commands/sync.ts
7
+ import { Command } from "commander";
8
+ import chalk from "chalk";
9
+ import inquirer from "inquirer";
10
+
11
+ // src/lib/storage.ts
12
+ import fs from "fs-extra";
13
+
14
+ // src/constants/index.ts
15
+ var ENC_FILE = ".env.enc";
16
+ var RAW_FILE = ".env";
17
+
18
+ // src/lib/storage.ts
19
+ async function hasRawEnv() {
20
+ return fs.pathExists(RAW_FILE);
21
+ }
22
+ async function hasEncEnv() {
23
+ return fs.pathExists(ENC_FILE);
24
+ }
25
+ async function readRawEnv() {
26
+ if (!await hasRawEnv()) return "";
27
+ return fs.readFile(RAW_FILE, "utf-8");
28
+ }
29
+ async function readEncEnv() {
30
+ if (!await hasEncEnv()) {
31
+ throw new Error("No encrypted .env.enc file found.");
32
+ }
33
+ return fs.readFile(ENC_FILE, "utf-8");
34
+ }
35
+ async function writeRawEnv(content) {
36
+ await fs.writeFile(RAW_FILE, content, "utf-8");
37
+ }
38
+ async function writeEncEnv(content) {
39
+ await fs.writeFile(ENC_FILE, content, "utf-8");
40
+ }
41
+ async function ensureGitIgnore() {
42
+ const gitignorePath = ".gitignore";
43
+ const ignoreRule = "\n# Added by lazy-vault\n.env\n";
44
+ if (!await fs.pathExists(gitignorePath)) {
45
+ await fs.writeFile(gitignorePath, ignoreRule);
46
+ return;
47
+ }
48
+ const content = await fs.readFile(gitignorePath, "utf-8");
49
+ if (!content.includes(".env")) {
50
+ await fs.appendFile(gitignorePath, ignoreRule);
51
+ }
52
+ }
53
+
54
+ // src/lib/crypto.ts
55
+ import crypto from "crypto";
56
+ import argon2 from "argon2";
57
+ var ALGORITHM = "aes-256-gcm";
58
+ var VERSION = "v1";
59
+ async function encrypt(text, password) {
60
+ const salt = crypto.randomBytes(16);
61
+ const iv = crypto.randomBytes(16);
62
+ const key = await argon2.hash(password, {
63
+ type: argon2.argon2id,
64
+ salt,
65
+ raw: true,
66
+ hashLength: 32,
67
+ timeCost: 3,
68
+ memoryCost: 65536,
69
+ parallelism: 1
70
+ });
71
+ if (ALGORITHM !== "aes-256-gcm") {
72
+ throw new Error("Unsupported encryption algorithm");
73
+ }
74
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
75
+ const encrypted = Buffer.concat([
76
+ cipher.update(text, "utf8"),
77
+ cipher.final()
78
+ ]);
79
+ const authTag = cipher.getAuthTag();
80
+ key.fill(0);
81
+ return `${VERSION}:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
82
+ }
83
+ async function decrypt(encryptedText, password) {
84
+ const parts = encryptedText.split(":");
85
+ if (parts.length !== 5) {
86
+ throw new Error("Invalid encrypted file format");
87
+ }
88
+ const [version, saltHex, ivHex, authTagHex, cipherTextHex] = parts;
89
+ if (version !== "v1") {
90
+ throw new Error(`Unsupported encrypted format version: ${version}`);
91
+ }
92
+ const salt = Buffer.from(saltHex, "hex");
93
+ const iv = Buffer.from(ivHex, "hex");
94
+ const authTag = Buffer.from(authTagHex, "hex");
95
+ const cipherText = Buffer.from(cipherTextHex, "hex");
96
+ const key = await argon2.hash(password, {
97
+ type: argon2.argon2id,
98
+ salt,
99
+ raw: true,
100
+ hashLength: 32,
101
+ timeCost: 3,
102
+ memoryCost: 65536,
103
+ parallelism: 1
104
+ });
105
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
106
+ decipher.setAuthTag(authTag);
107
+ const decrypted = Buffer.concat([
108
+ decipher.update(cipherText),
109
+ decipher.final()
110
+ ]);
111
+ key.fill(0);
112
+ return decrypted.toString("utf8");
113
+ }
114
+
115
+ // src/lib/parser.ts
116
+ function parse(content) {
117
+ const result = {};
118
+ const lines = content.split(/\r?\n/);
119
+ for (const line of lines) {
120
+ const trimmedLine = line.trim();
121
+ if (trimmedLine === "" || trimmedLine.startsWith("#")) {
122
+ continue;
123
+ }
124
+ if (!trimmedLine.includes("=")) {
125
+ throw new Error(`Invalid env line (missing '='): ${line}`);
126
+ }
127
+ const [rawKey, ...valueParts] = trimmedLine.split("=");
128
+ const key = rawKey.trim();
129
+ if (!key) {
130
+ throw new Error(`Invalid env line (empty key): ${line}`);
131
+ }
132
+ const value = valueParts.join("=");
133
+ const cleanValue = value.replace(/^['"](.*)['"]$/, "$1").trim();
134
+ result[key] = cleanValue;
135
+ }
136
+ return result;
137
+ }
138
+ function stringify(envObject) {
139
+ let result = "";
140
+ for (const [key, value] of Object.entries(envObject)) {
141
+ result += `${key}=${value}
142
+ `;
143
+ }
144
+ return result.trimEnd();
145
+ }
146
+ function merge(local, remote) {
147
+ return { ...local, ...remote };
148
+ }
149
+
150
+ // src/cli/commands/sync.ts
151
+ var syncCommand = new Command("sync").description("Decrypts .env.enc and merges it with local .env").action(async () => {
152
+ try {
153
+ console.log(chalk.blue("Preparing to Sync (Decrypt)..."));
154
+ if (!await hasEncEnv()) {
155
+ console.error(chalk.red("Error: No .env.enc file found."));
156
+ process.exit(1);
157
+ }
158
+ const answers = await inquirer.prompt([
159
+ {
160
+ type: "password",
161
+ name: "password",
162
+ message: "Enter password to decrypt:",
163
+ mask: "*"
164
+ }
165
+ ]);
166
+ const password = answers.password;
167
+ const encryptedContent = await readEncEnv();
168
+ let decryptedRaw = "";
169
+ try {
170
+ decryptedRaw = await decrypt(encryptedContent, password);
171
+ } catch (e) {
172
+ throw new Error("Invalid password or corrupted file.");
173
+ }
174
+ const remoteObj = parse(decryptedRaw);
175
+ let finalObj = remoteObj;
176
+ let actionMsg = "Created new .env";
177
+ if (await hasRawEnv()) {
178
+ console.log(chalk.gray("Local .env found. Merging..."));
179
+ const localRaw = await readRawEnv();
180
+ const localObj = parse(localRaw);
181
+ finalObj = merge(localObj, remoteObj);
182
+ actionMsg = "Merged with local .env";
183
+ }
184
+ const finalString = stringify(finalObj);
185
+ await writeRawEnv(finalString);
186
+ console.log(chalk.green(`
187
+ Success! ${actionMsg}`));
188
+ console.log(
189
+ chalk.white("Keys in final .env: ") + chalk.yellow(Object.keys(finalObj).length)
190
+ );
191
+ } catch (error) {
192
+ console.error(chalk.red("\nFailed:"), error.message);
193
+ process.exit(1);
194
+ }
195
+ });
196
+
197
+ // src/cli/commands/lock.ts
198
+ import { Command as Command2 } from "commander";
199
+ import chalk2 from "chalk";
200
+ import inquirer2 from "inquirer";
201
+ var lockCommand = new Command2("lock").description("Encrypts local .env file and saves it to env.enc").action(async () => {
202
+ try {
203
+ console.log(chalk2.blue("Preparing to lock (Encrypt)..."));
204
+ if (!await hasRawEnv()) {
205
+ console.error(chalk2.red("Error: No .env file found to encrypt."));
206
+ process.exit(1);
207
+ }
208
+ const answers = await inquirer2.prompt([
209
+ {
210
+ type: "password",
211
+ name: "password",
212
+ message: "Enter Password to encrypt: ",
213
+ mask: "*",
214
+ validate: (input) => input.length > 0 ? true : "Password cannot be empty."
215
+ },
216
+ {
217
+ type: "password",
218
+ name: "passwordConfirm",
219
+ message: "Confirm password:",
220
+ mask: "*",
221
+ validate: (input, answers2) => input === answers2.password || "Passwords do not match."
222
+ }
223
+ ]);
224
+ const password = answers.password;
225
+ const rawContent = await readRawEnv();
226
+ const parsed = parse(rawContent);
227
+ const keysCount = Object.keys(parsed).length;
228
+ const encryptedData = await encrypt(rawContent, password);
229
+ await writeEncEnv(encryptedData);
230
+ await ensureGitIgnore();
231
+ console.log(
232
+ chalk2.green(`
233
+ Success! Encrypted ${keysCount} secrets to .env.enc`)
234
+ );
235
+ console.log(chalk2.gray("You can now commit .env.enc to Git."));
236
+ } catch (error) {
237
+ console.error(chalk2.red("\n Failed:"), error.message);
238
+ process.exit(1);
239
+ }
240
+ });
241
+
242
+ // src/cli/bin.ts
243
+ import chalk3 from "chalk";
244
+ var program = new Command3();
245
+ program.name("lazy-vault").description("A secure, simple way to manage encrypted .env files in Git").version("1.0.0");
246
+ program.addCommand(syncCommand);
247
+ program.addCommand(lockCommand);
248
+ program.on("command:*", () => {
249
+ console.error(
250
+ chalk3.red(
251
+ "Invalid command: %s\nSee --help for a list of available commands."
252
+ ),
253
+ program.args.join(" ")
254
+ );
255
+ process.exit(1);
256
+ });
257
+ program.parse(process.argv);
258
+ if (!process.argv.slice(2).length) {
259
+ program.outputHelp();
260
+ }
261
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/bin.ts","../../src/cli/commands/sync.ts","../../src/lib/storage.ts","../../src/constants/index.ts","../../src/lib/crypto.ts","../../src/lib/parser.ts","../../src/cli/commands/lock.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from \"commander\";\r\nimport { syncCommand } from \"./commands/sync\";\r\nimport { lockCommand } from \"./commands/lock\";\r\nimport chalk from \"chalk\";\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name(\"lazy-vault\")\r\n .description(\"A secure, simple way to manage encrypted .env files in Git\")\r\n .version(\"1.0.0\");\r\n\r\nprogram.addCommand(syncCommand);\r\nprogram.addCommand(lockCommand);\r\n\r\nprogram.on(\"command:*\", () => {\r\n console.error(\r\n chalk.red(\r\n \"Invalid command: %s\\nSee --help for a list of available commands.\",\r\n ),\r\n program.args.join(\" \"),\r\n );\r\n process.exit(1);\r\n});\r\n\r\nprogram.parse(process.argv);\r\n\r\nif (!process.argv.slice(2).length) {\r\n program.outputHelp();\r\n}\r\n","import { Command } from \"commander\";\r\nimport chalk from \"chalk\";\r\nimport inquirer from \"inquirer\";\r\n\r\nimport * as storage from \"../../lib/storage\";\r\nimport * as crypto from \"../../lib/crypto\";\r\nimport * as parser from \"../../lib/parser\";\r\n\r\nexport const syncCommand = new Command(\"sync\")\r\n .description(\"Decrypts .env.enc and merges it with local .env\")\r\n .action(async () => {\r\n try {\r\n console.log(chalk.blue(\"Preparing to Sync (Decrypt)...\"));\r\n\r\n if (!(await storage.hasEncEnv())) {\r\n console.error(chalk.red(\"Error: No .env.enc file found.\"));\r\n process.exit(1);\r\n }\r\n\r\n const answers = await inquirer.prompt([\r\n {\r\n type: \"password\",\r\n name: \"password\",\r\n message: \"Enter password to decrypt:\",\r\n mask: \"*\",\r\n },\r\n ]);\r\n\r\n const password = answers.password;\r\n\r\n const encryptedContent = await storage.readEncEnv();\r\n let decryptedRaw = \"\";\r\n\r\n try {\r\n decryptedRaw = await crypto.decrypt(encryptedContent, password);\r\n } catch (e) {\r\n throw new Error(\"Invalid password or corrupted file.\");\r\n }\r\n\r\n const remoteObj = parser.parse(decryptedRaw);\r\n\r\n let finalObj = remoteObj;\r\n let actionMsg = \"Created new .env\";\r\n\r\n if (await storage.hasRawEnv()) {\r\n console.log(chalk.gray(\"Local .env found. Merging...\"));\r\n const localRaw = await storage.readRawEnv();\r\n const localObj = parser.parse(localRaw);\r\n\r\n // MERGE: Remote wins conflicts, Local keeps unique keys\r\n finalObj = parser.merge(localObj, remoteObj);\r\n actionMsg = \"Merged with local .env\";\r\n }\r\n\r\n const finalString = parser.stringify(finalObj);\r\n await storage.writeRawEnv(finalString);\r\n\r\n console.log(chalk.green(`\\nSuccess! ${actionMsg}`));\r\n console.log(\r\n chalk.white(\"Keys in final .env: \") +\r\n chalk.yellow(Object.keys(finalObj).length),\r\n );\r\n } catch (error: any) {\r\n console.error(chalk.red(\"\\nFailed:\"), error.message);\r\n process.exit(1);\r\n }\r\n });\r\n","import fs from \"fs-extra\";\r\nimport { ENC_FILE, RAW_FILE } from \"../constants\";\r\n\r\n/**\r\n * Checks if the raw .env file exists.\r\n * @return True if the file exists, false otherwise\r\n */\r\nexport async function hasRawEnv(): Promise<boolean> {\r\n return fs.pathExists(RAW_FILE);\r\n}\r\n\r\n/**\r\n * Checks if the encrypted .env file exists.\r\n * @return True if the file exists, false otherwise\r\n */\r\nexport async function hasEncEnv(): Promise<boolean> {\r\n return fs.pathExists(ENC_FILE);\r\n}\r\n\r\n/**\r\n * Reads the RAW .env file.\r\n * @return The content of the raw .env file\r\n */\r\nexport async function readRawEnv(): Promise<string> {\r\n if (!(await hasRawEnv())) return \"\";\r\n return fs.readFile(RAW_FILE, \"utf-8\");\r\n}\r\n\r\n/**\r\n * Reads the ENCRYPTED .env.enc file.\r\n * @return The content of the encrypted .env.enc file\r\n */\r\nexport async function readEncEnv(): Promise<string> {\r\n if (!(await hasEncEnv())) {\r\n throw new Error(\"No encrypted .env.enc file found.\");\r\n }\r\n return fs.readFile(ENC_FILE, \"utf-8\");\r\n}\r\n\r\n/**\r\n * Writes data to the RAW .env file.\r\n * @param content The content to write to the raw .env file\r\n * @return A promise that resolves when the write is complete\r\n */\r\nexport async function writeRawEnv(content: string): Promise<void> {\r\n await fs.writeFile(RAW_FILE, content, \"utf-8\");\r\n}\r\n\r\n/**\r\n * Writes data to the ENCRYPTED .env.enc file.\r\n * @param content The content to write to the encrypted .env.enc file\r\n * @return A promise that resolves when the write is complete\r\n */\r\nexport async function writeEncEnv(content: string): Promise<void> {\r\n await fs.writeFile(ENC_FILE, content, \"utf-8\");\r\n}\r\n\r\n/**\r\n * Adds .env to .gitignore if it's missing.\r\n * This is a safety feature to prevent accidental commits.\r\n */\r\nexport async function ensureGitIgnore(): Promise<void> {\r\n const gitignorePath = \".gitignore\";\r\n const ignoreRule = \"\\n# Added by lazy-vault\\n.env\\n\";\r\n\r\n if (!(await fs.pathExists(gitignorePath))) {\r\n await fs.writeFile(gitignorePath, ignoreRule);\r\n return;\r\n }\r\n\r\n const content = await fs.readFile(gitignorePath, \"utf-8\");\r\n if (!content.includes(\".env\")) {\r\n await fs.appendFile(gitignorePath, ignoreRule);\r\n }\r\n}\r\n","export const ALGORITHM = \"aes-256-gcm\";\r\nexport const ENC_FILE = \".env.enc\";\r\nexport const RAW_FILE = \".env\";\r\n","import crypto from \"node:crypto\";\r\nimport argon2 from \"argon2\";\r\n\r\nconst ALGORITHM = \"aes-256-gcm\" as const;\r\nconst VERSION = \"v1\";\r\n\r\n/**\r\n * Encrypts a raw string using a password.\r\n * Format: salt:iv:authTag:cipherText\r\n * @param text The string to encrypt\r\n * @param password The password to use for encryption\r\n */\r\n\r\nexport async function encrypt(text: string, password: string): Promise<string> {\r\n const salt = crypto.randomBytes(16);\r\n const iv = crypto.randomBytes(16);\r\n\r\n const key = await argon2.hash(password, {\r\n type: argon2.argon2id,\r\n salt: salt,\r\n raw: true,\r\n hashLength: 32,\r\n timeCost: 3,\r\n memoryCost: 65536,\r\n parallelism: 1,\r\n });\r\n\r\n if (ALGORITHM !== \"aes-256-gcm\") {\r\n throw new Error(\"Unsupported encryption algorithm\");\r\n }\r\n\r\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\r\n\r\n const encrypted = Buffer.concat([\r\n cipher.update(text, \"utf8\"),\r\n cipher.final(),\r\n ]);\r\n const authTag = cipher.getAuthTag();\r\n\r\n key.fill(0);\r\n\r\n return `${VERSION}:${salt.toString(\"hex\")}:${iv.toString(\"hex\")}:${authTag.toString(\"hex\")}:${encrypted.toString(\"hex\")}`;\r\n}\r\n\r\n/** * Decrypts an encrypted string using a password.\r\n * @param encryptedText The encrypted string to decrypt\r\n * @param password The password to use for decryption\r\n * @return The decrypted string\r\n */\r\nexport async function decrypt(\r\n encryptedText: string,\r\n password: string,\r\n): Promise<string> {\r\n const parts = encryptedText.split(\":\");\r\n\r\n if (parts.length !== 5) {\r\n throw new Error(\"Invalid encrypted file format\");\r\n }\r\n const [version, saltHex, ivHex, authTagHex, cipherTextHex] = parts as [\r\n string,\r\n string,\r\n string,\r\n string,\r\n string,\r\n ];\r\n\r\n if (version !== \"v1\") {\r\n throw new Error(`Unsupported encrypted format version: ${version}`);\r\n }\r\n const salt = Buffer.from(saltHex, \"hex\");\r\n const iv = Buffer.from(ivHex, \"hex\");\r\n const authTag = Buffer.from(authTagHex, \"hex\");\r\n const cipherText = Buffer.from(cipherTextHex, \"hex\");\r\n\r\n const key = await argon2.hash(password, {\r\n type: argon2.argon2id,\r\n salt: salt,\r\n raw: true,\r\n hashLength: 32,\r\n timeCost: 3,\r\n memoryCost: 65536,\r\n parallelism: 1,\r\n });\r\n\r\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\r\n decipher.setAuthTag(authTag);\r\n\r\n const decrypted = Buffer.concat([\r\n decipher.update(cipherText),\r\n decipher.final(),\r\n ]);\r\n\r\n key.fill(0);\r\n\r\n return decrypted.toString(\"utf8\");\r\n}\r\n","import type { EnvObject } from \"..\";\r\n\r\n/**\r\n * Parses a raw .env string into a Key-Value object.\r\n * Ignores comments and empty lines.\r\n * @param content The raw .env string\r\n * @return An object representing the parsed .env variables\r\n */\r\n\r\nexport function parse(content: string): EnvObject {\r\n const result: EnvObject = {};\r\n const lines = content.split(/\\r?\\n/);\r\n\r\n for (const line of lines) {\r\n const trimmedLine = line.trim();\r\n if (trimmedLine === \"\" || trimmedLine.startsWith(\"#\")) {\r\n continue;\r\n }\r\n\r\n if (!trimmedLine.includes(\"=\")) {\r\n throw new Error(`Invalid env line (missing '='): ${line}`);\r\n }\r\n\r\n const [rawKey, ...valueParts] = trimmedLine.split(\"=\") as [string, any];\r\n\r\n const key = rawKey.trim();\r\n if (!key) {\r\n throw new Error(`Invalid env line (empty key): ${line}`);\r\n }\r\n\r\n const value = valueParts.join(\"=\");\r\n const cleanValue = value.replace(/^['\"](.*)['\"]$/, \"$1\").trim();\r\n\r\n result[key] = cleanValue;\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Stringifies a Key-Value object into a raw .env string.\r\n * @param envObject The object representing .env variables\r\n * @return A raw .env string\r\n */\r\nexport function stringify(envObject: EnvObject): string {\r\n let result = \"\";\r\n for (const [key, value] of Object.entries(envObject)) {\r\n result += `${key}=${value}\\n`;\r\n }\r\n\r\n return result.trimEnd();\r\n}\r\n\r\n/**\r\n * Merge logic\r\n * Remote keys override local keys if they exist\r\n * Unique keys from both are preserved\r\n * @param local The local .env object\r\n * @param remote The remote .env object\r\n */\r\n\r\nexport function merge(local: EnvObject, remote: EnvObject): EnvObject {\r\n return { ...local, ...remote };\r\n}\r\n","import { Command } from \"commander\";\r\nimport chalk from \"chalk\";\r\nimport inquirer, { type Answers } from \"inquirer\";\r\n\r\nimport * as storage from \"../../lib/storage\";\r\nimport * as crypto from \"../../lib/crypto\";\r\nimport { parse } from \"../../lib/parser\";\r\n\r\nexport const lockCommand = new Command(\"lock\")\r\n .description(\"Encrypts local .env file and saves it to env.enc\")\r\n .action(async () => {\r\n try {\r\n console.log(chalk.blue(\"Preparing to lock (Encrypt)...\"));\r\n\r\n if (!(await storage.hasRawEnv())) {\r\n console.error(chalk.red(\"Error: No .env file found to encrypt.\"));\r\n process.exit(1);\r\n }\r\n\r\n const answers = await inquirer.prompt([\r\n {\r\n type: \"password\",\r\n name: \"password\",\r\n message: \"Enter Password to encrypt: \",\r\n mask: \"*\",\r\n validate: (input) =>\r\n input.length > 0 ? true : \"Password cannot be empty.\",\r\n },\r\n {\r\n type: \"password\",\r\n name: \"passwordConfirm\",\r\n message: \"Confirm password:\",\r\n mask: \"*\",\r\n validate: (input: string, answers: Answers) =>\r\n input === answers.password || \"Passwords do not match.\",\r\n },\r\n ]);\r\n\r\n const password = answers.password;\r\n\r\n const rawContent = await storage.readRawEnv();\r\n const parsed = parse(rawContent);\r\n const keysCount = Object.keys(parsed).length;\r\n\r\n const encryptedData = await crypto.encrypt(rawContent, password);\r\n await storage.writeEncEnv(encryptedData);\r\n await storage.ensureGitIgnore();\r\n\r\n console.log(\r\n chalk.green(`\\nSuccess! Encrypted ${keysCount} secrets to .env.enc`),\r\n );\r\n console.log(chalk.gray(\"You can now commit .env.enc to Git.\"));\r\n } catch (error: any) {\r\n console.error(chalk.red(\"\\n Failed:\"), error.message);\r\n process.exit(1);\r\n }\r\n });\r\n"],"mappings":"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","names":["Command","Command","chalk","inquirer","Command","chalk","inquirer","answers","chalk","Command"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "lazy-vault",
3
+ "version": "1.0.0",
4
+ "description": "A simple CLI for encrypting and syncing .env files safely in Git",
5
+ "license": "MIT",
6
+ "author": "ghost",
7
+ "type": "module",
8
+ "bin": {
9
+ "lazy-env": "./dist/cli/bin.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "lint": "tsc --noEmit",
18
+ "test": "vitest"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/Ghunter254/lazy-env"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/Ghunter254/lazy-env/issues"
31
+ },
32
+ "homepage": "https://github.com/Ghunter254/lazy-env#readme",
33
+ "keywords": [
34
+ "env",
35
+ "dotenv",
36
+ "environment",
37
+ "secrets",
38
+ "encryption",
39
+ "cli",
40
+ "git",
41
+ "nodejs",
42
+ "typescript"
43
+ ],
44
+ "dependencies": {
45
+ "argon2": "^0.44.0",
46
+ "chalk": "^5.6.2",
47
+ "commander": "^14.0.2",
48
+ "fs-extra": "^11.3.3",
49
+ "inquirer": "^13.2.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/fs-extra": "^11.0.4",
53
+ "@types/inquirer": "^9.0.9",
54
+ "@types/node": "^25.0.3",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^5.9.3",
57
+ "vitest": "^4.0.16",
58
+ "tsx": "^4.21.0"
59
+ }
60
+ }