@goodandready/dsh-remote-workspace 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/routes.js CHANGED
@@ -1,522 +1,53 @@
1
- export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store, dockerService, healthService, watcherService) {
2
- function writeJson(res, statusCode, body) {
3
- if (typeof res.setHeader === 'function') {
4
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
5
- res.statusCode = statusCode;
6
- } else if (typeof res.writeHead === 'function') {
7
- res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
8
- } else {
9
- res.statusCode = statusCode;
10
- }
11
- res.end(JSON.stringify(body));
12
- }
13
-
14
- function readBody(req) {
15
- return new Promise((resolve, reject) => {
16
- let body = '';
17
- req.on('data', (chunk) => { body += chunk.toString(); });
18
- req.on('end', () => {
19
- try {
20
- resolve(body ? JSON.parse(body) : {});
21
- } catch (err) {
22
- reject(err);
23
- }
24
- });
25
- req.on('error', reject);
26
- });
27
- }
28
-
29
- function isTrustedSettingsRequest(req) {
30
- const rawAuth = req.headers['authorization'];
31
- if (typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) return true;
32
- const cookieHeader = req.headers['cookie'];
33
- if (typeof cookieHeader === 'string' && (cookieHeader.includes('token=') || cookieHeader.includes('dsh_token='))) {
34
- return true;
35
- }
36
- const secFetch = req.headers['sec-fetch-site'];
37
- if (secFetch === 'same-origin') return true;
38
- const ip = req.socket?.remoteAddress || '';
39
- if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') return true;
40
- return false;
41
- }
42
-
1
+ import { registerPluginUpdater } from './updater-service.js';
2
+ import { isTrustedSettingsRequest } from './routes/helpers.js';
3
+ import { registerProfileRoutes } from './routes/profiles.js';
4
+ import { registerFileRoutes } from './routes/files.js';
5
+ import { registerEnvRoutes } from './routes/env.js';
6
+ import { registerTerminalRoutes } from './routes/terminal.js';
7
+ import { registerTunnelRoutes } from './routes/tunnels.js';
8
+ import { registerDockerRoutes } from './routes/docker.js';
9
+ import { registerSyncRoutes } from './routes/sync.js';
10
+
11
+ export { isTrustedSettingsRequest } from './routes/helpers.js';
12
+
13
+ export function registerApiRoutes(
14
+ ctx,
15
+ sshService,
16
+ remoteFs,
17
+ mirrorSync,
18
+ tunnelService,
19
+ store,
20
+ dockerService,
21
+ healthService,
22
+ watcherService,
23
+ diagnoseService,
24
+ envService,
25
+ tarSyncService,
26
+ alertService
27
+ ) {
43
28
  ctx.inject(['webServer'], (sctx) => {
44
29
  const webServer = sctx.get ? (sctx.get('webServer') ?? sctx.webServer) : sctx.webServer;
45
30
  if (!webServer || typeof webServer.register !== 'function') return;
46
31
 
47
- // 1. GET /dsh-remote-workspace/state
48
- sctx.effect(() => webServer.register({
49
- kind: 'exact',
50
- path: '/dsh-remote-workspace/state',
51
- handler: async (req, res) => {
52
- if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
53
- try {
54
- const profiles = store.getProfiles();
55
- const activeId = store.getActiveId() || (profiles[0]?.id || '');
56
- const tunnels = tunnelService.listActiveTunnels();
57
- const autoSync = store.isAutoSyncEnabled ? store.isAutoSyncEnabled() : false;
58
- writeJson(res, 200, {
59
- ok: true,
60
- profiles,
61
- activeId,
62
- tunnels,
63
- autoSync,
64
- vaultProtected: true
65
- });
66
- } catch (err) {
67
- writeJson(res, 500, { ok: false, error: err.message });
68
- }
69
- }
70
- }), 'dsh-remote-workspace: /state');
71
-
72
- // 2. POST /dsh-remote-workspace/profiles/save
73
- sctx.effect(() => webServer.register({
74
- kind: 'exact',
75
- path: '/dsh-remote-workspace/profiles/save',
76
- handler: async (req, res) => {
77
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
78
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
79
- try {
80
- const profile = await readBody(req);
81
- if (!profile.id || !profile.host) {
82
- return writeJson(res, 400, { ok: false, error: 'ID and Host are required' });
83
- }
84
- sshService.invalidate(profile.id);
85
- await store.saveProfile(profile);
86
- const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
87
- writeJson(res, 200, { ok: true, profile: sanitized });
88
- } catch (err) {
89
- writeJson(res, 500, { ok: false, error: err.message });
90
- }
91
- }
92
- }), 'dsh-remote-workspace: /profiles/save');
93
-
94
- // 3. POST /dsh-remote-workspace/profiles/delete
95
- sctx.effect(() => webServer.register({
96
- kind: 'exact',
97
- path: '/dsh-remote-workspace/profiles/delete',
98
- handler: async (req, res) => {
99
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
100
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
101
- try {
102
- const { id } = await readBody(req);
103
- if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
104
- sshService.disconnect(id);
105
- await store.deleteProfile(id);
106
- writeJson(res, 200, { ok: true });
107
- } catch (err) {
108
- writeJson(res, 500, { ok: false, error: err.message });
109
- }
110
- }
111
- }), 'dsh-remote-workspace: /profiles/delete');
112
-
113
- // 4. POST /dsh-remote-workspace/profiles/active
114
- sctx.effect(() => webServer.register({
115
- kind: 'exact',
116
- path: '/dsh-remote-workspace/profiles/active',
117
- handler: async (req, res) => {
118
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
119
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
120
- try {
121
- const { id } = await readBody(req);
122
- await store.setActiveId(id || null);
123
- writeJson(res, 200, { ok: true, activeId: store.getActiveId() });
124
- } catch (err) {
125
- writeJson(res, 500, { ok: false, error: err.message });
126
- }
127
- }
128
- }), 'dsh-remote-workspace: /profiles/active');
129
-
130
- // 5. POST /dsh-remote-workspace/test
131
- sctx.effect(() => webServer.register({
132
- kind: 'exact',
133
- path: '/dsh-remote-workspace/test',
134
- handler: async (req, res) => {
135
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
136
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
137
- try {
138
- const body = await readBody(req);
139
- let profile = body.profile || (body.id ? store.getProfile(body.id) : body);
140
- if (store.vault && profile) {
141
- profile = store.vault.hydrateProfile(profile);
142
- }
143
- if (!profile || !profile.host) {
144
- return writeJson(res, 400, { ok: false, error: 'Invalid profile configuration' });
145
- }
146
- const result = await sshService.testConnection(profile);
147
- writeJson(res, 200, {
148
- ok: result.success,
149
- success: result.success,
150
- latency: result.latency,
151
- latencyMs: result.latency,
152
- os: result.os,
153
- remoteOs: result.os
154
- });
155
- } catch (err) {
156
- writeJson(res, 200, { ok: false, error: err.message });
157
- }
158
- }
159
- }), 'dsh-remote-workspace: /test');
160
-
161
- // 6. POST /dsh-remote-workspace/browse
162
- sctx.effect(() => webServer.register({
163
- kind: 'exact',
164
- path: '/dsh-remote-workspace/browse',
165
- handler: async (req, res) => {
166
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
167
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
168
- try {
169
- const body = await readBody(req);
170
- const profile = body.profile || (body.profileId ? store.getProfile(body.profileId) : store.getActiveProfile());
171
- if (!profile || !profile.host) {
172
- return writeJson(res, 400, { ok: false, error: 'No profile specified' });
173
- }
174
- const remotePath = body.remotePath || '/';
175
- const entries = await remoteFs.listDir(profile, remotePath, Boolean(body.refresh));
176
- writeJson(res, 200, { ok: true, currentPath: remotePath, entries });
177
- } catch (err) {
178
- writeJson(res, 500, { ok: false, error: err.message });
179
- }
180
- }
181
- }), 'dsh-remote-workspace: /browse');
182
-
183
- // 7. POST /dsh-remote-workspace/file/view
184
- sctx.effect(() => webServer.register({
185
- kind: 'exact',
186
- path: '/dsh-remote-workspace/file/view',
187
- handler: async (req, res) => {
188
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
189
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
190
- try {
191
- const { profileId, filePath } = await readBody(req);
192
- const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
193
- if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
194
- if (!filePath) return writeJson(res, 400, { ok: false, error: 'filePath required' });
195
-
196
- const stat = await remoteFs.stat(profile, filePath);
197
- if (stat.size > 1024 * 512) {
198
- return writeJson(res, 400, { ok: false, error: `File too large for preview (${Math.round(stat.size / 1024)} KB). Max 512 KB.` });
199
- }
200
-
201
- const content = await remoteFs.readFile(profile, filePath, 'utf8');
202
- writeJson(res, 200, { ok: true, filePath, content, size: stat.size, mtime: stat.mtime });
203
- } catch (err) {
204
- writeJson(res, 500, { ok: false, error: err.message });
205
- }
206
- }
207
- }), 'dsh-remote-workspace: /file/view');
208
-
209
- // 8. POST /dsh-remote-workspace/file/save
210
- sctx.effect(() => webServer.register({
211
- kind: 'exact',
212
- path: '/dsh-remote-workspace/file/save',
213
- handler: async (req, res) => {
214
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
215
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
216
- try {
217
- const { profileId, filePath, content } = await readBody(req);
218
- const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
219
- if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
220
- if (!filePath || content === undefined) return writeJson(res, 400, { ok: false, error: 'filePath and content required' });
221
-
222
- const result = await remoteFs.writeFile(profile, filePath, content, 'utf8');
223
- writeJson(res, 200, { ok: true, ...result });
224
- } catch (err) {
225
- writeJson(res, 500, { ok: false, error: err.message });
226
- }
227
- }
228
- }), 'dsh-remote-workspace: /file/save');
229
-
230
- // 9. POST /dsh-remote-workspace/terminal/create
231
- sctx.effect(() => webServer.register({
232
- kind: 'exact',
233
- path: '/dsh-remote-workspace/terminal/create',
234
- handler: async (req, res) => {
235
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
236
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
237
- try {
238
- const { profileId, cols, rows } = await readBody(req);
239
- const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
240
- if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
241
-
242
- const session = await sshService.createTerminalSession(profile, { cols, rows });
243
- writeJson(res, 200, { ok: true, sessionId: session.id });
244
- } catch (err) {
245
- writeJson(res, 500, { ok: false, error: err.message });
246
- }
247
- }
248
- }), 'dsh-remote-workspace: /terminal/create');
249
-
250
- // 10. POST /dsh-remote-workspace/terminal/input
251
- sctx.effect(() => webServer.register({
252
- kind: 'exact',
253
- path: '/dsh-remote-workspace/terminal/input',
254
- handler: async (req, res) => {
255
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
256
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
257
- try {
258
- const { sessionId, data } = await readBody(req);
259
- const session = sshService.getTerminalSession(sessionId);
260
- if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
261
- session.write(data || '');
262
- writeJson(res, 200, { ok: true });
263
- } catch (err) {
264
- writeJson(res, 500, { ok: false, error: err.message });
265
- }
266
- }
267
- }), 'dsh-remote-workspace: /terminal/input');
268
-
269
- // 11. GET /dsh-remote-workspace/terminal/stream (SSE)
270
- sctx.effect(() => webServer.register({
271
- kind: 'exact',
272
- path: '/dsh-remote-workspace/terminal/stream',
273
- handler: async (req, res) => {
274
- const url = new URL(req.url, 'http://localhost');
275
- const sessionId = url.searchParams.get('sessionId');
276
- const session = sshService.getTerminalSession(sessionId);
277
- if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
278
-
279
- if (typeof res.writeHead === 'function') {
280
- res.writeHead(200, {
281
- 'Content-Type': 'text/event-stream',
282
- 'Cache-Control': 'no-cache, no-transform',
283
- 'Connection': 'keep-alive'
284
- });
285
- }
286
-
287
- for (const chunk of session.buffer) {
288
- res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
289
- }
290
-
291
- const listener = (chunk) => {
292
- res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
293
- };
294
- session.subscribers.add(listener);
295
-
296
- req.on('close', () => {
297
- session.subscribers.delete(listener);
298
- });
299
- }
300
- }), 'dsh-remote-workspace: /terminal/stream');
301
-
302
- // 12. POST /dsh-remote-workspace/terminal/close
303
- sctx.effect(() => webServer.register({
304
- kind: 'exact',
305
- path: '/dsh-remote-workspace/terminal/close',
306
- handler: async (req, res) => {
307
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
308
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
309
- try {
310
- const { sessionId } = await readBody(req);
311
- const ok = sshService.closeTerminalSession(sessionId);
312
- writeJson(res, 200, { ok });
313
- } catch (err) {
314
- writeJson(res, 500, { ok: false, error: err.message });
315
- }
316
- }
317
- }), 'dsh-remote-workspace: /terminal/close');
318
-
319
- // 13. POST /dsh-remote-workspace/sync
320
- sctx.effect(() => webServer.register({
321
- kind: 'exact',
322
- path: '/dsh-remote-workspace/sync',
323
- handler: async (req, res) => {
324
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
325
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
326
- try {
327
- const { profileId, direction, force } = await readBody(req);
328
- const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
329
- if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
330
- if (!profile.remoteWorkspace || !profile.localMirrorPath) {
331
- return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be configured' });
332
- }
333
- const resSync = direction === 'push'
334
- ? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, Boolean(force))
335
- : await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
336
- writeJson(res, 200, { ok: resSync.success, ...resSync });
337
- } catch (err) {
338
- writeJson(res, 500, { ok: false, error: err.message });
339
- }
340
- }
341
- }), 'dsh-remote-workspace: /sync');
342
-
343
- // 14. POST /dsh-remote-workspace/tunnels/start
344
- sctx.effect(() => webServer.register({
345
- kind: 'exact',
346
- path: '/dsh-remote-workspace/tunnels/start',
347
- handler: async (req, res) => {
348
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
349
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
350
- try {
351
- const { profileId, localPort, targetHost, remotePort } = await readBody(req);
352
- const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
353
- if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
354
- const lPort = parseInt(localPort, 10);
355
- const rPort = parseInt(remotePort, 10);
356
- if (!lPort || !rPort) {
357
- return writeJson(res, 400, { ok: false, error: 'Valid localPort and remotePort required' });
358
- }
359
- const result = await tunnelService.startLocalTunnel(profile, lPort, targetHost || '127.0.0.1', rPort);
360
- writeJson(res, 200, { ok: result.success, ...result });
361
- } catch (err) {
362
- writeJson(res, 500, { ok: false, error: err.message });
363
- }
364
- }
365
- }), 'dsh-remote-workspace: /tunnels/start');
366
-
367
- // 15. POST /dsh-remote-workspace/tunnels/stop
368
- sctx.effect(() => webServer.register({
369
- kind: 'exact',
370
- path: '/dsh-remote-workspace/tunnels/stop',
371
- handler: async (req, res) => {
372
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
373
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
374
- try {
375
- const { tunnelId } = await readBody(req);
376
- if (!tunnelId) return writeJson(res, 400, { ok: false, error: 'tunnelId required' });
377
- const stopped = tunnelService.stopTunnel(tunnelId);
378
- writeJson(res, 200, { ok: stopped });
379
- } catch (err) {
380
- writeJson(res, 500, { ok: false, error: err.message });
381
- }
382
- }
383
- }), 'dsh-remote-workspace: /tunnels/stop');
384
-
385
- // 16. GET /dsh-remote-workspace/tunnels/telemetry
386
- sctx.effect(() => webServer.register({
387
- kind: 'exact',
388
- path: '/dsh-remote-workspace/tunnels/telemetry',
389
- handler: async (req, res) => {
390
- if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
391
- try {
392
- writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
393
- } catch (err) {
394
- writeJson(res, 500, { ok: false, error: err.message });
395
- }
396
- }
397
- }), 'dsh-remote-workspace: /tunnels/telemetry');
398
-
399
- // 17. GET /dsh-remote-workspace/health
400
- sctx.effect(() => webServer.register({
401
- kind: 'exact',
402
- path: '/dsh-remote-workspace/health',
403
- handler: async (req, res) => {
404
- if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
405
- try {
406
- const url = new URL(req.url, 'http://localhost');
407
- const profileId = url.searchParams.get('profileId');
408
- const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
409
- if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
410
-
411
- const health = await healthService.getHealth(profile);
412
- writeJson(res, 200, { ok: true, health });
413
- } catch (err) {
414
- writeJson(res, 500, { ok: false, error: err.message });
415
- }
416
- }
417
- }), 'dsh-remote-workspace: /health');
418
-
419
- // 18. GET /dsh-remote-workspace/docker/list
420
- sctx.effect(() => webServer.register({
421
- kind: 'exact',
422
- path: '/dsh-remote-workspace/docker/list',
423
- handler: async (req, res) => {
424
- if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
425
- try {
426
- const url = new URL(req.url, 'http://localhost');
427
- const profileId = url.searchParams.get('profileId');
428
- const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
429
- if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
430
-
431
- const result = await dockerService.listContainers(profile, true);
432
- writeJson(res, 200, { ok: true, ...result });
433
- } catch (err) {
434
- writeJson(res, 500, { ok: false, error: err.message });
435
- }
436
- }
437
- }), 'dsh-remote-workspace: /docker/list');
438
-
439
- // 19. POST /dsh-remote-workspace/docker/action
440
- sctx.effect(() => webServer.register({
441
- kind: 'exact',
442
- path: '/dsh-remote-workspace/docker/action',
443
- handler: async (req, res) => {
444
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
445
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
446
- try {
447
- const { profileId, containerId, action } = await readBody(req);
448
- const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
449
- if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
450
- if (!containerId || !action) return writeJson(res, 400, { ok: false, error: 'containerId and action required' });
451
-
452
- const result = await dockerService.containerAction(profile, containerId, action);
453
- writeJson(res, 200, { ok: result.success, ...result });
454
- } catch (err) {
455
- writeJson(res, 500, { ok: false, error: err.message });
456
- }
457
- }
458
- }), 'dsh-remote-workspace: /docker/action');
459
-
460
- // 20. POST /dsh-remote-workspace/docker/logs
461
- sctx.effect(() => webServer.register({
462
- kind: 'exact',
463
- path: '/dsh-remote-workspace/docker/logs',
464
- handler: async (req, res) => {
465
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
466
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
467
- try {
468
- const { profileId, containerId, tail } = await readBody(req);
469
- const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
470
- if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
471
- if (!containerId) return writeJson(res, 400, { ok: false, error: 'containerId required' });
472
-
473
- const result = await dockerService.containerLogs(profile, containerId, tail || 100);
474
- writeJson(res, 200, { ok: result.success, ...result });
475
- } catch (err) {
476
- writeJson(res, 500, { ok: false, error: err.message });
477
- }
478
- }
479
- }), 'dsh-remote-workspace: /docker/logs');
480
-
481
- // 21. POST /dsh-remote-workspace/watcher/toggle
482
- sctx.effect(() => webServer.register({
483
- kind: 'exact',
484
- path: '/dsh-remote-workspace/watcher/toggle',
485
- handler: async (req, res) => {
486
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
487
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
488
- try {
489
- const { enabled } = await readBody(req);
490
- await store.setAutoSyncEnabled(Boolean(enabled));
491
- writeJson(res, 200, { ok: true, autoSync: store.isAutoSyncEnabled() });
492
- } catch (err) {
493
- writeJson(res, 500, { ok: false, error: err.message });
494
- }
495
- }
496
- }), 'dsh-remote-workspace: /watcher/toggle');
497
-
498
- // 22. POST /dsh-remote-workspace/transfer
499
- sctx.effect(() => webServer.register({
500
- kind: 'exact',
501
- path: '/dsh-remote-workspace/transfer',
502
- handler: async (req, res) => {
503
- if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
504
- if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
505
- try {
506
- const { sourceProfileId, sourcePath, destProfileId, destPath } = await readBody(req);
507
- const srcProfile = sourceProfileId ? store.getProfile(sourceProfileId) : store.getActiveProfile();
508
- const destProfile = store.getProfile(destProfileId);
509
-
510
- if (!srcProfile) return writeJson(res, 404, { ok: false, error: 'Source profile not found' });
511
- if (!destProfile) return writeJson(res, 404, { ok: false, error: 'Destination profile not found' });
512
- if (!sourcePath || !destPath) return writeJson(res, 400, { ok: false, error: 'sourcePath and destPath required' });
513
-
514
- const result = await sshService.transferFileBetweenHosts(srcProfile, sourcePath, destProfile, destPath);
515
- writeJson(res, 200, { ok: result.success, ...result });
516
- } catch (err) {
517
- writeJson(res, 500, { ok: false, error: err.message });
518
- }
519
- }
520
- }), 'dsh-remote-workspace: /transfer');
32
+ // Plugin updater registration
33
+ sctx.effect(() => registerPluginUpdater(sctx, {
34
+ packageName: '@goodandready/dsh-remote-workspace',
35
+ endpoint: '/dsh-remote-workspace/update',
36
+ manifestUrl: new URL('../package.json', import.meta.url)
37
+ }), 'dsh-remote-workspace: updater');
38
+
39
+ sctx.effect(() => registerPluginUpdater(sctx, {
40
+ packageName: '@goodandready/dsh-remote-workspace',
41
+ endpoint: '/api/dsh-remote-workspace/update',
42
+ manifestUrl: new URL('../package.json', import.meta.url)
43
+ }), 'dsh-remote-workspace: api-updater');
44
+
45
+ registerProfileRoutes(sctx, webServer, { store, sshService, alertService, tunnelService });
46
+ registerFileRoutes(sctx, webServer, { remoteFs, store, sshService });
47
+ registerEnvRoutes(sctx, webServer, { envService, diagnoseService, sshService, store });
48
+ registerTerminalRoutes(sctx, webServer, { sshService, store });
49
+ registerTunnelRoutes(sctx, webServer, { tunnelService, store });
50
+ registerDockerRoutes(sctx, webServer, { dockerService, healthService, store });
51
+ registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService, watcherService, store });
521
52
  });
522
53
  }
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ export class SshConfigParser {
6
+ /**
7
+ * Parse ssh config string into DSH remote profiles
8
+ * @param {string} text
9
+ * @returns {Array<any>}
10
+ */
11
+ static parse(text) {
12
+ if (!text || typeof text !== 'string') return [];
13
+
14
+ const lines = text.split('\n');
15
+ const profiles = [];
16
+ let current = null;
17
+
18
+ for (let rawLine of lines) {
19
+ const line = rawLine.trim();
20
+ if (!line || line.startsWith('#')) continue;
21
+
22
+ // Key Value pair (can be separated by whitespace or '=')
23
+ const match = line.match(/^(\w+)(?:\s+|=)(.+)$/);
24
+ if (!match) continue;
25
+
26
+ const key = match[1].toLowerCase();
27
+ const val = match[2].trim().replace(/^["']|["']$/g, '');
28
+
29
+ if (key === 'host') {
30
+ // Skip wildcards like Host *
31
+ if (val === '*' || val.includes('*') || val.includes('?')) {
32
+ current = null;
33
+ continue;
34
+ }
35
+
36
+ if (current && current.host) {
37
+ profiles.push(current);
38
+ }
39
+
40
+ current = {
41
+ id: `ssh-cfg-${val.toLowerCase().replace(/[^a-z0-9_-]/g, '-')}`,
42
+ name: val,
43
+ host: val,
44
+ port: 22,
45
+ username: os.userInfo().username || 'root',
46
+ authType: 'key',
47
+ privateKey: '~/.ssh/id_ed25519',
48
+ remoteProjectDir: '/root',
49
+ jumpHostId: ''
50
+ };
51
+ } else if (current) {
52
+ if (key === 'hostname') {
53
+ current.host = val;
54
+ } else if (key === 'user') {
55
+ current.username = val;
56
+ } else if (key === 'port') {
57
+ const p = parseInt(val, 10);
58
+ if (p > 0 && p < 65536) current.port = p;
59
+ } else if (key === 'identityfile') {
60
+ current.authType = 'key';
61
+ current.privateKey = val;
62
+ } else if (key === 'proxyjump') {
63
+ current.jumpHostId = val;
64
+ }
65
+ }
66
+ }
67
+
68
+ if (current && current.host) {
69
+ profiles.push(current);
70
+ }
71
+
72
+ return profiles;
73
+ }
74
+
75
+ /**
76
+ * Load local default ~/.ssh/config if exists
77
+ */
78
+ static readDefaultConfig() {
79
+ const configPath = path.join(os.homedir(), '.ssh', 'config');
80
+ if (fs.existsSync(configPath)) {
81
+ try {
82
+ return fs.readFileSync(configPath, 'utf8');
83
+ } catch {
84
+ return '';
85
+ }
86
+ }
87
+ return '';
88
+ }
89
+ }