@boltpl/envseal 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/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/envseal +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +226 -0
- package/dist/crypto.d.ts +10 -0
- package/dist/crypto.js +48 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.js +60 -0
- package/dist/run.d.ts +1 -0
- package/dist/run.js +20 -0
- package/dist/vault.d.ts +16 -0
- package/dist/vault.js +147 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mgrom
|
|
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,88 @@
|
|
|
1
|
+
# envseal
|
|
2
|
+
|
|
3
|
+
encrypt secrets at rest, inject at runtime. no plaintext on disk.
|
|
4
|
+
|
|
5
|
+
## install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g envseal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## usage
|
|
12
|
+
|
|
13
|
+
### passphrase mode (dev/laptop)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
envseal init
|
|
17
|
+
envseal set DATABASE_URL "postgres://..."
|
|
18
|
+
envseal set STRIPE_KEY "sk_live_..."
|
|
19
|
+
envseal run -- node server.js # prompts for passphrase, injects env
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### keyfile mode (servers)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd ~/projects/myapp
|
|
26
|
+
envseal keygen # generates ~/.envseal/keys/myapp.key (auto-named after directory)
|
|
27
|
+
envseal init --keyfile
|
|
28
|
+
envseal set DATABASE_URL "postgres://..." # auto-finds key, no config needed
|
|
29
|
+
envseal run -- node server.js # just works
|
|
30
|
+
|
|
31
|
+
# in systemd unit - zero interaction
|
|
32
|
+
# ExecStart=envseal run -- node server.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
key is stored in `~/.envseal/keys/<project-dir>.key`, vault is `.envseal.vault` in project dir. separated by default.
|
|
36
|
+
|
|
37
|
+
### other commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
envseal get KEY # decrypt single value
|
|
41
|
+
envseal list # show key names (not values)
|
|
42
|
+
envseal rm KEY # remove a secret
|
|
43
|
+
envseal export # decrypt all as KEY=VALUE on stdout
|
|
44
|
+
envseal import .env # bulk import from .env file
|
|
45
|
+
envseal keygen --out PATH # custom key location
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## key resolution
|
|
49
|
+
|
|
50
|
+
in keyfile mode, envseal looks for the key in order:
|
|
51
|
+
|
|
52
|
+
1. `ENVSEAL_KEY` env var (base64 key directly)
|
|
53
|
+
2. `ENVSEAL_KEY_FILE` env var (path to key file)
|
|
54
|
+
3. `~/.envseal/keys/<project-dir-name>.key` (auto-resolve)
|
|
55
|
+
|
|
56
|
+
in passphrase mode, same lookup order, then falls back to interactive prompt. `ENVSEAL_PASSPHRASE` env var skips the prompt.
|
|
57
|
+
|
|
58
|
+
## how it works
|
|
59
|
+
|
|
60
|
+
secrets are encrypted with AES-256-GCM. in passphrase mode, the encryption key is derived via scrypt. in keyfile mode, the key is a random 256-bit value.
|
|
61
|
+
|
|
62
|
+
the vault (`.envseal.vault`) stores key names in plaintext, values as encrypted blobs. `envseal run` decrypts everything in memory, passes secrets as env vars to the child process, then exits. nothing plaintext touches disk.
|
|
63
|
+
|
|
64
|
+
zero dependencies - uses only node's built-in `crypto` module.
|
|
65
|
+
|
|
66
|
+
## vault format
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"version": 2,
|
|
71
|
+
"keyMode": "keyfile",
|
|
72
|
+
"secrets": {
|
|
73
|
+
"DATABASE_URL": { "iv": "...", "data": "...", "tag": "..." }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## security model
|
|
79
|
+
|
|
80
|
+
protects secrets at rest and from automated exfiltration. scanners grepping for `.env`, `sk-`, `ghp_`, private keys find nothing.
|
|
81
|
+
|
|
82
|
+
in keyfile mode, key and vault are in different locations with different permissions. attacker needs both.
|
|
83
|
+
|
|
84
|
+
not a defense against an active attacker with same-UID shell access. they can read `/proc/<pid>/environ` or attach a debugger. no userspace tool prevents that.
|
|
85
|
+
|
|
86
|
+
## license
|
|
87
|
+
|
|
88
|
+
MIT
|
package/bin/envseal
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const vault_1 = require("./vault");
|
|
38
|
+
const prompt_1 = require("./prompt");
|
|
39
|
+
const run_1 = require("./run");
|
|
40
|
+
const crypto_1 = require("./crypto");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
function requireVault() {
|
|
44
|
+
const vp = (0, vault_1.findVault)();
|
|
45
|
+
if (!vp) {
|
|
46
|
+
console.error("no vault found. run `envseal init` first.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return vp;
|
|
50
|
+
}
|
|
51
|
+
function keysDir() {
|
|
52
|
+
return path.join(process.env.HOME || "/root", ".envseal", "keys");
|
|
53
|
+
}
|
|
54
|
+
function projectName() {
|
|
55
|
+
return path.basename(process.cwd());
|
|
56
|
+
}
|
|
57
|
+
function defaultKeyPath() {
|
|
58
|
+
return path.join(keysDir(), projectName() + ".key");
|
|
59
|
+
}
|
|
60
|
+
function resolveKeyFile() {
|
|
61
|
+
// 1. ENVSEAL_KEY — raw base64 key in env
|
|
62
|
+
const envKey = process.env.ENVSEAL_KEY;
|
|
63
|
+
if (envKey)
|
|
64
|
+
return Buffer.from(envKey, "base64");
|
|
65
|
+
// 2. ENVSEAL_KEY_FILE — path to key file
|
|
66
|
+
const envKeyFile = process.env.ENVSEAL_KEY_FILE;
|
|
67
|
+
if (envKeyFile) {
|
|
68
|
+
if (!fs.existsSync(envKeyFile)) {
|
|
69
|
+
console.error(`key file not found: ${envKeyFile}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(fs.readFileSync(envKeyFile, "utf8").trim(), "base64");
|
|
73
|
+
}
|
|
74
|
+
// 3. ~/.envseal/keys/<project-dir-name>.key
|
|
75
|
+
const autoKey = defaultKeyPath();
|
|
76
|
+
if (fs.existsSync(autoKey)) {
|
|
77
|
+
return Buffer.from(fs.readFileSync(autoKey, "utf8").trim(), "base64");
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function getCredentials(vaultPath) {
|
|
82
|
+
const mode = (0, vault_1.getVaultMode)(vaultPath);
|
|
83
|
+
if (mode === "keyfile") {
|
|
84
|
+
const keyBuf = resolveKeyFile();
|
|
85
|
+
if (!keyBuf) {
|
|
86
|
+
console.error("keyfile mode but no key found. set ENVSEAL_KEY, ENVSEAL_KEY_FILE, or place .envseal.key");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
return { keyBuf };
|
|
90
|
+
}
|
|
91
|
+
// passphrase mode — check for keyfile first (override), then prompt
|
|
92
|
+
const keyBuf = resolveKeyFile();
|
|
93
|
+
if (keyBuf)
|
|
94
|
+
return { keyBuf };
|
|
95
|
+
const passphrase = await (0, prompt_1.readPassphrase)();
|
|
96
|
+
return { passphrase };
|
|
97
|
+
}
|
|
98
|
+
async function main() {
|
|
99
|
+
const args = process.argv.slice(2);
|
|
100
|
+
const cmd = args[0];
|
|
101
|
+
switch (cmd) {
|
|
102
|
+
case "init": {
|
|
103
|
+
const useKeyfile = args.includes("--keyfile");
|
|
104
|
+
const p = (0, vault_1.createVault)(undefined, useKeyfile ? "keyfile" : "passphrase");
|
|
105
|
+
console.log(`created ${p} (${useKeyfile ? "keyfile" : "passphrase"} mode)`);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "keygen": {
|
|
109
|
+
const outIdx = args.indexOf("--out");
|
|
110
|
+
const outPath = outIdx !== -1 ? args[outIdx + 1] : defaultKeyPath();
|
|
111
|
+
const key = (0, crypto_1.generateKeyFile)();
|
|
112
|
+
const outDir = path.dirname(outPath);
|
|
113
|
+
if (outDir && !fs.existsSync(outDir))
|
|
114
|
+
fs.mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
115
|
+
fs.writeFileSync(outPath, key.toString("base64") + "\n", { mode: 0o400 });
|
|
116
|
+
console.log(`key written to ${outPath}`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "set": {
|
|
120
|
+
const vp = requireVault();
|
|
121
|
+
const key = args[1];
|
|
122
|
+
const val = args[2];
|
|
123
|
+
if (!key || val === undefined) {
|
|
124
|
+
console.error("usage: envseal set KEY VALUE");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const creds = await getCredentials(vp);
|
|
128
|
+
(0, vault_1.setSecret)(vp, key, val, creds.passphrase, creds.keyBuf);
|
|
129
|
+
console.log(`set ${key}`);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "get": {
|
|
133
|
+
const vp = requireVault();
|
|
134
|
+
const key = args[1];
|
|
135
|
+
if (!key) {
|
|
136
|
+
console.error("usage: envseal get KEY");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const creds = await getCredentials(vp);
|
|
140
|
+
console.log((0, vault_1.getSecret)(vp, key, creds.passphrase, creds.keyBuf));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "list": {
|
|
144
|
+
const vp = requireVault();
|
|
145
|
+
for (const k of (0, vault_1.listKeys)(vp))
|
|
146
|
+
console.log(k);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "rm": {
|
|
150
|
+
const vp = requireVault();
|
|
151
|
+
const key = args[1];
|
|
152
|
+
if (!key) {
|
|
153
|
+
console.error("usage: envseal rm KEY");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
(0, vault_1.removeKey)(vp, key);
|
|
157
|
+
console.log(`removed ${key}`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "export": {
|
|
161
|
+
const vp = requireVault();
|
|
162
|
+
const creds = await getCredentials(vp);
|
|
163
|
+
const secrets = (0, vault_1.getAllSecrets)(vp, creds.passphrase, creds.keyBuf);
|
|
164
|
+
for (const [k, v] of Object.entries(secrets)) {
|
|
165
|
+
console.log(`${k}=${v}`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "import": {
|
|
170
|
+
const vp = requireVault();
|
|
171
|
+
const file = args[1];
|
|
172
|
+
if (!file) {
|
|
173
|
+
console.error("usage: envseal import FILE");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const creds = await getCredentials(vp);
|
|
177
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
178
|
+
let count = 0;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
182
|
+
continue;
|
|
183
|
+
const eq = trimmed.indexOf("=");
|
|
184
|
+
if (eq === -1)
|
|
185
|
+
continue;
|
|
186
|
+
const key = trimmed.slice(0, eq).trim();
|
|
187
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
188
|
+
(0, vault_1.setSecret)(vp, key, val, creds.passphrase, creds.keyBuf);
|
|
189
|
+
count++;
|
|
190
|
+
}
|
|
191
|
+
console.log(`imported ${count} secrets`);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "run": {
|
|
195
|
+
const vp = requireVault();
|
|
196
|
+
const dashIdx = args.indexOf("--");
|
|
197
|
+
if (dashIdx === -1 || dashIdx === args.length - 1) {
|
|
198
|
+
console.error("usage: envseal run -- COMMAND [ARGS...]");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const creds = await getCredentials(vp);
|
|
202
|
+
const secrets = (0, vault_1.getAllSecrets)(vp, creds.passphrase, creds.keyBuf);
|
|
203
|
+
const childArgs = args.slice(dashIdx + 1);
|
|
204
|
+
const code = await (0, run_1.runCommand)(childArgs, secrets);
|
|
205
|
+
process.exit(code);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
console.error("usage: envseal <init|keygen|set|get|list|rm|export|import|run>");
|
|
210
|
+
console.error("");
|
|
211
|
+
console.error(" init [--keyfile] create vault (passphrase or keyfile mode)");
|
|
212
|
+
console.error(" keygen [--out PATH] generate random key file");
|
|
213
|
+
console.error(" set KEY VALUE add or update a secret");
|
|
214
|
+
console.error(" get KEY decrypt and print a secret");
|
|
215
|
+
console.error(" list show secret names");
|
|
216
|
+
console.error(" rm KEY remove a secret");
|
|
217
|
+
console.error(" export decrypt all as KEY=VALUE");
|
|
218
|
+
console.error(" import FILE bulk import from .env file");
|
|
219
|
+
console.error(" run -- CMD [ARGS] run command with secrets in env");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
main().catch((err) => {
|
|
224
|
+
console.error(err.message);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface EncryptedValue {
|
|
2
|
+
iv: string;
|
|
3
|
+
data: string;
|
|
4
|
+
tag: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function deriveKey(passphrase: string, salt: Buffer): Buffer;
|
|
7
|
+
export declare function generateKeyFile(): Buffer;
|
|
8
|
+
export declare function generateSalt(): Buffer;
|
|
9
|
+
export declare function encrypt(plaintext: string, key: Buffer): EncryptedValue;
|
|
10
|
+
export declare function decrypt(enc: EncryptedValue, key: Buffer): string;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveKey = deriveKey;
|
|
4
|
+
exports.generateKeyFile = generateKeyFile;
|
|
5
|
+
exports.generateSalt = generateSalt;
|
|
6
|
+
exports.encrypt = encrypt;
|
|
7
|
+
exports.decrypt = decrypt;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const ALGO = "aes-256-gcm";
|
|
10
|
+
const SCRYPT_N = 2 ** 14;
|
|
11
|
+
const SCRYPT_R = 8;
|
|
12
|
+
const SCRYPT_P = 1;
|
|
13
|
+
const KEY_LEN = 32;
|
|
14
|
+
const IV_LEN = 12;
|
|
15
|
+
const SALT_LEN = 16;
|
|
16
|
+
function deriveKey(passphrase, salt) {
|
|
17
|
+
return (0, crypto_1.scryptSync)(passphrase, salt, KEY_LEN, {
|
|
18
|
+
N: SCRYPT_N,
|
|
19
|
+
r: SCRYPT_R,
|
|
20
|
+
p: SCRYPT_P,
|
|
21
|
+
maxmem: 64 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function generateKeyFile() {
|
|
25
|
+
return (0, crypto_1.randomBytes)(KEY_LEN);
|
|
26
|
+
}
|
|
27
|
+
function generateSalt() {
|
|
28
|
+
return (0, crypto_1.randomBytes)(SALT_LEN);
|
|
29
|
+
}
|
|
30
|
+
function encrypt(plaintext, key) {
|
|
31
|
+
const iv = (0, crypto_1.randomBytes)(IV_LEN);
|
|
32
|
+
const cipher = (0, crypto_1.createCipheriv)(ALGO, key, iv);
|
|
33
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
34
|
+
const tag = cipher.getAuthTag();
|
|
35
|
+
return {
|
|
36
|
+
iv: iv.toString("base64"),
|
|
37
|
+
data: encrypted.toString("base64"),
|
|
38
|
+
tag: tag.toString("base64"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function decrypt(enc, key) {
|
|
42
|
+
const iv = Buffer.from(enc.iv, "base64");
|
|
43
|
+
const data = Buffer.from(enc.data, "base64");
|
|
44
|
+
const tag = Buffer.from(enc.tag, "base64");
|
|
45
|
+
const decipher = (0, crypto_1.createDecipheriv)(ALGO, key, iv);
|
|
46
|
+
decipher.setAuthTag(tag);
|
|
47
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
|
|
48
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readPassphrase(prompt?: string): Promise<string>;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readPassphrase = readPassphrase;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const readline = __importStar(require("readline"));
|
|
39
|
+
function readPassphrase(prompt = "passphrase: ") {
|
|
40
|
+
const envPass = process.env.ENVSEAL_PASSPHRASE;
|
|
41
|
+
if (envPass)
|
|
42
|
+
return Promise.resolve(envPass);
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let ttyFd;
|
|
45
|
+
try {
|
|
46
|
+
ttyFd = fs.openSync("/dev/tty", "r");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
reject(new Error("cannot open /dev/tty — set ENVSEAL_PASSPHRASE env var"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const ttyStream = fs.createReadStream("", { fd: ttyFd });
|
|
53
|
+
const rl = readline.createInterface({ input: ttyStream, output: process.stderr });
|
|
54
|
+
rl.question(prompt, (answer) => {
|
|
55
|
+
rl.close();
|
|
56
|
+
ttyStream.destroy();
|
|
57
|
+
resolve(answer);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCommand(command: string[], extraEnv: Record<string, string>): Promise<number>;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCommand = runCommand;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
function runCommand(command, extraEnv) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const merged = {};
|
|
8
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
9
|
+
if (v !== undefined)
|
|
10
|
+
merged[k] = v;
|
|
11
|
+
}
|
|
12
|
+
Object.assign(merged, extraEnv);
|
|
13
|
+
const child = (0, child_process_1.spawn)(command[0], command.slice(1), {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
env: merged,
|
|
16
|
+
});
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
19
|
+
});
|
|
20
|
+
}
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EncryptedValue } from "./crypto";
|
|
2
|
+
export interface Vault {
|
|
3
|
+
version: number;
|
|
4
|
+
keyMode: "passphrase" | "keyfile";
|
|
5
|
+
salt?: string;
|
|
6
|
+
secrets: Record<string, EncryptedValue>;
|
|
7
|
+
}
|
|
8
|
+
export declare function findVault(from?: string): string | null;
|
|
9
|
+
export declare function createVault(dir?: string, keyMode?: "passphrase" | "keyfile"): string;
|
|
10
|
+
export declare function resolveKey(vault: Vault, passphrase?: string, keyBuf?: Buffer): Buffer;
|
|
11
|
+
export declare function getVaultMode(vaultPath: string): "passphrase" | "keyfile";
|
|
12
|
+
export declare function setSecret(vaultPath: string, key: string, value: string, passphrase?: string, keyBuf?: Buffer): void;
|
|
13
|
+
export declare function getSecret(vaultPath: string, key: string, passphrase?: string, keyBuf?: Buffer): string;
|
|
14
|
+
export declare function listKeys(vaultPath: string): string[];
|
|
15
|
+
export declare function removeKey(vaultPath: string, key: string): void;
|
|
16
|
+
export declare function getAllSecrets(vaultPath: string, passphrase?: string, keyBuf?: Buffer): Record<string, string>;
|
package/dist/vault.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.findVault = findVault;
|
|
37
|
+
exports.createVault = createVault;
|
|
38
|
+
exports.resolveKey = resolveKey;
|
|
39
|
+
exports.getVaultMode = getVaultMode;
|
|
40
|
+
exports.setSecret = setSecret;
|
|
41
|
+
exports.getSecret = getSecret;
|
|
42
|
+
exports.listKeys = listKeys;
|
|
43
|
+
exports.removeKey = removeKey;
|
|
44
|
+
exports.getAllSecrets = getAllSecrets;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const crypto_1 = require("./crypto");
|
|
48
|
+
const VAULT_FILE = ".envseal.vault";
|
|
49
|
+
function findVault(from) {
|
|
50
|
+
let dir;
|
|
51
|
+
try {
|
|
52
|
+
dir = from || process.cwd();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
while (true) {
|
|
58
|
+
const candidate = path.join(dir, VAULT_FILE);
|
|
59
|
+
if (fs.existsSync(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
const parent = path.dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function createVault(dir, keyMode = "passphrase") {
|
|
68
|
+
const target = path.join(dir || process.cwd(), VAULT_FILE);
|
|
69
|
+
if (fs.existsSync(target)) {
|
|
70
|
+
throw new Error("vault already exists at " + target);
|
|
71
|
+
}
|
|
72
|
+
const vault = {
|
|
73
|
+
version: 2,
|
|
74
|
+
keyMode,
|
|
75
|
+
secrets: {},
|
|
76
|
+
};
|
|
77
|
+
if (keyMode === "passphrase") {
|
|
78
|
+
vault.salt = (0, crypto_1.generateSalt)().toString("base64");
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(target, JSON.stringify(vault, null, 2) + "\n");
|
|
81
|
+
return target;
|
|
82
|
+
}
|
|
83
|
+
function readVault(vaultPath) {
|
|
84
|
+
const raw = fs.readFileSync(vaultPath, "utf8");
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
// migrate v1 vaults
|
|
87
|
+
if (parsed.version === 1) {
|
|
88
|
+
parsed.version = 2;
|
|
89
|
+
parsed.keyMode = "passphrase";
|
|
90
|
+
}
|
|
91
|
+
if (parsed.version !== 2) {
|
|
92
|
+
throw new Error("unsupported vault version: " + parsed.version);
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
function writeVault(vaultPath, vault) {
|
|
97
|
+
fs.writeFileSync(vaultPath, JSON.stringify(vault, null, 2) + "\n");
|
|
98
|
+
}
|
|
99
|
+
function resolveKey(vault, passphrase, keyBuf) {
|
|
100
|
+
if (vault.keyMode === "keyfile") {
|
|
101
|
+
if (!keyBuf)
|
|
102
|
+
throw new Error("keyfile required but not provided");
|
|
103
|
+
return keyBuf;
|
|
104
|
+
}
|
|
105
|
+
// passphrase mode
|
|
106
|
+
if (!passphrase)
|
|
107
|
+
throw new Error("passphrase required");
|
|
108
|
+
if (!vault.salt)
|
|
109
|
+
throw new Error("vault missing salt");
|
|
110
|
+
return (0, crypto_1.deriveKey)(passphrase, Buffer.from(vault.salt, "base64"));
|
|
111
|
+
}
|
|
112
|
+
function getVaultMode(vaultPath) {
|
|
113
|
+
return readVault(vaultPath).keyMode;
|
|
114
|
+
}
|
|
115
|
+
function setSecret(vaultPath, key, value, passphrase, keyBuf) {
|
|
116
|
+
const vault = readVault(vaultPath);
|
|
117
|
+
const dk = resolveKey(vault, passphrase, keyBuf);
|
|
118
|
+
vault.secrets[key] = (0, crypto_1.encrypt)(value, dk);
|
|
119
|
+
writeVault(vaultPath, vault);
|
|
120
|
+
}
|
|
121
|
+
function getSecret(vaultPath, key, passphrase, keyBuf) {
|
|
122
|
+
const vault = readVault(vaultPath);
|
|
123
|
+
const enc = vault.secrets[key];
|
|
124
|
+
if (!enc)
|
|
125
|
+
throw new Error("secret not found: " + key);
|
|
126
|
+
const dk = resolveKey(vault, passphrase, keyBuf);
|
|
127
|
+
return (0, crypto_1.decrypt)(enc, dk);
|
|
128
|
+
}
|
|
129
|
+
function listKeys(vaultPath) {
|
|
130
|
+
return Object.keys(readVault(vaultPath).secrets);
|
|
131
|
+
}
|
|
132
|
+
function removeKey(vaultPath, key) {
|
|
133
|
+
const vault = readVault(vaultPath);
|
|
134
|
+
if (!vault.secrets[key])
|
|
135
|
+
throw new Error("secret not found: " + key);
|
|
136
|
+
delete vault.secrets[key];
|
|
137
|
+
writeVault(vaultPath, vault);
|
|
138
|
+
}
|
|
139
|
+
function getAllSecrets(vaultPath, passphrase, keyBuf) {
|
|
140
|
+
const vault = readVault(vaultPath);
|
|
141
|
+
const dk = resolveKey(vault, passphrase, keyBuf);
|
|
142
|
+
const result = {};
|
|
143
|
+
for (const [k, enc] of Object.entries(vault.secrets)) {
|
|
144
|
+
result[k] = (0, crypto_1.decrypt)(enc, dk);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boltpl/envseal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "encrypt secrets at rest, inject at runtime",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"envseal": "bin/envseal"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "tsc"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["env", "secrets", "encryption", "vault", "dotenv", "security"],
|
|
14
|
+
"author": "mgrom",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/mgrom/envseal.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/mgrom/envseal",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist", "bin", "LICENSE", "README.md"],
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"typescript": "^5.5.0"
|
|
28
|
+
}
|
|
29
|
+
}
|