@mooncompany/uplink-chat 0.5.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config & Settings Routes
|
|
3
|
+
* ========================
|
|
4
|
+
* Express routes for the Uplink Configuration System.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints:
|
|
7
|
+
* GET /api/config — Non-sensitive config
|
|
8
|
+
* POST /api/config/test-gateway — Test OpenClaw gateway connection
|
|
9
|
+
* POST /api/config/test-voice — Test TTS with current settings
|
|
10
|
+
* POST /api/onboarding/step/:stepNumber — Save onboarding step progress
|
|
11
|
+
* POST /api/onboarding/complete — Finalize onboarding
|
|
12
|
+
* PUT /api/settings/connections — Update gateway/connection settings
|
|
13
|
+
* PUT /api/settings/voice — Update TTS/voice settings
|
|
14
|
+
* PUT /api/settings/security — Update password/security settings
|
|
15
|
+
* POST /api/settings/export — Download encrypted config backup
|
|
16
|
+
* POST /api/settings/import — Restore config from uploaded backup
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Router } from 'express';
|
|
20
|
+
import http from 'http';
|
|
21
|
+
import https from 'https';
|
|
22
|
+
import { GATEWAY_HEALTH_CHECK_TIMEOUT_MS, MIN_RECONNECT_INTERVAL_MS } from '../config.js';
|
|
23
|
+
import configStore from '../config-store.js';
|
|
24
|
+
import { createLogger } from '../logger.js';
|
|
25
|
+
|
|
26
|
+
const log = createLogger('config-routes');
|
|
27
|
+
|
|
28
|
+
const router = Router();
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function asyncHandler(fn) {
|
|
35
|
+
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ok(res, data = {}, status = 200) {
|
|
39
|
+
return res.status(status).json({ success: true, ...data });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fail(res, message, status = 400) {
|
|
43
|
+
return res.status(status).json({ success: false, error: message });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// 1. GET /api/config
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
router.get('/api/config', asyncHandler(async (_req, res) => {
|
|
50
|
+
return ok(res, { config: configStore.getSafe() });
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// 2. POST /api/config/test-gateway
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
router.post('/api/config/test-gateway', asyncHandler(async (req, res) => {
|
|
57
|
+
const { gatewayUrl, gatewayToken } = req.body || {};
|
|
58
|
+
const raw = configStore.getRaw();
|
|
59
|
+
const url = gatewayUrl || raw.connections.gatewayUrl;
|
|
60
|
+
const token = gatewayToken || raw.connections.gatewayToken;
|
|
61
|
+
|
|
62
|
+
if (!url) {
|
|
63
|
+
return fail(res, 'No gateway URL configured or provided.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await testGateway(url, token);
|
|
68
|
+
return ok(res, result);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return ok(res, { reachable: false, latencyMs: null, error: err.message });
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
function testGateway(gatewayUrl, token) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = new URL(gatewayUrl);
|
|
79
|
+
} catch {
|
|
80
|
+
return reject(new Error('Invalid gateway URL'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parsed.pathname = parsed.pathname.replace(/\/$/, '') + '/status';
|
|
84
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
85
|
+
const headers = {};
|
|
86
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
87
|
+
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
const request = transport.get(parsed.toString(), { headers, timeout: GATEWAY_HEALTH_CHECK_TIMEOUT_MS }, (response) => {
|
|
90
|
+
const latencyMs = Date.now() - start;
|
|
91
|
+
let body = '';
|
|
92
|
+
response.on('data', (chunk) => { body += chunk; });
|
|
93
|
+
response.on('end', () => {
|
|
94
|
+
const reachable = response.statusCode >= 200 && response.statusCode < 400;
|
|
95
|
+
let version = null;
|
|
96
|
+
try {
|
|
97
|
+
const json = JSON.parse(body);
|
|
98
|
+
version = json.version || json.serverVersion || null;
|
|
99
|
+
} catch { /* not JSON */ }
|
|
100
|
+
resolve({ reachable, latencyMs, version });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
request.on('error', (err) => reject(err));
|
|
105
|
+
request.on('timeout', () => {
|
|
106
|
+
request.destroy();
|
|
107
|
+
reject(new Error('Gateway connection timed out'));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// 3. POST /api/config/test-voice
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
router.post('/api/config/test-voice', asyncHandler(async (req, res) => {
|
|
116
|
+
const { text } = req.body || {};
|
|
117
|
+
const sampleText = text || 'Hello! This is a test of the voice configuration.';
|
|
118
|
+
const raw = configStore.getRaw();
|
|
119
|
+
|
|
120
|
+
if (!raw.voice.enabled) {
|
|
121
|
+
return fail(res, 'Voice/TTS is not enabled in the current configuration.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// TODO: Wire to actual TTS provider when integrated
|
|
125
|
+
return ok(res, {
|
|
126
|
+
played: true,
|
|
127
|
+
provider: raw.voice.ttsProvider,
|
|
128
|
+
voice: raw.voice.ttsVoice,
|
|
129
|
+
textUsed: sampleText,
|
|
130
|
+
durationMs: Math.ceil(sampleText.length * 60),
|
|
131
|
+
});
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// 4. POST /api/onboarding/step/:stepNumber
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
router.post('/api/onboarding/step/:stepNumber', asyncHandler(async (req, res) => {
|
|
138
|
+
const stepNumber = parseInt(req.params.stepNumber, 10);
|
|
139
|
+
|
|
140
|
+
if (isNaN(stepNumber) || stepNumber < 1) {
|
|
141
|
+
return fail(res, 'Invalid step number. Must be a positive integer.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = req.body;
|
|
145
|
+
if (!data || Object.keys(data).length === 0) {
|
|
146
|
+
return fail(res, 'Step data is required in the request body.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
configStore.saveOnboardingStep(stepNumber, data);
|
|
150
|
+
await configStore.save();
|
|
151
|
+
|
|
152
|
+
return ok(res, {
|
|
153
|
+
step: stepNumber,
|
|
154
|
+
currentStep: configStore.getSafe().onboarding.currentStep,
|
|
155
|
+
});
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// 5. POST /api/onboarding/complete
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
router.post('/api/onboarding/complete', asyncHandler(async (req, res) => {
|
|
162
|
+
const { password } = req.body || {};
|
|
163
|
+
|
|
164
|
+
if (!password || typeof password !== 'string' || password.length < 8) {
|
|
165
|
+
return fail(res, 'A password of at least 8 characters is required.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const current = configStore.getSafe();
|
|
169
|
+
if (current.onboarding.completed) {
|
|
170
|
+
return fail(res, 'Onboarding has already been completed.', 409);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
configStore.setPassword(password);
|
|
174
|
+
configStore.completeOnboarding();
|
|
175
|
+
await configStore.save();
|
|
176
|
+
|
|
177
|
+
return ok(res, {
|
|
178
|
+
onboarding: configStore.getSafe().onboarding,
|
|
179
|
+
message: 'Onboarding complete. Configuration encrypted and saved.',
|
|
180
|
+
}, 201);
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// 6. PUT /api/settings/connections
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
router.put('/api/settings/connections', asyncHandler(async (req, res) => {
|
|
187
|
+
const allowed = ['gatewayUrl', 'gatewayToken', 'autoReconnect', 'reconnectIntervalMs'];
|
|
188
|
+
const updates = {};
|
|
189
|
+
let hasUpdate = false;
|
|
190
|
+
|
|
191
|
+
for (const key of allowed) {
|
|
192
|
+
if (req.body[key] !== undefined) {
|
|
193
|
+
updates[key] = req.body[key];
|
|
194
|
+
hasUpdate = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!hasUpdate) {
|
|
199
|
+
return fail(res, `No valid fields provided. Allowed: ${allowed.join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (updates.gatewayUrl !== undefined && typeof updates.gatewayUrl !== 'string') {
|
|
203
|
+
return fail(res, 'gatewayUrl must be a string.');
|
|
204
|
+
}
|
|
205
|
+
if (updates.reconnectIntervalMs !== undefined) {
|
|
206
|
+
const ms = Number(updates.reconnectIntervalMs);
|
|
207
|
+
if (isNaN(ms) || ms < MIN_RECONNECT_INTERVAL_MS) return fail(res, `reconnectIntervalMs must be >= ${MIN_RECONNECT_INTERVAL_MS}.`);
|
|
208
|
+
updates.reconnectIntervalMs = ms;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const updated = configStore.updateSection('connections', updates);
|
|
212
|
+
await configStore.save();
|
|
213
|
+
return ok(res, { connections: updated.connections });
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// 7. PUT /api/settings/voice
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
router.put('/api/settings/voice', asyncHandler(async (req, res) => {
|
|
220
|
+
const allowed = ['enabled', 'ttsProvider', 'ttsVoice', 'volume', 'speed'];
|
|
221
|
+
const updates = {};
|
|
222
|
+
let hasUpdate = false;
|
|
223
|
+
|
|
224
|
+
for (const key of allowed) {
|
|
225
|
+
if (req.body[key] !== undefined) {
|
|
226
|
+
updates[key] = req.body[key];
|
|
227
|
+
hasUpdate = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!hasUpdate) {
|
|
232
|
+
return fail(res, `No valid fields provided. Allowed: ${allowed.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (updates.volume !== undefined) {
|
|
236
|
+
const v = Number(updates.volume);
|
|
237
|
+
if (isNaN(v) || v < 0 || v > 1) return fail(res, 'volume must be 0-1.');
|
|
238
|
+
updates.volume = v;
|
|
239
|
+
}
|
|
240
|
+
if (updates.speed !== undefined) {
|
|
241
|
+
const s = Number(updates.speed);
|
|
242
|
+
if (isNaN(s) || s < 0.25 || s > 4) return fail(res, 'speed must be 0.25-4.');
|
|
243
|
+
updates.speed = s;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const updated = configStore.updateSection('voice', updates);
|
|
247
|
+
await configStore.save();
|
|
248
|
+
return ok(res, { voice: updated.voice });
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// 8. PUT /api/settings/security
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
router.put('/api/settings/security', asyncHandler(async (req, res) => {
|
|
255
|
+
const { currentPassword, newPassword, lockTimeoutMin, requirePasswordOnStart } = req.body || {};
|
|
256
|
+
|
|
257
|
+
if (newPassword !== undefined) {
|
|
258
|
+
if (!currentPassword) {
|
|
259
|
+
return fail(res, 'currentPassword is required when setting a new password.', 401);
|
|
260
|
+
}
|
|
261
|
+
if (!configStore.verifyPassword(currentPassword)) {
|
|
262
|
+
return fail(res, 'Current password is incorrect.', 401);
|
|
263
|
+
}
|
|
264
|
+
if (typeof newPassword !== 'string' || newPassword.length < 8) {
|
|
265
|
+
return fail(res, 'newPassword must be at least 8 characters.');
|
|
266
|
+
}
|
|
267
|
+
configStore.setPassword(newPassword);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const sectionUpdates = {};
|
|
271
|
+
if (lockTimeoutMin !== undefined) {
|
|
272
|
+
const t = Number(lockTimeoutMin);
|
|
273
|
+
if (isNaN(t) || t < 1) return fail(res, 'lockTimeoutMin must be positive.');
|
|
274
|
+
sectionUpdates.lockTimeoutMin = t;
|
|
275
|
+
}
|
|
276
|
+
if (requirePasswordOnStart !== undefined) {
|
|
277
|
+
sectionUpdates.requirePasswordOnStart = Boolean(requirePasswordOnStart);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (Object.keys(sectionUpdates).length) {
|
|
281
|
+
configStore.updateSection('security', sectionUpdates);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await configStore.save();
|
|
285
|
+
return ok(res, { security: configStore.getSafe().security });
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// 9. POST /api/settings/export
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
router.post('/api/settings/export', asyncHandler(async (req, res) => {
|
|
292
|
+
const { password } = req.body || {};
|
|
293
|
+
|
|
294
|
+
if (!password) return fail(res, 'Password is required.', 401);
|
|
295
|
+
if (!configStore.verifyPassword(password)) return fail(res, 'Incorrect password.', 401);
|
|
296
|
+
|
|
297
|
+
const envelope = configStore.exportEncrypted(password);
|
|
298
|
+
const payload = JSON.stringify(envelope, null, 2);
|
|
299
|
+
|
|
300
|
+
res.setHeader('Content-Type', 'application/json');
|
|
301
|
+
res.setHeader('Content-Disposition', 'attachment; filename="uplink-config-backup.json"');
|
|
302
|
+
return res.status(200).send(payload);
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// 10. POST /api/settings/import
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
router.post('/api/settings/import', asyncHandler(async (req, res) => {
|
|
309
|
+
const { password, backup } = req.body || {};
|
|
310
|
+
|
|
311
|
+
if (!password) return fail(res, 'Password is required.', 401);
|
|
312
|
+
if (!backup || typeof backup !== 'object') {
|
|
313
|
+
return fail(res, 'A "backup" object is required in the request body.');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const required = ['salt', 'iv', 'tag', 'ciphertext'];
|
|
317
|
+
const missing = required.filter((f) => !backup[f]);
|
|
318
|
+
if (missing.length) {
|
|
319
|
+
return fail(res, `Invalid backup. Missing: ${missing.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const restored = configStore.importEncrypted(backup, password);
|
|
324
|
+
await configStore.save();
|
|
325
|
+
return ok(res, { message: 'Configuration restored.', config: restored });
|
|
326
|
+
} catch (err) {
|
|
327
|
+
const msg = err.message.includes('unable to authenticate')
|
|
328
|
+
? 'Decryption failed. Wrong password or corrupted backup.'
|
|
329
|
+
: `Import failed: ${err.message}`;
|
|
330
|
+
return fail(res, msg, 401);
|
|
331
|
+
}
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Error handler
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
router.use((err, _req, res, _next) => {
|
|
338
|
+
log.error(err);
|
|
339
|
+
return res.status(500).json({
|
|
340
|
+
success: false,
|
|
341
|
+
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
export default router;
|