@envsave/cli 1.0.40 → 1.0.42
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 +2 -2
- package/dist/cli.js +244 -239
- package/dist/index.js +8 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ const openai = new OpenAI({ apiKey: get("es_7a3f2b1e9c8d4f6b2a1c3d5f") });
|
|
|
28
28
|
|
|
29
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.
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Pricing
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Local personal use is **free and unlimited** — store as many secrets as you want with no license. A license is only required to sync your vault to the cloud; activate one with `envsave activate <token>` — get yours at https://envsave.com.
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
package/dist/cli.js
CHANGED
|
@@ -91,167 +91,6 @@ function parseEnvFile(filePath) {
|
|
|
91
91
|
}
|
|
92
92
|
var init_env_parser = () => {};
|
|
93
93
|
|
|
94
|
-
// src/license.ts
|
|
95
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
96
|
-
import { join } from "node:path";
|
|
97
|
-
import { homedir } from "node:os";
|
|
98
|
-
function ensureDir() {
|
|
99
|
-
if (!existsSync(ENVSAVE_DIR)) {
|
|
100
|
-
mkdirSync(ENVSAVE_DIR, { recursive: true, mode: 448 });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function parseToken(token) {
|
|
104
|
-
if (!token.startsWith("es_"))
|
|
105
|
-
return null;
|
|
106
|
-
try {
|
|
107
|
-
const b64 = token.slice(3);
|
|
108
|
-
const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
|
|
109
|
-
const raw = Buffer.from(b64.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
110
|
-
const parts = raw.split("|");
|
|
111
|
-
if (parts.length !== 4)
|
|
112
|
-
return null;
|
|
113
|
-
return {
|
|
114
|
-
email: parts[0],
|
|
115
|
-
createdAt: new Date(Number(parts[1]) * 1000),
|
|
116
|
-
expiresAt: new Date(Number(parts[2]) * 1000)
|
|
117
|
-
};
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async function activateLicense(token, offline = false) {
|
|
123
|
-
const dates = parseToken(token);
|
|
124
|
-
const OFFLINE_ALLOWED_EMAILS = [
|
|
125
|
-
"support@envsave.com"
|
|
126
|
-
];
|
|
127
|
-
if (offline) {
|
|
128
|
-
if (!dates) {
|
|
129
|
-
console.error("Invalid token format. Must be a signed es_ token.");
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
if (!OFFLINE_ALLOWED_EMAILS.includes(dates.email)) {
|
|
133
|
-
console.error("Offline activation is not available for this account.");
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
if (dates.expiresAt < new Date) {
|
|
137
|
-
console.error("Token is expired.");
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
ensureDir();
|
|
141
|
-
writeFileSync(LICENSE_FILE, JSON.stringify({
|
|
142
|
-
token,
|
|
143
|
-
validatedAt: new Date().toISOString(),
|
|
144
|
-
expiresAt: dates.expiresAt.toISOString()
|
|
145
|
-
}, null, 2), { mode: 384 });
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
const response = await fetch(LICENSE_API_URL, {
|
|
150
|
-
method: "POST",
|
|
151
|
-
headers: { "Content-Type": "application/json" },
|
|
152
|
-
body: JSON.stringify({ token })
|
|
153
|
-
});
|
|
154
|
-
if (!response.ok) {
|
|
155
|
-
console.error("License validation failed: server returned", response.status);
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
const data = await response.json();
|
|
159
|
-
if (!data.valid) {
|
|
160
|
-
console.error("Invalid license token.");
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
ensureDir();
|
|
164
|
-
const licenseData = {
|
|
165
|
-
token,
|
|
166
|
-
validatedAt: new Date().toISOString(),
|
|
167
|
-
expiresAt: dates?.expiresAt.toISOString() ?? data.expiresAt ?? null
|
|
168
|
-
};
|
|
169
|
-
writeFileSync(LICENSE_FILE, JSON.stringify(licenseData, null, 2), { mode: 384 });
|
|
170
|
-
return true;
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.error("Failed to contact license server:", err.message);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
function getLicenseToken() {
|
|
177
|
-
if (!existsSync(LICENSE_FILE))
|
|
178
|
-
return null;
|
|
179
|
-
try {
|
|
180
|
-
const data = JSON.parse(readFileSync2(LICENSE_FILE, "utf8"));
|
|
181
|
-
return data.token ?? null;
|
|
182
|
-
} catch {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
async function tryAutoRenew(token) {
|
|
187
|
-
try {
|
|
188
|
-
const response = await fetch(RENEW_API_URL, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
headers: { "Content-Type": "application/json" },
|
|
191
|
-
body: JSON.stringify({ token })
|
|
192
|
-
});
|
|
193
|
-
if (!response.ok)
|
|
194
|
-
return false;
|
|
195
|
-
const result = await response.json();
|
|
196
|
-
if (!result.token || !result.renewed)
|
|
197
|
-
return false;
|
|
198
|
-
const newDates = parseToken(result.token);
|
|
199
|
-
ensureDir();
|
|
200
|
-
writeFileSync(LICENSE_FILE, JSON.stringify({
|
|
201
|
-
token: result.token,
|
|
202
|
-
validatedAt: new Date().toISOString(),
|
|
203
|
-
expiresAt: newDates?.expiresAt.toISOString() ?? result.expiresAt ?? null
|
|
204
|
-
}, null, 2), { mode: 384 });
|
|
205
|
-
console.error("License renewed automatically.");
|
|
206
|
-
return true;
|
|
207
|
-
} catch {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
function hasValidLicense() {
|
|
212
|
-
if (!existsSync(LICENSE_FILE))
|
|
213
|
-
return false;
|
|
214
|
-
try {
|
|
215
|
-
const data = JSON.parse(readFileSync2(LICENSE_FILE, "utf8"));
|
|
216
|
-
const dates = parseToken(data.token);
|
|
217
|
-
if (dates)
|
|
218
|
-
return dates.expiresAt > new Date;
|
|
219
|
-
if (data.expiresAt)
|
|
220
|
-
return new Date(data.expiresAt) > new Date;
|
|
221
|
-
return true;
|
|
222
|
-
} catch {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async function checkLicense() {
|
|
227
|
-
if (!existsSync(LICENSE_FILE))
|
|
228
|
-
return false;
|
|
229
|
-
try {
|
|
230
|
-
const data = JSON.parse(readFileSync2(LICENSE_FILE, "utf8"));
|
|
231
|
-
const dates = parseToken(data.token);
|
|
232
|
-
if (dates) {
|
|
233
|
-
if (dates.expiresAt > new Date)
|
|
234
|
-
return true;
|
|
235
|
-
return await tryAutoRenew(data.token);
|
|
236
|
-
}
|
|
237
|
-
if (data.expiresAt) {
|
|
238
|
-
if (new Date(data.expiresAt) > new Date)
|
|
239
|
-
return true;
|
|
240
|
-
return await tryAutoRenew(data.token);
|
|
241
|
-
}
|
|
242
|
-
return true;
|
|
243
|
-
} catch {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
var ENVSAVE_DIR, LICENSE_FILE, LICENSE_API_URL, RENEW_API_URL;
|
|
248
|
-
var init_license = __esm(() => {
|
|
249
|
-
ENVSAVE_DIR = join(homedir(), ".envsave");
|
|
250
|
-
LICENSE_FILE = join(ENVSAVE_DIR, "license.json");
|
|
251
|
-
LICENSE_API_URL = process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate";
|
|
252
|
-
RENEW_API_URL = (process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate").replace("/validate", "/renew");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
94
|
// src/vault.ts
|
|
256
95
|
var exports_vault = {};
|
|
257
96
|
__export(exports_vault, {
|
|
@@ -275,47 +114,47 @@ __export(exports_vault, {
|
|
|
275
114
|
configureRateLimit: () => configureRateLimit,
|
|
276
115
|
changePassword: () => changePassword
|
|
277
116
|
});
|
|
278
|
-
import { readFileSync as
|
|
279
|
-
import { join
|
|
280
|
-
import { homedir
|
|
117
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdirSync, chmodSync, appendFileSync } from "node:fs";
|
|
118
|
+
import { join } from "node:path";
|
|
119
|
+
import { homedir } from "node:os";
|
|
281
120
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
282
121
|
function getEnvsaveDir() {
|
|
283
|
-
return process.env.ENVSAVE_DIR ||
|
|
122
|
+
return process.env.ENVSAVE_DIR || join(homedir(), ".envsave");
|
|
284
123
|
}
|
|
285
124
|
function getVaultFile() {
|
|
286
|
-
return
|
|
125
|
+
return join(getEnvsaveDir(), "vault.enc");
|
|
287
126
|
}
|
|
288
127
|
function getCacheFile() {
|
|
289
|
-
return
|
|
128
|
+
return join(getEnvsaveDir(), "cache.enc");
|
|
290
129
|
}
|
|
291
130
|
function getSessionFile() {
|
|
292
|
-
return
|
|
131
|
+
return join(getEnvsaveDir(), ".session");
|
|
293
132
|
}
|
|
294
133
|
function getRateFile() {
|
|
295
|
-
return
|
|
134
|
+
return join(getEnvsaveDir(), ".rate");
|
|
296
135
|
}
|
|
297
136
|
function getAccessLog() {
|
|
298
|
-
return
|
|
137
|
+
return join(getEnvsaveDir(), "access.log");
|
|
299
138
|
}
|
|
300
139
|
function getConfigFile() {
|
|
301
|
-
return
|
|
140
|
+
return join(getEnvsaveDir(), "config.json");
|
|
302
141
|
}
|
|
303
142
|
function guardRealVault() {
|
|
304
143
|
if (false) {}
|
|
305
144
|
}
|
|
306
145
|
function readConfig() {
|
|
307
|
-
if (!
|
|
146
|
+
if (!existsSync(getConfigFile())) {
|
|
308
147
|
return { maxGetsPerMinute: DEFAULT_RATE_LIMIT_MAX };
|
|
309
148
|
}
|
|
310
149
|
try {
|
|
311
|
-
return JSON.parse(
|
|
150
|
+
return JSON.parse(readFileSync2(getConfigFile(), "utf8"));
|
|
312
151
|
} catch {
|
|
313
152
|
return { maxGetsPerMinute: DEFAULT_RATE_LIMIT_MAX };
|
|
314
153
|
}
|
|
315
154
|
}
|
|
316
155
|
function writeConfig(config) {
|
|
317
|
-
|
|
318
|
-
|
|
156
|
+
ensureDir();
|
|
157
|
+
writeFileSync(getConfigFile(), JSON.stringify(config, null, 2), { mode: 384 });
|
|
319
158
|
}
|
|
320
159
|
function configureRateLimit(password, options) {
|
|
321
160
|
readMainVault(password);
|
|
@@ -372,28 +211,28 @@ function computeOpaqueNameV2(hmacSeed, cleanName, sshKeyContent) {
|
|
|
372
211
|
const hash = hmacSha256(effectiveSeed, cleanName);
|
|
373
212
|
return "es_" + hash.slice(0, 24);
|
|
374
213
|
}
|
|
375
|
-
function
|
|
214
|
+
function ensureDir() {
|
|
376
215
|
guardRealVault();
|
|
377
|
-
if (!
|
|
378
|
-
|
|
216
|
+
if (!existsSync(getEnvsaveDir())) {
|
|
217
|
+
mkdirSync(getEnvsaveDir(), { recursive: true, mode: 448 });
|
|
379
218
|
}
|
|
380
219
|
}
|
|
381
220
|
function vaultExists() {
|
|
382
|
-
return
|
|
221
|
+
return existsSync(getVaultFile());
|
|
383
222
|
}
|
|
384
223
|
function readMainVault(password) {
|
|
385
224
|
guardRealVault();
|
|
386
|
-
if (!
|
|
225
|
+
if (!existsSync(getVaultFile())) {
|
|
387
226
|
throw new Error("Vault not found. Run 'envsave init' first.");
|
|
388
227
|
}
|
|
389
|
-
const payload =
|
|
228
|
+
const payload = readFileSync2(getVaultFile());
|
|
390
229
|
const json = decrypt(payload, password);
|
|
391
230
|
const data = JSON.parse(json);
|
|
392
231
|
if (!data.version || data.version < 2) {
|
|
393
232
|
throw new Error("Vault format outdated. Delete ~/.envsave/ and re-initialize.");
|
|
394
233
|
}
|
|
395
234
|
if (data.version === 2 && data.registry?.length > 0) {
|
|
396
|
-
const sshKeyContent = data.sshKeyPath &&
|
|
235
|
+
const sshKeyContent = data.sshKeyPath && existsSync(data.sshKeyPath) ? readFileSync2(data.sshKeyPath, "utf8") : undefined;
|
|
397
236
|
const newSecrets = {};
|
|
398
237
|
for (const cleanName of data.registry) {
|
|
399
238
|
const oldKey = computeOpaqueNameV2(data.hmacSeed, cleanName, sshKeyContent);
|
|
@@ -408,7 +247,7 @@ function readMainVault(password) {
|
|
|
408
247
|
console.error("Vault migrated to v3 (stronger opaque keys).");
|
|
409
248
|
}
|
|
410
249
|
if (data.version === 3 && data.registry?.length > 0) {
|
|
411
|
-
const sshKeyContent = data.sshKeyPath &&
|
|
250
|
+
const sshKeyContent = data.sshKeyPath && existsSync(data.sshKeyPath) ? readFileSync2(data.sshKeyPath, "utf8") : undefined;
|
|
412
251
|
const newSecrets = {};
|
|
413
252
|
for (const cleanName of data.registry) {
|
|
414
253
|
const oldKey = computeOpaqueNameV3(data.hmacSeed, cleanName, sshKeyContent);
|
|
@@ -423,7 +262,7 @@ function readMainVault(password) {
|
|
|
423
262
|
console.error("Vault migrated to v4 (shell-safe opaque keys).");
|
|
424
263
|
}
|
|
425
264
|
if (data.version === 4 && data.registry?.length > 0) {
|
|
426
|
-
const sshKeyContent = data.sshKeyPath &&
|
|
265
|
+
const sshKeyContent = data.sshKeyPath && existsSync(data.sshKeyPath) ? readFileSync2(data.sshKeyPath, "utf8") : undefined;
|
|
427
266
|
const newSecrets = {};
|
|
428
267
|
for (const cleanName of data.registry) {
|
|
429
268
|
const oldKey = computeOpaqueNameV4(data.hmacSeed, cleanName, sshKeyContent);
|
|
@@ -440,27 +279,27 @@ function readMainVault(password) {
|
|
|
440
279
|
return data;
|
|
441
280
|
}
|
|
442
281
|
function writeMainVault(data, password) {
|
|
443
|
-
|
|
282
|
+
ensureDir();
|
|
444
283
|
const json = JSON.stringify(data);
|
|
445
284
|
const encrypted = encrypt(json, password);
|
|
446
|
-
|
|
285
|
+
writeFileSync(getVaultFile(), encrypted, { mode: 384 });
|
|
447
286
|
}
|
|
448
287
|
function writeSessionCache(secrets, registry) {
|
|
449
|
-
|
|
288
|
+
ensureDir();
|
|
450
289
|
const sessionKey = randomBytes2(32);
|
|
451
290
|
const cache = { secrets, registry };
|
|
452
291
|
const json = JSON.stringify(cache);
|
|
453
292
|
const encrypted = encryptWithKey(json, sessionKey);
|
|
454
|
-
|
|
455
|
-
|
|
293
|
+
writeFileSync(getCacheFile(), encrypted, { mode: 384 });
|
|
294
|
+
writeFileSync(getSessionFile(), sessionKey.toString("hex"), { mode: 384 });
|
|
456
295
|
}
|
|
457
296
|
function readSessionCache() {
|
|
458
|
-
if (!
|
|
297
|
+
if (!existsSync(getSessionFile()) || !existsSync(getCacheFile()))
|
|
459
298
|
return null;
|
|
460
299
|
try {
|
|
461
|
-
const sessionKeyHex =
|
|
300
|
+
const sessionKeyHex = readFileSync2(getSessionFile(), "utf8").trim();
|
|
462
301
|
const sessionKey = Buffer.from(sessionKeyHex, "hex");
|
|
463
|
-
const payload =
|
|
302
|
+
const payload = readFileSync2(getCacheFile());
|
|
464
303
|
const json = decryptWithKey(payload, sessionKey);
|
|
465
304
|
return JSON.parse(json);
|
|
466
305
|
} catch {
|
|
@@ -471,9 +310,9 @@ function checkRateLimit() {
|
|
|
471
310
|
const config = readConfig();
|
|
472
311
|
const now = Date.now();
|
|
473
312
|
let timestamps = [];
|
|
474
|
-
if (
|
|
313
|
+
if (existsSync(getRateFile())) {
|
|
475
314
|
try {
|
|
476
|
-
timestamps = JSON.parse(
|
|
315
|
+
timestamps = JSON.parse(readFileSync2(getRateFile(), "utf8"));
|
|
477
316
|
} catch {
|
|
478
317
|
timestamps = [];
|
|
479
318
|
}
|
|
@@ -489,10 +328,10 @@ function checkRateLimit() {
|
|
|
489
328
|
console.error(`⚠ Rate limit warning: ${remaining} get(s) remaining this minute.`);
|
|
490
329
|
}
|
|
491
330
|
timestamps.push(now);
|
|
492
|
-
|
|
331
|
+
writeFileSync(getRateFile(), JSON.stringify(timestamps), { mode: 384 });
|
|
493
332
|
}
|
|
494
333
|
function logAccess(opaqueKey, source) {
|
|
495
|
-
|
|
334
|
+
ensureDir();
|
|
496
335
|
const entry = `${new Date().toISOString()} ${source} ${opaqueKey.slice(0, 10)}...
|
|
497
336
|
`;
|
|
498
337
|
appendFileSync(getAccessLog(), entry, { mode: 384 });
|
|
@@ -500,20 +339,20 @@ function logAccess(opaqueKey, source) {
|
|
|
500
339
|
function loadSshKey(vault) {
|
|
501
340
|
if (!vault.sshKeyPath)
|
|
502
341
|
return;
|
|
503
|
-
if (!
|
|
342
|
+
if (!existsSync(vault.sshKeyPath)) {
|
|
504
343
|
throw new Error(`SSH key not found: ${vault.sshKeyPath}. This vault requires the SSH key to operate.`);
|
|
505
344
|
}
|
|
506
|
-
return
|
|
345
|
+
return readFileSync2(vault.sshKeyPath, "utf8");
|
|
507
346
|
}
|
|
508
347
|
function initVault(password, options) {
|
|
509
348
|
if (vaultExists()) {
|
|
510
349
|
throw new Error("Vault already exists. Delete ~/.envsave/vault.enc to reinitialize.");
|
|
511
350
|
}
|
|
512
|
-
if (options?.sshKeyPath && !
|
|
351
|
+
if (options?.sshKeyPath && !existsSync(options.sshKeyPath)) {
|
|
513
352
|
throw new Error(`SSH key not found: ${options.sshKeyPath}`);
|
|
514
353
|
}
|
|
515
354
|
const hmacSeed = randomBytes2(32).toString("hex");
|
|
516
|
-
const sshKeyContent = options?.sshKeyPath ?
|
|
355
|
+
const sshKeyContent = options?.sshKeyPath ? readFileSync2(options.sshKeyPath, "utf8") : undefined;
|
|
517
356
|
const vault = {
|
|
518
357
|
version: 5,
|
|
519
358
|
hmacSeed,
|
|
@@ -524,11 +363,6 @@ function initVault(password, options) {
|
|
|
524
363
|
let count = 0;
|
|
525
364
|
if (options?.envFile) {
|
|
526
365
|
const envSecrets = parseEnvFile(options.envFile);
|
|
527
|
-
if (!hasValidLicense() && Object.keys(envSecrets).length > FREE_TIER_MAX_SECRETS) {
|
|
528
|
-
throw new Error(`Free tier limit: max ${FREE_TIER_MAX_SECRETS} secrets. .env file has ${Object.keys(envSecrets).length} keys.
|
|
529
|
-
Run 'envsave activate <token>' to unlock unlimited secrets.
|
|
530
|
-
Get your license at https://api.envsave.com`);
|
|
531
|
-
}
|
|
532
366
|
for (const [key, value] of Object.entries(envSecrets)) {
|
|
533
367
|
const opaque = computeOpaqueName(hmacSeed, key, sshKeyContent);
|
|
534
368
|
vault.secrets[opaque] = value;
|
|
@@ -546,12 +380,6 @@ function setSecret(key, value, password) {
|
|
|
546
380
|
const sshKeyContent = loadSshKey(vault);
|
|
547
381
|
const opaque = computeOpaqueName(vault.hmacSeed, key, sshKeyContent);
|
|
548
382
|
const updated = opaque in vault.secrets;
|
|
549
|
-
if (!updated && !hasValidLicense()) {
|
|
550
|
-
if (vault.registry.length >= FREE_TIER_MAX_SECRETS) {
|
|
551
|
-
throw new Error(`Free tier limit: max ${FREE_TIER_MAX_SECRETS} secrets. Run 'envsave activate <token>' to unlock unlimited secrets.
|
|
552
|
-
Get your license at https://api.envsave.com`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
383
|
vault.secrets[opaque] = value;
|
|
556
384
|
if (!vault.registry.includes(key)) {
|
|
557
385
|
vault.registry.push(key);
|
|
@@ -627,17 +455,6 @@ function importEnv(filePath, password) {
|
|
|
627
455
|
const envSecrets = parseEnvFile(filePath);
|
|
628
456
|
const vault = readMainVault(password);
|
|
629
457
|
const sshKeyContent = loadSshKey(vault);
|
|
630
|
-
if (!hasValidLicense()) {
|
|
631
|
-
const newKeys = Object.keys(envSecrets).filter((key) => {
|
|
632
|
-
const opaque = computeOpaqueName(vault.hmacSeed, key, sshKeyContent);
|
|
633
|
-
return !(opaque in vault.secrets);
|
|
634
|
-
});
|
|
635
|
-
if (vault.registry.length + newKeys.length > FREE_TIER_MAX_SECRETS) {
|
|
636
|
-
throw new Error(`Free tier limit: max ${FREE_TIER_MAX_SECRETS} secrets. Import would add ${newKeys.length} new keys (${vault.registry.length} existing).
|
|
637
|
-
Run 'envsave activate <token>' to unlock unlimited secrets.
|
|
638
|
-
Get your license at https://api.envsave.com`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
458
|
let count = 0;
|
|
642
459
|
for (const [key, value] of Object.entries(envSecrets)) {
|
|
643
460
|
const opaque = computeOpaqueName(vault.hmacSeed, key, sshKeyContent);
|
|
@@ -673,12 +490,13 @@ function searchKeys(pattern) {
|
|
|
673
490
|
const lower = pattern.toLowerCase();
|
|
674
491
|
return cache.registry.filter((k) => k.toLowerCase().includes(lower));
|
|
675
492
|
}
|
|
676
|
-
function getDeploySecrets(
|
|
493
|
+
function getDeploySecrets(keys, password) {
|
|
677
494
|
const vault = readMainVault(password);
|
|
678
495
|
const sshKeyContent = loadSshKey(vault);
|
|
679
496
|
writeSessionCache(vault.secrets, vault.registry);
|
|
680
497
|
const results = [];
|
|
681
|
-
for (const
|
|
498
|
+
for (const key of keys) {
|
|
499
|
+
const opaque = key.startsWith("es_") ? key : computeOpaqueName(vault.hmacSeed, key, sshKeyContent);
|
|
682
500
|
if (opaque in vault.secrets) {
|
|
683
501
|
const cleanName = vault.registry?.find((r) => computeOpaqueName(vault.hmacSeed, r, sshKeyContent) === opaque) || opaque;
|
|
684
502
|
results.push({ name: cleanName, opaque, value: vault.secrets[opaque] });
|
|
@@ -699,11 +517,10 @@ function hasSecretWithSession(opaqueKey) {
|
|
|
699
517
|
return false;
|
|
700
518
|
}
|
|
701
519
|
}
|
|
702
|
-
var
|
|
520
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS, DEFAULT_RATE_LIMIT_MAX = 30, BASE_CHARS_V3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$&*=-!", BASE_CHARS_V4 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", BASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
|
|
703
521
|
var init_vault = __esm(() => {
|
|
704
522
|
init_crypto();
|
|
705
523
|
init_env_parser();
|
|
706
|
-
init_license();
|
|
707
524
|
DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
708
525
|
});
|
|
709
526
|
|
|
@@ -867,13 +684,206 @@ var init_scanner = __esm(() => {
|
|
|
867
684
|
|
|
868
685
|
// src/cli.ts
|
|
869
686
|
init_vault();
|
|
870
|
-
init_license();
|
|
871
687
|
import { createInterface } from "node:readline";
|
|
872
688
|
import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
873
689
|
import { spawnSync } from "node:child_process";
|
|
874
690
|
import { createHash } from "node:crypto";
|
|
875
691
|
import { join as join4 } from "node:path";
|
|
876
692
|
import { homedir as homedir3 } from "node:os";
|
|
693
|
+
|
|
694
|
+
// src/license.ts
|
|
695
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
696
|
+
import { join as join2 } from "node:path";
|
|
697
|
+
import { homedir as homedir2 } from "node:os";
|
|
698
|
+
var ENVSAVE_DIR = join2(homedir2(), ".envsave");
|
|
699
|
+
var LICENSE_FILE = join2(ENVSAVE_DIR, "license.json");
|
|
700
|
+
var LICENSE_API_URL = process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate";
|
|
701
|
+
function ensureDir2() {
|
|
702
|
+
if (!existsSync2(ENVSAVE_DIR)) {
|
|
703
|
+
mkdirSync2(ENVSAVE_DIR, { recursive: true, mode: 448 });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function parseToken(token) {
|
|
707
|
+
if (!token.startsWith("es_"))
|
|
708
|
+
return null;
|
|
709
|
+
try {
|
|
710
|
+
const b64 = token.slice(3);
|
|
711
|
+
const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
|
|
712
|
+
const raw = Buffer.from(b64.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
713
|
+
const parts = raw.split("|");
|
|
714
|
+
if (parts.length !== 4)
|
|
715
|
+
return null;
|
|
716
|
+
return {
|
|
717
|
+
email: parts[0],
|
|
718
|
+
createdAt: new Date(Number(parts[1]) * 1000),
|
|
719
|
+
expiresAt: new Date(Number(parts[2]) * 1000)
|
|
720
|
+
};
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function activateLicense(token, offline = false) {
|
|
726
|
+
const dates = parseToken(token);
|
|
727
|
+
const OFFLINE_ALLOWED_EMAILS = [
|
|
728
|
+
"support@envsave.com"
|
|
729
|
+
];
|
|
730
|
+
if (offline) {
|
|
731
|
+
if (!dates) {
|
|
732
|
+
console.error("Invalid token format. Must be a signed es_ token.");
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
if (!OFFLINE_ALLOWED_EMAILS.includes(dates.email)) {
|
|
736
|
+
console.error("Offline activation is not available for this account.");
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (dates.expiresAt < new Date) {
|
|
740
|
+
console.error("Token is expired.");
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
ensureDir2();
|
|
744
|
+
writeFileSync2(LICENSE_FILE, JSON.stringify({
|
|
745
|
+
token,
|
|
746
|
+
validatedAt: new Date().toISOString(),
|
|
747
|
+
expiresAt: dates.expiresAt.toISOString()
|
|
748
|
+
}, null, 2), { mode: 384 });
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
const response = await fetch(LICENSE_API_URL, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { "Content-Type": "application/json" },
|
|
755
|
+
body: JSON.stringify({ token })
|
|
756
|
+
});
|
|
757
|
+
if (!response.ok) {
|
|
758
|
+
console.error("License validation failed: server returned", response.status);
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const data = await response.json();
|
|
762
|
+
if (!data.valid) {
|
|
763
|
+
console.error("Invalid license token.");
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
ensureDir2();
|
|
767
|
+
const licenseData = {
|
|
768
|
+
token,
|
|
769
|
+
validatedAt: new Date().toISOString(),
|
|
770
|
+
expiresAt: dates?.expiresAt.toISOString() ?? data.expiresAt ?? null
|
|
771
|
+
};
|
|
772
|
+
writeFileSync2(LICENSE_FILE, JSON.stringify(licenseData, null, 2), { mode: 384 });
|
|
773
|
+
return true;
|
|
774
|
+
} catch (err) {
|
|
775
|
+
console.error("Failed to contact license server:", err.message);
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function getLicenseToken() {
|
|
780
|
+
if (!existsSync2(LICENSE_FILE))
|
|
781
|
+
return null;
|
|
782
|
+
try {
|
|
783
|
+
const data = JSON.parse(readFileSync3(LICENSE_FILE, "utf8"));
|
|
784
|
+
return data.token ?? null;
|
|
785
|
+
} catch {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
var RENEW_API_URL = (process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate").replace("/validate", "/renew");
|
|
790
|
+
async function tryAutoRenew(token) {
|
|
791
|
+
try {
|
|
792
|
+
const response = await fetch(RENEW_API_URL, {
|
|
793
|
+
method: "POST",
|
|
794
|
+
headers: { "Content-Type": "application/json" },
|
|
795
|
+
body: JSON.stringify({ token })
|
|
796
|
+
});
|
|
797
|
+
if (!response.ok)
|
|
798
|
+
return false;
|
|
799
|
+
const result = await response.json();
|
|
800
|
+
if (!result.token || !result.renewed)
|
|
801
|
+
return false;
|
|
802
|
+
const newDates = parseToken(result.token);
|
|
803
|
+
ensureDir2();
|
|
804
|
+
writeFileSync2(LICENSE_FILE, JSON.stringify({
|
|
805
|
+
token: result.token,
|
|
806
|
+
validatedAt: new Date().toISOString(),
|
|
807
|
+
expiresAt: newDates?.expiresAt.toISOString() ?? result.expiresAt ?? null
|
|
808
|
+
}, null, 2), { mode: 384 });
|
|
809
|
+
console.error("License renewed automatically.");
|
|
810
|
+
return true;
|
|
811
|
+
} catch {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function checkLicense() {
|
|
816
|
+
if (!existsSync2(LICENSE_FILE))
|
|
817
|
+
return false;
|
|
818
|
+
try {
|
|
819
|
+
const data = JSON.parse(readFileSync3(LICENSE_FILE, "utf8"));
|
|
820
|
+
const dates = parseToken(data.token);
|
|
821
|
+
if (dates) {
|
|
822
|
+
if (dates.expiresAt > new Date)
|
|
823
|
+
return true;
|
|
824
|
+
return await tryAutoRenew(data.token);
|
|
825
|
+
}
|
|
826
|
+
if (data.expiresAt) {
|
|
827
|
+
if (new Date(data.expiresAt) > new Date)
|
|
828
|
+
return true;
|
|
829
|
+
return await tryAutoRenew(data.token);
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
} catch {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// package.json
|
|
837
|
+
var package_default = {
|
|
838
|
+
name: "@envsave/cli",
|
|
839
|
+
version: "1.0.42",
|
|
840
|
+
description: "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
|
|
841
|
+
type: "module",
|
|
842
|
+
main: "dist/index.js",
|
|
843
|
+
types: "dist/index.d.ts",
|
|
844
|
+
bin: {
|
|
845
|
+
envsave: "dist/cli.js"
|
|
846
|
+
},
|
|
847
|
+
files: [
|
|
848
|
+
"dist",
|
|
849
|
+
"python"
|
|
850
|
+
],
|
|
851
|
+
scripts: {
|
|
852
|
+
prebuild: `bun -e "const p=require('./package.json');const v=p.version.split('.');v[2]=+v[2]+1;p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')"`,
|
|
853
|
+
build: "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js",
|
|
854
|
+
dev: "bun run src/cli.ts",
|
|
855
|
+
webapp: "bun run webapp/server.ts",
|
|
856
|
+
"lint:claude": "claude -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"
|
|
857
|
+
},
|
|
858
|
+
devDependencies: {
|
|
859
|
+
"@types/bun": "latest",
|
|
860
|
+
"@types/node": "^25.5.0"
|
|
861
|
+
},
|
|
862
|
+
peerDependencies: {
|
|
863
|
+
typescript: "^5"
|
|
864
|
+
},
|
|
865
|
+
keywords: [
|
|
866
|
+
"vault",
|
|
867
|
+
"secrets",
|
|
868
|
+
"encryption",
|
|
869
|
+
"llm",
|
|
870
|
+
"security",
|
|
871
|
+
"env"
|
|
872
|
+
],
|
|
873
|
+
license: "MIT",
|
|
874
|
+
repository: {
|
|
875
|
+
type: "git",
|
|
876
|
+
url: "git+https://github.com/jcsancho/envsave.git"
|
|
877
|
+
},
|
|
878
|
+
homepage: "https://api.envsave.com",
|
|
879
|
+
author: "Jose C Sancho",
|
|
880
|
+
dependencies: {
|
|
881
|
+
"@clerk/backend": "^3.2.1",
|
|
882
|
+
"@supabase/supabase-js": "^2.99.2"
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// src/cli.ts
|
|
877
887
|
var args = process.argv.slice(2);
|
|
878
888
|
var command = args[0];
|
|
879
889
|
function checkEnvFiles() {
|
|
@@ -923,7 +933,7 @@ Usage:
|
|
|
923
933
|
envsave backup Upload encrypted vault to server
|
|
924
934
|
envsave restore Download vault backup from server
|
|
925
935
|
envsave sync Sync vault with server (pull if cloud newer)
|
|
926
|
-
envsave vercel <
|
|
936
|
+
envsave vercel <KEY|es_key>... Push secrets to Vercel (accepts names or opaque keys)
|
|
927
937
|
envsave help Show this help
|
|
928
938
|
|
|
929
939
|
Security model:
|
|
@@ -998,6 +1008,10 @@ async function main() {
|
|
|
998
1008
|
checkEnvFiles();
|
|
999
1009
|
return;
|
|
1000
1010
|
}
|
|
1011
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
1012
|
+
console.log(package_default.version);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1001
1015
|
switch (command) {
|
|
1002
1016
|
case "activate": {
|
|
1003
1017
|
const token = args[1];
|
|
@@ -1283,9 +1297,6 @@ ${results.length} reference(s), ${uniqueVars.size} unique variable(s) found.`);
|
|
|
1283
1297
|
}
|
|
1284
1298
|
console.log(`
|
|
1285
1299
|
${keys.length} secret(s). Use 'envsave reveal <KEY>' to get the opaque key.`);
|
|
1286
|
-
if (!hasValidLicense()) {
|
|
1287
|
-
console.log(`\x1B[33mFree tier: ${keys.length}/5 secrets used. Activate a license for unlimited.\x1B[0m`);
|
|
1288
|
-
}
|
|
1289
1300
|
}
|
|
1290
1301
|
break;
|
|
1291
1302
|
}
|
|
@@ -1540,19 +1551,13 @@ Use 'envsave list' to verify your secrets.`);
|
|
|
1540
1551
|
keyArgs.splice(envIdx, 1);
|
|
1541
1552
|
}
|
|
1542
1553
|
if (keyArgs.length === 0) {
|
|
1543
|
-
console.error("Usage: envsave vercel <es_key> [es_key2]... [--env production,preview]");
|
|
1544
|
-
process.exit(1);
|
|
1545
|
-
}
|
|
1546
|
-
const invalidKeys = keyArgs.filter((k) => !k.startsWith("es_"));
|
|
1547
|
-
if (invalidKeys.length > 0) {
|
|
1548
|
-
console.error(`Only opaque keys (es_...) are accepted. Use 'envsave reveal <NAME>' to get the opaque key.`);
|
|
1549
|
-
console.error(`Invalid: ${invalidKeys.join(", ")}`);
|
|
1554
|
+
console.error("Usage: envsave vercel <KEY|es_key> [KEY|es_key2]... [--env production,preview]");
|
|
1550
1555
|
process.exit(1);
|
|
1551
1556
|
}
|
|
1552
1557
|
const password = await requirePassword();
|
|
1553
1558
|
const keysToPush = keyArgs;
|
|
1554
1559
|
const secrets = getDeploySecrets(keysToPush, password);
|
|
1555
|
-
const notFound = keysToPush.filter((k) => !secrets.find((s) => s.opaque === k));
|
|
1560
|
+
const notFound = keysToPush.filter((k) => !secrets.find((s) => s.opaque === k || s.name === k));
|
|
1556
1561
|
if (notFound.length > 0) {
|
|
1557
1562
|
console.error(`Secrets not found: ${notFound.join(", ")}`);
|
|
1558
1563
|
if (secrets.length === 0)
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/vault.ts
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, appendFileSync } from "node:fs";
|
|
3
|
-
import { join
|
|
4
|
-
import { homedir
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
// src/crypto.ts
|
|
7
7
|
import { createCipheriv, createDecipheriv, createHmac, randomBytes, pbkdf2Sync } from "node:crypto";
|
|
@@ -16,34 +16,26 @@ function decryptWithKey(payload, key) {
|
|
|
16
16
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// src/license.ts
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
var ENVSAVE_DIR = join(homedir(), ".envsave");
|
|
23
|
-
var LICENSE_FILE = join(ENVSAVE_DIR, "license.json");
|
|
24
|
-
var LICENSE_API_URL = process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate";
|
|
25
|
-
var RENEW_API_URL = (process.env.ENVSAVE_LICENSE_API || "https://api.envsave.com/api/license/validate").replace("/validate", "/renew");
|
|
26
|
-
|
|
27
19
|
// src/vault.ts
|
|
28
20
|
function getEnvsaveDir() {
|
|
29
|
-
return process.env.ENVSAVE_DIR ||
|
|
21
|
+
return process.env.ENVSAVE_DIR || join(homedir(), ".envsave");
|
|
30
22
|
}
|
|
31
23
|
function getCacheFile() {
|
|
32
|
-
return
|
|
24
|
+
return join(getEnvsaveDir(), "cache.enc");
|
|
33
25
|
}
|
|
34
26
|
function getSessionFile() {
|
|
35
|
-
return
|
|
27
|
+
return join(getEnvsaveDir(), ".session");
|
|
36
28
|
}
|
|
37
29
|
function getRateFile() {
|
|
38
|
-
return
|
|
30
|
+
return join(getEnvsaveDir(), ".rate");
|
|
39
31
|
}
|
|
40
32
|
function getAccessLog() {
|
|
41
|
-
return
|
|
33
|
+
return join(getEnvsaveDir(), "access.log");
|
|
42
34
|
}
|
|
43
35
|
var DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
44
36
|
var DEFAULT_RATE_LIMIT_MAX = 30;
|
|
45
37
|
function getConfigFile() {
|
|
46
|
-
return
|
|
38
|
+
return join(getEnvsaveDir(), "config.json");
|
|
47
39
|
}
|
|
48
40
|
function guardRealVault() {
|
|
49
41
|
if (false) {}
|