@fentz26/envcp 1.0.0 → 1.0.2

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.
Files changed (64) hide show
  1. package/.github/workflows/publish.yml +23 -2
  2. package/README.md +79 -130
  3. package/__tests__/config.test.ts +65 -0
  4. package/__tests__/crypto.test.ts +76 -0
  5. package/__tests__/http.test.ts +49 -0
  6. package/__tests__/storage.test.ts +94 -0
  7. package/dist/adapters/base.d.ts +1 -2
  8. package/dist/adapters/base.d.ts.map +1 -1
  9. package/dist/adapters/base.js +139 -14
  10. package/dist/adapters/base.js.map +1 -1
  11. package/dist/adapters/gemini.d.ts +1 -0
  12. package/dist/adapters/gemini.d.ts.map +1 -1
  13. package/dist/adapters/gemini.js +13 -99
  14. package/dist/adapters/gemini.js.map +1 -1
  15. package/dist/adapters/openai.d.ts +1 -0
  16. package/dist/adapters/openai.d.ts.map +1 -1
  17. package/dist/adapters/openai.js +13 -99
  18. package/dist/adapters/openai.js.map +1 -1
  19. package/dist/adapters/rest.d.ts +1 -0
  20. package/dist/adapters/rest.d.ts.map +1 -1
  21. package/dist/adapters/rest.js +16 -13
  22. package/dist/adapters/rest.js.map +1 -1
  23. package/dist/cli/index.js +132 -196
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/config/manager.d.ts.map +1 -1
  26. package/dist/config/manager.js +4 -1
  27. package/dist/config/manager.js.map +1 -1
  28. package/dist/mcp/server.d.ts +1 -16
  29. package/dist/mcp/server.d.ts.map +1 -1
  30. package/dist/mcp/server.js +23 -511
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/server/unified.d.ts +1 -0
  33. package/dist/server/unified.d.ts.map +1 -1
  34. package/dist/server/unified.js +31 -19
  35. package/dist/server/unified.js.map +1 -1
  36. package/dist/storage/index.d.ts +2 -0
  37. package/dist/storage/index.d.ts.map +1 -1
  38. package/dist/storage/index.js +18 -4
  39. package/dist/storage/index.js.map +1 -1
  40. package/dist/types.d.ts +10 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +2 -0
  43. package/dist/types.js.map +1 -1
  44. package/dist/utils/http.d.ts +13 -1
  45. package/dist/utils/http.d.ts.map +1 -1
  46. package/dist/utils/http.js +65 -2
  47. package/dist/utils/http.js.map +1 -1
  48. package/dist/utils/session.d.ts.map +1 -1
  49. package/dist/utils/session.js +8 -3
  50. package/dist/utils/session.js.map +1 -1
  51. package/jest.config.js +11 -0
  52. package/package.json +4 -3
  53. package/src/adapters/base.ts +147 -16
  54. package/src/adapters/gemini.ts +19 -105
  55. package/src/adapters/openai.ts +19 -105
  56. package/src/adapters/rest.ts +19 -15
  57. package/src/cli/index.ts +135 -259
  58. package/src/config/manager.ts +4 -1
  59. package/src/mcp/server.ts +26 -582
  60. package/src/server/unified.ts +37 -23
  61. package/src/storage/index.ts +22 -6
  62. package/src/types.ts +2 -0
  63. package/src/utils/http.ts +76 -2
  64. package/src/utils/session.ts +13 -8
@@ -2,14 +2,14 @@ name: Publish to npm
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main]
6
5
  tags: ['v*']
7
6
 
8
7
  jobs:
9
8
  publish:
10
9
  runs-on: ubuntu-latest
11
10
  permissions:
12
- contents: read
11
+ contents: write
12
+ packages: write
13
13
  steps:
14
14
  - uses: actions/checkout@v4
15
15
 
@@ -25,3 +25,24 @@ jobs:
25
25
  - run: npm publish --access=public
26
26
  env:
27
27
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28
+
29
+ # Publish to GitHub Packages
30
+ - uses: actions/setup-node@v4
31
+ with:
32
+ node-version: '20'
33
+ registry-url: 'https://npm.pkg.github.com'
34
+
35
+ - run: npm publish --access=public
36
+ env:
37
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38
+
39
+ # Create GitHub Release
40
+ - name: Create GitHub Release
41
+ env:
42
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43
+ run: |
44
+ TAG="${GITHUB_REF#refs/tags/}"
45
+ gh release create "$TAG" \
46
+ --repo "$GITHUB_REPOSITORY" \
47
+ --title "$TAG" \
48
+ --generate-notes
package/README.md CHANGED
@@ -2,50 +2,7 @@
2
2
 
3
3
  **Secure Environment Variable Management for AI-Assisted Coding**
4
4
 
5
- EnvCP is a universal tool server that allows developers to safely use AI assistants for coding without exposing sensitive environment variables, API keys, or secrets. Works with Claude, ChatGPT, Gemini, Cursor, and any AI tool.
6
-
7
- ## Why EnvCP?
8
-
9
- When using AI coding assistants, you often need to reference environment variables, API keys, or other secrets. But you don't want to share these with the AI. EnvCP solves this by:
10
-
11
- - **Local-only storage** - Your secrets never leave your machine
12
- - **Encrypted at rest** - AES-256-GCM encryption with PBKDF2 key derivation (100,000 iterations)
13
- - **Reference-based access** - AI references variables by name, never sees the actual values
14
- - **Automatic .env injection** - Values can be automatically injected into your .env files
15
- - **AI Access Control** - Block AI from proactively checking or listing your secrets
16
- - **Universal Compatibility** - Works with any AI tool via multiple protocols
17
-
18
- ## Platform Compatibility
19
-
20
- | Platform | Support | Protocol |
21
- |----------|---------|----------|
22
- | Claude Desktop | Native | MCP |
23
- | Claude Code | Native | MCP |
24
- | Cursor | Native | MCP |
25
- | Cline (VS Code) | Native | MCP |
26
- | Continue.dev | Native | MCP |
27
- | Zed Editor | Native | MCP |
28
- | ChatGPT | Via API | OpenAI Function Calling |
29
- | GPT-4 API | Via API | OpenAI Function Calling |
30
- | Gemini | Via API | Google Function Calling |
31
- | Gemini API | Via API | Google Function Calling |
32
- | Local LLMs (Ollama) | Via API | REST / OpenAI-compatible |
33
- | LM Studio | Via API | REST / OpenAI-compatible |
34
- | Open WebUI | Via API | REST |
35
- | Any HTTP Client | Via API | REST |
36
-
37
- ## Features
38
-
39
- - **Multi-Protocol Support** - MCP, REST API, OpenAI, and Gemini protocols
40
- - **Auto-Detection** - Automatically detects client type from request headers
41
- - **AES-256-GCM Encryption** - Military-grade encryption with PBKDF2-SHA512 key derivation
42
- - **Flexible Passwords** - No complexity requirements - use any password you want
43
- - **Session Management** - Quick password mode with configurable session duration
44
- - **AI Access Control** - Prevent AI from actively checking variables without permission
45
- - **Blacklist Patterns** - Block AI access to sensitive variables matching patterns
46
- - **Project-based Organization** - Separate environments per project
47
- - **Auto .env Sync** - Automatically sync to .env files
48
- - **Audit Logging** - All operations logged for security review
5
+ EnvCP lets you safely use AI assistants (Claude, ChatGPT, Gemini, Cursor, etc.) without exposing your secrets. Your API keys and environment variables stay encrypted on your machine AI only references them by name.
49
6
 
50
7
  ## Installation
51
8
 
@@ -53,7 +10,7 @@ When using AI coding assistants, you often need to reference environment variabl
53
10
  npm install -g envcp
54
11
  ```
55
12
 
56
- Or use with npx:
13
+ Or use without installing:
57
14
 
58
15
  ```bash
59
16
  npx envcp init
@@ -61,38 +18,53 @@ npx envcp init
61
18
 
62
19
  ## Quick Start
63
20
 
64
- ### 1. Initialize EnvCP in your project
65
-
66
21
  ```bash
22
+ # 1. Initialize in your project
67
23
  envcp init
68
- ```
69
-
70
- ### 2. Add your secrets
71
24
 
72
- ```bash
25
+ # 2. Add your secrets
73
26
  envcp add API_KEY --value "your-secret-key"
74
27
  envcp add DATABASE_URL --value "postgres://..."
28
+
29
+ # 3. Start the server (auto-detects client type)
30
+ envcp serve --mode auto --port 3456
75
31
  ```
76
32
 
77
- ### 3. Start the server
33
+ ## Basic CLI Commands
78
34
 
79
35
  ```bash
80
- # Auto mode - detects client type automatically (recommended)
81
- envcp serve --mode auto --port 3456
36
+ # Variable Management
37
+ envcp add <name> [options] # Add a variable
38
+ envcp list [--show-values] # List variables
39
+ envcp get <name> # Get a variable
40
+ envcp remove <name> # Remove a variable
41
+
42
+ # Session Management
43
+ envcp unlock # Unlock with password
44
+ envcp lock # Lock immediately
45
+ envcp status # Check session status
82
46
 
83
- # Or specific modes:
84
- envcp serve --mode mcp # For Claude Desktop, Cursor, etc.
85
- envcp serve --mode rest # For REST API clients
86
- envcp serve --mode openai # For ChatGPT/OpenAI-compatible
87
- envcp serve --mode gemini # For Google Gemini
88
- envcp serve --mode all # All protocols on same port
47
+ # Sync and Export
48
+ envcp sync # Sync to .env file
49
+ envcp export [--format env|json|yaml]
89
50
  ```
90
51
 
52
+ ## Why EnvCP?
53
+
54
+ - **Local-only storage** — Your secrets never leave your machine
55
+ - **Encrypted at rest** — AES-256-GCM with PBKDF2 key derivation (100,000 iterations)
56
+ - **Reference-based access** — AI references variables by name, never sees the actual values
57
+ - **Automatic .env injection** — Values can be automatically injected into your .env files
58
+ - **AI Access Control** — Block AI from proactively listing or checking your secrets
59
+ - **Universal Compatibility** — Works with any AI tool via MCP, OpenAI, Gemini, or REST protocols
60
+
61
+ ---
62
+
91
63
  ## Integration Guides
92
64
 
93
65
  ### Claude Desktop / Cursor / Cline (MCP)
94
66
 
95
- Add to your config file:
67
+ Add to your MCP config file:
96
68
 
97
69
  ```json
98
70
  {
@@ -107,26 +79,18 @@ Add to your config file:
107
79
 
108
80
  ### ChatGPT / OpenAI API
109
81
 
110
- Start the server in OpenAI mode:
111
-
112
82
  ```bash
113
83
  envcp serve --mode openai --port 3456 --api-key your-secret-key
114
84
  ```
115
85
 
116
- Use with OpenAI client:
117
-
118
86
  ```python
119
87
  import openai
120
88
 
121
- # Point to EnvCP server
122
89
  client = openai.OpenAI(
123
90
  base_url="http://localhost:3456/v1",
124
91
  api_key="your-secret-key"
125
92
  )
126
93
 
127
- # Get available functions
128
- functions = client.get("/functions")
129
-
130
94
  # Call a function
131
95
  result = client.post("/functions/call", json={
132
96
  "name": "envcp_get",
@@ -136,14 +100,10 @@ result = client.post("/functions/call", json={
136
100
 
137
101
  ### Gemini / Google AI
138
102
 
139
- Start the server in Gemini mode:
140
-
141
103
  ```bash
142
104
  envcp serve --mode gemini --port 3456 --api-key your-secret-key
143
105
  ```
144
106
 
145
- Use with Gemini:
146
-
147
107
  ```python
148
108
  import requests
149
109
 
@@ -163,22 +123,18 @@ result = requests.post(
163
123
 
164
124
  ### Local LLMs (Ollama, LM Studio)
165
125
 
166
- For local LLMs, use REST API mode or OpenAI-compatible mode:
167
-
168
126
  ```bash
169
- # REST API (universal)
170
- envcp serve --mode rest --port 3456
171
-
172
127
  # OpenAI-compatible (works with most local LLM tools)
173
128
  envcp serve --mode openai --port 3456
129
+
130
+ # Or universal REST
131
+ envcp serve --mode rest --port 3456
174
132
  ```
175
133
 
176
- Then configure your LLM tool to use `http://localhost:3456` as the tool server.
134
+ Configure your LLM tool to use `http://localhost:3456` as the tool server.
177
135
 
178
136
  ### REST API (Universal)
179
137
 
180
- Start the server:
181
-
182
138
  ```bash
183
139
  envcp serve --mode rest --port 3456 --api-key your-secret-key
184
140
  ```
@@ -198,8 +154,6 @@ GET /api/tools - List available tools
198
154
  POST /api/tools/:name - Call tool by name
199
155
  ```
200
156
 
201
- Example:
202
-
203
157
  ```bash
204
158
  # List variables
205
159
  curl -H "X-API-Key: your-secret-key" http://localhost:3456/api/variables
@@ -218,36 +172,14 @@ curl -X POST -H "X-API-Key: your-secret-key" \
218
172
 
219
173
  | Mode | Description | Use Case |
220
174
  |------|-------------|----------|
175
+ | `auto` | Auto-detect client from headers | Universal (recommended for HTTP) |
221
176
  | `mcp` | Model Context Protocol (stdio) | Claude Desktop, Cursor, Cline |
222
177
  | `rest` | REST API (HTTP) | Any HTTP client, custom integrations |
223
178
  | `openai` | OpenAI function calling format | ChatGPT, GPT-4 API, OpenAI-compatible tools |
224
179
  | `gemini` | Google function calling format | Gemini, Google AI |
225
180
  | `all` | All HTTP protocols on same port | Multiple clients |
226
- | `auto` | Auto-detect client from headers | Universal (recommended for HTTP) |
227
-
228
- ## CLI Commands
229
181
 
230
182
  ```bash
231
- # Initialize
232
- envcp init [options]
233
-
234
- # Session Management
235
- envcp unlock # Unlock session with password
236
- envcp lock # Lock session immediately
237
- envcp status # Check session status
238
- envcp extend [minutes] # Extend session duration
239
-
240
- # Variable Management
241
- envcp add <name> [options]
242
- envcp list [--show-values]
243
- envcp get <name>
244
- envcp remove <name>
245
-
246
- # Sync and Export
247
- envcp sync # Sync to .env file
248
- envcp export [--format env|json|yaml]
249
-
250
- # Server
251
183
  envcp serve [options]
252
184
  --mode, -m Server mode: mcp, rest, openai, gemini, all, auto
253
185
  --port HTTP port (default: 3456)
@@ -256,9 +188,40 @@ envcp serve [options]
256
188
  --password, -p Encryption password
257
189
  ```
258
190
 
259
- ## Configuration
191
+ ## Platform Compatibility
192
+
193
+ | Platform | Support | Protocol |
194
+ |----------|---------|----------|
195
+ | Claude Desktop | Native | MCP |
196
+ | Claude Code | Native | MCP |
197
+ | Cursor | Native | MCP |
198
+ | Cline (VS Code) | Native | MCP |
199
+ | Continue.dev | Native | MCP |
200
+ | Zed Editor | Native | MCP |
201
+ | ChatGPT | Via API | OpenAI Function Calling |
202
+ | GPT-4 API | Via API | OpenAI Function Calling |
203
+ | Gemini | Via API | Google Function Calling |
204
+ | Gemini API | Via API | Google Function Calling |
205
+ | Local LLMs (Ollama) | Via API | REST / OpenAI-compatible |
206
+ | LM Studio | Via API | REST / OpenAI-compatible |
207
+ | Open WebUI | Via API | REST |
208
+ | Any HTTP Client | Via API | REST |
260
209
 
261
- ### envcp.yaml
210
+ ## Available Tools
211
+
212
+ All protocols expose the same tools:
213
+
214
+ | Tool | Description |
215
+ |------|-------------|
216
+ | `envcp_list` | List variable names (not values) |
217
+ | `envcp_get` | Get a variable (masked by default) |
218
+ | `envcp_set` | Create/update a variable |
219
+ | `envcp_delete` | Delete a variable |
220
+ | `envcp_sync` | Sync to .env file |
221
+ | `envcp_run` | Run command with env vars injected |
222
+ | `envcp_check_access` | Check if variable is accessible |
223
+
224
+ ## Configuration (envcp.yaml)
262
225
 
263
226
  ```yaml
264
227
  version: "1.0"
@@ -298,20 +261,6 @@ sync:
298
261
  - "*_SECRET"
299
262
  ```
300
263
 
301
- ## Available Tools
302
-
303
- All protocols expose the same tools:
304
-
305
- | Tool | Description |
306
- |------|-------------|
307
- | `envcp_list` | List variable names (not values) |
308
- | `envcp_get` | Get a variable (masked by default) |
309
- | `envcp_set` | Create/update a variable |
310
- | `envcp_delete` | Delete a variable |
311
- | `envcp_sync` | Sync to .env file |
312
- | `envcp_run` | Run command with env vars injected |
313
- | `envcp_check_access` | Check if variable is accessible |
314
-
315
264
  ## AI Access Control
316
265
 
317
266
  ### Disable Active Checking
@@ -338,7 +287,7 @@ access:
338
287
 
339
288
  ## Security
340
289
 
341
- ### Encryption
290
+ ### Encryption Details
342
291
 
343
292
  - **Cipher**: AES-256-GCM
344
293
  - **Key Derivation**: PBKDF2-SHA512 (100,000 iterations)
@@ -364,16 +313,16 @@ Authorization: Bearer your-secret-key
364
313
 
365
314
  ## Best Practices
366
315
 
367
- 1. **Never commit `.envcp/`** - Add to `.gitignore`
368
- 2. **Use API keys for HTTP modes** - Protect your server endpoints
369
- 3. **Disable `allow_ai_active_check`** - Prevent AI from probing for variables
370
- 4. **Use blacklist patterns** - Block sensitive variable patterns
371
- 5. **Use `auto` mode for HTTP** - Let EnvCP detect the client type
372
- 6. **Review access logs** - Check `.envcp/logs/` regularly
316
+ 1. **Never commit `.envcp/`** Add to `.gitignore`
317
+ 2. **Use API keys for HTTP modes** Protect your server endpoints
318
+ 3. **Disable `allow_ai_active_check`** Prevent AI from probing for variables
319
+ 4. **Use blacklist patterns** Block sensitive variable patterns
320
+ 5. **Use `auto` mode for HTTP** Let EnvCP detect the client type
321
+ 6. **Review access logs** Check `.envcp/logs/` regularly
373
322
 
374
323
  ## License
375
324
 
376
- MIT License - See LICENSE file for details.
325
+ MIT License See LICENSE file for details.
377
326
 
378
327
  ## Support
379
328
 
@@ -0,0 +1,65 @@
1
+ import { matchesPattern, canAccess, isBlacklisted, validateVariableName } from '../src/config/manager';
2
+ import { EnvCPConfigSchema } from '../src/types';
3
+
4
+ describe('matchesPattern', () => {
5
+ it('matches exact names', () => {
6
+ expect(matchesPattern('API_KEY', 'API_KEY')).toBe(true);
7
+ expect(matchesPattern('API_KEY', 'OTHER')).toBe(false);
8
+ });
9
+
10
+ it('matches wildcard patterns', () => {
11
+ expect(matchesPattern('DB_SECRET', '*_SECRET')).toBe(true);
12
+ expect(matchesPattern('DB_KEY', '*_SECRET')).toBe(false);
13
+ expect(matchesPattern('ADMIN_TOKEN', 'ADMIN_*')).toBe(true);
14
+ });
15
+
16
+ it('does not treat dots as wildcards (regex escaping)', () => {
17
+ expect(matchesPattern('DB_SECRET', 'DB.SECRET')).toBe(false);
18
+ expect(matchesPattern('DBXSECRET', 'DB.SECRET')).toBe(false);
19
+ });
20
+
21
+ it('escapes other regex special chars', () => {
22
+ expect(matchesPattern('A+B', 'A+B')).toBe(true);
23
+ expect(matchesPattern('AB', 'A+B')).toBe(false);
24
+ expect(matchesPattern('A?B', 'A?B')).toBe(true);
25
+ expect(matchesPattern('AB', 'A?B')).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe('validateVariableName', () => {
30
+ it('accepts valid names', () => {
31
+ expect(validateVariableName('API_KEY')).toBe(true);
32
+ expect(validateVariableName('_private')).toBe(true);
33
+ expect(validateVariableName('a')).toBe(true);
34
+ });
35
+
36
+ it('rejects invalid names', () => {
37
+ expect(validateVariableName('123')).toBe(false);
38
+ expect(validateVariableName('has space')).toBe(false);
39
+ expect(validateVariableName('has-dash')).toBe(false);
40
+ expect(validateVariableName('')).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe('canAccess / isBlacklisted', () => {
45
+ const makeConfig = (overrides: Record<string, unknown> = {}) =>
46
+ EnvCPConfigSchema.parse({ access: { ...overrides } });
47
+
48
+ it('blacklists matching patterns', () => {
49
+ const config = makeConfig({ blacklist_patterns: ['*_SECRET'] });
50
+ expect(isBlacklisted('DB_SECRET', config)).toBe(true);
51
+ expect(isBlacklisted('DB_KEY', config)).toBe(false);
52
+ });
53
+
54
+ it('denies access for denied patterns', () => {
55
+ const config = makeConfig({ denied_patterns: ['ADMIN_*'] });
56
+ expect(canAccess('ADMIN_KEY', config)).toBe(false);
57
+ expect(canAccess('USER_KEY', config)).toBe(true);
58
+ });
59
+
60
+ it('restricts to allowed patterns when set', () => {
61
+ const config = makeConfig({ allowed_patterns: ['APP_*'] });
62
+ expect(canAccess('APP_KEY', config)).toBe(true);
63
+ expect(canAccess('DB_KEY', config)).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,76 @@
1
+ import { encrypt, decrypt, maskValue, validatePassword, quickHash, generateId, generateSessionToken } from '../src/utils/crypto';
2
+
3
+ describe('encrypt/decrypt', () => {
4
+ it('round-trips correctly', () => {
5
+ const text = 'hello world secret';
6
+ const password = 'test-password-123';
7
+ const encrypted = encrypt(text, password);
8
+ expect(encrypted).not.toBe(text);
9
+ expect(decrypt(encrypted, password)).toBe(text);
10
+ });
11
+
12
+ it('fails with wrong password', () => {
13
+ const encrypted = encrypt('secret', 'correct');
14
+ expect(() => decrypt(encrypted, 'wrong')).toThrow();
15
+ });
16
+
17
+ it('produces different ciphertext each time (random salt/iv)', () => {
18
+ const text = 'same text';
19
+ const password = 'same password';
20
+ const a = encrypt(text, password);
21
+ const b = encrypt(text, password);
22
+ expect(a).not.toBe(b);
23
+ });
24
+ });
25
+
26
+ describe('maskValue', () => {
27
+ it('fully masks short values', () => {
28
+ expect(maskValue('ab')).toBe('**');
29
+ expect(maskValue('abcdefgh')).toBe('********');
30
+ });
31
+
32
+ it('shows prefix/suffix for longer values', () => {
33
+ const result = maskValue('abcdefghijklmnop');
34
+ expect(result.startsWith('abcd')).toBe(true);
35
+ expect(result.endsWith('mnop')).toBe(true);
36
+ expect(result).toContain('*');
37
+ });
38
+ });
39
+
40
+ describe('validatePassword', () => {
41
+ it('accepts valid password with defaults', () => {
42
+ expect(validatePassword('a', {}).valid).toBe(true);
43
+ });
44
+
45
+ it('rejects too short password', () => {
46
+ expect(validatePassword('', { min_length: 1 }).valid).toBe(false);
47
+ });
48
+
49
+ it('rejects single char when disallowed', () => {
50
+ expect(validatePassword('a', { allow_single_char: false }).valid).toBe(false);
51
+ });
52
+
53
+ it('rejects numeric-only when disallowed', () => {
54
+ expect(validatePassword('1234', { allow_numeric_only: false }).valid).toBe(false);
55
+ });
56
+
57
+ it('enforces complexity', () => {
58
+ expect(validatePassword('abc', { require_complexity: true }).valid).toBe(false);
59
+ expect(validatePassword('Abc123!', { require_complexity: true }).valid).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe('helpers', () => {
64
+ it('generateId returns 32-char hex', () => {
65
+ expect(generateId()).toMatch(/^[a-f0-9]{32}$/);
66
+ });
67
+
68
+ it('generateSessionToken returns 64-char hex', () => {
69
+ expect(generateSessionToken()).toMatch(/^[a-f0-9]{64}$/);
70
+ });
71
+
72
+ it('quickHash returns 16-char hex', () => {
73
+ expect(quickHash('test')).toMatch(/^[a-f0-9]{16}$/);
74
+ expect(quickHash('test')).toBe(quickHash('test'));
75
+ });
76
+ });
@@ -0,0 +1,49 @@
1
+ import { validateApiKey, RateLimiter } from '../src/utils/http';
2
+
3
+ describe('validateApiKey', () => {
4
+ it('returns false for undefined', () => {
5
+ expect(validateApiKey(undefined, 'expected')).toBe(false);
6
+ });
7
+
8
+ it('returns false for wrong length', () => {
9
+ expect(validateApiKey('short', 'expected-key')).toBe(false);
10
+ });
11
+
12
+ it('returns false for wrong key', () => {
13
+ expect(validateApiKey('wrong-key-xx', 'expected-key')).toBe(false);
14
+ });
15
+
16
+ it('returns true for correct key', () => {
17
+ expect(validateApiKey('my-secret-key', 'my-secret-key')).toBe(true);
18
+ });
19
+ });
20
+
21
+ describe('RateLimiter', () => {
22
+ it('allows requests within limit', () => {
23
+ const limiter = new RateLimiter(3, 60000);
24
+ expect(limiter.isAllowed('ip1')).toBe(true);
25
+ expect(limiter.isAllowed('ip1')).toBe(true);
26
+ expect(limiter.isAllowed('ip1')).toBe(true);
27
+ });
28
+
29
+ it('blocks requests over limit', () => {
30
+ const limiter = new RateLimiter(2, 60000);
31
+ expect(limiter.isAllowed('ip1')).toBe(true);
32
+ expect(limiter.isAllowed('ip1')).toBe(true);
33
+ expect(limiter.isAllowed('ip1')).toBe(false);
34
+ });
35
+
36
+ it('tracks IPs independently', () => {
37
+ const limiter = new RateLimiter(1, 60000);
38
+ expect(limiter.isAllowed('ip1')).toBe(true);
39
+ expect(limiter.isAllowed('ip2')).toBe(true);
40
+ expect(limiter.isAllowed('ip1')).toBe(false);
41
+ });
42
+
43
+ it('reports remaining requests', () => {
44
+ const limiter = new RateLimiter(3, 60000);
45
+ expect(limiter.getRemainingRequests('ip1')).toBe(3);
46
+ limiter.isAllowed('ip1');
47
+ expect(limiter.getRemainingRequests('ip1')).toBe(2);
48
+ });
49
+ });
@@ -0,0 +1,94 @@
1
+ import * as fs from 'fs-extra';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { StorageManager } from '../src/storage/index';
5
+
6
+ describe('StorageManager', () => {
7
+ let tmpDir: string;
8
+ let storePath: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'envcp-test-'));
12
+ storePath = path.join(tmpDir, 'store.enc');
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(tmpDir);
17
+ });
18
+
19
+ it('stores and retrieves variables (encrypted)', async () => {
20
+ const storage = new StorageManager(storePath, true);
21
+ storage.setPassword('test123');
22
+
23
+ await storage.set('API_KEY', {
24
+ name: 'API_KEY',
25
+ value: 'secret-value',
26
+ encrypted: true,
27
+ created: new Date().toISOString(),
28
+ updated: new Date().toISOString(),
29
+ sync_to_env: true,
30
+ });
31
+
32
+ const result = await storage.get('API_KEY');
33
+ expect(result).toBeDefined();
34
+ expect(result!.value).toBe('secret-value');
35
+ });
36
+
37
+ it('lists variable names', async () => {
38
+ const storage = new StorageManager(storePath, true);
39
+ storage.setPassword('test123');
40
+
41
+ const now = new Date().toISOString();
42
+ await storage.set('A', { name: 'A', value: '1', encrypted: true, created: now, updated: now, sync_to_env: true });
43
+ await storage.set('B', { name: 'B', value: '2', encrypted: true, created: now, updated: now, sync_to_env: true });
44
+
45
+ const names = await storage.list();
46
+ expect(names.sort()).toEqual(['A', 'B']);
47
+ });
48
+
49
+ it('deletes variables', async () => {
50
+ const storage = new StorageManager(storePath, true);
51
+ storage.setPassword('test123');
52
+
53
+ const now = new Date().toISOString();
54
+ await storage.set('X', { name: 'X', value: 'v', encrypted: true, created: now, updated: now, sync_to_env: true });
55
+
56
+ expect(await storage.delete('X')).toBe(true);
57
+ expect(await storage.get('X')).toBeUndefined();
58
+ expect(await storage.delete('X')).toBe(false);
59
+ });
60
+
61
+ it('returns empty for non-existent store', async () => {
62
+ const storage = new StorageManager(storePath, true);
63
+ storage.setPassword('test123');
64
+
65
+ const variables = await storage.load();
66
+ expect(variables).toEqual({});
67
+ });
68
+
69
+ it('fails with wrong password', async () => {
70
+ const storage = new StorageManager(storePath, true);
71
+ storage.setPassword('correct');
72
+
73
+ const now = new Date().toISOString();
74
+ await storage.set('KEY', { name: 'KEY', value: 'v', encrypted: true, created: now, updated: now, sync_to_env: true });
75
+
76
+ const storage2 = new StorageManager(storePath, true);
77
+ storage2.setPassword('wrong');
78
+
79
+ await expect(storage2.load()).rejects.toThrow('Failed to decrypt');
80
+ });
81
+
82
+ it('uses cache on subsequent loads', async () => {
83
+ const storage = new StorageManager(storePath, true);
84
+ storage.setPassword('test123');
85
+
86
+ const now = new Date().toISOString();
87
+ await storage.set('A', { name: 'A', value: '1', encrypted: true, created: now, updated: now, sync_to_env: true });
88
+
89
+ // Second load should use cache
90
+ const v1 = await storage.load();
91
+ const v2 = await storage.load();
92
+ expect(v1).toBe(v2); // same reference = cache hit
93
+ });
94
+ });
@@ -10,6 +10,7 @@ export declare abstract class BaseAdapter {
10
10
  protected tools: Map<string, ToolDefinition>;
11
11
  constructor(config: EnvCPConfig, projectPath: string, password?: string);
12
12
  protected abstract registerTools(): void;
13
+ protected registerDefaultTools(): void;
13
14
  init(): Promise<void>;
14
15
  protected ensurePassword(): Promise<void>;
15
16
  getToolDefinitions(): ToolDefinition[];
@@ -60,9 +61,7 @@ export declare abstract class BaseAdapter {
60
61
  name: string;
61
62
  }): Promise<{
62
63
  name: string;
63
- exists: boolean;
64
64
  accessible: boolean;
65
- blacklisted: boolean;
66
65
  message: string;
67
66
  }>;
68
67
  private parseCommand;