@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.
- 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
package/src/server/unified.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { RESTAdapter } from '../adapters/rest.js';
|
|
|
3
3
|
import { OpenAIAdapter } from '../adapters/openai.js';
|
|
4
4
|
import { GeminiAdapter } from '../adapters/gemini.js';
|
|
5
5
|
import { EnvCPServer } from '../mcp/server.js';
|
|
6
|
-
import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
|
|
6
|
+
import { setCorsHeaders, sendJson, parseBody, validateApiKey, RateLimiter, rateLimitMiddleware } from '../utils/http.js';
|
|
7
7
|
import * as http from 'http';
|
|
8
|
-
import * as url from 'url';
|
|
9
8
|
|
|
10
9
|
export class UnifiedServer {
|
|
11
10
|
private config: EnvCPConfig;
|
|
@@ -18,6 +17,7 @@ export class UnifiedServer {
|
|
|
18
17
|
private geminiAdapter: GeminiAdapter | null = null;
|
|
19
18
|
private mcpServer: EnvCPServer | null = null;
|
|
20
19
|
private httpServer: http.Server | null = null;
|
|
20
|
+
private rateLimiter: RateLimiter = new RateLimiter(60, 60000);
|
|
21
21
|
|
|
22
22
|
constructor(config: EnvCPConfig, serverConfig: ServerConfig, projectPath: string, password?: string) {
|
|
23
23
|
this.config = config;
|
|
@@ -29,8 +29,7 @@ export class UnifiedServer {
|
|
|
29
29
|
// Detect client type from request headers
|
|
30
30
|
detectClientType(req: http.IncomingMessage): ClientType {
|
|
31
31
|
const userAgent = req.headers['user-agent']?.toLowerCase() || '';
|
|
32
|
-
const
|
|
33
|
-
const pathname = url.parse(req.url || '/', true).pathname || '/';
|
|
32
|
+
const pathname = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`).pathname;
|
|
34
33
|
|
|
35
34
|
// Check for OpenAI-style requests
|
|
36
35
|
if (pathname.startsWith('/v1/chat') ||
|
|
@@ -111,7 +110,7 @@ export class UnifiedServer {
|
|
|
111
110
|
|
|
112
111
|
// Auto or All mode - unified server that routes based on detection
|
|
113
112
|
this.httpServer = http.createServer(async (req, res) => {
|
|
114
|
-
setCorsHeaders(res);
|
|
113
|
+
setCorsHeaders(res, undefined, req.headers.origin);
|
|
115
114
|
|
|
116
115
|
if (req.method === 'OPTIONS') {
|
|
117
116
|
res.writeHead(204);
|
|
@@ -119,6 +118,10 @@ export class UnifiedServer {
|
|
|
119
118
|
return;
|
|
120
119
|
}
|
|
121
120
|
|
|
121
|
+
if (!rateLimitMiddleware(this.rateLimiter, req, res)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
122
125
|
// API key validation
|
|
123
126
|
if (api_key) {
|
|
124
127
|
const providedKey = (req.headers['x-api-key'] ||
|
|
@@ -130,8 +133,8 @@ export class UnifiedServer {
|
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
const parsedUrl =
|
|
134
|
-
const pathname = parsedUrl.pathname
|
|
136
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
137
|
+
const pathname = parsedUrl.pathname;
|
|
135
138
|
|
|
136
139
|
// Root endpoint - show server info and detected mode
|
|
137
140
|
if (pathname === '/' && req.method === 'GET') {
|
|
@@ -159,7 +162,7 @@ export class UnifiedServer {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
// Force mode query param
|
|
162
|
-
const forceMode = parsedUrl.
|
|
165
|
+
const forceMode = parsedUrl.searchParams.get('mode') || undefined;
|
|
163
166
|
if (forceMode && ['rest', 'openai', 'gemini'].includes(forceMode)) {
|
|
164
167
|
clientType = forceMode as ClientType;
|
|
165
168
|
}
|
|
@@ -201,11 +204,19 @@ export class UnifiedServer {
|
|
|
201
204
|
},
|
|
202
205
|
});
|
|
203
206
|
|
|
204
|
-
} catch (error:
|
|
205
|
-
|
|
207
|
+
} catch (error: unknown) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
sendJson(res, 500, { error: message });
|
|
206
210
|
}
|
|
207
211
|
});
|
|
208
212
|
|
|
213
|
+
const shutdown = () => {
|
|
214
|
+
this.stop();
|
|
215
|
+
process.exit(0);
|
|
216
|
+
};
|
|
217
|
+
process.on('SIGTERM', shutdown);
|
|
218
|
+
process.on('SIGINT', shutdown);
|
|
219
|
+
|
|
209
220
|
return new Promise((resolve) => {
|
|
210
221
|
this.httpServer!.listen(port, host, () => {
|
|
211
222
|
resolve();
|
|
@@ -215,7 +226,7 @@ export class UnifiedServer {
|
|
|
215
226
|
|
|
216
227
|
private async handleRESTRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
217
228
|
// Delegate to REST adapter's internal handling
|
|
218
|
-
const parsedUrl =
|
|
229
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
219
230
|
const pathname = parsedUrl.pathname || '/';
|
|
220
231
|
const segments = pathname.split('/').filter(Boolean);
|
|
221
232
|
|
|
@@ -224,7 +235,7 @@ export class UnifiedServer {
|
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
|
|
227
|
-
const body = await parseBody(req);
|
|
238
|
+
const body = (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') ? await parseBody(req) : {};
|
|
228
239
|
|
|
229
240
|
try {
|
|
230
241
|
if (segments[0] === 'api') {
|
|
@@ -262,7 +273,7 @@ export class UnifiedServer {
|
|
|
262
273
|
return;
|
|
263
274
|
}
|
|
264
275
|
if (segments[2] && req.method === 'GET') {
|
|
265
|
-
const result = await this.restAdapter.callTool('envcp_get', { name: segments[2], show_value: parsedUrl.
|
|
276
|
+
const result = await this.restAdapter.callTool('envcp_get', { name: segments[2], show_value: parsedUrl.searchParams.get('show_value') === 'true' });
|
|
266
277
|
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
267
278
|
return;
|
|
268
279
|
}
|
|
@@ -295,8 +306,9 @@ export class UnifiedServer {
|
|
|
295
306
|
|
|
296
307
|
sendJson(res, 404, { success: false, error: 'Not found', timestamp: new Date().toISOString() });
|
|
297
308
|
|
|
298
|
-
} catch (error:
|
|
299
|
-
|
|
309
|
+
} catch (error: unknown) {
|
|
310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
311
|
+
sendJson(res, 500, { success: false, error: message, timestamp: new Date().toISOString() });
|
|
300
312
|
}
|
|
301
313
|
}
|
|
302
314
|
|
|
@@ -306,9 +318,9 @@ export class UnifiedServer {
|
|
|
306
318
|
return;
|
|
307
319
|
}
|
|
308
320
|
|
|
309
|
-
const parsedUrl =
|
|
321
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
310
322
|
const pathname = parsedUrl.pathname || '/';
|
|
311
|
-
const body = await parseBody(req);
|
|
323
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
312
324
|
|
|
313
325
|
try {
|
|
314
326
|
if (pathname === '/v1/models' && req.method === 'GET') {
|
|
@@ -366,8 +378,9 @@ export class UnifiedServer {
|
|
|
366
378
|
|
|
367
379
|
sendJson(res, 404, { error: { message: 'Not found', type: 'not_found' } });
|
|
368
380
|
|
|
369
|
-
} catch (error:
|
|
370
|
-
|
|
381
|
+
} catch (error: unknown) {
|
|
382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
383
|
+
sendJson(res, 500, { error: { message, type: 'internal_error' } });
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
386
|
|
|
@@ -377,9 +390,9 @@ export class UnifiedServer {
|
|
|
377
390
|
return;
|
|
378
391
|
}
|
|
379
392
|
|
|
380
|
-
const parsedUrl =
|
|
393
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
381
394
|
const pathname = parsedUrl.pathname || '/';
|
|
382
|
-
const body = await parseBody(req);
|
|
395
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
383
396
|
|
|
384
397
|
try {
|
|
385
398
|
if (pathname === '/v1/tools' && req.method === 'GET') {
|
|
@@ -436,8 +449,9 @@ export class UnifiedServer {
|
|
|
436
449
|
|
|
437
450
|
sendJson(res, 404, { error: { code: 404, message: 'Not found', status: 'NOT_FOUND' } });
|
|
438
451
|
|
|
439
|
-
} catch (error:
|
|
440
|
-
|
|
452
|
+
} catch (error: unknown) {
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
sendJson(res, 500, { error: { code: 500, message, status: 'INTERNAL' } });
|
|
441
455
|
}
|
|
442
456
|
}
|
|
443
457
|
|
package/src/storage/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export class StorageManager {
|
|
|
7
7
|
private storePath: string;
|
|
8
8
|
private encrypted: boolean;
|
|
9
9
|
private password?: string;
|
|
10
|
+
private cache: Record<string, Variable> | null = null;
|
|
10
11
|
|
|
11
12
|
constructor(storePath: string, encrypted: boolean = true) {
|
|
12
13
|
this.storePath = storePath;
|
|
@@ -14,31 +15,42 @@ export class StorageManager {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
setPassword(password: string): void {
|
|
17
|
-
this.password
|
|
18
|
+
if (this.password !== password) {
|
|
19
|
+
this.password = password;
|
|
20
|
+
this.cache = null;
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
async load(): Promise<Record<string, Variable>> {
|
|
25
|
+
if (this.cache !== null) {
|
|
26
|
+
return this.cache;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
if (!await fs.pathExists(this.storePath)) {
|
|
22
|
-
|
|
30
|
+
this.cache = {};
|
|
31
|
+
return this.cache;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
const data = await fs.readFile(this.storePath, 'utf8');
|
|
26
|
-
|
|
35
|
+
|
|
27
36
|
if (this.encrypted && this.password) {
|
|
28
37
|
try {
|
|
29
38
|
const decrypted = decrypt(data, this.password);
|
|
30
|
-
|
|
39
|
+
this.cache = JSON.parse(decrypted);
|
|
40
|
+
return this.cache!;
|
|
31
41
|
} catch (error) {
|
|
32
42
|
throw new Error('Failed to decrypt storage. Invalid password or corrupted data.');
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
this.cache = JSON.parse(data);
|
|
47
|
+
return this.cache!;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
async save(variables: Record<string, Variable>): Promise<void> {
|
|
51
|
+
this.cache = variables;
|
|
40
52
|
const data = JSON.stringify(variables, null, 2);
|
|
41
|
-
|
|
53
|
+
|
|
42
54
|
await fs.ensureDir(path.dirname(this.storePath));
|
|
43
55
|
|
|
44
56
|
if (this.encrypted && this.password) {
|
|
@@ -78,6 +90,10 @@ export class StorageManager {
|
|
|
78
90
|
async exists(): Promise<boolean> {
|
|
79
91
|
return fs.pathExists(this.storePath);
|
|
80
92
|
}
|
|
93
|
+
|
|
94
|
+
invalidateCache(): void {
|
|
95
|
+
this.cache = null;
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
export class LogManager {
|
package/src/types.ts
CHANGED
|
@@ -16,8 +16,10 @@ export const EnvCPConfigSchema = z.object({
|
|
|
16
16
|
allow_ai_write: z.boolean().default(false),
|
|
17
17
|
allow_ai_delete: z.boolean().default(false),
|
|
18
18
|
allow_ai_export: z.boolean().default(false),
|
|
19
|
+
allow_ai_execute: z.boolean().default(false),
|
|
19
20
|
allow_ai_active_check: z.boolean().default(false),
|
|
20
21
|
require_user_reference: z.boolean().default(true),
|
|
22
|
+
allowed_commands: z.array(z.string()).optional(),
|
|
21
23
|
require_confirmation: z.boolean().default(true),
|
|
22
24
|
mask_values: z.boolean().default(true),
|
|
23
25
|
audit_log: z.boolean().default(true),
|
package/src/utils/http.ts
CHANGED
|
@@ -3,8 +3,14 @@ import * as http from 'http';
|
|
|
3
3
|
|
|
4
4
|
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
5
5
|
|
|
6
|
-
export function setCorsHeaders(res: http.ServerResponse, allowedOrigin
|
|
7
|
-
|
|
6
|
+
export function setCorsHeaders(res: http.ServerResponse, allowedOrigin?: string, requestOrigin?: string): void {
|
|
7
|
+
const localOrigins = ['http://127.0.0.1', 'http://localhost', 'http://[::1]'];
|
|
8
|
+
let origin = allowedOrigin || '*';
|
|
9
|
+
if (!allowedOrigin && requestOrigin) {
|
|
10
|
+
const matches = localOrigins.some(lo => requestOrigin.startsWith(lo));
|
|
11
|
+
origin = matches ? requestOrigin : '';
|
|
12
|
+
}
|
|
13
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
8
14
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
9
15
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Goog-Api-Key, OpenAI-Organization');
|
|
10
16
|
}
|
|
@@ -43,3 +49,71 @@ export function validateApiKey(provided: string | undefined, expected: string):
|
|
|
43
49
|
if (provided.length !== expected.length) return false;
|
|
44
50
|
return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
|
|
45
51
|
}
|
|
52
|
+
|
|
53
|
+
export class RateLimiter {
|
|
54
|
+
private requests: Map<string, number[]> = new Map();
|
|
55
|
+
private maxRequests: number;
|
|
56
|
+
private windowMs: number;
|
|
57
|
+
private cleanupTimer: ReturnType<typeof setInterval>;
|
|
58
|
+
|
|
59
|
+
constructor(maxRequests: number = 60, windowMs: number = 60000) {
|
|
60
|
+
this.maxRequests = maxRequests;
|
|
61
|
+
this.windowMs = windowMs;
|
|
62
|
+
// Periodically clean up stale entries
|
|
63
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), windowMs * 2);
|
|
64
|
+
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isAllowed(key: string): boolean {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const timestamps = this.requests.get(key) || [];
|
|
70
|
+
const recent = timestamps.filter(t => now - t < this.windowMs);
|
|
71
|
+
|
|
72
|
+
if (recent.length >= this.maxRequests) {
|
|
73
|
+
this.requests.set(key, recent);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
recent.push(now);
|
|
78
|
+
this.requests.set(key, recent);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getRemainingRequests(key: string): number {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const timestamps = this.requests.get(key) || [];
|
|
85
|
+
const recent = timestamps.filter(t => now - t < this.windowMs);
|
|
86
|
+
return Math.max(0, this.maxRequests - recent.length);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private cleanup(): void {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
for (const [key, timestamps] of this.requests) {
|
|
92
|
+
const recent = timestamps.filter(t => now - t < this.windowMs);
|
|
93
|
+
if (recent.length === 0) {
|
|
94
|
+
this.requests.delete(key);
|
|
95
|
+
} else {
|
|
96
|
+
this.requests.set(key, recent);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
destroy(): void {
|
|
102
|
+
clearInterval(this.cleanupTimer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function rateLimitMiddleware(
|
|
107
|
+
limiter: RateLimiter,
|
|
108
|
+
req: http.IncomingMessage,
|
|
109
|
+
res: http.ServerResponse
|
|
110
|
+
): boolean {
|
|
111
|
+
const key = req.socket.remoteAddress || 'unknown';
|
|
112
|
+
if (!limiter.isAllowed(key)) {
|
|
113
|
+
res.setHeader('Retry-After', '60');
|
|
114
|
+
sendJson(res, 429, { error: 'Too many requests' });
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
res.setHeader('X-RateLimit-Remaining', String(limiter.getRemainingRequests(key)));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
package/src/utils/session.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { Session, SessionSchema } from '../types.js';
|
|
4
4
|
import { generateId, encrypt, decrypt } from './crypto.js';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
5
6
|
|
|
6
7
|
export class SessionManager {
|
|
7
8
|
private sessionPath: string;
|
|
@@ -23,7 +24,7 @@ export class SessionManager {
|
|
|
23
24
|
async create(password: string): Promise<Session> {
|
|
24
25
|
const now = new Date();
|
|
25
26
|
const expires = new Date(now.getTime() + this.timeoutMinutes * 60 * 1000);
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
this.session = {
|
|
28
29
|
id: generateId(),
|
|
29
30
|
created: now.toISOString(),
|
|
@@ -31,17 +32,19 @@ export class SessionManager {
|
|
|
31
32
|
extensions: 0,
|
|
32
33
|
last_access: now.toISOString(),
|
|
33
34
|
};
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
this.password = password;
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
// Store a verification hash instead of the raw password
|
|
39
|
+
const passwordHash = crypto.createHash('sha256').update(password).digest('hex');
|
|
37
40
|
const sessionData = JSON.stringify({
|
|
38
41
|
session: this.session,
|
|
39
|
-
|
|
42
|
+
passwordHash,
|
|
40
43
|
});
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
const encrypted = encrypt(sessionData, password);
|
|
43
46
|
await fs.writeFile(this.sessionPath, encrypted, 'utf8');
|
|
44
|
-
|
|
47
|
+
|
|
45
48
|
return this.session;
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -61,7 +64,8 @@ export class SessionManager {
|
|
|
61
64
|
const decrypted = decrypt(encrypted, pwd);
|
|
62
65
|
const data = JSON.parse(decrypted);
|
|
63
66
|
this.session = SessionSchema.parse(data.session);
|
|
64
|
-
|
|
67
|
+
// Password is verified by successful decryption — no longer stored in file
|
|
68
|
+
this.password = pwd;
|
|
65
69
|
|
|
66
70
|
if (new Date() > new Date(this.session.expires)) {
|
|
67
71
|
await this.destroy();
|
|
@@ -102,9 +106,10 @@ export class SessionManager {
|
|
|
102
106
|
this.session.extensions += 1;
|
|
103
107
|
this.session.last_access = now.toISOString();
|
|
104
108
|
|
|
109
|
+
const passwordHash = crypto.createHash('sha256').update(this.password).digest('hex');
|
|
105
110
|
const sessionData = JSON.stringify({
|
|
106
111
|
session: this.session,
|
|
107
|
-
|
|
112
|
+
passwordHash,
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
const encrypted = encrypt(sessionData, this.password);
|