@fentz26/envcp 1.0.1 → 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 (63) hide show
  1. package/README.md +79 -130
  2. package/__tests__/config.test.ts +65 -0
  3. package/__tests__/crypto.test.ts +76 -0
  4. package/__tests__/http.test.ts +49 -0
  5. package/__tests__/storage.test.ts +94 -0
  6. package/dist/adapters/base.d.ts +1 -2
  7. package/dist/adapters/base.d.ts.map +1 -1
  8. package/dist/adapters/base.js +139 -14
  9. package/dist/adapters/base.js.map +1 -1
  10. package/dist/adapters/gemini.d.ts +1 -0
  11. package/dist/adapters/gemini.d.ts.map +1 -1
  12. package/dist/adapters/gemini.js +13 -99
  13. package/dist/adapters/gemini.js.map +1 -1
  14. package/dist/adapters/openai.d.ts +1 -0
  15. package/dist/adapters/openai.d.ts.map +1 -1
  16. package/dist/adapters/openai.js +13 -99
  17. package/dist/adapters/openai.js.map +1 -1
  18. package/dist/adapters/rest.d.ts +1 -0
  19. package/dist/adapters/rest.d.ts.map +1 -1
  20. package/dist/adapters/rest.js +16 -13
  21. package/dist/adapters/rest.js.map +1 -1
  22. package/dist/cli/index.js +132 -196
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/config/manager.d.ts.map +1 -1
  25. package/dist/config/manager.js +4 -1
  26. package/dist/config/manager.js.map +1 -1
  27. package/dist/mcp/server.d.ts +1 -16
  28. package/dist/mcp/server.d.ts.map +1 -1
  29. package/dist/mcp/server.js +23 -511
  30. package/dist/mcp/server.js.map +1 -1
  31. package/dist/server/unified.d.ts +1 -0
  32. package/dist/server/unified.d.ts.map +1 -1
  33. package/dist/server/unified.js +31 -19
  34. package/dist/server/unified.js.map +1 -1
  35. package/dist/storage/index.d.ts +2 -0
  36. package/dist/storage/index.d.ts.map +1 -1
  37. package/dist/storage/index.js +18 -4
  38. package/dist/storage/index.js.map +1 -1
  39. package/dist/types.d.ts +10 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +1 -1
  43. package/dist/utils/http.d.ts +13 -1
  44. package/dist/utils/http.d.ts.map +1 -1
  45. package/dist/utils/http.js +65 -2
  46. package/dist/utils/http.js.map +1 -1
  47. package/dist/utils/session.d.ts.map +1 -1
  48. package/dist/utils/session.js +8 -3
  49. package/dist/utils/session.js.map +1 -1
  50. package/jest.config.js +11 -0
  51. package/package.json +4 -3
  52. package/src/adapters/base.ts +147 -16
  53. package/src/adapters/gemini.ts +19 -105
  54. package/src/adapters/openai.ts +19 -105
  55. package/src/adapters/rest.ts +19 -15
  56. package/src/cli/index.ts +135 -259
  57. package/src/config/manager.ts +4 -1
  58. package/src/mcp/server.ts +26 -582
  59. package/src/server/unified.ts +37 -23
  60. package/src/storage/index.ts +22 -6
  61. package/src/types.ts +2 -0
  62. package/src/utils/http.ts +76 -2
  63. package/src/utils/session.ts +13 -8
@@ -1,110 +1,18 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { EnvCPConfig, OpenAIFunction, OpenAIToolCall, OpenAIMessage, ToolDefinition } from '../types.js';
3
- import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
2
+ import { EnvCPConfig, OpenAIFunction, OpenAIToolCall, OpenAIMessage } from '../types.js';
3
+ import { setCorsHeaders, sendJson, parseBody, validateApiKey, RateLimiter, rateLimitMiddleware } from '../utils/http.js';
4
4
  import * as http from 'http';
5
- import * as url from 'url';
6
5
 
7
6
  export class OpenAIAdapter extends BaseAdapter {
8
7
  private server: http.Server | null = null;
8
+ private rateLimiter = new RateLimiter(60, 60000);
9
9
 
10
10
  constructor(config: EnvCPConfig, projectPath: string, password?: string) {
11
11
  super(config, projectPath, password);
12
12
  }
13
13
 
14
14
  protected registerTools(): void {
15
- const tools: ToolDefinition[] = [
16
- {
17
- name: 'envcp_list',
18
- description: 'List all available environment variable names. Values are never shown.',
19
- parameters: {
20
- type: 'object',
21
- properties: {
22
- tags: {
23
- type: 'array',
24
- items: { type: 'string' },
25
- description: 'Filter by tags',
26
- },
27
- },
28
- },
29
- handler: async (params) => this.listVariables(params as { tags?: string[] }),
30
- },
31
- {
32
- name: 'envcp_get',
33
- description: 'Get an environment variable. Returns masked value by default.',
34
- parameters: {
35
- type: 'object',
36
- properties: {
37
- name: { type: 'string', description: 'Variable name' },
38
- show_value: { type: 'boolean', description: 'Show actual value (requires user confirmation)' },
39
- },
40
- required: ['name'],
41
- },
42
- handler: async (params) => this.getVariable(params as { name: string; show_value?: boolean }),
43
- },
44
- {
45
- name: 'envcp_set',
46
- description: 'Create or update an environment variable.',
47
- parameters: {
48
- type: 'object',
49
- properties: {
50
- name: { type: 'string', description: 'Variable name' },
51
- value: { type: 'string', description: 'Variable value' },
52
- tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
53
- description: { type: 'string', description: 'Description' },
54
- },
55
- required: ['name', 'value'],
56
- },
57
- handler: async (params) => this.setVariable(params as any),
58
- },
59
- {
60
- name: 'envcp_delete',
61
- description: 'Delete an environment variable.',
62
- parameters: {
63
- type: 'object',
64
- properties: {
65
- name: { type: 'string', description: 'Variable name' },
66
- },
67
- required: ['name'],
68
- },
69
- handler: async (params) => this.deleteVariable(params as { name: string }),
70
- },
71
- {
72
- name: 'envcp_sync',
73
- description: 'Sync variables to .env file.',
74
- parameters: {
75
- type: 'object',
76
- properties: {},
77
- },
78
- handler: async () => this.syncToEnv(),
79
- },
80
- {
81
- name: 'envcp_run',
82
- description: 'Execute a command with environment variables injected.',
83
- parameters: {
84
- type: 'object',
85
- properties: {
86
- command: { type: 'string', description: 'Command to execute' },
87
- variables: { type: 'array', items: { type: 'string' }, description: 'Variables to inject' },
88
- },
89
- required: ['command', 'variables'],
90
- },
91
- handler: async (params) => this.runCommand(params as { command: string; variables: string[] }),
92
- },
93
- {
94
- name: 'envcp_check_access',
95
- description: 'Check if a variable exists and can be accessed.',
96
- parameters: {
97
- type: 'object',
98
- properties: {
99
- name: { type: 'string', description: 'Variable name' },
100
- },
101
- required: ['name'],
102
- },
103
- handler: async (params) => this.checkAccess(params as { name: string }),
104
- },
105
- ];
106
-
107
- tools.forEach(tool => this.tools.set(tool.name, tool));
15
+ this.registerDefaultTools();
108
16
  }
109
17
 
110
18
  // Convert tools to OpenAI function format
@@ -114,8 +22,8 @@ export class OpenAIAdapter extends BaseAdapter {
114
22
  description: tool.description,
115
23
  parameters: {
116
24
  type: 'object' as const,
117
- properties: (tool.parameters as any).properties || {},
118
- required: (tool.parameters as any).required,
25
+ properties: (tool.parameters as Record<string, unknown>).properties as Record<string, unknown> || {},
26
+ required: (tool.parameters as Record<string, unknown>).required as string[] | undefined,
119
27
  },
120
28
  }));
121
29
  }
@@ -133,11 +41,12 @@ export class OpenAIAdapter extends BaseAdapter {
133
41
  tool_call_id: call.id,
134
42
  content: JSON.stringify(result),
135
43
  });
136
- } catch (error: any) {
44
+ } catch (error: unknown) {
45
+ const message = error instanceof Error ? error.message : String(error);
137
46
  results.push({
138
47
  role: 'tool',
139
48
  tool_call_id: call.id,
140
- content: JSON.stringify({ error: error.message }),
49
+ content: JSON.stringify({ error: message }),
141
50
  });
142
51
  }
143
52
  }
@@ -150,7 +59,7 @@ export class OpenAIAdapter extends BaseAdapter {
150
59
  await this.init();
151
60
 
152
61
  this.server = http.createServer(async (req, res) => {
153
- setCorsHeaders(res);
62
+ setCorsHeaders(res, undefined, req.headers.origin);
154
63
 
155
64
  if (req.method === 'OPTIONS') {
156
65
  res.writeHead(204);
@@ -158,6 +67,10 @@ export class OpenAIAdapter extends BaseAdapter {
158
67
  return;
159
68
  }
160
69
 
70
+ if (!rateLimitMiddleware(this.rateLimiter, req, res)) {
71
+ return;
72
+ }
73
+
161
74
  // API key validation
162
75
  if (apiKey) {
163
76
  const providedKey = req.headers['authorization']?.replace('Bearer ', '');
@@ -167,8 +80,8 @@ export class OpenAIAdapter extends BaseAdapter {
167
80
  }
168
81
  }
169
82
 
170
- const parsedUrl = url.parse(req.url || '/', true);
171
- const pathname = parsedUrl.pathname || '/';
83
+ const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
84
+ const pathname = parsedUrl.pathname;
172
85
 
173
86
  try {
174
87
  // OpenAI-compatible endpoints
@@ -303,8 +216,9 @@ export class OpenAIAdapter extends BaseAdapter {
303
216
  // 404
304
217
  sendJson(res, 404, { error: { message: 'Not found', type: 'not_found' } });
305
218
 
306
- } catch (error: any) {
307
- sendJson(res, 500, { error: { message: error.message, type: 'internal_error' } });
219
+ } catch (error: unknown) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ sendJson(res, 500, { error: { message, type: 'internal_error' } });
308
222
  }
309
223
  });
310
224
 
@@ -1,11 +1,11 @@
1
1
  import { BaseAdapter } from './base.js';
2
2
  import { EnvCPConfig, RESTResponse, ToolDefinition } from '../types.js';
3
- import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
3
+ import { setCorsHeaders, sendJson, parseBody, validateApiKey, RateLimiter, rateLimitMiddleware } from '../utils/http.js';
4
4
  import * as http from 'http';
5
- import * as url from 'url';
6
5
 
7
6
  export class RESTAdapter extends BaseAdapter {
8
7
  private server: http.Server | null = null;
8
+ private rateLimiter = new RateLimiter(60, 60000);
9
9
 
10
10
  constructor(config: EnvCPConfig, projectPath: string, password?: string) {
11
11
  super(config, projectPath, password);
@@ -39,7 +39,7 @@ export class RESTAdapter extends BaseAdapter {
39
39
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
40
40
  description: { type: 'string', description: 'Description' },
41
41
  },
42
- handler: async (params) => this.setVariable(params as any),
42
+ handler: async (params) => this.setVariable(params as { name: string; value: string; tags?: string[]; description?: string }),
43
43
  },
44
44
  {
45
45
  name: 'envcp_delete',
@@ -100,7 +100,7 @@ export class RESTAdapter extends BaseAdapter {
100
100
  await this.init();
101
101
 
102
102
  this.server = http.createServer(async (req, res) => {
103
- setCorsHeaders(res);
103
+ setCorsHeaders(res, undefined, req.headers.origin);
104
104
 
105
105
  if (req.method === 'OPTIONS') {
106
106
  res.writeHead(204);
@@ -108,6 +108,10 @@ export class RESTAdapter extends BaseAdapter {
108
108
  return;
109
109
  }
110
110
 
111
+ if (!rateLimitMiddleware(this.rateLimiter, req, res)) {
112
+ return;
113
+ }
114
+
111
115
  // API key validation
112
116
  if (apiKey) {
113
117
  const providedKey = (req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '')) as string | undefined;
@@ -117,8 +121,8 @@ export class RESTAdapter extends BaseAdapter {
117
121
  }
118
122
  }
119
123
 
120
- const parsedUrl = url.parse(req.url || '/', true);
121
- const pathname = parsedUrl.pathname || '/';
124
+ const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
125
+ const pathname = parsedUrl.pathname;
122
126
  const segments = pathname.split('/').filter(Boolean);
123
127
 
124
128
  try {
@@ -171,9 +175,8 @@ export class RESTAdapter extends BaseAdapter {
171
175
  const varName = segments[2];
172
176
 
173
177
  if (!varName && req.method === 'GET') {
174
- const tags = parsedUrl.query.tags
175
- ? (Array.isArray(parsedUrl.query.tags) ? parsedUrl.query.tags : [parsedUrl.query.tags])
176
- : undefined;
178
+ const tagsParam = parsedUrl.searchParams.getAll('tags');
179
+ const tags = tagsParam.length > 0 ? tagsParam : undefined;
177
180
  const result = await this.callTool('envcp_list', { tags });
178
181
  sendJson(res, 200, this.createResponse(true, result));
179
182
  return;
@@ -187,7 +190,7 @@ export class RESTAdapter extends BaseAdapter {
187
190
  }
188
191
 
189
192
  if (varName && req.method === 'GET') {
190
- const showValue = parsedUrl.query.show_value === 'true';
193
+ const showValue = parsedUrl.searchParams.get('show_value') === 'true';
191
194
  const result = await this.callTool('envcp_get', { name: varName, show_value: showValue });
192
195
  sendJson(res, 200, this.createResponse(true, result));
193
196
  return;
@@ -233,11 +236,12 @@ export class RESTAdapter extends BaseAdapter {
233
236
  // 404
234
237
  sendJson(res, 404, this.createResponse(false, undefined, 'Not found'));
235
238
 
236
- } catch (error: any) {
237
- const status = error.message.includes('locked') ? 401 :
238
- error.message.includes('not found') ? 404 :
239
- error.message.includes('disabled') ? 403 : 500;
240
- sendJson(res, status, this.createResponse(false, undefined, error.message));
239
+ } catch (error: unknown) {
240
+ const message = error instanceof Error ? error.message : String(error);
241
+ const status = message.includes('locked') ? 401 :
242
+ message.includes('not found') ? 404 :
243
+ message.includes('disabled') ? 403 : 500;
244
+ sendJson(res, status, this.createResponse(false, undefined, message));
241
245
  }
242
246
  });
243
247