@goodandready/dsh-remote-workspace 0.1.0 → 0.2.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/docs/design/DESIGN.md +60 -51
- package/docs/plans/findings.md +14 -2
- package/docs/plans/progress.md +11 -10
- package/docs/plans/task_plan.md +16 -19
- package/lib/client.js +1766 -966
- package/lib/index.js +79 -18
- package/lib/mirror-sync-service.js +73 -16
- package/lib/remote-fs-service.js +58 -7
- package/lib/routes.js +291 -57
- package/lib/ssh-service.js +188 -17
- package/lib/tools.js +25 -11
- package/lib/tunnel-service.js +85 -32
- package/lib/vault-service.js +184 -0
- package/package.json +1 -1
package/lib/routes.js
CHANGED
|
@@ -1,63 +1,88 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
res.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
1
|
+
export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store) {
|
|
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
|
+
}
|
|
7
13
|
|
|
8
|
-
function readBody(req
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
req.on('end', () => {
|
|
22
|
-
try {
|
|
23
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
24
|
-
resolve(raw ? JSON.parse(raw) : {});
|
|
25
|
-
} catch (err) {
|
|
26
|
-
reject(new Error('Invalid JSON'));
|
|
27
|
-
}
|
|
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);
|
|
28
26
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
42
|
|
|
33
|
-
export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store) {
|
|
34
43
|
ctx.inject(['webServer'], (sctx) => {
|
|
44
|
+
const webServer = sctx.get ? (sctx.get('webServer') ?? sctx.webServer) : sctx.webServer;
|
|
45
|
+
if (!webServer || typeof webServer.register !== 'function') return;
|
|
46
|
+
|
|
35
47
|
// 1. GET /dsh-remote-workspace/state
|
|
36
|
-
sctx.effect(() =>
|
|
48
|
+
sctx.effect(() => webServer.register({
|
|
37
49
|
kind: 'exact',
|
|
38
50
|
path: '/dsh-remote-workspace/state',
|
|
39
51
|
handler: async (req, res) => {
|
|
40
|
-
if (req.method !== 'GET') return writeJson(res, 405, { error: 'GET only' });
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
writeJson(res, 200, {
|
|
58
|
+
ok: true,
|
|
59
|
+
profiles,
|
|
60
|
+
activeId,
|
|
61
|
+
tunnels,
|
|
62
|
+
vaultProtected: true
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
66
|
+
}
|
|
45
67
|
}
|
|
46
68
|
}), 'dsh-remote-workspace: /state');
|
|
47
69
|
|
|
48
70
|
// 2. POST /dsh-remote-workspace/profiles/save
|
|
49
|
-
sctx.effect(() =>
|
|
71
|
+
sctx.effect(() => webServer.register({
|
|
50
72
|
kind: 'exact',
|
|
51
73
|
path: '/dsh-remote-workspace/profiles/save',
|
|
52
74
|
handler: async (req, res) => {
|
|
53
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
75
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
76
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
54
77
|
try {
|
|
55
78
|
const profile = await readBody(req);
|
|
56
79
|
if (!profile.id || !profile.host) {
|
|
57
80
|
return writeJson(res, 400, { ok: false, error: 'ID and Host are required' });
|
|
58
81
|
}
|
|
82
|
+
sshService.invalidate(profile.id);
|
|
59
83
|
await store.saveProfile(profile);
|
|
60
|
-
|
|
84
|
+
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
|
|
85
|
+
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
61
86
|
} catch (err) {
|
|
62
87
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
63
88
|
}
|
|
@@ -65,14 +90,16 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
65
90
|
}), 'dsh-remote-workspace: /profiles/save');
|
|
66
91
|
|
|
67
92
|
// 3. POST /dsh-remote-workspace/profiles/delete
|
|
68
|
-
sctx.effect(() =>
|
|
93
|
+
sctx.effect(() => webServer.register({
|
|
69
94
|
kind: 'exact',
|
|
70
95
|
path: '/dsh-remote-workspace/profiles/delete',
|
|
71
96
|
handler: async (req, res) => {
|
|
72
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
97
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
98
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
73
99
|
try {
|
|
74
100
|
const { id } = await readBody(req);
|
|
75
101
|
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
102
|
+
sshService.disconnect(id);
|
|
76
103
|
await store.deleteProfile(id);
|
|
77
104
|
writeJson(res, 200, { ok: true });
|
|
78
105
|
} catch (err) {
|
|
@@ -82,11 +109,12 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
82
109
|
}), 'dsh-remote-workspace: /profiles/delete');
|
|
83
110
|
|
|
84
111
|
// 4. POST /dsh-remote-workspace/profiles/active
|
|
85
|
-
sctx.effect(() =>
|
|
112
|
+
sctx.effect(() => webServer.register({
|
|
86
113
|
kind: 'exact',
|
|
87
114
|
path: '/dsh-remote-workspace/profiles/active',
|
|
88
115
|
handler: async (req, res) => {
|
|
89
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
116
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
117
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
90
118
|
try {
|
|
91
119
|
const { id } = await readBody(req);
|
|
92
120
|
await store.setActiveId(id || null);
|
|
@@ -98,19 +126,30 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
98
126
|
}), 'dsh-remote-workspace: /profiles/active');
|
|
99
127
|
|
|
100
128
|
// 5. POST /dsh-remote-workspace/test
|
|
101
|
-
sctx.effect(() =>
|
|
129
|
+
sctx.effect(() => webServer.register({
|
|
102
130
|
kind: 'exact',
|
|
103
131
|
path: '/dsh-remote-workspace/test',
|
|
104
132
|
handler: async (req, res) => {
|
|
105
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
133
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
134
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
106
135
|
try {
|
|
107
136
|
const body = await readBody(req);
|
|
108
|
-
|
|
137
|
+
let profile = body.profile || (body.id ? store.getProfile(body.id) : body);
|
|
138
|
+
if (store.vault && profile) {
|
|
139
|
+
profile = store.vault.hydrateProfile(profile);
|
|
140
|
+
}
|
|
109
141
|
if (!profile || !profile.host) {
|
|
110
142
|
return writeJson(res, 400, { ok: false, error: 'Invalid profile configuration' });
|
|
111
143
|
}
|
|
112
144
|
const result = await sshService.testConnection(profile);
|
|
113
|
-
writeJson(res, 200, {
|
|
145
|
+
writeJson(res, 200, {
|
|
146
|
+
ok: result.success,
|
|
147
|
+
success: result.success,
|
|
148
|
+
latency: result.latency,
|
|
149
|
+
latencyMs: result.latency,
|
|
150
|
+
os: result.os,
|
|
151
|
+
remoteOs: result.os
|
|
152
|
+
});
|
|
114
153
|
} catch (err) {
|
|
115
154
|
writeJson(res, 200, { ok: false, error: err.message });
|
|
116
155
|
}
|
|
@@ -118,11 +157,12 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
118
157
|
}), 'dsh-remote-workspace: /test');
|
|
119
158
|
|
|
120
159
|
// 6. POST /dsh-remote-workspace/browse
|
|
121
|
-
sctx.effect(() =>
|
|
160
|
+
sctx.effect(() => webServer.register({
|
|
122
161
|
kind: 'exact',
|
|
123
162
|
path: '/dsh-remote-workspace/browse',
|
|
124
163
|
handler: async (req, res) => {
|
|
125
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
164
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
165
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
126
166
|
try {
|
|
127
167
|
const body = await readBody(req);
|
|
128
168
|
const profile = body.profile || (body.profileId ? store.getProfile(body.profileId) : store.getActiveProfile());
|
|
@@ -130,7 +170,7 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
130
170
|
return writeJson(res, 400, { ok: false, error: 'No profile specified' });
|
|
131
171
|
}
|
|
132
172
|
const remotePath = body.remotePath || '/';
|
|
133
|
-
const entries = await remoteFs.listDir(profile, remotePath);
|
|
173
|
+
const entries = await remoteFs.listDir(profile, remotePath, Boolean(body.refresh));
|
|
134
174
|
writeJson(res, 200, { ok: true, currentPath: remotePath, entries });
|
|
135
175
|
} catch (err) {
|
|
136
176
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
@@ -138,21 +178,159 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
138
178
|
}
|
|
139
179
|
}), 'dsh-remote-workspace: /browse');
|
|
140
180
|
|
|
141
|
-
// 7. POST /dsh-remote-workspace/
|
|
142
|
-
sctx.effect(() =>
|
|
181
|
+
// 7. POST /dsh-remote-workspace/file/view
|
|
182
|
+
sctx.effect(() => webServer.register({
|
|
183
|
+
kind: 'exact',
|
|
184
|
+
path: '/dsh-remote-workspace/file/view',
|
|
185
|
+
handler: async (req, res) => {
|
|
186
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
187
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
188
|
+
try {
|
|
189
|
+
const { profileId, filePath } = await readBody(req);
|
|
190
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
191
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
192
|
+
if (!filePath) return writeJson(res, 400, { ok: false, error: 'filePath required' });
|
|
193
|
+
|
|
194
|
+
const stat = await remoteFs.stat(profile, filePath);
|
|
195
|
+
if (stat.size > 1024 * 512) {
|
|
196
|
+
return writeJson(res, 400, { ok: false, error: `File too large for preview (${Math.round(stat.size / 1024)} KB). Max 512 KB.` });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const content = await remoteFs.readFile(profile, filePath, 'utf8');
|
|
200
|
+
writeJson(res, 200, { ok: true, filePath, content, size: stat.size, mtime: stat.mtime });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}), 'dsh-remote-workspace: /file/view');
|
|
206
|
+
|
|
207
|
+
// 8. POST /dsh-remote-workspace/file/save
|
|
208
|
+
sctx.effect(() => webServer.register({
|
|
209
|
+
kind: 'exact',
|
|
210
|
+
path: '/dsh-remote-workspace/file/save',
|
|
211
|
+
handler: async (req, res) => {
|
|
212
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
213
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
214
|
+
try {
|
|
215
|
+
const { profileId, filePath, content } = await readBody(req);
|
|
216
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
217
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
218
|
+
if (!filePath || content === undefined) return writeJson(res, 400, { ok: false, error: 'filePath and content required' });
|
|
219
|
+
|
|
220
|
+
const result = await remoteFs.writeFile(profile, filePath, content, 'utf8');
|
|
221
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}), 'dsh-remote-workspace: /file/save');
|
|
227
|
+
|
|
228
|
+
// 9. POST /dsh-remote-workspace/terminal/create
|
|
229
|
+
sctx.effect(() => webServer.register({
|
|
230
|
+
kind: 'exact',
|
|
231
|
+
path: '/dsh-remote-workspace/terminal/create',
|
|
232
|
+
handler: async (req, res) => {
|
|
233
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
234
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
235
|
+
try {
|
|
236
|
+
const { profileId, cols, rows } = await readBody(req);
|
|
237
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
238
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
239
|
+
|
|
240
|
+
const session = await sshService.createTerminalSession(profile, { cols, rows });
|
|
241
|
+
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}), 'dsh-remote-workspace: /terminal/create');
|
|
247
|
+
|
|
248
|
+
// 10. POST /dsh-remote-workspace/terminal/input
|
|
249
|
+
sctx.effect(() => webServer.register({
|
|
250
|
+
kind: 'exact',
|
|
251
|
+
path: '/dsh-remote-workspace/terminal/input',
|
|
252
|
+
handler: async (req, res) => {
|
|
253
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
254
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
255
|
+
try {
|
|
256
|
+
const { sessionId, data } = await readBody(req);
|
|
257
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
258
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
259
|
+
session.write(data || '');
|
|
260
|
+
writeJson(res, 200, { ok: true });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}), 'dsh-remote-workspace: /terminal/input');
|
|
266
|
+
|
|
267
|
+
// 11. GET /dsh-remote-workspace/terminal/stream (SSE)
|
|
268
|
+
sctx.effect(() => webServer.register({
|
|
269
|
+
kind: 'exact',
|
|
270
|
+
path: '/dsh-remote-workspace/terminal/stream',
|
|
271
|
+
handler: async (req, res) => {
|
|
272
|
+
const url = new URL(req.url, 'http://localhost');
|
|
273
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
274
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
275
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
276
|
+
|
|
277
|
+
if (typeof res.writeHead === 'function') {
|
|
278
|
+
res.writeHead(200, {
|
|
279
|
+
'Content-Type': 'text/event-stream',
|
|
280
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
281
|
+
'Connection': 'keep-alive'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Send historical buffer
|
|
286
|
+
for (const chunk of session.buffer) {
|
|
287
|
+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const listener = (chunk) => {
|
|
291
|
+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
292
|
+
};
|
|
293
|
+
session.subscribers.add(listener);
|
|
294
|
+
|
|
295
|
+
req.on('close', () => {
|
|
296
|
+
session.subscribers.delete(listener);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}), 'dsh-remote-workspace: /terminal/stream');
|
|
300
|
+
|
|
301
|
+
// 12. POST /dsh-remote-workspace/terminal/close
|
|
302
|
+
sctx.effect(() => webServer.register({
|
|
303
|
+
kind: 'exact',
|
|
304
|
+
path: '/dsh-remote-workspace/terminal/close',
|
|
305
|
+
handler: async (req, res) => {
|
|
306
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
307
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
308
|
+
try {
|
|
309
|
+
const { sessionId } = await readBody(req);
|
|
310
|
+
const ok = sshService.closeTerminalSession(sessionId);
|
|
311
|
+
writeJson(res, 200, { ok });
|
|
312
|
+
} catch (err) {
|
|
313
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}), 'dsh-remote-workspace: /terminal/close');
|
|
317
|
+
|
|
318
|
+
// 13. POST /dsh-remote-workspace/sync
|
|
319
|
+
sctx.effect(() => webServer.register({
|
|
143
320
|
kind: 'exact',
|
|
144
321
|
path: '/dsh-remote-workspace/sync',
|
|
145
322
|
handler: async (req, res) => {
|
|
146
|
-
if (req.method !== 'POST') return writeJson(res, 405, { error: 'POST only' });
|
|
323
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
324
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
147
325
|
try {
|
|
148
|
-
const { profileId, direction } = await readBody(req);
|
|
149
|
-
const profile = store.getProfile(profileId) || store.getActiveProfile();
|
|
326
|
+
const { profileId, direction, force } = await readBody(req);
|
|
327
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
150
328
|
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
151
329
|
if (!profile.remoteWorkspace || !profile.localMirrorPath) {
|
|
152
|
-
return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be
|
|
330
|
+
return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be configured' });
|
|
153
331
|
}
|
|
154
332
|
const resSync = direction === 'push'
|
|
155
|
-
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace)
|
|
333
|
+
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, Boolean(force))
|
|
156
334
|
: await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
157
335
|
writeJson(res, 200, { ok: resSync.success, ...resSync });
|
|
158
336
|
} catch (err) {
|
|
@@ -160,5 +338,61 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
160
338
|
}
|
|
161
339
|
}
|
|
162
340
|
}), 'dsh-remote-workspace: /sync');
|
|
341
|
+
|
|
342
|
+
// 14. POST /dsh-remote-workspace/tunnels/start
|
|
343
|
+
sctx.effect(() => webServer.register({
|
|
344
|
+
kind: 'exact',
|
|
345
|
+
path: '/dsh-remote-workspace/tunnels/start',
|
|
346
|
+
handler: async (req, res) => {
|
|
347
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
348
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
349
|
+
try {
|
|
350
|
+
const { profileId, localPort, targetHost, remotePort } = await readBody(req);
|
|
351
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
352
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
353
|
+
const lPort = parseInt(localPort, 10);
|
|
354
|
+
const rPort = parseInt(remotePort, 10);
|
|
355
|
+
if (!lPort || !rPort) {
|
|
356
|
+
return writeJson(res, 400, { ok: false, error: 'Valid localPort and remotePort required' });
|
|
357
|
+
}
|
|
358
|
+
const result = await tunnelService.startLocalTunnel(profile, lPort, targetHost || '127.0.0.1', rPort);
|
|
359
|
+
writeJson(res, 200, { ok: result.success, ...result });
|
|
360
|
+
} catch (err) {
|
|
361
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}), 'dsh-remote-workspace: /tunnels/start');
|
|
365
|
+
|
|
366
|
+
// 15. POST /dsh-remote-workspace/tunnels/stop
|
|
367
|
+
sctx.effect(() => webServer.register({
|
|
368
|
+
kind: 'exact',
|
|
369
|
+
path: '/dsh-remote-workspace/tunnels/stop',
|
|
370
|
+
handler: async (req, res) => {
|
|
371
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
372
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
373
|
+
try {
|
|
374
|
+
const { tunnelId } = await readBody(req);
|
|
375
|
+
if (!tunnelId) return writeJson(res, 400, { ok: false, error: 'tunnelId required' });
|
|
376
|
+
const stopped = tunnelService.stopTunnel(tunnelId);
|
|
377
|
+
writeJson(res, 200, { ok: stopped });
|
|
378
|
+
} catch (err) {
|
|
379
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}), 'dsh-remote-workspace: /tunnels/stop');
|
|
383
|
+
|
|
384
|
+
// 16. GET /dsh-remote-workspace/tunnels/telemetry
|
|
385
|
+
sctx.effect(() => webServer.register({
|
|
386
|
+
kind: 'exact',
|
|
387
|
+
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
388
|
+
handler: async (req, res) => {
|
|
389
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
390
|
+
try {
|
|
391
|
+
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
392
|
+
} catch (err) {
|
|
393
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}), 'dsh-remote-workspace: /tunnels/telemetry');
|
|
163
397
|
});
|
|
164
398
|
}
|