@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.
- package/.github/workflows/publish.yml +23 -2
- package/README.md +79 -130
- package/__tests__/config.test.ts +65 -0
- package/__tests__/crypto.test.ts +76 -0
- package/__tests__/http.test.ts +49 -0
- package/__tests__/storage.test.ts +94 -0
- package/dist/adapters/base.d.ts +1 -2
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +139 -14
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +13 -99
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +13 -99
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +1 -0
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +16 -13
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +132 -196
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +4 -1
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -16
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -511
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/unified.d.ts +1 -0
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +31 -19
- package/dist/server/unified.js.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +18 -4
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/http.d.ts +13 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +65 -2
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +8 -3
- package/dist/utils/session.js.map +1 -1
- package/jest.config.js +11 -0
- package/package.json +4 -3
- package/src/adapters/base.ts +147 -16
- package/src/adapters/gemini.ts +19 -105
- package/src/adapters/openai.ts +19 -105
- package/src/adapters/rest.ts +19 -15
- package/src/cli/index.ts +135 -259
- package/src/config/manager.ts +4 -1
- package/src/mcp/server.ts +26 -582
- package/src/server/unified.ts +37 -23
- package/src/storage/index.ts +22 -6
- package/src/types.ts +2 -0
- package/src/utils/http.ts +76 -2
- 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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
## Basic CLI Commands
|
|
78
34
|
|
|
79
35
|
```bash
|
|
80
|
-
#
|
|
81
|
-
envcp
|
|
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
|
-
#
|
|
84
|
-
envcp
|
|
85
|
-
envcp
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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/`**
|
|
368
|
-
2. **Use API keys for HTTP modes**
|
|
369
|
-
3. **Disable `allow_ai_active_check`**
|
|
370
|
-
4. **Use blacklist patterns**
|
|
371
|
-
5. **Use `auto` mode for HTTP**
|
|
372
|
-
6. **Review access logs**
|
|
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
|
|
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
|
+
});
|
package/dist/adapters/base.d.ts
CHANGED
|
@@ -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;
|