@goodandready/dsh-remote-workspace 0.3.1 → 0.3.3

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.
@@ -38,7 +38,7 @@ export class DockerService {
38
38
  ports: item.Ports || '',
39
39
  created: item.CreatedAt || item.Created || ''
40
40
  });
41
- } catch (_) {}
41
+ } catch (err) { /* best-effort cleanup */ }
42
42
  }
43
43
 
44
44
  return {
package/lib/index.js CHANGED
@@ -82,6 +82,19 @@ export function apply(ctx, config = {}) {
82
82
  }
83
83
  }
84
84
 
85
+ const persistConfig = async (actionLabel) => {
86
+ if (settingsApi?.replace) {
87
+ try {
88
+ await settingsApi.replace(Config(currentConfig));
89
+ } catch (err) {
90
+ if (ctx.logger?.warn) {
91
+ ctx.logger.warn(`[dsh-remote-workspace] Failed to persist config after ${actionLabel}: ${err.message}`);
92
+ }
93
+ throw new Error(`Failed to save configuration: ${err.message}`);
94
+ }
95
+ }
96
+ };
97
+
85
98
  const store = {
86
99
  vault: vaultService,
87
100
  getRawProfiles() { return currentConfig.profiles || []; },
@@ -105,9 +118,7 @@ export function apply(ctx, config = {}) {
105
118
  if (enabled) watcherService.start(active, mirrorSync);
106
119
  else watcherService.stop(active.id);
107
120
  }
108
- if (settingsApi?.replace) {
109
- try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
110
- }
121
+ await persistConfig('update');
111
122
  },
112
123
  async saveProfile(rawP) {
113
124
  const list = [...(currentConfig.profiles || [])];
@@ -140,9 +151,7 @@ export function apply(ctx, config = {}) {
140
151
  currentConfig.activeProfileId = p.id;
141
152
  }
142
153
  sshService.invalidate(p.id);
143
- if (settingsApi?.replace) {
144
- try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
145
- }
154
+ await persistConfig('update');
146
155
  },
147
156
  async deleteProfile(id) {
148
157
  watcherService.stop(id);
@@ -152,9 +161,7 @@ export function apply(ctx, config = {}) {
152
161
  }
153
162
  vaultService.deleteProfileSecrets(id);
154
163
  sshService.disconnect(id);
155
- if (settingsApi?.replace) {
156
- try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
157
- }
164
+ await persistConfig('update');
158
165
  },
159
166
  async setActiveId(id) {
160
167
  currentConfig.activeProfileId = id;
@@ -163,13 +170,34 @@ export function apply(ctx, config = {}) {
163
170
  const active = store.getActiveProfile();
164
171
  if (active) watcherService.start(active, mirrorSync);
165
172
  }
166
- if (settingsApi?.replace) {
167
- try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
168
- }
173
+ await persistConfig('update');
169
174
  }
170
175
  };
171
176
 
172
177
  sshService.setJumpProfileResolver((jumpId) => store.getProfile(jumpId));
178
+ sshService.setProfileResolver((id) => store.getProfile(id));
179
+
180
+ const safeProfilesService = {
181
+ list() {
182
+ return (store.getProfiles() || []).map((p) => ({
183
+ id: p.id,
184
+ name: p.name || p.host,
185
+ host: p.host,
186
+ port: p.port || 22,
187
+ username: p.username || 'root',
188
+ authType: p.authType || 'key'
189
+ }));
190
+ },
191
+ get(id) {
192
+ return store.getProfile(id);
193
+ },
194
+ getActiveId() {
195
+ return store.getActiveId();
196
+ },
197
+ getActive() {
198
+ return store.getActiveProfile();
199
+ }
200
+ };
173
201
 
174
202
  // If autoSync was enabled on restart, resume watcher for active profile
175
203
  if (currentConfig.autoSyncActive && currentConfig.activeProfileId) {
@@ -208,6 +236,8 @@ export function apply(ctx, config = {}) {
208
236
  ctx.provide('remoteDiagnose');
209
237
  ctx.provide('remoteEnv');
210
238
  ctx.provide('remoteAlert');
239
+ ctx.provide('remoteProfiles');
240
+ ctx.remoteProfiles = safeProfilesService;
211
241
 
212
242
  ctx.remoteSsh = sshService;
213
243
  ctx.remoteFs = remoteFs;
@@ -51,7 +51,7 @@ export class MirrorSyncService {
51
51
  if (fs.existsSync(snapshotFile)) {
52
52
  try {
53
53
  lastSnapshot = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
54
- } catch (_) {}
54
+ } catch (err) { /* best-effort cleanup */ }
55
55
  }
56
56
 
57
57
  const conflicts = [];
@@ -172,7 +172,7 @@ export class MirrorSyncService {
172
172
  if (fs.existsSync(snapshotFile)) {
173
173
  try {
174
174
  snapshot = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
175
- } catch (_) {}
175
+ } catch (err) { /* best-effort cleanup */ }
176
176
  }
177
177
 
178
178
  const pushed = [];
@@ -0,0 +1,85 @@
1
+ import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
2
+
3
+ export function registerDockerRoutes(sctx, webServer, { dockerService, healthService, store }) {
4
+ // 21. GET /dsh-remote-workspace/health
5
+ sctx.effect(() => webServer.register({
6
+ kind: 'exact',
7
+ path: '/dsh-remote-workspace/health',
8
+ handler: async (req, res) => {
9
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
10
+ try {
11
+ const url = new URL(req.url, 'http://localhost');
12
+ const profileId = url.searchParams.get('profileId');
13
+ const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
14
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
15
+
16
+ const health = await healthService.getHealth(profile);
17
+ writeJson(res, 200, { ok: true, health });
18
+ } catch (err) {
19
+ writeJson(res, 500, { ok: false, error: err.message });
20
+ }
21
+ }
22
+ }), 'dsh-remote-workspace: /health');
23
+
24
+ // 22. GET /dsh-remote-workspace/docker/list
25
+ sctx.effect(() => webServer.register({
26
+ kind: 'exact',
27
+ path: '/dsh-remote-workspace/docker/list',
28
+ handler: async (req, res) => {
29
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
30
+ try {
31
+ const url = new URL(req.url, 'http://localhost');
32
+ const profileId = url.searchParams.get('profileId');
33
+ const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
34
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
35
+
36
+ const result = await dockerService.listContainers(profile, true);
37
+ writeJson(res, 200, { ok: true, ...result });
38
+ } catch (err) {
39
+ writeJson(res, 500, { ok: false, error: err.message });
40
+ }
41
+ }
42
+ }), 'dsh-remote-workspace: /docker/list');
43
+
44
+ // 23. POST /dsh-remote-workspace/docker/action
45
+ sctx.effect(() => webServer.register({
46
+ kind: 'exact',
47
+ path: '/dsh-remote-workspace/docker/action',
48
+ handler: async (req, res) => {
49
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
50
+ if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
51
+ try {
52
+ const { profileId, containerId, action } = await readBody(req);
53
+ const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
54
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
55
+ if (!containerId || !action) return writeJson(res, 400, { ok: false, error: 'containerId and action required' });
56
+
57
+ const result = await dockerService.containerAction(profile, containerId, action);
58
+ writeJson(res, 200, { ok: result.success, ...result });
59
+ } catch (err) {
60
+ writeJson(res, 500, { ok: false, error: err.message });
61
+ }
62
+ }
63
+ }), 'dsh-remote-workspace: /docker/action');
64
+
65
+ // 24. POST /dsh-remote-workspace/docker/logs
66
+ sctx.effect(() => webServer.register({
67
+ kind: 'exact',
68
+ path: '/dsh-remote-workspace/docker/logs',
69
+ handler: async (req, res) => {
70
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
71
+ if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
72
+ try {
73
+ const { profileId, containerId, tail } = await readBody(req);
74
+ const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
75
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
76
+ if (!containerId) return writeJson(res, 400, { ok: false, error: 'containerId required' });
77
+
78
+ const result = await dockerService.containerLogs(profile, containerId, tail || 100);
79
+ writeJson(res, 200, { ok: result.success, ...result });
80
+ } catch (err) {
81
+ writeJson(res, 500, { ok: false, error: err.message });
82
+ }
83
+ }
84
+ }), 'dsh-remote-workspace: /docker/logs');
85
+ }
@@ -0,0 +1,60 @@
1
+ import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
2
+
3
+ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService, sshService, store }) {
4
+ // 10. POST /dsh-remote-workspace/env/view
5
+ sctx.effect(() => webServer.register({
6
+ kind: 'exact',
7
+ path: '/dsh-remote-workspace/env/view',
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, remotePath } = await readBody(req);
13
+ const profile = store.getProfile(profileId);
14
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
15
+ const result = await envService.readRemoteEnv(sshService, profile, remotePath);
16
+ writeJson(res, 200, { ok: true, ...result });
17
+ } catch (err) {
18
+ writeJson(res, 500, { ok: false, error: err.message });
19
+ }
20
+ }
21
+ }), 'dsh-remote-workspace: /env/view');
22
+
23
+ // 11. POST /dsh-remote-workspace/env/save
24
+ sctx.effect(() => webServer.register({
25
+ kind: 'exact',
26
+ path: '/dsh-remote-workspace/env/save',
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 { profileId, remotePath, content } = await readBody(req);
32
+ const profile = store.getProfile(profileId);
33
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
34
+ const result = await envService.saveRemoteEnv(sshService, profile, remotePath, content);
35
+ writeJson(res, 200, { ok: true, ...result });
36
+ } catch (err) {
37
+ writeJson(res, 500, { ok: false, error: err.message });
38
+ }
39
+ }
40
+ }), 'dsh-remote-workspace: /env/save');
41
+
42
+ // 12. POST /dsh-remote-workspace/diagnose
43
+ sctx.effect(() => webServer.register({
44
+ kind: 'exact',
45
+ path: '/dsh-remote-workspace/diagnose',
46
+ handler: async (req, res) => {
47
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
48
+ if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
49
+ try {
50
+ const { profileId, checks } = await readBody(req);
51
+ const profile = store.getProfile(profileId);
52
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
53
+ const results = await diagnoseService.diagnose(sshService, profile, checks);
54
+ writeJson(res, 200, { ok: true, results });
55
+ } catch (err) {
56
+ writeJson(res, 500, { ok: false, error: err.message });
57
+ }
58
+ }
59
+ }), 'dsh-remote-workspace: /diagnose');
60
+ }
@@ -0,0 +1,88 @@
1
+ import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
2
+
3
+ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshService }) {
4
+ // 7. POST /dsh-remote-workspace/browse
5
+ sctx.effect(() => webServer.register({
6
+ kind: 'exact',
7
+ path: '/dsh-remote-workspace/browse',
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, remotePath } = await readBody(req);
13
+ const profile = store.getProfile(profileId);
14
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
15
+ const items = await remoteFs.list(profile, remotePath || profile.remotePath || '.');
16
+ writeJson(res, 200, { ok: true, items });
17
+ } catch (err) {
18
+ writeJson(res, 500, { ok: false, error: err.message });
19
+ }
20
+ }
21
+ }), 'dsh-remote-workspace: /browse');
22
+
23
+ // 8. POST /dsh-remote-workspace/file/view
24
+ sctx.effect(() => webServer.register({
25
+ kind: 'exact',
26
+ path: '/dsh-remote-workspace/file/view',
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 { profileId, filePath } = await readBody(req);
32
+ const profile = store.getProfile(profileId);
33
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
34
+ const stat = await remoteFs.stat(profile, filePath);
35
+ if (stat.size > 2 * 1024 * 1024) {
36
+ return writeJson(res, 400, { ok: false, error: 'File exceeds 2MB preview limit' });
37
+ }
38
+ const buf = await remoteFs.readFile(profile, filePath);
39
+ writeJson(res, 200, { ok: true, content: buf.toString('utf8'), size: stat.size });
40
+ } catch (err) {
41
+ writeJson(res, 500, { ok: false, error: err.message });
42
+ }
43
+ }
44
+ }), 'dsh-remote-workspace: /file/view');
45
+
46
+ // 9. POST /dsh-remote-workspace/file/save
47
+ sctx.effect(() => webServer.register({
48
+ kind: 'exact',
49
+ path: '/dsh-remote-workspace/file/save',
50
+ handler: async (req, res) => {
51
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
52
+ if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
53
+ try {
54
+ const { profileId, filePath, content } = await readBody(req);
55
+ const profile = store.getProfile(profileId);
56
+ if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
57
+ await remoteFs.writeFile(profile, filePath, Buffer.from(content, 'utf8'));
58
+ writeJson(res, 200, { ok: true });
59
+ } catch (err) {
60
+ writeJson(res, 500, { ok: false, error: err.message });
61
+ }
62
+ }
63
+ }), 'dsh-remote-workspace: /file/save');
64
+
65
+ // 26. POST /dsh-remote-workspace/transfer
66
+ sctx.effect(() => webServer.register({
67
+ kind: 'exact',
68
+ path: '/dsh-remote-workspace/transfer',
69
+ handler: async (req, res) => {
70
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
71
+ if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
72
+ try {
73
+ const { sourceProfileId, sourcePath, destProfileId, destPath } = await readBody(req);
74
+ const srcProfile = sourceProfileId ? store.getProfile(sourceProfileId) : store.getActiveProfile();
75
+ const destProfile = store.getProfile(destProfileId);
76
+
77
+ if (!srcProfile) return writeJson(res, 404, { ok: false, error: 'Source profile not found' });
78
+ if (!destProfile) return writeJson(res, 404, { ok: false, error: 'Destination profile not found' });
79
+ if (!sourcePath || !destPath) return writeJson(res, 400, { ok: false, error: 'sourcePath and destPath required' });
80
+
81
+ const result = await sshService.transferFileBetweenHosts(srcProfile, sourcePath, destProfile, destPath);
82
+ writeJson(res, 200, { ok: result.success, ...result });
83
+ } catch (err) {
84
+ writeJson(res, 500, { ok: false, error: err.message });
85
+ }
86
+ }
87
+ }), 'dsh-remote-workspace: /transfer');
88
+ }
@@ -0,0 +1,40 @@
1
+ export function writeJson(res, statusCode, body) {
2
+ if (typeof res.setHeader === 'function') {
3
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
4
+ res.statusCode = statusCode;
5
+ } else if (typeof res.writeHead === 'function') {
6
+ res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
7
+ } else {
8
+ res.statusCode = statusCode;
9
+ }
10
+ res.end(JSON.stringify(body));
11
+ }
12
+
13
+ export function readBody(req) {
14
+ return new Promise((resolve, reject) => {
15
+ let body = '';
16
+ req.on('data', (chunk) => { body += chunk.toString(); });
17
+ req.on('end', () => {
18
+ try {
19
+ resolve(body ? JSON.parse(body) : {});
20
+ } catch (err) {
21
+ reject(err);
22
+ }
23
+ });
24
+ req.on('error', reject);
25
+ });
26
+ }
27
+
28
+ export function isTrustedSettingsRequest(req) {
29
+ const rawAuth = req.headers['authorization'];
30
+ if (typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) return true;
31
+ const cookieHeader = req.headers['cookie'];
32
+ if (typeof cookieHeader === 'string' && (cookieHeader.includes('token=') || cookieHeader.includes('dsh_token='))) {
33
+ return true;
34
+ }
35
+ const secFetch = req.headers['sec-fetch-site'];
36
+ if (secFetch === 'same-origin') return true;
37
+ const ip = req.socket?.remoteAddress || '';
38
+ if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') return true;
39
+ return false;
40
+ }
@@ -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
+ }