@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 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
- ## Free tier
31
+ ## Pricing
32
32
 
33
- Up to **5 secrets** with no license. Beyond that you'll need to activate a license (`envsave activate <token>`) — get yours at https://envsave.com.
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 readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, appendFileSync } from "node:fs";
279
- import { join as join2 } from "node:path";
280
- import { homedir as homedir2 } from "node:os";
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 || join2(homedir2(), ".envsave");
122
+ return process.env.ENVSAVE_DIR || join(homedir(), ".envsave");
284
123
  }
285
124
  function getVaultFile() {
286
- return join2(getEnvsaveDir(), "vault.enc");
125
+ return join(getEnvsaveDir(), "vault.enc");
287
126
  }
288
127
  function getCacheFile() {
289
- return join2(getEnvsaveDir(), "cache.enc");
128
+ return join(getEnvsaveDir(), "cache.enc");
290
129
  }
291
130
  function getSessionFile() {
292
- return join2(getEnvsaveDir(), ".session");
131
+ return join(getEnvsaveDir(), ".session");
293
132
  }
294
133
  function getRateFile() {
295
- return join2(getEnvsaveDir(), ".rate");
134
+ return join(getEnvsaveDir(), ".rate");
296
135
  }
297
136
  function getAccessLog() {
298
- return join2(getEnvsaveDir(), "access.log");
137
+ return join(getEnvsaveDir(), "access.log");
299
138
  }
300
139
  function getConfigFile() {
301
- return join2(getEnvsaveDir(), "config.json");
140
+ return join(getEnvsaveDir(), "config.json");
302
141
  }
303
142
  function guardRealVault() {
304
143
  if (false) {}
305
144
  }
306
145
  function readConfig() {
307
- if (!existsSync2(getConfigFile())) {
146
+ if (!existsSync(getConfigFile())) {
308
147
  return { maxGetsPerMinute: DEFAULT_RATE_LIMIT_MAX };
309
148
  }
310
149
  try {
311
- return JSON.parse(readFileSync3(getConfigFile(), "utf8"));
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
- ensureDir2();
318
- writeFileSync2(getConfigFile(), JSON.stringify(config, null, 2), { mode: 384 });
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 ensureDir2() {
214
+ function ensureDir() {
376
215
  guardRealVault();
377
- if (!existsSync2(getEnvsaveDir())) {
378
- mkdirSync2(getEnvsaveDir(), { recursive: true, mode: 448 });
216
+ if (!existsSync(getEnvsaveDir())) {
217
+ mkdirSync(getEnvsaveDir(), { recursive: true, mode: 448 });
379
218
  }
380
219
  }
381
220
  function vaultExists() {
382
- return existsSync2(getVaultFile());
221
+ return existsSync(getVaultFile());
383
222
  }
384
223
  function readMainVault(password) {
385
224
  guardRealVault();
386
- if (!existsSync2(getVaultFile())) {
225
+ if (!existsSync(getVaultFile())) {
387
226
  throw new Error("Vault not found. Run 'envsave init' first.");
388
227
  }
389
- const payload = readFileSync3(getVaultFile());
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 && existsSync2(data.sshKeyPath) ? readFileSync3(data.sshKeyPath, "utf8") : undefined;
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 && existsSync2(data.sshKeyPath) ? readFileSync3(data.sshKeyPath, "utf8") : undefined;
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 && existsSync2(data.sshKeyPath) ? readFileSync3(data.sshKeyPath, "utf8") : undefined;
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
- ensureDir2();
282
+ ensureDir();
444
283
  const json = JSON.stringify(data);
445
284
  const encrypted = encrypt(json, password);
446
- writeFileSync2(getVaultFile(), encrypted, { mode: 384 });
285
+ writeFileSync(getVaultFile(), encrypted, { mode: 384 });
447
286
  }
448
287
  function writeSessionCache(secrets, registry) {
449
- ensureDir2();
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
- writeFileSync2(getCacheFile(), encrypted, { mode: 384 });
455
- writeFileSync2(getSessionFile(), sessionKey.toString("hex"), { mode: 384 });
293
+ writeFileSync(getCacheFile(), encrypted, { mode: 384 });
294
+ writeFileSync(getSessionFile(), sessionKey.toString("hex"), { mode: 384 });
456
295
  }
457
296
  function readSessionCache() {
458
- if (!existsSync2(getSessionFile()) || !existsSync2(getCacheFile()))
297
+ if (!existsSync(getSessionFile()) || !existsSync(getCacheFile()))
459
298
  return null;
460
299
  try {
461
- const sessionKeyHex = readFileSync3(getSessionFile(), "utf8").trim();
300
+ const sessionKeyHex = readFileSync2(getSessionFile(), "utf8").trim();
462
301
  const sessionKey = Buffer.from(sessionKeyHex, "hex");
463
- const payload = readFileSync3(getCacheFile());
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 (existsSync2(getRateFile())) {
313
+ if (existsSync(getRateFile())) {
475
314
  try {
476
- timestamps = JSON.parse(readFileSync3(getRateFile(), "utf8"));
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
- writeFileSync2(getRateFile(), JSON.stringify(timestamps), { mode: 384 });
331
+ writeFileSync(getRateFile(), JSON.stringify(timestamps), { mode: 384 });
493
332
  }
494
333
  function logAccess(opaqueKey, source) {
495
- ensureDir2();
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 (!existsSync2(vault.sshKeyPath)) {
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 readFileSync3(vault.sshKeyPath, "utf8");
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 && !existsSync2(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 ? readFileSync3(options.sshKeyPath, "utf8") : undefined;
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(opaqueKeys, password) {
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 opaque of opaqueKeys) {
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 FREE_TIER_MAX_SECRETS = 5, DEFAULT_RATE_LIMIT_WINDOW_MS, DEFAULT_RATE_LIMIT_MAX = 30, BASE_CHARS_V3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$&*=-!", BASE_CHARS_V4 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", BASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
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 <key> [key2]... Push secrets to Vercel as opaque env vars
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 as join2 } from "node:path";
4
- import { homedir as homedir2 } from "node:os";
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 || join2(homedir2(), ".envsave");
21
+ return process.env.ENVSAVE_DIR || join(homedir(), ".envsave");
30
22
  }
31
23
  function getCacheFile() {
32
- return join2(getEnvsaveDir(), "cache.enc");
24
+ return join(getEnvsaveDir(), "cache.enc");
33
25
  }
34
26
  function getSessionFile() {
35
- return join2(getEnvsaveDir(), ".session");
27
+ return join(getEnvsaveDir(), ".session");
36
28
  }
37
29
  function getRateFile() {
38
- return join2(getEnvsaveDir(), ".rate");
30
+ return join(getEnvsaveDir(), ".rate");
39
31
  }
40
32
  function getAccessLog() {
41
- return join2(getEnvsaveDir(), "access.log");
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 join2(getEnvsaveDir(), "config.json");
38
+ return join(getEnvsaveDir(), "config.json");
47
39
  }
48
40
  function guardRealVault() {
49
41
  if (false) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsave/cli",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",