@envsave/cli 1.0.37

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/dist/index.js ADDED
@@ -0,0 +1,159 @@
1
+ // src/vault.ts
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, appendFileSync, statSync } from "node:fs";
3
+ import { join as join2 } from "node:path";
4
+ import { homedir as homedir2 } from "node:os";
5
+
6
+ // src/crypto.ts
7
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes, pbkdf2Sync } from "node:crypto";
8
+ var IV_LENGTH = 12;
9
+ var AUTH_TAG_LENGTH = 16;
10
+ function decryptWithKey(payload, key) {
11
+ const iv = payload.subarray(0, IV_LENGTH);
12
+ const authTag = payload.subarray(payload.length - AUTH_TAG_LENGTH);
13
+ const encrypted = payload.subarray(IV_LENGTH, payload.length - AUTH_TAG_LENGTH);
14
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
15
+ decipher.setAuthTag(authTag);
16
+ return decipher.update(encrypted) + decipher.final("utf8");
17
+ }
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
+ // src/vault.ts
28
+ function getEnvsaveDir() {
29
+ return process.env.ENVSAVE_DIR || join2(homedir2(), ".envsave");
30
+ }
31
+ function getCacheFile() {
32
+ return join2(getEnvsaveDir(), "cache.enc");
33
+ }
34
+ function getSessionFile() {
35
+ return join2(getEnvsaveDir(), ".session");
36
+ }
37
+ function getRateFile() {
38
+ return join2(getEnvsaveDir(), ".rate");
39
+ }
40
+ function getAccessLog() {
41
+ return join2(getEnvsaveDir(), "access.log");
42
+ }
43
+ var DEFAULT_SESSION_TTL_MS = 4 * 60 * 60 * 1000;
44
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 1000;
45
+ var DEFAULT_RATE_LIMIT_MAX = 30;
46
+ function getConfigFile() {
47
+ return join2(getEnvsaveDir(), "config.json");
48
+ }
49
+ function guardRealVault() {
50
+ if (false) {}
51
+ }
52
+ function readConfig() {
53
+ if (!existsSync(getConfigFile())) {
54
+ return { maxGetsPerMinute: DEFAULT_RATE_LIMIT_MAX, sessionTtlHours: DEFAULT_SESSION_TTL_MS / 3600000 };
55
+ }
56
+ try {
57
+ return JSON.parse(readFileSync(getConfigFile(), "utf8"));
58
+ } catch {
59
+ return { maxGetsPerMinute: DEFAULT_RATE_LIMIT_MAX, sessionTtlHours: DEFAULT_SESSION_TTL_MS / 3600000 };
60
+ }
61
+ }
62
+ function ensureDir() {
63
+ guardRealVault();
64
+ if (!existsSync(getEnvsaveDir())) {
65
+ mkdirSync(getEnvsaveDir(), { recursive: true, mode: 448 });
66
+ }
67
+ }
68
+ function readSessionCache() {
69
+ if (!existsSync(getSessionFile()) || !existsSync(getCacheFile()))
70
+ return null;
71
+ try {
72
+ const sessionStat = statSync(getSessionFile());
73
+ const age = Date.now() - sessionStat.mtimeMs;
74
+ const config = readConfig();
75
+ if (age > config.sessionTtlHours * 3600000) {
76
+ return null;
77
+ }
78
+ const sessionKeyHex = readFileSync(getSessionFile(), "utf8").trim();
79
+ const sessionKey = Buffer.from(sessionKeyHex, "hex");
80
+ const payload = readFileSync(getCacheFile());
81
+ const json = decryptWithKey(payload, sessionKey);
82
+ return JSON.parse(json);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function checkRateLimit() {
88
+ const config = readConfig();
89
+ const now = Date.now();
90
+ let timestamps = [];
91
+ if (existsSync(getRateFile())) {
92
+ try {
93
+ timestamps = JSON.parse(readFileSync(getRateFile(), "utf8"));
94
+ } catch {
95
+ timestamps = [];
96
+ }
97
+ }
98
+ timestamps = timestamps.filter((t) => now - t < DEFAULT_RATE_LIMIT_WINDOW_MS);
99
+ if (timestamps.length >= config.maxGetsPerMinute) {
100
+ logAccess("RATE_LIMIT_BLOCKED", "blocked");
101
+ throw new Error(`Rate limit exceeded. Max ${config.maxGetsPerMinute} get calls per minute. Try again in ${Math.ceil((DEFAULT_RATE_LIMIT_WINDOW_MS - (now - timestamps[0])) / 1000)}s.`);
102
+ }
103
+ const threshold = Math.floor(config.maxGetsPerMinute * 0.8);
104
+ if (timestamps.length >= threshold) {
105
+ const remaining = config.maxGetsPerMinute - timestamps.length;
106
+ console.error(`⚠ Rate limit warning: ${remaining} get(s) remaining this minute.`);
107
+ }
108
+ timestamps.push(now);
109
+ writeFileSync(getRateFile(), JSON.stringify(timestamps), { mode: 384 });
110
+ }
111
+ function logAccess(opaqueKey, source) {
112
+ ensureDir();
113
+ const entry = `${new Date().toISOString()} ${source} ${opaqueKey.slice(0, 10)}...
114
+ `;
115
+ appendFileSync(getAccessLog(), entry, { mode: 384 });
116
+ }
117
+ function getSecretFromCache(opaqueKey) {
118
+ checkRateLimit();
119
+ const cache = readSessionCache();
120
+ if (!cache) {
121
+ throw new Error("No session found. Run any envsave command with your password first.");
122
+ }
123
+ logAccess(opaqueKey, "cache");
124
+ return cache.secrets[opaqueKey];
125
+ }
126
+ function getSecretWithSession(opaqueKey) {
127
+ return getSecretFromCache(opaqueKey);
128
+ }
129
+ function hasSecretWithSession(opaqueKey) {
130
+ try {
131
+ const cache = readSessionCache();
132
+ if (!cache)
133
+ return false;
134
+ return opaqueKey in cache.secrets;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ // src/index.ts
141
+ function get(key) {
142
+ try {
143
+ const value = getSecretWithSession(key);
144
+ if (value !== undefined)
145
+ return value;
146
+ } catch {}
147
+ return process.env[key];
148
+ }
149
+ function has(key) {
150
+ if (hasSecretWithSession(key))
151
+ return true;
152
+ return process.env[key] !== undefined;
153
+ }
154
+ var src_default = { get, has };
155
+ export {
156
+ has,
157
+ get,
158
+ src_default as default
159
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@envsave/cli",
3
+ "version": "1.0.37",
4
+ "description": "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "envsave": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "python"
14
+ ],
15
+ "scripts": {
16
+ "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')\"",
17
+ "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",
18
+ "dev": "bun run src/cli.ts",
19
+ "webapp": "bun run webapp/server.ts",
20
+ "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.'"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bun": "latest",
24
+ "@types/node": "^25.5.0"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ },
29
+ "keywords": [
30
+ "vault",
31
+ "secrets",
32
+ "encryption",
33
+ "llm",
34
+ "security",
35
+ "env"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/jcsancho/envsave.git"
41
+ },
42
+ "homepage": "https://api.envsave.com",
43
+ "author": "Jose C Sancho",
44
+ "dependencies": {
45
+ "@clerk/backend": "^3.2.1",
46
+ "@supabase/supabase-js": "^2.99.2"
47
+ }
48
+ }
@@ -0,0 +1,79 @@
1
+ """
2
+ EnvSave Python — Local secret vault for LLM-safe environments.
3
+
4
+ Usage:
5
+ from envsave import get, has
6
+
7
+ api_key = get("es_7a3f2b1e9c8d4f6b2a1c3d5f")
8
+ # Falls back to os.environ if vault is unavailable
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
16
+
17
+ ENVSAVE_DIR = Path.home() / ".envsave"
18
+ CACHE_FILE = ENVSAVE_DIR / "cache.enc"
19
+ SESSION_FILE = ENVSAVE_DIR / ".session"
20
+
21
+ IV_LENGTH = 12
22
+
23
+
24
+ def _decrypt_cache() -> dict:
25
+ """Decrypt session cache using random session key (NOT the master password)."""
26
+ if not SESSION_FILE.exists() or not CACHE_FILE.exists():
27
+ raise RuntimeError("No session found. Run any envsave command with your password first.")
28
+
29
+ session_key_hex = SESSION_FILE.read_text().strip()
30
+ session_key = bytes.fromhex(session_key_hex)
31
+
32
+ payload = CACHE_FILE.read_bytes()
33
+ iv = payload[:IV_LENGTH]
34
+ encrypted_with_tag = payload[IV_LENGTH:]
35
+
36
+ aesgcm = AESGCM(session_key)
37
+ decrypted = aesgcm.decrypt(iv, encrypted_with_tag, None)
38
+ return json.loads(decrypted.decode("utf-8"))
39
+
40
+
41
+ _cache: dict | None = None
42
+
43
+
44
+ def _get_cache() -> dict:
45
+ global _cache
46
+ if _cache is None:
47
+ _cache = _decrypt_cache()
48
+ return _cache
49
+
50
+
51
+ def get(key: str) -> str | None:
52
+ """Get a secret by its opaque key. Falls back to os.environ[key] if vault is unavailable."""
53
+ try:
54
+ cache = _get_cache()
55
+ value = cache.get("secrets", {}).get(key)
56
+ if value is not None:
57
+ return value
58
+ except RuntimeError:
59
+ pass
60
+
61
+ return os.environ.get(key)
62
+
63
+
64
+ def has(key: str) -> bool:
65
+ """Check if an opaque key exists in the session cache or as an environment variable."""
66
+ try:
67
+ cache = _get_cache()
68
+ if key in cache.get("secrets", {}):
69
+ return True
70
+ except RuntimeError:
71
+ pass
72
+
73
+ return key in os.environ
74
+
75
+
76
+ def clear_cache():
77
+ """Clear the in-memory cache."""
78
+ global _cache
79
+ _cache = None
@@ -0,0 +1,85 @@
1
+ """
2
+ EnvSave Python Bridge — reads the session cache (NOT the main vault).
3
+
4
+ The session cache contains only the secrets map (opaque_key → value).
5
+ No aliases, no HMAC seed, no registry — an LLM reading this file
6
+ can only access secrets whose opaque key it already knows.
7
+
8
+ Usage:
9
+ import envsave
10
+ api_key = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f")
11
+
12
+ Requirements:
13
+ pip install cryptography
14
+ """
15
+
16
+ import json
17
+ import os
18
+ from pathlib import Path
19
+
20
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
21
+
22
+ ENVSAVE_DIR = Path.home() / ".envsave"
23
+ CACHE_FILE = ENVSAVE_DIR / "cache.enc"
24
+ SESSION_FILE = ENVSAVE_DIR / ".session"
25
+
26
+ IV_LENGTH = 12
27
+
28
+
29
+ def _decrypt_cache() -> dict:
30
+ """Decrypt session cache using random session key (NOT the master password)."""
31
+ if not SESSION_FILE.exists() or not CACHE_FILE.exists():
32
+ raise RuntimeError("No session found. Run any envsave command with your password first.")
33
+
34
+ session_key_hex = SESSION_FILE.read_text().strip()
35
+ session_key = bytes.fromhex(session_key_hex)
36
+
37
+ payload = CACHE_FILE.read_bytes()
38
+ iv = payload[:IV_LENGTH]
39
+ encrypted_with_tag = payload[IV_LENGTH:]
40
+
41
+ aesgcm = AESGCM(session_key)
42
+ decrypted = aesgcm.decrypt(iv, encrypted_with_tag, None)
43
+ return json.loads(decrypted.decode("utf-8"))
44
+
45
+
46
+ # Cache for repeated calls
47
+ _cache: dict | None = None
48
+
49
+
50
+ def _get_cache() -> dict:
51
+ global _cache
52
+ if _cache is None:
53
+ _cache = _decrypt_cache()
54
+ return _cache
55
+
56
+
57
+ def get(key: str) -> str | None:
58
+ """Get a secret by its opaque key. Falls back to os.environ[key] if vault is unavailable."""
59
+ try:
60
+ cache = _get_cache()
61
+ value = cache.get("secrets", {}).get(key)
62
+ if value is not None:
63
+ return value
64
+ except RuntimeError:
65
+ pass
66
+
67
+ return os.environ.get(key)
68
+
69
+
70
+ def has(key: str) -> bool:
71
+ """Check if an opaque key exists in the session cache or as an environment variable."""
72
+ try:
73
+ cache = _get_cache()
74
+ if key in cache.get("secrets", {}):
75
+ return True
76
+ except RuntimeError:
77
+ pass
78
+
79
+ return key in os.environ
80
+
81
+
82
+ def clear_cache():
83
+ """Clear the in-memory cache."""
84
+ global _cache
85
+ _cache = None
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "envsave"
3
+ version = "1.0.0"
4
+ description = "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover"
5
+ license = "MIT"
6
+ requires-python = ">=3.10"
7
+ dependencies = ["cryptography>=41.0"]
8
+ keywords = ["vault", "secrets", "encryption", "llm", "security"]
9
+
10
+ [project.urls]
11
+ Homepage = "https://github.com/jcsancho/envsave"
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["envsave"]