@envsave/cli 1.0.43 → 1.0.45
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 +79 -17
- package/dist/cli.js +47 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,11 @@ import { get } from "@envsave/cli";
|
|
|
26
26
|
const openai = new OpenAI({ apiKey: get("es_7a3f2b1e9c8d4f6b2a1c3d5f") });
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
This is the single problem this project is built around. Everything else — the CLI, the license server, the Python bridge, the cloud backup — exists to make opaque-key-based secrets practical to use day to day.
|
|
29
|
+
This is the single problem this project is built around. Everything else — the CLI, the license server, the Python bridge, the Vercel push, the cloud backup — exists to make opaque-key-based secrets practical to use day to day.
|
|
30
30
|
|
|
31
31
|
## Pricing
|
|
32
32
|
|
|
33
|
-
Local personal use is **free and unlimited** — store as many secrets as you want with no license. A license is only
|
|
33
|
+
Local personal use is **free and unlimited** — store as many secrets as you want, on as many machines as you want, with no license required. A license is only needed to sync your vault to the EnvSave cloud (backup / restore / sync across machines). Activate one with `envsave activate <token>` — get yours at https://envsave.com.
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
@@ -48,8 +48,16 @@ Python users:
|
|
|
48
48
|
pip install envsave
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
Check the installed version any time:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
envsave --version
|
|
55
|
+
```
|
|
56
|
+
|
|
51
57
|
## Activation
|
|
52
58
|
|
|
59
|
+
Activation is only required for cloud sync. Local use works without it.
|
|
60
|
+
|
|
53
61
|
### Online (production)
|
|
54
62
|
|
|
55
63
|
Requires the license server running at `https://api.envsave.com`.
|
|
@@ -92,12 +100,12 @@ You'll be prompted to set and confirm a master password.
|
|
|
92
100
|
```bash
|
|
93
101
|
envsave set OPENAI_API_KEY sk-abc123
|
|
94
102
|
# Output:
|
|
95
|
-
# OPENAI_API_KEY → es_7a3f2b1e9c8d4f6b
|
|
103
|
+
# Added: OPENAI_API_KEY → es_7a3f2b1e9c8d4f6b
|
|
96
104
|
# Use this in your code to access OPENAI_API_KEY:
|
|
97
105
|
# envsave.get("es_7a3f2b1e9c8d4f6b")
|
|
98
106
|
```
|
|
99
107
|
|
|
100
|
-
The opaque key (`es_...`) is an HMAC-SHA256 hash — no trace of the original key name.
|
|
108
|
+
The opaque key (`es_...`) is an HMAC-SHA256 hash — no trace of the original key name. Re-running `envsave set` on an existing key updates the value in place; the opaque key stays the same, so you don't need to touch any code.
|
|
101
109
|
|
|
102
110
|
## Retrieving Secrets
|
|
103
111
|
|
|
@@ -106,6 +114,9 @@ The opaque key (`es_...`) is an HMAC-SHA256 hash — no trace of the original ke
|
|
|
106
114
|
```bash
|
|
107
115
|
# By opaque key (tries session cache first, then prompts for password)
|
|
108
116
|
envsave get es_7a3f2b1e9c8d4f6b
|
|
117
|
+
|
|
118
|
+
# By clean name (password required — returns the plaintext value)
|
|
119
|
+
envsave show OPENAI_API_KEY
|
|
109
120
|
```
|
|
110
121
|
|
|
111
122
|
### Node.js / Bun
|
|
@@ -131,28 +142,78 @@ api_key = get("es_7a3f2b1e9c8d4f6b")
|
|
|
131
142
|
## Other Commands
|
|
132
143
|
|
|
133
144
|
```bash
|
|
134
|
-
# Show opaque key for a
|
|
135
|
-
envsave reveal OPENAI_API_KEY
|
|
145
|
+
# Show opaque key(s) for any name matching a keyword (password required)
|
|
146
|
+
envsave reveal OPENAI # matches OPENAI_API_KEY, OPENAI_ORG, …
|
|
147
|
+
envsave reveal OPENAI_API_KEY DATABASE_URL # multiple keywords at once
|
|
136
148
|
|
|
137
|
-
#
|
|
138
|
-
envsave
|
|
149
|
+
# Show the real value by clean name (password required)
|
|
150
|
+
envsave show OPENAI_API_KEY
|
|
139
151
|
|
|
140
|
-
#
|
|
152
|
+
# Search clean names without revealing opaque keys (no password if session is warm)
|
|
153
|
+
envsave search openai
|
|
154
|
+
|
|
155
|
+
# Scan the current project for hardcoded env references and opaque keys
|
|
156
|
+
envsave scan [dir]
|
|
157
|
+
|
|
158
|
+
# List all stored key names (password required)
|
|
141
159
|
envsave list
|
|
142
160
|
|
|
143
|
-
# Delete a secret (
|
|
161
|
+
# Delete a secret (password required)
|
|
144
162
|
envsave delete OPENAI_API_KEY
|
|
145
163
|
|
|
146
|
-
#
|
|
164
|
+
# Rename a secret (password required)
|
|
165
|
+
# Creates a new opaque key derived from the new name, copies the value over,
|
|
166
|
+
# and deletes the old opaque key. The output prints OLD → NEW for easy
|
|
167
|
+
# search-and-replace in your code.
|
|
168
|
+
envsave rename CLERK_SECRET_KEY_THEHOTELAGENTS CLERK_SECRET_KEY_DIRECTBOOKAI
|
|
169
|
+
# Output:
|
|
170
|
+
# Renamed: CLERK_SECRET_KEY_THEHOTELAGENTS → CLERK_SECRET_KEY_DIRECTBOOKAI
|
|
171
|
+
# Replace in your code:
|
|
172
|
+
# OLD (deleted): es_7cXxcbsVaJQv...
|
|
173
|
+
# NEW (created): es_enLaxEjzTO5r...
|
|
174
|
+
|
|
175
|
+
# Bulk import from .env file (password required)
|
|
147
176
|
envsave import .env
|
|
148
177
|
|
|
149
|
-
# Export all secrets as .env format (
|
|
178
|
+
# Export all secrets as .env format (password required)
|
|
150
179
|
envsave export
|
|
180
|
+
|
|
181
|
+
# Change master password
|
|
182
|
+
envsave passwd
|
|
183
|
+
|
|
184
|
+
# Rate limit config (password required to change)
|
|
185
|
+
envsave config # show current
|
|
186
|
+
envsave config --max-gets 120 # set max gets/minute
|
|
187
|
+
|
|
188
|
+
# Show version
|
|
189
|
+
envsave --version
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Deploying to Vercel
|
|
193
|
+
|
|
194
|
+
`envsave vercel` pushes selected secrets to a Vercel project so serverless functions and build-time code both work without any `.env` on disk.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# In a directory already linked with `vercel link`
|
|
198
|
+
envsave vercel OPENAI_API_KEY DATABASE_URL
|
|
199
|
+
|
|
200
|
+
# Push by opaque id instead of clean name
|
|
201
|
+
envsave vercel es_7a3f2b1e9c8d4f6b
|
|
202
|
+
|
|
203
|
+
# Restrict to specific environments (default is production, preview, development)
|
|
204
|
+
envsave vercel OPENAI_API_KEY --env production,preview
|
|
151
205
|
```
|
|
152
206
|
|
|
207
|
+
Each secret is uploaded under **both** the opaque id (`es_...`) and the clean name (`OPENAI_API_KEY`). That means:
|
|
208
|
+
|
|
209
|
+
- **Build-time / server code** can keep calling `envsave.get("es_7a3f...")` — the opaque id is present as an env var, so no password or vault is needed in the deployed environment.
|
|
210
|
+
- **Runtime serverless functions** that expect `process.env.OPENAI_API_KEY` keep working unchanged.
|
|
211
|
+
|
|
212
|
+
Only the secrets you name on the command line are pushed — the rest of your vault stays local.
|
|
213
|
+
|
|
153
214
|
## Cloud Backup & Sync
|
|
154
215
|
|
|
155
|
-
The encrypted vault can be backed up to the EnvSave cloud server for disaster recovery or syncing across machines. The server only stores the encrypted bytes — it never sees plaintext secrets.
|
|
216
|
+
Requires an active license. The encrypted vault can be backed up to the EnvSave cloud server for disaster recovery or syncing across machines. The server only stores the encrypted bytes — it never sees plaintext secrets.
|
|
156
217
|
|
|
157
218
|
```bash
|
|
158
219
|
# Upload encrypted vault to cloud
|
|
@@ -179,7 +240,7 @@ envsave sync
|
|
|
179
240
|
|
|
180
241
|
```bash
|
|
181
242
|
# 1. Install envsave
|
|
182
|
-
|
|
243
|
+
npm install -g @envsave/cli
|
|
183
244
|
|
|
184
245
|
# 2. Activate with same license token
|
|
185
246
|
envsave activate <token>
|
|
@@ -198,8 +259,9 @@ envsave list
|
|
|
198
259
|
2. Stored at `~/.envsave/vault.enc` (binary: salt + iv + encrypted data + auth tag)
|
|
199
260
|
3. Key names are HMAC-SHA256 hashes — fully opaque, no trace of original name
|
|
200
261
|
4. Session cache stores decrypted secrets in memory — no password needed for repeated reads
|
|
201
|
-
5.
|
|
202
|
-
6.
|
|
262
|
+
5. `envsave get` is rate-limited (configurable via `envsave config --max-gets`) to blunt automated scraping from a compromised session
|
|
263
|
+
6. Optional SSH key adds a "something you have" hardware factor
|
|
264
|
+
7. LLM agents only see opaque keys — they can read values but can't discover or enumerate secrets
|
|
203
265
|
|
|
204
266
|
## License Tokens
|
|
205
267
|
|
|
@@ -219,5 +281,5 @@ When a license token expires, the CLI automatically contacts the server to renew
|
|
|
219
281
|
| File | Purpose |
|
|
220
282
|
|------|---------|
|
|
221
283
|
| `~/.envsave/vault.enc` | Encrypted vault |
|
|
222
|
-
| `~/.envsave/license.json` | Activated license |
|
|
284
|
+
| `~/.envsave/license.json` | Activated license (cloud sync only) |
|
|
223
285
|
| `~/.envsave/session.json` | Session cache (decrypted secrets) |
|
package/dist/cli.js
CHANGED
|
@@ -100,6 +100,7 @@ __export(exports_vault, {
|
|
|
100
100
|
searchKeys: () => searchKeys,
|
|
101
101
|
revealKeysByPattern: () => revealKeysByPattern,
|
|
102
102
|
revealKey: () => revealKey,
|
|
103
|
+
renameSecret: () => renameSecret,
|
|
103
104
|
listKeys: () => listKeys,
|
|
104
105
|
initVault: () => initVault,
|
|
105
106
|
importEnv: () => importEnv,
|
|
@@ -446,6 +447,28 @@ function deleteSecret(key, password) {
|
|
|
446
447
|
writeSessionCache(vault.secrets, vault.registry);
|
|
447
448
|
return true;
|
|
448
449
|
}
|
|
450
|
+
function renameSecret(oldName, newName, password) {
|
|
451
|
+
if (oldName === newName) {
|
|
452
|
+
throw new Error("Old and new names are the same.");
|
|
453
|
+
}
|
|
454
|
+
const vault = readMainVault(password);
|
|
455
|
+
const sshKeyContent = loadSshKey(vault);
|
|
456
|
+
const oldOpaque = computeOpaqueName(vault.hmacSeed, oldName, sshKeyContent);
|
|
457
|
+
if (!vault.registry.includes(oldName) || !(oldOpaque in vault.secrets)) {
|
|
458
|
+
throw new Error(`Secret '${oldName}' not found.`);
|
|
459
|
+
}
|
|
460
|
+
if (vault.registry.includes(newName)) {
|
|
461
|
+
throw new Error(`Secret '${newName}' already exists.`);
|
|
462
|
+
}
|
|
463
|
+
const newOpaque = computeOpaqueName(vault.hmacSeed, newName, sshKeyContent);
|
|
464
|
+
const value = vault.secrets[oldOpaque];
|
|
465
|
+
vault.secrets[newOpaque] = value;
|
|
466
|
+
delete vault.secrets[oldOpaque];
|
|
467
|
+
vault.registry = vault.registry.map((k) => k === oldName ? newName : k);
|
|
468
|
+
writeMainVault(vault, password);
|
|
469
|
+
writeSessionCache(vault.secrets, vault.registry);
|
|
470
|
+
return { oldOpaque, newOpaque };
|
|
471
|
+
}
|
|
449
472
|
function listKeys(password) {
|
|
450
473
|
const vault = readMainVault(password);
|
|
451
474
|
writeSessionCache(vault.secrets, vault.registry);
|
|
@@ -836,7 +859,7 @@ async function checkLicense() {
|
|
|
836
859
|
// package.json
|
|
837
860
|
var package_default = {
|
|
838
861
|
name: "@envsave/cli",
|
|
839
|
-
version: "1.0.
|
|
862
|
+
version: "1.0.45",
|
|
840
863
|
description: "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
|
|
841
864
|
type: "module",
|
|
842
865
|
main: "dist/index.js",
|
|
@@ -922,6 +945,7 @@ Usage:
|
|
|
922
945
|
envsave show <key> Show real value by key name (password required)
|
|
923
946
|
envsave reveal <keyword> [keyword2]... Show opaque key(s) for any name matching keyword (password required)
|
|
924
947
|
envsave delete <key> Remove a secret (password required)
|
|
948
|
+
envsave rename <old> <new> Rename a secret (creates new opaque key, deletes old) (password required)
|
|
925
949
|
envsave search <keyword> Search keys matching keyword (no password)
|
|
926
950
|
envsave scan [dir] Scan project for env variable usage
|
|
927
951
|
envsave passwd Change master password
|
|
@@ -1168,6 +1192,28 @@ Use 'envsave reveal <KEY>' to get the opaque key for each secret.`);
|
|
|
1168
1192
|
}
|
|
1169
1193
|
break;
|
|
1170
1194
|
}
|
|
1195
|
+
case "rename": {
|
|
1196
|
+
const oldName = args[1];
|
|
1197
|
+
const newName = args[2];
|
|
1198
|
+
if (!oldName || !newName) {
|
|
1199
|
+
console.error("Usage: envsave rename <old> <new>");
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
const password = await requirePassword();
|
|
1203
|
+
const { oldOpaque, newOpaque } = renameSecret(oldName, newName, password);
|
|
1204
|
+
console.log(`
|
|
1205
|
+
Renamed: ${oldName} → ${newName}`);
|
|
1206
|
+
console.log(`
|
|
1207
|
+
Replace in your code:`);
|
|
1208
|
+
console.log(` OLD (deleted): ${oldOpaque}`);
|
|
1209
|
+
console.log(` NEW (created): ${newOpaque}`);
|
|
1210
|
+
console.log(`
|
|
1211
|
+
Or replace the full call:`);
|
|
1212
|
+
console.log(` envsave.get("${oldOpaque}")`);
|
|
1213
|
+
console.log(` envsave.get("${newOpaque}")
|
|
1214
|
+
`);
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1171
1217
|
case "config": {
|
|
1172
1218
|
const maxGets = getFlag("--max-gets");
|
|
1173
1219
|
if (!maxGets) {
|