@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/README.md +17 -0
- package/README.ru.md +17 -0
- package/README.zh.md +17 -0
- package/docs/design/DESIGN.md +1 -1
- package/lib/alert-service.js +109 -0
- package/lib/client.js +2316 -1991
- package/lib/diagnose-service.js +57 -0
- package/lib/docker-service.js +1 -1
- package/lib/env-service.js +122 -0
- package/lib/index.js +83 -16
- package/lib/mirror-sync-service.js +2 -2
- package/lib/routes/docker.js +85 -0
- package/lib/routes/env.js +60 -0
- package/lib/routes/files.js +88 -0
- package/lib/routes/helpers.js +40 -0
- package/lib/routes/profiles.js +141 -0
- package/lib/routes/sync.js +52 -0
- package/lib/routes/terminal.js +103 -0
- package/lib/routes/tunnels.js +59 -0
- package/lib/routes.js +47 -516
- package/lib/ssh-config-parser.js +89 -0
- package/lib/ssh-service.js +24 -9
- package/lib/tar-sync-service.js +85 -0
- package/lib/tools.js +78 -3
- package/lib/tunnel-service.js +2 -2
- package/lib/updater-service.js +338 -0
- package/lib/vault-service.js +29 -5
- package/lib/watcher-service.js +3 -3
- package/package.json +6 -3
- package/docs/plans/findings.md +0 -19
- package/docs/plans/progress.md +0 -15
- package/docs/plans/task_plan.md +0 -23
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
2
|
+
import { SshConfigParser } from '../ssh-config-parser.js';
|
|
3
|
+
|
|
4
|
+
export function registerProfileRoutes(sctx, webServer, { store, sshService, alertService, tunnelService }) {
|
|
5
|
+
// 1. GET /dsh-remote-workspace/state
|
|
6
|
+
sctx.effect(() => webServer.register({
|
|
7
|
+
kind: 'exact',
|
|
8
|
+
path: '/dsh-remote-workspace/state',
|
|
9
|
+
handler: async (req, res) => {
|
|
10
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
11
|
+
try {
|
|
12
|
+
const profiles = store.getProfiles();
|
|
13
|
+
const activeId = store.getActiveId() || (profiles[0]?.id || '');
|
|
14
|
+
const tunnels = tunnelService.listActiveTunnels();
|
|
15
|
+
const autoSync = store.isAutoSyncEnabled ? store.isAutoSyncEnabled() : false;
|
|
16
|
+
const alerts = alertService ? alertService.getAlerts() : [];
|
|
17
|
+
const sanitized = profiles.map((p) => (store.vault ? store.vault.sanitizeProfile(p) : p));
|
|
18
|
+
writeJson(res, 200, { ok: true, activeId, profiles: sanitized, tunnels, autoSync, alerts });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}), 'dsh-remote-workspace: /state');
|
|
24
|
+
|
|
25
|
+
// 2. POST /dsh-remote-workspace/profiles/save
|
|
26
|
+
sctx.effect(() => webServer.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dsh-remote-workspace/profiles/save',
|
|
29
|
+
handler: async (req, res) => {
|
|
30
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
31
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
32
|
+
try {
|
|
33
|
+
const profile = await readBody(req);
|
|
34
|
+
if (!profile.id || !profile.host) {
|
|
35
|
+
return writeJson(res, 400, { ok: false, error: 'ID and Host are required' });
|
|
36
|
+
}
|
|
37
|
+
sshService.invalidate(profile.id);
|
|
38
|
+
await store.saveProfile(profile);
|
|
39
|
+
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
|
|
40
|
+
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}), 'dsh-remote-workspace: /profiles/save');
|
|
46
|
+
|
|
47
|
+
// 3. POST /dsh-remote-workspace/profiles/delete
|
|
48
|
+
sctx.effect(() => webServer.register({
|
|
49
|
+
kind: 'exact',
|
|
50
|
+
path: '/dsh-remote-workspace/profiles/delete',
|
|
51
|
+
handler: async (req, res) => {
|
|
52
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
53
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
54
|
+
try {
|
|
55
|
+
const { id } = await readBody(req);
|
|
56
|
+
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
57
|
+
sshService.disconnect(id);
|
|
58
|
+
await store.deleteProfile(id);
|
|
59
|
+
writeJson(res, 200, { ok: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}), 'dsh-remote-workspace: /profiles/delete');
|
|
65
|
+
|
|
66
|
+
// 4. POST /dsh-remote-workspace/profiles/active
|
|
67
|
+
sctx.effect(() => webServer.register({
|
|
68
|
+
kind: 'exact',
|
|
69
|
+
path: '/dsh-remote-workspace/profiles/active',
|
|
70
|
+
handler: async (req, res) => {
|
|
71
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
72
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
73
|
+
try {
|
|
74
|
+
const { id } = await readBody(req);
|
|
75
|
+
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
76
|
+
await store.setActiveId(id);
|
|
77
|
+
writeJson(res, 200, { ok: true, activeId: id });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}), 'dsh-remote-workspace: /profiles/active');
|
|
83
|
+
|
|
84
|
+
// 5. POST /dsh-remote-workspace/profiles/import-ssh-config
|
|
85
|
+
sctx.effect(() => webServer.register({
|
|
86
|
+
kind: 'exact',
|
|
87
|
+
path: '/dsh-remote-workspace/profiles/import-ssh-config',
|
|
88
|
+
handler: async (req, res) => {
|
|
89
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
90
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
91
|
+
try {
|
|
92
|
+
const { content } = await readBody(req);
|
|
93
|
+
if (!content || typeof content !== 'string') {
|
|
94
|
+
return writeJson(res, 400, { ok: false, error: 'SSH config content is required' });
|
|
95
|
+
}
|
|
96
|
+
const importedProfiles = SshConfigParser.parse(content);
|
|
97
|
+
for (const p of importedProfiles) {
|
|
98
|
+
await store.saveProfile(p);
|
|
99
|
+
}
|
|
100
|
+
writeJson(res, 200, { ok: true, count: importedProfiles.length, profiles: importedProfiles });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}), 'dsh-remote-workspace: /profiles/import-ssh-config');
|
|
106
|
+
|
|
107
|
+
// 6. POST /dsh-remote-workspace/test
|
|
108
|
+
sctx.effect(() => webServer.register({
|
|
109
|
+
kind: 'exact',
|
|
110
|
+
path: '/dsh-remote-workspace/test',
|
|
111
|
+
handler: async (req, res) => {
|
|
112
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
113
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
114
|
+
try {
|
|
115
|
+
const profile = await readBody(req);
|
|
116
|
+
if (!profile.host) return writeJson(res, 400, { ok: false, error: 'Host is required' });
|
|
117
|
+
|
|
118
|
+
const existing = store.getProfile(profile.id);
|
|
119
|
+
const effectiveProfile = {
|
|
120
|
+
...profile,
|
|
121
|
+
password: profile.password && profile.password !== '••••••••'
|
|
122
|
+
? profile.password
|
|
123
|
+
: (existing?.password || ''),
|
|
124
|
+
privateKey: profile.privateKey && profile.privateKey !== '••••••••'
|
|
125
|
+
? profile.privateKey
|
|
126
|
+
: (existing?.privateKey || ''),
|
|
127
|
+
passphrase: profile.passphrase && profile.passphrase !== '••••••••'
|
|
128
|
+
? profile.passphrase
|
|
129
|
+
: (existing?.passphrase || '')
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = await sshService.testConnection(effectiveProfile);
|
|
133
|
+
const latencyMs = result.latency !== undefined ? result.latency : (result.latencyMs ?? 0);
|
|
134
|
+
const remoteOs = result.os !== undefined ? result.os : (result.remoteOs ?? 'Linux/Unix');
|
|
135
|
+
writeJson(res, 200, { ok: true, latency: latencyMs, latencyMs, os: remoteOs, remoteOs });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
writeJson(res, 200, { ok: false, error: err.message });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}), 'dsh-remote-workspace: /test');
|
|
141
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
export function registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService, watcherService, store }) {
|
|
4
|
+
// 17. POST /dsh-remote-workspace/sync
|
|
5
|
+
sctx.effect(() => webServer.register({
|
|
6
|
+
kind: 'exact',
|
|
7
|
+
path: '/dsh-remote-workspace/sync',
|
|
8
|
+
handler: async (req, res) => {
|
|
9
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
|
+
try {
|
|
12
|
+
const { profileId, direction, force, mode } = await readBody(req);
|
|
13
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
14
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
15
|
+
if (!profile.remoteWorkspace || !profile.localMirrorPath) {
|
|
16
|
+
return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be configured' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (mode === 'tarball' && tarSyncService) {
|
|
20
|
+
const resTar = direction === 'push'
|
|
21
|
+
? await tarSyncService.pushTar(profile, profile.localMirrorPath, profile.remoteWorkspace)
|
|
22
|
+
: await tarSyncService.pullTar(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
23
|
+
return writeJson(res, 200, { ok: true, ...resTar });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resSync = direction === 'push'
|
|
27
|
+
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, Boolean(force))
|
|
28
|
+
: await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
29
|
+
writeJson(res, 200, { ok: resSync.success, ...resSync });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}), 'dsh-remote-workspace: /sync');
|
|
35
|
+
|
|
36
|
+
// 25. POST /dsh-remote-workspace/watcher/toggle
|
|
37
|
+
sctx.effect(() => webServer.register({
|
|
38
|
+
kind: 'exact',
|
|
39
|
+
path: '/dsh-remote-workspace/watcher/toggle',
|
|
40
|
+
handler: async (req, res) => {
|
|
41
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
42
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
43
|
+
try {
|
|
44
|
+
const { enabled } = await readBody(req);
|
|
45
|
+
await store.setAutoSyncEnabled(Boolean(enabled));
|
|
46
|
+
writeJson(res, 200, { ok: true, autoSync: store.isAutoSyncEnabled() });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}), 'dsh-remote-workspace: /watcher/toggle');
|
|
52
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
4
|
+
// 13. POST /dsh-remote-workspace/terminal/create
|
|
5
|
+
sctx.effect(() => webServer.register({
|
|
6
|
+
kind: 'exact',
|
|
7
|
+
path: '/dsh-remote-workspace/terminal/create',
|
|
8
|
+
handler: async (req, res) => {
|
|
9
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
|
+
try {
|
|
12
|
+
const { profileId, cols, rows } = await readBody(req);
|
|
13
|
+
const profile = store.getProfile(profileId);
|
|
14
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
15
|
+
const session = await sshService.createTerminalSession(profile, cols || 80, rows || 24);
|
|
16
|
+
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}), 'dsh-remote-workspace: /terminal/create');
|
|
22
|
+
|
|
23
|
+
// 14. POST /dsh-remote-workspace/terminal/input
|
|
24
|
+
sctx.effect(() => webServer.register({
|
|
25
|
+
kind: 'exact',
|
|
26
|
+
path: '/dsh-remote-workspace/terminal/input',
|
|
27
|
+
handler: async (req, res) => {
|
|
28
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
29
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
30
|
+
try {
|
|
31
|
+
const { sessionId, data, resize } = await readBody(req);
|
|
32
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
33
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Session not found' });
|
|
34
|
+
if (resize && typeof resize.cols === 'number' && typeof resize.rows === 'number') {
|
|
35
|
+
session.resize(resize.cols, resize.rows);
|
|
36
|
+
}
|
|
37
|
+
if (typeof data === 'string') {
|
|
38
|
+
session.write(data);
|
|
39
|
+
}
|
|
40
|
+
writeJson(res, 200, { ok: true });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}), 'dsh-remote-workspace: /terminal/input');
|
|
46
|
+
|
|
47
|
+
// 15. GET /dsh-remote-workspace/terminal/stream (SSE)
|
|
48
|
+
sctx.effect(() => webServer.register({
|
|
49
|
+
kind: 'exact',
|
|
50
|
+
path: '/dsh-remote-workspace/terminal/stream',
|
|
51
|
+
handler: async (req, res) => {
|
|
52
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(req.url, 'http://localhost');
|
|
55
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
56
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
57
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Session not found' });
|
|
58
|
+
|
|
59
|
+
res.writeHead(200, {
|
|
60
|
+
'Content-Type': 'text/event-stream',
|
|
61
|
+
'Cache-Control': 'no-cache',
|
|
62
|
+
Connection: 'keep-alive'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (session.buffer.length > 0) {
|
|
66
|
+
res.write(`data: ${JSON.stringify(session.buffer.join(''))}\n\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sub = (chunk) => {
|
|
70
|
+
try {
|
|
71
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
session.subscribers.delete(sub);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
session.subscribers.add(sub);
|
|
77
|
+
|
|
78
|
+
req.on('close', () => {
|
|
79
|
+
session.subscribers.delete(sub);
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}), 'dsh-remote-workspace: /terminal/stream');
|
|
86
|
+
|
|
87
|
+
// 16. POST /dsh-remote-workspace/terminal/close
|
|
88
|
+
sctx.effect(() => webServer.register({
|
|
89
|
+
kind: 'exact',
|
|
90
|
+
path: '/dsh-remote-workspace/terminal/close',
|
|
91
|
+
handler: async (req, res) => {
|
|
92
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
93
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
94
|
+
try {
|
|
95
|
+
const { sessionId } = await readBody(req);
|
|
96
|
+
sshService.closeTerminalSession(sessionId);
|
|
97
|
+
writeJson(res, 200, { ok: true });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}), 'dsh-remote-workspace: /terminal/close');
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
export function registerTunnelRoutes(sctx, webServer, { tunnelService, store }) {
|
|
4
|
+
// 18. POST /dsh-remote-workspace/tunnels/start
|
|
5
|
+
sctx.effect(() => webServer.register({
|
|
6
|
+
kind: 'exact',
|
|
7
|
+
path: '/dsh-remote-workspace/tunnels/start',
|
|
8
|
+
handler: async (req, res) => {
|
|
9
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
|
+
try {
|
|
12
|
+
const { profileId, localPort, targetHost, remotePort } = await readBody(req);
|
|
13
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
14
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
15
|
+
const lPort = parseInt(localPort, 10);
|
|
16
|
+
const rPort = parseInt(remotePort, 10);
|
|
17
|
+
if (!lPort || !rPort) {
|
|
18
|
+
return writeJson(res, 400, { ok: false, error: 'Valid localPort and remotePort required' });
|
|
19
|
+
}
|
|
20
|
+
const result = await tunnelService.startLocalTunnel(profile, lPort, targetHost || '127.0.0.1', rPort);
|
|
21
|
+
writeJson(res, 200, { ok: result.success, ...result });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}), 'dsh-remote-workspace: /tunnels/start');
|
|
27
|
+
|
|
28
|
+
// 19. POST /dsh-remote-workspace/tunnels/stop
|
|
29
|
+
sctx.effect(() => webServer.register({
|
|
30
|
+
kind: 'exact',
|
|
31
|
+
path: '/dsh-remote-workspace/tunnels/stop',
|
|
32
|
+
handler: async (req, res) => {
|
|
33
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
34
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
35
|
+
try {
|
|
36
|
+
const { tunnelId } = await readBody(req);
|
|
37
|
+
if (!tunnelId) return writeJson(res, 400, { ok: false, error: 'tunnelId required' });
|
|
38
|
+
const stopped = tunnelService.stopTunnel(tunnelId);
|
|
39
|
+
writeJson(res, 200, { ok: stopped });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}), 'dsh-remote-workspace: /tunnels/stop');
|
|
45
|
+
|
|
46
|
+
// 20. GET /dsh-remote-workspace/tunnels/telemetry
|
|
47
|
+
sctx.effect(() => webServer.register({
|
|
48
|
+
kind: 'exact',
|
|
49
|
+
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
50
|
+
handler: async (req, res) => {
|
|
51
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
52
|
+
try {
|
|
53
|
+
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}), 'dsh-remote-workspace: /tunnels/telemetry');
|
|
59
|
+
}
|