@fenwave/agent 1.1.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/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
package/utils/prompts.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prompt for registration token
|
|
7
|
+
*
|
|
8
|
+
* @param {string} backstageUrl - Backstage URL to display
|
|
9
|
+
* @returns {Promise<string>} Registration token
|
|
10
|
+
*/
|
|
11
|
+
export async function promptRegistrationToken(backstageUrl) {
|
|
12
|
+
console.log(chalk.blue('\nš Registration Token Required\n'));
|
|
13
|
+
console.log(chalk.gray(`Get your token from: ${backstageUrl}/agent-installer\n`));
|
|
14
|
+
|
|
15
|
+
const answers = await inquirer.prompt([
|
|
16
|
+
{
|
|
17
|
+
type: 'password',
|
|
18
|
+
name: 'token',
|
|
19
|
+
message: 'Enter your registration token:',
|
|
20
|
+
mask: '*',
|
|
21
|
+
validate: (input) => {
|
|
22
|
+
if (!input || input.trim().length === 0) {
|
|
23
|
+
return 'Registration token is required';
|
|
24
|
+
}
|
|
25
|
+
if (input.length < 32) {
|
|
26
|
+
return 'Invalid token format';
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
return answers.token.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Prompt for confirmation
|
|
38
|
+
*
|
|
39
|
+
* @param {string} message - Confirmation message
|
|
40
|
+
* @param {boolean} defaultValue - Default value (true/false)
|
|
41
|
+
* @returns {Promise<boolean>} User's answer
|
|
42
|
+
*/
|
|
43
|
+
export async function promptConfirmation(message, defaultValue = true) {
|
|
44
|
+
const answers = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'confirm',
|
|
47
|
+
name: 'confirmed',
|
|
48
|
+
message,
|
|
49
|
+
default: defaultValue,
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return answers.confirmed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prompt for Docker registry configuration
|
|
58
|
+
*
|
|
59
|
+
* @returns {Promise<Object>} Registry configuration
|
|
60
|
+
*/
|
|
61
|
+
export async function promptDockerRegistries() {
|
|
62
|
+
console.log(chalk.blue('\nš³ Docker Registry Configuration\n'));
|
|
63
|
+
|
|
64
|
+
const answers = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'confirm',
|
|
67
|
+
name: 'configureEcr',
|
|
68
|
+
message: 'Configure AWS ECR for Fenwave images?',
|
|
69
|
+
default: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'awsRegion',
|
|
74
|
+
message: 'AWS Region:',
|
|
75
|
+
default: 'eu-west-1',
|
|
76
|
+
when: (answers) => answers.configureEcr,
|
|
77
|
+
validate: (input) => {
|
|
78
|
+
if (!input || input.trim().length === 0) {
|
|
79
|
+
return 'AWS Region is required';
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
name: 'addCustomRegistry',
|
|
87
|
+
message: 'Add custom Docker registry?',
|
|
88
|
+
default: false,
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const registries = [];
|
|
93
|
+
|
|
94
|
+
if (answers.configureEcr) {
|
|
95
|
+
registries.push({
|
|
96
|
+
type: 'ecr',
|
|
97
|
+
region: answers.awsRegion,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (answers.addCustomRegistry) {
|
|
102
|
+
const customRegistry = await inquirer.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'url',
|
|
106
|
+
message: 'Registry URL:',
|
|
107
|
+
validate: (input) => {
|
|
108
|
+
if (!input || input.trim().length === 0) {
|
|
109
|
+
return 'Registry URL is required';
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'username',
|
|
117
|
+
message: 'Username:',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'password',
|
|
121
|
+
name: 'password',
|
|
122
|
+
message: 'Password:',
|
|
123
|
+
mask: '*',
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
registries.push({
|
|
128
|
+
type: 'custom',
|
|
129
|
+
...customRegistry,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return registries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Prompt for choice from a list
|
|
138
|
+
*
|
|
139
|
+
* @param {string} message - Prompt message
|
|
140
|
+
* @param {Array} choices - Array of choices
|
|
141
|
+
* @param {string} defaultChoice - Default choice
|
|
142
|
+
* @returns {Promise<string>} Selected choice
|
|
143
|
+
*/
|
|
144
|
+
export async function promptChoice(message, choices, defaultChoice) {
|
|
145
|
+
const answers = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'list',
|
|
148
|
+
name: 'choice',
|
|
149
|
+
message,
|
|
150
|
+
choices,
|
|
151
|
+
default: defaultChoice,
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
return answers.choice;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Prompt for text input
|
|
160
|
+
*
|
|
161
|
+
* @param {string} message - Prompt message
|
|
162
|
+
* @param {string} defaultValue - Default value
|
|
163
|
+
* @param {Function} validate - Validation function
|
|
164
|
+
* @returns {Promise<string>} User input
|
|
165
|
+
*/
|
|
166
|
+
export async function promptInput(message, defaultValue, validate) {
|
|
167
|
+
const answers = await inquirer.prompt([
|
|
168
|
+
{
|
|
169
|
+
type: 'input',
|
|
170
|
+
name: 'input',
|
|
171
|
+
message,
|
|
172
|
+
default: defaultValue,
|
|
173
|
+
validate,
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
return answers.input;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Display progress with spinner
|
|
182
|
+
*
|
|
183
|
+
* @param {string} message - Progress message
|
|
184
|
+
* @param {Promise} promise - Promise to await
|
|
185
|
+
* @returns {Promise} Result of the promise
|
|
186
|
+
*/
|
|
187
|
+
export async function displayProgress(message, promise) {
|
|
188
|
+
const spinner = ora(message).start();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const result = await promise;
|
|
192
|
+
spinner.succeed(chalk.green(message));
|
|
193
|
+
return result;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
spinner.fail(chalk.red(`${message} - ${error.message}`));
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Display success message
|
|
202
|
+
*
|
|
203
|
+
* @param {string} message - Success message
|
|
204
|
+
*/
|
|
205
|
+
export function displaySuccess(message) {
|
|
206
|
+
console.log(chalk.green('ā
' + message));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Display error message
|
|
211
|
+
*
|
|
212
|
+
* @param {string} message - Error message
|
|
213
|
+
*/
|
|
214
|
+
export function displayError(message) {
|
|
215
|
+
console.log(chalk.red('ā ' + message));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Display warning message
|
|
220
|
+
*
|
|
221
|
+
* @param {string} message - Warning message
|
|
222
|
+
*/
|
|
223
|
+
export function displayWarning(message) {
|
|
224
|
+
console.log(chalk.yellow('ā ļø ' + message));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Display info message
|
|
229
|
+
*
|
|
230
|
+
* @param {string} message - Info message
|
|
231
|
+
*/
|
|
232
|
+
export function displayInfo(message) {
|
|
233
|
+
console.log(chalk.blue('ā¹ļø ' + message));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Display header with decorative border
|
|
238
|
+
*
|
|
239
|
+
* @param {string} title - Header title
|
|
240
|
+
*/
|
|
241
|
+
export function displayHeader(title) {
|
|
242
|
+
const border = 'ā'.repeat(70);
|
|
243
|
+
console.log('\n' + chalk.cyan(border));
|
|
244
|
+
console.log(chalk.cyan.bold(` ${title}`));
|
|
245
|
+
console.log(chalk.cyan(border) + '\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Display step indicator
|
|
250
|
+
*
|
|
251
|
+
* @param {number} current - Current step
|
|
252
|
+
* @param {number} total - Total steps
|
|
253
|
+
* @param {string} description - Step description
|
|
254
|
+
*/
|
|
255
|
+
export function displayStep(current, total, description) {
|
|
256
|
+
console.log(chalk.bold(`\nStep ${current}/${total}: ${description}`));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Display a list of items
|
|
261
|
+
*
|
|
262
|
+
* @param {Array} items - Items to display
|
|
263
|
+
* @param {string} prefix - Prefix for each item (default: ' ⢠')
|
|
264
|
+
*/
|
|
265
|
+
export function displayList(items, prefix = ' ⢠') {
|
|
266
|
+
items.forEach((item) => {
|
|
267
|
+
console.log(chalk.gray(prefix + item));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Display key-value pairs
|
|
273
|
+
*
|
|
274
|
+
* @param {Object} data - Data to display
|
|
275
|
+
* @param {string} indent - Indentation (default: ' ')
|
|
276
|
+
*/
|
|
277
|
+
export function displayKeyValue(data, indent = ' ') {
|
|
278
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
279
|
+
console.log(chalk.gray(`${indent}${key}: `) + chalk.white(value));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Clear console
|
|
285
|
+
*/
|
|
286
|
+
export function clearConsole() {
|
|
287
|
+
console.clear();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Prompt for backend URL
|
|
292
|
+
*
|
|
293
|
+
* @param {string} defaultUrl - Default backend URL
|
|
294
|
+
* @returns {Promise<string>} Backend URL
|
|
295
|
+
*/
|
|
296
|
+
export async function promptBackendUrl(defaultUrl = 'http://localhost:7007') {
|
|
297
|
+
const answers = await inquirer.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: 'input',
|
|
300
|
+
name: 'url',
|
|
301
|
+
message: 'Backstage Backend URL:',
|
|
302
|
+
default: defaultUrl,
|
|
303
|
+
validate: (input) => {
|
|
304
|
+
if (!input || input.trim().length === 0) {
|
|
305
|
+
return 'Backend URL is required';
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
new URL(input);
|
|
309
|
+
return true;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return 'Invalid URL format';
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
return answers.url.trim();
|
|
318
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import { loadConfig } from "./store/configStore.js";
|
|
9
|
+
|
|
10
|
+
// Import handlers
|
|
11
|
+
import containerHandlers from "./docker-actions/containers.js";
|
|
12
|
+
import imageHandlers from "./docker-actions/images.js";
|
|
13
|
+
import volumeHandlers from "./docker-actions/volumes.js";
|
|
14
|
+
import appHandlers from "./docker-actions/apps.js";
|
|
15
|
+
import registryHandlers from "./docker-actions/registry.js";
|
|
16
|
+
import logHandlers from "./docker-actions/logs.js";
|
|
17
|
+
import metricsHandlers from "./docker-actions/metrics.js";
|
|
18
|
+
import terminalHandlers from "./docker-actions/terminal.js";
|
|
19
|
+
import generalHandlers from "./docker-actions/general.js";
|
|
20
|
+
import agentSessionStore from "./store/agentSessionStore.js";
|
|
21
|
+
|
|
22
|
+
// Store active streams and sessions
|
|
23
|
+
const activeStreams = new Map();
|
|
24
|
+
const terminalSessions = new Map();
|
|
25
|
+
|
|
26
|
+
// Load configuration
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
const AGENT_ROOT_DIR = path.join(os.homedir(), config.agentRootDir);
|
|
29
|
+
const WS_TOKEN_FILE = path.join(AGENT_ROOT_DIR, "ws-token");
|
|
30
|
+
const AUTH_TIMEOUT_MS = 30000; // 30 seconds to authenticate
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a secure random WebSocket authentication token
|
|
34
|
+
* @returns {string} 64-character hex token
|
|
35
|
+
*/
|
|
36
|
+
function generateWsToken() {
|
|
37
|
+
return crypto.randomBytes(32).toString("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Save WebSocket token to file for local-env app to read
|
|
42
|
+
* @param {string} token - The WebSocket authentication token
|
|
43
|
+
*/
|
|
44
|
+
function saveWsToken(token) {
|
|
45
|
+
// Ensure directory exists
|
|
46
|
+
if (!fs.existsSync(AGENT_ROOT_DIR)) {
|
|
47
|
+
fs.mkdirSync(AGENT_ROOT_DIR, { recursive: true, mode: 0o700 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Write token to file with restricted permissions (owner only)
|
|
51
|
+
fs.writeFileSync(WS_TOKEN_FILE, token, { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read WebSocket token from file
|
|
56
|
+
* @returns {string|null} The WebSocket token or null if not found
|
|
57
|
+
*/
|
|
58
|
+
function readWsToken() {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(WS_TOKEN_FILE)) {
|
|
61
|
+
return fs.readFileSync(WS_TOKEN_FILE, "utf-8").trim();
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Error reading WS token:", error.message);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setupWebSocketServer(
|
|
70
|
+
server,
|
|
71
|
+
clients,
|
|
72
|
+
agentId,
|
|
73
|
+
sessionInfo = {},
|
|
74
|
+
existingToken = null
|
|
75
|
+
) {
|
|
76
|
+
// Store agent session info for use in handlers
|
|
77
|
+
agentSessionStore.setAgentSessionInfo({
|
|
78
|
+
agentId: agentId,
|
|
79
|
+
userEntityRef: sessionInfo.userEntityRef || null,
|
|
80
|
+
sessionExpiresAt: sessionInfo.expiresAt || null,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Use existing token or generate a new one
|
|
84
|
+
const wsToken = existingToken || generateWsToken();
|
|
85
|
+
if (!existingToken) {
|
|
86
|
+
// Only save if we generated a new token
|
|
87
|
+
saveWsToken(wsToken);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const wss = new WebSocketServer({ server });
|
|
91
|
+
|
|
92
|
+
// Set up cleanup timer for inactive terminal sessions
|
|
93
|
+
setInterval(() => {
|
|
94
|
+
terminalHandlers.cleanupInactiveTerminalSessions(terminalSessions);
|
|
95
|
+
}, 15 * 60 * 1000); // Run every 15 minutes
|
|
96
|
+
|
|
97
|
+
wss.on("connection", (ws) => {
|
|
98
|
+
// Generate a unique client ID for each WebSocket connection
|
|
99
|
+
const clientId = uuidv4();
|
|
100
|
+
const connectionTime = Date.now();
|
|
101
|
+
let authenticated = false;
|
|
102
|
+
|
|
103
|
+
// Set authentication timeout
|
|
104
|
+
const authTimeout = setTimeout(() => {
|
|
105
|
+
if (!authenticated) {
|
|
106
|
+
ws.send(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
type: "auth_error",
|
|
109
|
+
error: "Authentication timeout. Connection closed.",
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
ws.close(4001, "Authentication timeout");
|
|
113
|
+
}
|
|
114
|
+
}, AUTH_TIMEOUT_MS);
|
|
115
|
+
|
|
116
|
+
// Send authentication challenge
|
|
117
|
+
ws.send(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
type: "auth_required",
|
|
120
|
+
message:
|
|
121
|
+
"Authentication required. Please send auth message with token.",
|
|
122
|
+
clientId,
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Handle messages from client
|
|
127
|
+
ws.on("message", async (message) => {
|
|
128
|
+
let data;
|
|
129
|
+
try {
|
|
130
|
+
data = JSON.parse(message);
|
|
131
|
+
} catch (parseError) {
|
|
132
|
+
console.error("Error parsing message: ", parseError);
|
|
133
|
+
ws.send(
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
type: "error",
|
|
136
|
+
error: "Failed to parse message: " + parseError.message,
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle authentication
|
|
143
|
+
if (!authenticated) {
|
|
144
|
+
if (data.type === "auth" && data.token) {
|
|
145
|
+
if (data.token === wsToken) {
|
|
146
|
+
// Authentication successful
|
|
147
|
+
authenticated = true;
|
|
148
|
+
clearTimeout(authTimeout);
|
|
149
|
+
|
|
150
|
+
// Store client info
|
|
151
|
+
clients.set(clientId, {
|
|
152
|
+
ws,
|
|
153
|
+
connectionTime,
|
|
154
|
+
lastActivity: Date.now(),
|
|
155
|
+
isActive: true,
|
|
156
|
+
authenticated: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log(`ā
Client authenticated: ${clientId}`);
|
|
160
|
+
|
|
161
|
+
// Send success response
|
|
162
|
+
ws.send(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
type: "auth_success",
|
|
165
|
+
clientId,
|
|
166
|
+
agentId: agentId,
|
|
167
|
+
message: "Successfully authenticated with Fenwave Agent",
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
// Invalid token
|
|
172
|
+
console.log(
|
|
173
|
+
chalk.red(
|
|
174
|
+
`ā Invalid authentication token from client: ${clientId}`
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
ws.send(
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
type: "auth_error",
|
|
180
|
+
error: "Invalid authentication token",
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
ws.close(4003, "Invalid authentication token");
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Not an auth message
|
|
187
|
+
ws.send(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
type: "auth_error",
|
|
190
|
+
error: "Authentication required before sending messages",
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update last activity time for authenticated clients
|
|
198
|
+
const client = clients.get(clientId);
|
|
199
|
+
if (client) {
|
|
200
|
+
client.lastActivity = Date.now();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(`šØ Received message from client ${clientId}:`, data.action);
|
|
204
|
+
|
|
205
|
+
// Route messages to appropriate handlers
|
|
206
|
+
await routeMessage(ws, clientId, data);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Handle client disconnect
|
|
210
|
+
ws.on("close", () => {
|
|
211
|
+
console.log(`š“ Client disconnected: ${clientId}`);
|
|
212
|
+
clients.delete(clientId);
|
|
213
|
+
|
|
214
|
+
// Clean up any active streams for this client
|
|
215
|
+
for (const [streamId, stream] of activeStreams.entries()) {
|
|
216
|
+
if (streamId.startsWith(clientId)) {
|
|
217
|
+
if (stream.destroy) stream.destroy();
|
|
218
|
+
activeStreams.delete(streamId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return wss;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function routeMessage(ws, clientId, data) {
|
|
228
|
+
const { action, payload = {} } = data;
|
|
229
|
+
|
|
230
|
+
// Container actions
|
|
231
|
+
if (
|
|
232
|
+
action === "fetchContainers" ||
|
|
233
|
+
action === "startContainer" ||
|
|
234
|
+
action === "stopContainer" ||
|
|
235
|
+
action === "restartContainer" ||
|
|
236
|
+
action === "deleteContainer" ||
|
|
237
|
+
action === "createContainer" ||
|
|
238
|
+
action === "inspectContainer"
|
|
239
|
+
) {
|
|
240
|
+
return await containerHandlers.handleContainerAction(ws, action, payload);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Image actions
|
|
244
|
+
if (
|
|
245
|
+
action === "fetchImages" ||
|
|
246
|
+
action === "pullImage" ||
|
|
247
|
+
action === "pushImage" ||
|
|
248
|
+
action === "deleteImage" ||
|
|
249
|
+
action === "tagImage" ||
|
|
250
|
+
action === "inspectImage"
|
|
251
|
+
) {
|
|
252
|
+
return await imageHandlers.handleImageAction(ws, action, payload);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Volume actions
|
|
256
|
+
if (
|
|
257
|
+
action === "fetchVolumes" ||
|
|
258
|
+
action === "createVolume" ||
|
|
259
|
+
action === "deleteVolume" ||
|
|
260
|
+
action === "inspectVolume" ||
|
|
261
|
+
action === "getContainersUsingVolume" ||
|
|
262
|
+
action === "fetchImages" ||
|
|
263
|
+
action === "pullImage" ||
|
|
264
|
+
action === "backupVolume" ||
|
|
265
|
+
action === "listVolumeBackups" ||
|
|
266
|
+
action === "fetchImages" ||
|
|
267
|
+
action === "pullImage" ||
|
|
268
|
+
action === "restoreVolumeFromBackup" ||
|
|
269
|
+
action === "deleteVolumeBackup" ||
|
|
270
|
+
action === "getBackupDownloadUrl"
|
|
271
|
+
) {
|
|
272
|
+
return await volumeHandlers.handleVolumeAction(ws, action, payload);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// App actions
|
|
276
|
+
if (
|
|
277
|
+
action === "fetchApps" ||
|
|
278
|
+
action === "fetchAppVersions" ||
|
|
279
|
+
action === "startApp" ||
|
|
280
|
+
action === "stopApp" ||
|
|
281
|
+
action === "restartApp" ||
|
|
282
|
+
action === "deleteApp" ||
|
|
283
|
+
action === "deleteAppVersions" ||
|
|
284
|
+
action === "createApp" ||
|
|
285
|
+
action === "validateCompose" ||
|
|
286
|
+
action === "syncApp" ||
|
|
287
|
+
action === "changeVersion"
|
|
288
|
+
) {
|
|
289
|
+
return await appHandlers.handleAppAction(ws, action, payload);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Registry actions
|
|
293
|
+
if (
|
|
294
|
+
action === "fetchRegistries" ||
|
|
295
|
+
action === "connectRegistry" ||
|
|
296
|
+
action === "disconnectRegistry" ||
|
|
297
|
+
action === "renameRegistry" ||
|
|
298
|
+
action === "fetchRegistryImages" ||
|
|
299
|
+
action === "setActiveRegistry"
|
|
300
|
+
) {
|
|
301
|
+
return await registryHandlers.handleRegistryAction(ws, action, payload);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Log actions
|
|
305
|
+
if (
|
|
306
|
+
action === "fetchContainerLogs" ||
|
|
307
|
+
action === "streamContainerLogs" ||
|
|
308
|
+
action === "stopStreamLogs"
|
|
309
|
+
) {
|
|
310
|
+
return await logHandlers.handleLogAction(
|
|
311
|
+
ws,
|
|
312
|
+
clientId,
|
|
313
|
+
action,
|
|
314
|
+
payload,
|
|
315
|
+
activeStreams
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Metrics actions
|
|
320
|
+
if (
|
|
321
|
+
action === "fetchSystemMetrics" ||
|
|
322
|
+
action === "fetchContainerMetrics" ||
|
|
323
|
+
action === "systemMetrics"
|
|
324
|
+
) {
|
|
325
|
+
return await metricsHandlers.handleMetricsAction(ws, action, payload);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Terminal actions
|
|
329
|
+
if (
|
|
330
|
+
action === "openTerminalSession" ||
|
|
331
|
+
action === "closeTerminalSession" ||
|
|
332
|
+
action === "sendTerminalInput" ||
|
|
333
|
+
action === "resizeTerminal"
|
|
334
|
+
) {
|
|
335
|
+
return await terminalHandlers.handleTerminalAction(
|
|
336
|
+
ws,
|
|
337
|
+
clientId,
|
|
338
|
+
action,
|
|
339
|
+
payload,
|
|
340
|
+
terminalSessions
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// General actions
|
|
345
|
+
if (["getAgentInfo", "getDockerInfo", "execInContainer"].includes(action)) {
|
|
346
|
+
return await generalHandlers.handleGeneralAction(
|
|
347
|
+
ws,
|
|
348
|
+
clientId,
|
|
349
|
+
action,
|
|
350
|
+
payload
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Unknown action
|
|
355
|
+
ws.send(
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
type: "error",
|
|
358
|
+
requestId: payload.requestId,
|
|
359
|
+
error: `Unknown action: ${action}`,
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export { setupWebSocketServer, generateWsToken, saveWsToken, readWsToken };
|