@fentz26/envcp 1.0.2 → 1.0.4
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 +13 -6
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +5 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/cli/index.js +462 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts +6 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +243 -6
- package/dist/config/manager.js.map +1 -1
- package/dist/storage/index.d.ts +10 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +89 -6
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -0
- package/dist/utils/crypto.js.map +1 -1
- package/package.json +6 -1
- package/.github/workflows/publish.yml +0 -48
- package/__tests__/config.test.ts +0 -65
- package/__tests__/crypto.test.ts +0 -76
- package/__tests__/http.test.ts +0 -49
- package/__tests__/storage.test.ts +0 -94
- package/jest.config.js +0 -11
- package/src/adapters/base.ts +0 -542
- package/src/adapters/gemini.ts +0 -228
- package/src/adapters/index.ts +0 -4
- package/src/adapters/openai.ts +0 -238
- package/src/adapters/rest.ts +0 -298
- package/src/cli/index.ts +0 -516
- package/src/cli.ts +0 -2
- package/src/config/manager.ts +0 -137
- package/src/index.ts +0 -4
- package/src/mcp/index.ts +0 -1
- package/src/mcp/server.ts +0 -67
- package/src/server/index.ts +0 -1
- package/src/server/unified.ts +0 -474
- package/src/storage/index.ts +0 -128
- package/src/types.ts +0 -183
- package/src/utils/crypto.ts +0 -100
- package/src/utils/http.ts +0 -119
- package/src/utils/session.ts +0 -146
- package/tsconfig.json +0 -20
package/src/types.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const EnvCPConfigSchema = z.object({
|
|
4
|
-
version: z.string().default('1.0'),
|
|
5
|
-
project: z.string().optional(),
|
|
6
|
-
|
|
7
|
-
storage: z.object({
|
|
8
|
-
path: z.string().default('.envcp/store.enc'),
|
|
9
|
-
encrypted: z.boolean().default(true),
|
|
10
|
-
algorithm: z.enum(['aes-256-gcm', 'aes-256-cbc']).default('aes-256-gcm'),
|
|
11
|
-
compression: z.boolean().default(false),
|
|
12
|
-
}).default({}),
|
|
13
|
-
|
|
14
|
-
access: z.object({
|
|
15
|
-
allow_ai_read: z.boolean().default(false),
|
|
16
|
-
allow_ai_write: z.boolean().default(false),
|
|
17
|
-
allow_ai_delete: z.boolean().default(false),
|
|
18
|
-
allow_ai_export: z.boolean().default(false),
|
|
19
|
-
allow_ai_execute: z.boolean().default(false),
|
|
20
|
-
allow_ai_active_check: z.boolean().default(false),
|
|
21
|
-
require_user_reference: z.boolean().default(true),
|
|
22
|
-
allowed_commands: z.array(z.string()).optional(),
|
|
23
|
-
require_confirmation: z.boolean().default(true),
|
|
24
|
-
mask_values: z.boolean().default(true),
|
|
25
|
-
audit_log: z.boolean().default(true),
|
|
26
|
-
allowed_patterns: z.array(z.string()).optional(),
|
|
27
|
-
denied_patterns: z.array(z.string()).optional(),
|
|
28
|
-
blacklist_patterns: z.array(z.string()).default([]),
|
|
29
|
-
}).default({}),
|
|
30
|
-
|
|
31
|
-
sync: z.object({
|
|
32
|
-
enabled: z.boolean().default(false),
|
|
33
|
-
target: z.string().default('.env'),
|
|
34
|
-
exclude: z.array(z.string()).default([]),
|
|
35
|
-
include: z.array(z.string()).optional(),
|
|
36
|
-
format: z.enum(['dotenv', 'json', 'yaml']).default('dotenv'),
|
|
37
|
-
header: z.string().optional(),
|
|
38
|
-
}).default({}),
|
|
39
|
-
|
|
40
|
-
session: z.object({
|
|
41
|
-
enabled: z.boolean().default(true),
|
|
42
|
-
timeout_minutes: z.number().default(30),
|
|
43
|
-
max_extensions: z.number().default(5),
|
|
44
|
-
path: z.string().default('.envcp/.session'),
|
|
45
|
-
}).default({}),
|
|
46
|
-
|
|
47
|
-
password: z.object({
|
|
48
|
-
min_length: z.number().default(1),
|
|
49
|
-
require_complexity: z.boolean().default(false),
|
|
50
|
-
allow_numeric_only: z.boolean().default(true),
|
|
51
|
-
allow_single_char: z.boolean().default(true),
|
|
52
|
-
}).default({}),
|
|
53
|
-
|
|
54
|
-
variables: z.record(z.object({
|
|
55
|
-
value: z.string(),
|
|
56
|
-
encrypted: z.boolean().default(false),
|
|
57
|
-
tags: z.array(z.string()).optional(),
|
|
58
|
-
description: z.string().optional(),
|
|
59
|
-
created: z.string().optional(),
|
|
60
|
-
updated: z.string().optional(),
|
|
61
|
-
accessed: z.string().optional(),
|
|
62
|
-
sync_to_env: z.boolean().default(true),
|
|
63
|
-
})).optional(),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export type EnvCPConfig = z.infer<typeof EnvCPConfigSchema>;
|
|
67
|
-
|
|
68
|
-
export const VariableSchema = z.object({
|
|
69
|
-
name: z.string(),
|
|
70
|
-
value: z.string(),
|
|
71
|
-
encrypted: z.boolean().default(false),
|
|
72
|
-
tags: z.array(z.string()).optional(),
|
|
73
|
-
description: z.string().optional(),
|
|
74
|
-
created: z.string(),
|
|
75
|
-
updated: z.string(),
|
|
76
|
-
accessed: z.string().optional(),
|
|
77
|
-
sync_to_env: z.boolean().default(true),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export type Variable = z.infer<typeof VariableSchema>;
|
|
81
|
-
|
|
82
|
-
export const OperationLogSchema = z.object({
|
|
83
|
-
timestamp: z.string(),
|
|
84
|
-
operation: z.enum(['add', 'get', 'update', 'delete', 'list', 'sync', 'export', 'unlock', 'lock', 'check_access']),
|
|
85
|
-
variable: z.string().optional(),
|
|
86
|
-
source: z.enum(['cli', 'mcp', 'api']),
|
|
87
|
-
success: z.boolean(),
|
|
88
|
-
message: z.string().optional(),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
export type OperationLog = z.infer<typeof OperationLogSchema>;
|
|
92
|
-
|
|
93
|
-
export const SessionSchema = z.object({
|
|
94
|
-
id: z.string(),
|
|
95
|
-
created: z.string(),
|
|
96
|
-
expires: z.string(),
|
|
97
|
-
extensions: z.number().default(0),
|
|
98
|
-
last_access: z.string(),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
export type Session = z.infer<typeof SessionSchema>;
|
|
102
|
-
|
|
103
|
-
// Server mode types
|
|
104
|
-
export const ServerModeSchema = z.enum(['mcp', 'rest', 'openai', 'gemini', 'all', 'auto']);
|
|
105
|
-
export type ServerMode = z.infer<typeof ServerModeSchema>;
|
|
106
|
-
|
|
107
|
-
export const ServerConfigSchema = z.object({
|
|
108
|
-
mode: ServerModeSchema.default('auto'),
|
|
109
|
-
port: z.number().default(3456),
|
|
110
|
-
host: z.string().default('127.0.0.1'),
|
|
111
|
-
cors: z.boolean().default(true),
|
|
112
|
-
api_key: z.string().optional(),
|
|
113
|
-
auto_detect: z.boolean().default(true),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
|
|
117
|
-
|
|
118
|
-
// Tool definition for adapters
|
|
119
|
-
export interface ToolDefinition {
|
|
120
|
-
name: string;
|
|
121
|
-
description: string;
|
|
122
|
-
parameters: Record<string, unknown>;
|
|
123
|
-
handler: (params: Record<string, unknown>) => Promise<unknown>;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// OpenAI function calling format
|
|
127
|
-
export interface OpenAIFunction {
|
|
128
|
-
name: string;
|
|
129
|
-
description: string;
|
|
130
|
-
parameters: {
|
|
131
|
-
type: 'object';
|
|
132
|
-
properties: Record<string, unknown>;
|
|
133
|
-
required?: string[];
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface OpenAIToolCall {
|
|
138
|
-
id: string;
|
|
139
|
-
type: 'function';
|
|
140
|
-
function: {
|
|
141
|
-
name: string;
|
|
142
|
-
arguments: string;
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface OpenAIMessage {
|
|
147
|
-
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
148
|
-
content: string | null;
|
|
149
|
-
tool_calls?: OpenAIToolCall[];
|
|
150
|
-
tool_call_id?: string;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Gemini function calling format
|
|
154
|
-
export interface GeminiFunctionDeclaration {
|
|
155
|
-
name: string;
|
|
156
|
-
description: string;
|
|
157
|
-
parameters: {
|
|
158
|
-
type: 'object';
|
|
159
|
-
properties: Record<string, unknown>;
|
|
160
|
-
required?: string[];
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface GeminiFunctionCall {
|
|
165
|
-
name: string;
|
|
166
|
-
args: Record<string, unknown>;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface GeminiFunctionResponse {
|
|
170
|
-
name: string;
|
|
171
|
-
response: Record<string, unknown>;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// REST API types
|
|
175
|
-
export interface RESTResponse<T = unknown> {
|
|
176
|
-
success: boolean;
|
|
177
|
-
data?: T;
|
|
178
|
-
error?: string;
|
|
179
|
-
timestamp: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Detected client type
|
|
183
|
-
export type ClientType = 'mcp' | 'openai' | 'gemini' | 'rest' | 'unknown';
|
package/src/utils/crypto.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
|
-
|
|
3
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
-
const IV_LENGTH = 16;
|
|
5
|
-
const AUTH_TAG_LENGTH = 16;
|
|
6
|
-
const SALT_LENGTH = 64;
|
|
7
|
-
const ITERATIONS = 100000;
|
|
8
|
-
|
|
9
|
-
export function deriveKey(password: string, salt: Buffer): Buffer {
|
|
10
|
-
return crypto.pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha512');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function encrypt(text: string, password: string): string {
|
|
14
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
15
|
-
const key = deriveKey(password, salt);
|
|
16
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
17
|
-
|
|
18
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
19
|
-
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
20
|
-
encrypted += cipher.final('hex');
|
|
21
|
-
|
|
22
|
-
const authTag = cipher.getAuthTag();
|
|
23
|
-
|
|
24
|
-
return salt.toString('hex') + iv.toString('hex') + authTag.toString('hex') + encrypted;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function decrypt(encryptedData: string, password: string): string {
|
|
28
|
-
const salt = Buffer.from(encryptedData.slice(0, SALT_LENGTH * 2), 'hex');
|
|
29
|
-
const iv = Buffer.from(encryptedData.slice(SALT_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2), 'hex');
|
|
30
|
-
const authTag = Buffer.from(encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2), 'hex');
|
|
31
|
-
const encrypted = encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2);
|
|
32
|
-
|
|
33
|
-
const key = deriveKey(password, salt);
|
|
34
|
-
|
|
35
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
36
|
-
decipher.setAuthTag(authTag);
|
|
37
|
-
|
|
38
|
-
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
39
|
-
decrypted += decipher.final('utf8');
|
|
40
|
-
|
|
41
|
-
return decrypted;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function generateId(): string {
|
|
45
|
-
return crypto.randomBytes(16).toString('hex');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function generateSessionToken(): string {
|
|
49
|
-
return crypto.randomBytes(32).toString('hex');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function maskValue(value: string, showLength: number = 4): string {
|
|
53
|
-
if (value.length <= showLength * 2) {
|
|
54
|
-
return '*'.repeat(value.length);
|
|
55
|
-
}
|
|
56
|
-
return value.slice(0, showLength) + '*'.repeat(value.length - showLength * 2) + value.slice(-showLength);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function validatePassword(password: string, config: {
|
|
60
|
-
min_length?: number;
|
|
61
|
-
require_complexity?: boolean;
|
|
62
|
-
allow_numeric_only?: boolean;
|
|
63
|
-
allow_single_char?: boolean;
|
|
64
|
-
}): { valid: boolean; error?: string } {
|
|
65
|
-
const minLength = config.min_length ?? 1;
|
|
66
|
-
const requireComplexity = config.require_complexity ?? false;
|
|
67
|
-
const allowNumericOnly = config.allow_numeric_only ?? true;
|
|
68
|
-
const allowSingleChar = config.allow_single_char ?? true;
|
|
69
|
-
|
|
70
|
-
if (password.length < minLength) {
|
|
71
|
-
return { valid: false, error: `Password must be at least ${minLength} character(s)` };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!allowSingleChar && password.length === 1) {
|
|
75
|
-
return { valid: false, error: 'Single character passwords are not allowed' };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!allowNumericOnly && /^\d+$/.test(password)) {
|
|
79
|
-
return { valid: false, error: 'Numeric-only passwords are not allowed' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (requireComplexity) {
|
|
83
|
-
const hasLower = /[a-z]/.test(password);
|
|
84
|
-
const hasUpper = /[A-Z]/.test(password);
|
|
85
|
-
const hasNumber = /[0-9]/.test(password);
|
|
86
|
-
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
|
87
|
-
|
|
88
|
-
const complexityCount = [hasLower, hasUpper, hasNumber, hasSpecial].filter(Boolean).length;
|
|
89
|
-
|
|
90
|
-
if (complexityCount < 3) {
|
|
91
|
-
return { valid: false, error: 'Password must contain at least 3 of: lowercase, uppercase, numbers, special characters' };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { valid: true };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function quickHash(input: string): string {
|
|
99
|
-
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
100
|
-
}
|
package/src/utils/http.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
|
-
import * as http from 'http';
|
|
3
|
-
|
|
4
|
-
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
5
|
-
|
|
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);
|
|
14
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
15
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Goog-Api-Key, OpenAI-Organization');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sendJson(res: http.ServerResponse, status: number, data: unknown): void {
|
|
19
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
20
|
-
res.end(JSON.stringify(data));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function parseBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
let body = '';
|
|
26
|
-
let size = 0;
|
|
27
|
-
req.on('data', (chunk: Buffer) => {
|
|
28
|
-
size += chunk.length;
|
|
29
|
-
if (size > MAX_BODY_SIZE) {
|
|
30
|
-
req.destroy();
|
|
31
|
-
reject(new Error('Request body too large'));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
body += chunk;
|
|
35
|
-
});
|
|
36
|
-
req.on('end', () => {
|
|
37
|
-
try {
|
|
38
|
-
resolve(body ? JSON.parse(body) : {});
|
|
39
|
-
} catch {
|
|
40
|
-
reject(new Error('Invalid JSON body'));
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
req.on('error', reject);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function validateApiKey(provided: string | undefined, expected: string): boolean {
|
|
48
|
-
if (!provided) return false;
|
|
49
|
-
if (provided.length !== expected.length) return false;
|
|
50
|
-
return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
|
|
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
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs-extra';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { Session, SessionSchema } from '../types.js';
|
|
4
|
-
import { generateId, encrypt, decrypt } from './crypto.js';
|
|
5
|
-
import * as crypto from 'crypto';
|
|
6
|
-
|
|
7
|
-
export class SessionManager {
|
|
8
|
-
private sessionPath: string;
|
|
9
|
-
private session: Session | null = null;
|
|
10
|
-
private password: string | null = null;
|
|
11
|
-
private timeoutMinutes: number;
|
|
12
|
-
private maxExtensions: number;
|
|
13
|
-
|
|
14
|
-
constructor(sessionPath: string, timeoutMinutes: number = 30, maxExtensions: number = 5) {
|
|
15
|
-
this.sessionPath = sessionPath;
|
|
16
|
-
this.timeoutMinutes = timeoutMinutes;
|
|
17
|
-
this.maxExtensions = maxExtensions;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async init(): Promise<void> {
|
|
21
|
-
await fs.ensureDir(path.dirname(this.sessionPath));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async create(password: string): Promise<Session> {
|
|
25
|
-
const now = new Date();
|
|
26
|
-
const expires = new Date(now.getTime() + this.timeoutMinutes * 60 * 1000);
|
|
27
|
-
|
|
28
|
-
this.session = {
|
|
29
|
-
id: generateId(),
|
|
30
|
-
created: now.toISOString(),
|
|
31
|
-
expires: expires.toISOString(),
|
|
32
|
-
extensions: 0,
|
|
33
|
-
last_access: now.toISOString(),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
this.password = password;
|
|
37
|
-
|
|
38
|
-
// Store a verification hash instead of the raw password
|
|
39
|
-
const passwordHash = crypto.createHash('sha256').update(password).digest('hex');
|
|
40
|
-
const sessionData = JSON.stringify({
|
|
41
|
-
session: this.session,
|
|
42
|
-
passwordHash,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const encrypted = encrypt(sessionData, password);
|
|
46
|
-
await fs.writeFile(this.sessionPath, encrypted, 'utf8');
|
|
47
|
-
|
|
48
|
-
return this.session;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async load(password?: string): Promise<Session | null> {
|
|
52
|
-
if (!await fs.pathExists(this.sessionPath)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const encrypted = await fs.readFile(this.sessionPath, 'utf8');
|
|
58
|
-
|
|
59
|
-
const pwd = password || this.password;
|
|
60
|
-
if (!pwd) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const decrypted = decrypt(encrypted, pwd);
|
|
65
|
-
const data = JSON.parse(decrypted);
|
|
66
|
-
this.session = SessionSchema.parse(data.session);
|
|
67
|
-
// Password is verified by successful decryption — no longer stored in file
|
|
68
|
-
this.password = pwd;
|
|
69
|
-
|
|
70
|
-
if (new Date() > new Date(this.session.expires)) {
|
|
71
|
-
await this.destroy();
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return this.session;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async isValid(): Promise<boolean> {
|
|
82
|
-
if (!this.session) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return new Date() < new Date(this.session.expires);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async extend(): Promise<Session | null> {
|
|
90
|
-
if (!this.session || !this.password) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this.session.extensions >= this.maxExtensions) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!await this.isValid()) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const now = new Date();
|
|
103
|
-
const expires = new Date(now.getTime() + this.timeoutMinutes * 60 * 1000);
|
|
104
|
-
|
|
105
|
-
this.session.expires = expires.toISOString();
|
|
106
|
-
this.session.extensions += 1;
|
|
107
|
-
this.session.last_access = now.toISOString();
|
|
108
|
-
|
|
109
|
-
const passwordHash = crypto.createHash('sha256').update(this.password).digest('hex');
|
|
110
|
-
const sessionData = JSON.stringify({
|
|
111
|
-
session: this.session,
|
|
112
|
-
passwordHash,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const encrypted = encrypt(sessionData, this.password);
|
|
116
|
-
await fs.writeFile(this.sessionPath, encrypted, 'utf8');
|
|
117
|
-
|
|
118
|
-
return this.session;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async destroy(): Promise<void> {
|
|
122
|
-
this.session = null;
|
|
123
|
-
this.password = null;
|
|
124
|
-
|
|
125
|
-
if (await fs.pathExists(this.sessionPath)) {
|
|
126
|
-
await fs.unlink(this.sessionPath);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
getPassword(): string | null {
|
|
131
|
-
return this.password;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
getSession(): Session | null {
|
|
135
|
-
return this.session;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
getRemainingTime(): number {
|
|
139
|
-
if (!this.session) {
|
|
140
|
-
return 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const remaining = new Date(this.session.expires).getTime() - Date.now();
|
|
144
|
-
return Math.max(0, Math.floor(remaining / 1000 / 60));
|
|
145
|
-
}
|
|
146
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "Node16",
|
|
5
|
-
"moduleResolution": "Node16",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|