ezpm2gui 1.6.0 → 1.9.0
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 +40 -6
- package/dist/server/config/project-configs.json +3 -4
- package/dist/server/index.js +44 -5
- package/dist/server/routes/deployApplication.js +43 -41
- package/dist/server/routes/logStreaming.js +66 -28
- package/dist/server/routes/modules.js +55 -0
- package/dist/server/routes/pageAuth.js +3 -3
- package/dist/server/routes/remoteConnections.js +57 -21
- package/dist/server/routes/remoteMetrics.d.ts +3 -0
- package/dist/server/routes/remoteMetrics.js +84 -0
- package/dist/server/services/ProjectSetupService.d.ts +1 -1
- package/dist/server/services/ProjectSetupService.js +25 -9
- package/dist/server/utils/encryption.d.ts +22 -0
- package/dist/server/utils/encryption.js +53 -0
- package/dist/server/utils/metrics-history.d.ts +21 -0
- package/dist/server/utils/metrics-history.js +68 -0
- package/dist/server/utils/remote-connection.js +3 -3
- package/dist/server/utils/remote-metrics-db.d.ts +29 -0
- package/dist/server/utils/remote-metrics-db.js +134 -0
- package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
- package/dist/server/utils/remote-metrics-poller.js +67 -0
- package/package.json +15 -2
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/index.html +1 -1
- package/src/client/build/static/css/main.2836d066.css +5 -0
- package/src/client/build/static/css/main.2836d066.css.map +1 -0
- package/src/client/build/static/js/main.d5c19622.js +3 -0
- package/src/client/build/static/js/main.d5c19622.js.map +1 -0
- package/src/client/build/static/css/main.775772ee.css +0 -5
- package/src/client/build/static/css/main.775772ee.css.map +0 -1
- package/src/client/build/static/js/main.cbcb09c9.js +0 -3
- package/src/client/build/static/js/main.cbcb09c9.js.map +0 -1
- /package/src/client/build/static/js/{main.cbcb09c9.js.LICENSE.txt → main.d5c19622.js.LICENSE.txt} +0 -0
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const express_1 = __importDefault(require("express"));
|
|
7
7
|
const remote_connection_1 = require("../utils/remote-connection");
|
|
8
|
+
const encryption_1 = require("../utils/encryption");
|
|
8
9
|
const router = express_1.default.Router();
|
|
9
10
|
// @group Security : Validate remote log file paths before shell interpolation
|
|
10
11
|
const SHELL_UNSAFE_CHARS = /['"`;$|&<>(){}\\\n\r\0]/;
|
|
@@ -26,6 +27,29 @@ const safeLogLines = (raw) => {
|
|
|
26
27
|
return 200;
|
|
27
28
|
return Math.min(n, MAX_LOG_LINES);
|
|
28
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Decrypts a field that may be either a plain string (legacy/internal)
|
|
32
|
+
* or an EncryptedPayload object produced by the client-side hybrid encryptor.
|
|
33
|
+
*/
|
|
34
|
+
function decryptField(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof value === 'string')
|
|
38
|
+
return value;
|
|
39
|
+
try {
|
|
40
|
+
return (0, encryption_1.decryptTransitPayload)(value);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('Failed to decrypt field — possible tampering or key mismatch');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Expose RSA public key so the client can encrypt sensitive fields before sending.
|
|
48
|
+
* GET /api/remote/public-key
|
|
49
|
+
*/
|
|
50
|
+
router.get('/public-key', (_req, res) => {
|
|
51
|
+
res.json({ publicKey: (0, encryption_1.getRSAPublicKey)() });
|
|
52
|
+
});
|
|
29
53
|
/**
|
|
30
54
|
* Connect to an existing remote server
|
|
31
55
|
* POST /api/remote/:connectionId/connect
|
|
@@ -819,15 +843,19 @@ router.get('/:connectionId/log-file/download', async (req, res) => {
|
|
|
819
843
|
router.get('/connections', async (req, res) => {
|
|
820
844
|
try {
|
|
821
845
|
const connections = remote_connection_1.remoteConnectionManager.getAllConnections();
|
|
822
|
-
const connectionsList = Array.from(connections.entries()).map(([id, conn]) =>
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
846
|
+
const connectionsList = Array.from(connections.entries()).map(([id, conn]) => {
|
|
847
|
+
const connected = conn.isConnected();
|
|
848
|
+
return {
|
|
849
|
+
id,
|
|
850
|
+
name: conn.name || `${conn.username}@${conn.host}`,
|
|
851
|
+
host: conn.host,
|
|
852
|
+
port: conn.port,
|
|
853
|
+
username: conn.username,
|
|
854
|
+
connected,
|
|
855
|
+
isPM2Installed: conn.isPM2Installed,
|
|
856
|
+
status: connected ? 'connected' : 'disconnected',
|
|
857
|
+
};
|
|
858
|
+
});
|
|
831
859
|
res.json(connectionsList);
|
|
832
860
|
}
|
|
833
861
|
catch (error) {
|
|
@@ -844,17 +872,21 @@ router.get('/connections', async (req, res) => {
|
|
|
844
872
|
*/
|
|
845
873
|
router.post('/connections', (req, res) => {
|
|
846
874
|
try {
|
|
847
|
-
const
|
|
848
|
-
if (!
|
|
875
|
+
const body = req.body;
|
|
876
|
+
if (!body.name || !body.host || !body.username) {
|
|
849
877
|
return res.status(400).json({
|
|
850
878
|
success: false,
|
|
851
879
|
error: 'Missing required connection parameters: name, host, or username'
|
|
852
880
|
});
|
|
853
881
|
}
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
882
|
+
// Decrypt sensitive fields that may have been encrypted by the client
|
|
883
|
+
const connectionConfig = {
|
|
884
|
+
...body,
|
|
885
|
+
port: body.port || 22,
|
|
886
|
+
password: decryptField(body.password),
|
|
887
|
+
privateKey: decryptField(body.privateKey),
|
|
888
|
+
passphrase: decryptField(body.passphrase),
|
|
889
|
+
};
|
|
858
890
|
// Create the connection
|
|
859
891
|
const connectionId = remote_connection_1.remoteConnectionManager.createConnection(connectionConfig);
|
|
860
892
|
res.status(201).json({
|
|
@@ -877,17 +909,21 @@ router.post('/connections', (req, res) => {
|
|
|
877
909
|
router.put('/connections/:connectionId', async (req, res) => {
|
|
878
910
|
try {
|
|
879
911
|
const { connectionId } = req.params;
|
|
880
|
-
const
|
|
881
|
-
if (!
|
|
912
|
+
const body = req.body;
|
|
913
|
+
if (!body.name || !body.host || !body.username) {
|
|
882
914
|
return res.status(400).json({
|
|
883
915
|
success: false,
|
|
884
916
|
error: 'Missing required connection parameters: name, host, or username'
|
|
885
917
|
});
|
|
886
918
|
}
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
919
|
+
// Decrypt sensitive fields that may have been encrypted by the client
|
|
920
|
+
const connectionConfig = {
|
|
921
|
+
...body,
|
|
922
|
+
port: body.port || 22,
|
|
923
|
+
password: decryptField(body.password),
|
|
924
|
+
privateKey: decryptField(body.privateKey),
|
|
925
|
+
passphrase: decryptField(body.passphrase),
|
|
926
|
+
};
|
|
891
927
|
// Update the connection
|
|
892
928
|
const success = await remote_connection_1.remoteConnectionManager.updateConnection(connectionId, connectionConfig);
|
|
893
929
|
if (!success) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const remote_metrics_db_1 = require("../utils/remote-metrics-db");
|
|
8
|
+
const router = express_1.default.Router();
|
|
9
|
+
// @group Constants : Default and maximum query bounds
|
|
10
|
+
const DEFAULT_RANGE_MS = 3600000; // 1 hour
|
|
11
|
+
const MAX_RANGE_MS = 7 * 24 * 3600000; // 7 days
|
|
12
|
+
const MAX_POINTS = 500;
|
|
13
|
+
// @group Utilities : Parse and clamp a query-string integer
|
|
14
|
+
const parseIntParam = (raw, fallback, min, max) => {
|
|
15
|
+
const n = parseInt(raw, 10);
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return fallback;
|
|
18
|
+
return Math.max(min, Math.min(max, n));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* List all remote connections that have recorded metrics
|
|
22
|
+
* GET /api/remote-metrics/connections
|
|
23
|
+
*/
|
|
24
|
+
router.get('/connections', (_req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const connections = remote_metrics_db_1.remoteMetricsDB.getConnectionsWithData();
|
|
27
|
+
res.json({ success: true, connections });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
res.status(500).json({ success: false, error: err.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* List all process names recorded for a connection
|
|
35
|
+
* GET /api/remote-metrics/:connectionId/processes
|
|
36
|
+
*/
|
|
37
|
+
router.get('/:connectionId/processes', (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { connectionId } = req.params;
|
|
40
|
+
const names = remote_metrics_db_1.remoteMetricsDB.getProcessNames(connectionId);
|
|
41
|
+
res.json({ success: true, processes: names });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
res.status(500).json({ success: false, error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Latest snapshot (most recent metric point per process) for a connection
|
|
49
|
+
* GET /api/remote-metrics/:connectionId/snapshot
|
|
50
|
+
*/
|
|
51
|
+
router.get('/:connectionId/snapshot', (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const { connectionId } = req.params;
|
|
54
|
+
const snapshot = remote_metrics_db_1.remoteMetricsDB.getLatestSnapshot(connectionId);
|
|
55
|
+
res.json({ success: true, snapshot });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
res.status(500).json({ success: false, error: err.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Time-series metrics for one process on a connection
|
|
63
|
+
* GET /api/remote-metrics/:connectionId/:processName
|
|
64
|
+
* ?from=<unix-ms> (default: now - 1h)
|
|
65
|
+
* ?to=<unix-ms> (default: now)
|
|
66
|
+
* ?maxPoints=<n> (default: 500, max: 1000)
|
|
67
|
+
*/
|
|
68
|
+
router.get('/:connectionId/:processName', (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { connectionId, processName } = req.params;
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const to = parseIntParam(req.query.to, now, 0, now + 60000);
|
|
73
|
+
const from = parseIntParam(req.query.from, to - DEFAULT_RANGE_MS, 0, now);
|
|
74
|
+
// Clamp range to max window
|
|
75
|
+
const clampedFrom = Math.max(from, to - MAX_RANGE_MS);
|
|
76
|
+
const maxPoints = parseIntParam(req.query.maxPoints, MAX_POINTS, 50, 1000);
|
|
77
|
+
const metrics = remote_metrics_db_1.remoteMetricsDB.getMetrics(connectionId, processName, clampedFrom, to, maxPoints);
|
|
78
|
+
res.json({ success: true, metrics });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
res.status(500).json({ success: false, error: err.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
exports.default = router;
|
|
@@ -62,7 +62,7 @@ export declare class ProjectSetupService {
|
|
|
62
62
|
private ensureLogDirectory;
|
|
63
63
|
private log;
|
|
64
64
|
detectProjectType(projectPath: string): string | null;
|
|
65
|
-
setupProject(projectPath: string, projectType: string): Promise<SetupResult>;
|
|
65
|
+
setupProject(projectPath: string, projectType: string, onLog?: (msg: string) => void): Promise<SetupResult>;
|
|
66
66
|
private shouldSkipStep;
|
|
67
67
|
private executeStep;
|
|
68
68
|
private validateSetup;
|
|
@@ -78,8 +78,9 @@ class ProjectSetupService {
|
|
|
78
78
|
this.log('Could not detect project type');
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
async setupProject(projectPath, projectType) {
|
|
81
|
+
async setupProject(projectPath, projectType, onLog) {
|
|
82
82
|
this.log(`Starting setup for ${projectType} project at: ${projectPath}`);
|
|
83
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`Starting ${projectType} project setup at: ${projectPath}`);
|
|
83
84
|
const config = this.configs[projectType];
|
|
84
85
|
if (!config) {
|
|
85
86
|
throw new Error(`Unknown project type: ${projectType}`);
|
|
@@ -96,6 +97,7 @@ class ProjectSetupService {
|
|
|
96
97
|
try {
|
|
97
98
|
// Check if step should be skipped
|
|
98
99
|
if (this.shouldSkipStep(step, projectPath)) {
|
|
100
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[SKIP] ${step.name}`);
|
|
99
101
|
result.steps.push({
|
|
100
102
|
name: step.name,
|
|
101
103
|
success: true,
|
|
@@ -106,24 +108,31 @@ class ProjectSetupService {
|
|
|
106
108
|
continue;
|
|
107
109
|
}
|
|
108
110
|
this.log(`Executing step: ${step.name}`);
|
|
109
|
-
|
|
111
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`\n[STEP] ${step.name}`);
|
|
112
|
+
const stepResult = await this.executeStep(step, projectPath, result.environment, onLog);
|
|
110
113
|
const duration = Date.now() - stepStart;
|
|
111
114
|
result.steps.push({
|
|
112
115
|
...stepResult,
|
|
113
116
|
duration
|
|
114
117
|
});
|
|
115
118
|
if (!stepResult.success && step.required) {
|
|
119
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] Step failed: ${step.name}`);
|
|
116
120
|
result.success = false;
|
|
117
121
|
result.errors.push(`Required step failed: ${step.name} - ${stepResult.error}`);
|
|
118
122
|
break;
|
|
119
123
|
}
|
|
120
124
|
else if (!stepResult.success) {
|
|
125
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] Optional step failed: ${step.name}`);
|
|
121
126
|
result.warnings.push(`Optional step failed: ${step.name} - ${stepResult.error}`);
|
|
122
127
|
}
|
|
128
|
+
else {
|
|
129
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[OK] ${step.name} completed (${duration}ms)`);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
catch (error) {
|
|
125
133
|
const duration = Date.now() - stepStart;
|
|
126
134
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
135
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] ${step.name}: ${errorMessage}`);
|
|
127
136
|
result.steps.push({
|
|
128
137
|
name: step.name,
|
|
129
138
|
success: false,
|
|
@@ -152,13 +161,13 @@ class ProjectSetupService {
|
|
|
152
161
|
result.environment.PYTHON_INTERPRETER = venvPath;
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
|
-
// Validate the setup
|
|
164
|
+
// Validate the setup (advisory only — step success/failure is the real gate)
|
|
156
165
|
const validationResult = await this.validateSetup(projectPath, config);
|
|
157
166
|
if (!validationResult.success) {
|
|
158
|
-
result.
|
|
159
|
-
result.errors.push(...validationResult.errors);
|
|
167
|
+
result.warnings.push(...validationResult.errors);
|
|
160
168
|
}
|
|
161
169
|
this.log(`Setup completed. Success: ${result.success}`);
|
|
170
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`\nSetup ${result.success ? 'completed successfully' : 'failed'}.`);
|
|
162
171
|
return result;
|
|
163
172
|
}
|
|
164
173
|
shouldSkipStep(step, projectPath) {
|
|
@@ -216,7 +225,7 @@ class ProjectSetupService {
|
|
|
216
225
|
}
|
|
217
226
|
return false;
|
|
218
227
|
}
|
|
219
|
-
async executeStep(step, projectPath, environment) {
|
|
228
|
+
async executeStep(step, projectPath, environment, onLog) {
|
|
220
229
|
const workingDir = step.workingDirectory === 'project' ? projectPath : process.cwd();
|
|
221
230
|
let command = step.command;
|
|
222
231
|
// Handle virtual environment activation for Python
|
|
@@ -245,13 +254,20 @@ class ProjectSetupService {
|
|
|
245
254
|
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
246
255
|
const text = data.toString();
|
|
247
256
|
output += text;
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
for (const line of text.split('\n')) {
|
|
258
|
+
const trimmed = line.trim();
|
|
259
|
+
if (trimmed)
|
|
260
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(trimmed);
|
|
261
|
+
}
|
|
250
262
|
});
|
|
251
263
|
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
252
264
|
const text = data.toString();
|
|
253
265
|
error += text;
|
|
254
|
-
|
|
266
|
+
for (const line of text.split('\n')) {
|
|
267
|
+
const trimmed = line.trim();
|
|
268
|
+
if (trimmed)
|
|
269
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] ${trimmed}`);
|
|
270
|
+
}
|
|
255
271
|
});
|
|
256
272
|
child.on('close', (code) => {
|
|
257
273
|
const success = code === 0;
|
|
@@ -10,3 +10,25 @@ export declare function encrypt(text: string): string;
|
|
|
10
10
|
* @returns The decrypted text
|
|
11
11
|
*/
|
|
12
12
|
export declare function decrypt(encryptedText: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the server's RSA public key in PEM (SPKI) format.
|
|
15
|
+
* Expose this via a GET endpoint so clients can encrypt before sending.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getRSAPublicKey(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Hybrid-encrypted payload sent by the client.
|
|
20
|
+
* - encryptedKey : RSA-OAEP encrypted 256-bit AES key (base64)
|
|
21
|
+
* - iv : 12-byte AES-GCM IV (base64)
|
|
22
|
+
* - data : AES-256-GCM ciphertext + 16-byte auth-tag (base64)
|
|
23
|
+
*/
|
|
24
|
+
export interface EncryptedPayload {
|
|
25
|
+
encryptedKey: string;
|
|
26
|
+
iv: string;
|
|
27
|
+
data: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decrypts a hybrid-encrypted payload produced by the client.
|
|
31
|
+
* 1. Unwraps the AES key with RSA-OAEP (SHA-256)
|
|
32
|
+
* 2. Decrypts the data with AES-256-GCM
|
|
33
|
+
*/
|
|
34
|
+
export declare function decryptTransitPayload(payload: EncryptedPayload): string;
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.encrypt = encrypt;
|
|
7
7
|
exports.decrypt = decrypt;
|
|
8
|
+
exports.getRSAPublicKey = getRSAPublicKey;
|
|
9
|
+
exports.decryptTransitPayload = decryptTransitPayload;
|
|
8
10
|
/**
|
|
9
11
|
* Utility for encrypting and decrypting sensitive data
|
|
10
12
|
*/
|
|
@@ -58,3 +60,54 @@ function decrypt(encryptedText) {
|
|
|
58
60
|
return '';
|
|
59
61
|
}
|
|
60
62
|
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// RSA-OAEP key pair for in-transit encryption
|
|
65
|
+
// A fresh key pair is generated once per server process and kept in memory.
|
|
66
|
+
// The public key is shared with clients so they can encrypt sensitive fields
|
|
67
|
+
// before sending them over the network. The private key never leaves the server.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
let _rsaPrivateKey = null;
|
|
70
|
+
let _rsaPublicKeyPem = null;
|
|
71
|
+
function getOrCreateRSAKeyPair() {
|
|
72
|
+
if (!_rsaPrivateKey || !_rsaPublicKeyPem) {
|
|
73
|
+
const { privateKey, publicKey } = crypto_1.default.generateKeyPairSync('rsa', {
|
|
74
|
+
modulusLength: 2048,
|
|
75
|
+
});
|
|
76
|
+
_rsaPrivateKey = privateKey;
|
|
77
|
+
_rsaPublicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
78
|
+
}
|
|
79
|
+
return { publicKeyPem: _rsaPublicKeyPem, privateKey: _rsaPrivateKey };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the server's RSA public key in PEM (SPKI) format.
|
|
83
|
+
* Expose this via a GET endpoint so clients can encrypt before sending.
|
|
84
|
+
*/
|
|
85
|
+
function getRSAPublicKey() {
|
|
86
|
+
return getOrCreateRSAKeyPair().publicKeyPem;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decrypts a hybrid-encrypted payload produced by the client.
|
|
90
|
+
* 1. Unwraps the AES key with RSA-OAEP (SHA-256)
|
|
91
|
+
* 2. Decrypts the data with AES-256-GCM
|
|
92
|
+
*/
|
|
93
|
+
function decryptTransitPayload(payload) {
|
|
94
|
+
const { privateKey } = getOrCreateRSAKeyPair();
|
|
95
|
+
// Step 1 — decrypt the AES-256 key with RSA-OAEP / SHA-256
|
|
96
|
+
const encryptedAesKey = Buffer.from(payload.encryptedKey, 'base64');
|
|
97
|
+
const aesKey = crypto_1.default.privateDecrypt({
|
|
98
|
+
key: privateKey,
|
|
99
|
+
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
100
|
+
oaepHash: 'sha256',
|
|
101
|
+
}, encryptedAesKey);
|
|
102
|
+
// Step 2 — decrypt data with AES-256-GCM
|
|
103
|
+
// WebCrypto appends the 16-byte auth tag at the end of the ciphertext buffer
|
|
104
|
+
const iv = Buffer.from(payload.iv, 'base64');
|
|
105
|
+
const encryptedBuf = Buffer.from(payload.data, 'base64');
|
|
106
|
+
const authTag = encryptedBuf.slice(encryptedBuf.length - 16);
|
|
107
|
+
const ciphertext = encryptedBuf.slice(0, encryptedBuf.length - 16);
|
|
108
|
+
const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', aesKey, iv);
|
|
109
|
+
decipher.setAuthTag(authTag);
|
|
110
|
+
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
111
|
+
decrypted += decipher.final('utf8');
|
|
112
|
+
return decrypted;
|
|
113
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MetricPoint {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
cpu: number;
|
|
4
|
+
memory: number;
|
|
5
|
+
memoryMB: number;
|
|
6
|
+
memoryPercent: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ProcessHistory {
|
|
9
|
+
pm_id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
status: string;
|
|
12
|
+
history: MetricPoint[];
|
|
13
|
+
}
|
|
14
|
+
declare class MetricsHistoryStore {
|
|
15
|
+
private readonly store;
|
|
16
|
+
record(processList: any[]): void;
|
|
17
|
+
getOne(pm_id: number): ProcessHistory | null;
|
|
18
|
+
getAll(): ProcessHistory[];
|
|
19
|
+
}
|
|
20
|
+
export declare const metricsHistory: MetricsHistoryStore;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.metricsHistory = void 0;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
// @group Constants : Ring-buffer capacity — 60 points × 3 s = 3 minutes of history
|
|
9
|
+
const MAX_POINTS = 60;
|
|
10
|
+
// @group MetricsHistory : In-memory ring buffer; key = pm_id (string)
|
|
11
|
+
class MetricsHistoryStore {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.store = new Map();
|
|
14
|
+
}
|
|
15
|
+
// @group MetricsHistory : Record a snapshot from the PM2 process list
|
|
16
|
+
record(processList) {
|
|
17
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
18
|
+
const totalMem = os_1.default.totalmem();
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const proc of processList) {
|
|
21
|
+
const pm_id = proc.pm_id;
|
|
22
|
+
seen.add(pm_id);
|
|
23
|
+
const cpu = (_b = (_a = proc.monit) === null || _a === void 0 ? void 0 : _a.cpu) !== null && _b !== void 0 ? _b : 0;
|
|
24
|
+
const memory = (_d = (_c = proc.monit) === null || _c === void 0 ? void 0 : _c.memory) !== null && _d !== void 0 ? _d : 0;
|
|
25
|
+
const point = {
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
cpu: parseFloat(cpu.toFixed(2)),
|
|
28
|
+
memory,
|
|
29
|
+
memoryMB: parseFloat((memory / 1048576).toFixed(2)),
|
|
30
|
+
memoryPercent: parseFloat(((memory / totalMem) * 100).toFixed(2)),
|
|
31
|
+
};
|
|
32
|
+
if (!this.store.has(pm_id)) {
|
|
33
|
+
this.store.set(pm_id, {
|
|
34
|
+
pm_id,
|
|
35
|
+
name: (_e = proc.name) !== null && _e !== void 0 ? _e : String(pm_id),
|
|
36
|
+
status: (_g = (_f = proc.pm2_env) === null || _f === void 0 ? void 0 : _f.status) !== null && _g !== void 0 ? _g : 'unknown',
|
|
37
|
+
history: [],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const entry = this.store.get(pm_id);
|
|
41
|
+
// Refresh mutable fields on every poll
|
|
42
|
+
entry.name = (_h = proc.name) !== null && _h !== void 0 ? _h : entry.name;
|
|
43
|
+
entry.status = (_k = (_j = proc.pm2_env) === null || _j === void 0 ? void 0 : _j.status) !== null && _k !== void 0 ? _k : entry.status;
|
|
44
|
+
// Append point, trim to ring-buffer size
|
|
45
|
+
entry.history.push(point);
|
|
46
|
+
if (entry.history.length > MAX_POINTS) {
|
|
47
|
+
entry.history.shift();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Remove processes that no longer exist in PM2
|
|
51
|
+
for (const id of this.store.keys()) {
|
|
52
|
+
if (!seen.has(id)) {
|
|
53
|
+
this.store.delete(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// @group MetricsHistory : Return history for a single process; null if not found
|
|
58
|
+
getOne(pm_id) {
|
|
59
|
+
var _a;
|
|
60
|
+
return (_a = this.store.get(pm_id)) !== null && _a !== void 0 ? _a : null;
|
|
61
|
+
}
|
|
62
|
+
// @group MetricsHistory : Return history for all tracked processes
|
|
63
|
+
getAll() {
|
|
64
|
+
return Array.from(this.store.values());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// @group Exports : Singleton instance used across the server
|
|
68
|
+
exports.metricsHistory = new MetricsHistoryStore();
|
|
@@ -78,9 +78,9 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
78
78
|
host: this.config.host,
|
|
79
79
|
port: this.config.port || 22,
|
|
80
80
|
username: this.config.username,
|
|
81
|
-
// Add reasonable timeouts
|
|
82
|
-
readyTimeout:
|
|
83
|
-
keepaliveInterval:
|
|
81
|
+
// Add reasonable timeouts optimized for VPN/remote scenarios
|
|
82
|
+
readyTimeout: 20000, // 20 seconds to establish connection (was 10s)
|
|
83
|
+
keepaliveInterval: 10000 // Send keepalive every 10 seconds (was 30s) for aggressive firewalls
|
|
84
84
|
};
|
|
85
85
|
// Debug output
|
|
86
86
|
console.log(`Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}`);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RemoteMetricRow {
|
|
2
|
+
id: number;
|
|
3
|
+
connection_id: string;
|
|
4
|
+
connection_name: string;
|
|
5
|
+
process_name: string;
|
|
6
|
+
pm_id: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
cpu: number;
|
|
9
|
+
memory_bytes: number;
|
|
10
|
+
memory_mb: number;
|
|
11
|
+
}
|
|
12
|
+
declare class RemoteMetricsDB {
|
|
13
|
+
private db;
|
|
14
|
+
constructor();
|
|
15
|
+
private initSchema;
|
|
16
|
+
private purgeOld;
|
|
17
|
+
insert(row: Omit<RemoteMetricRow, 'id'>): void;
|
|
18
|
+
insertBatch(rows: Omit<RemoteMetricRow, 'id'>[]): void;
|
|
19
|
+
getConnectionsWithData(): {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}[];
|
|
23
|
+
getProcessNames(connectionId: string): string[];
|
|
24
|
+
getMetrics(connectionId: string, processName: string, from: number, to: number, maxPoints?: number): RemoteMetricRow[];
|
|
25
|
+
getLatestSnapshot(connectionId: string): RemoteMetricRow[];
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare const remoteMetricsDB: RemoteMetricsDB;
|
|
29
|
+
export {};
|