@goodandready/dsh-remote-workspace 0.3.14 → 0.3.16
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/CHANGELOG.md +141 -0
- package/lib/alert-service.js +18 -5
- package/lib/client.js +17 -17
- package/lib/env-service.js +17 -8
- package/lib/file-transfer.js +0 -7
- package/lib/host-projection.js +0 -4
- package/lib/index.js +25 -2
- package/lib/mirror-sync-service.js +21 -1
- package/lib/proxy-command.js +44 -5
- package/lib/remote-fs-service.js +49 -5
- package/lib/routes/docker.js +1 -1
- package/lib/routes/env.js +33 -17
- package/lib/routes/files.js +13 -11
- package/lib/routes/helpers.js +117 -28
- package/lib/routes/profiles.js +15 -3
- package/lib/routes/sync.js +1 -1
- package/lib/routes/terminal.js +1 -1
- package/lib/routes/tunnels.js +1 -1
- package/lib/routes.js +1 -1
- package/lib/ssh-config-parser.js +50 -7
- package/lib/ssh-service.js +27 -3
- package/lib/tar-sync-service.js +7 -7
- package/lib/tools.js +145 -17
- package/lib/updater-service.js +14 -11
- package/lib/vault-service.js +14 -5
- package/package.json +3 -2
- package/lib/workspace-mount.js +0 -5
package/lib/tar-sync-service.js
CHANGED
|
@@ -40,7 +40,7 @@ export class TarSyncService {
|
|
|
40
40
|
|
|
41
41
|
localTar.on('close', (code) => {
|
|
42
42
|
if (code !== 0) {
|
|
43
|
-
try { stream.destroy(); } catch (
|
|
43
|
+
try { stream.destroy(); } catch (_err) { /* ignore best-effort stream cleanup */ }
|
|
44
44
|
reject(new Error(`Local tar extraction failed (code ${code}): ${stderr}`));
|
|
45
45
|
} else {
|
|
46
46
|
resolve({ ok: true, direction: 'pull', remoteDir, localDir });
|
|
@@ -48,17 +48,17 @@ export class TarSyncService {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
localTar.on('error', (err) => {
|
|
51
|
-
try { stream.destroy(); } catch (
|
|
51
|
+
try { stream.destroy(); } catch (_err) { /* ignore best-effort stream cleanup */ }
|
|
52
52
|
reject(err);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
stream.on('error', (err) => {
|
|
56
|
-
try { localTar.kill('SIGTERM'); } catch (
|
|
56
|
+
try { localTar.kill('SIGTERM'); } catch (_err) { /* ignore best-effort process termination */ }
|
|
57
57
|
reject(err);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
localTar.stdin.on('error', (err) => {
|
|
61
|
-
try { stream.destroy(); } catch (
|
|
61
|
+
try { stream.destroy(); } catch (_err) { /* ignore best-effort stream cleanup */ }
|
|
62
62
|
reject(err);
|
|
63
63
|
});
|
|
64
64
|
});
|
|
@@ -93,7 +93,7 @@ export class TarSyncService {
|
|
|
93
93
|
|
|
94
94
|
stream.on('close', (code) => {
|
|
95
95
|
if (code !== 0) {
|
|
96
|
-
try { localTar.kill('SIGTERM'); } catch (
|
|
96
|
+
try { localTar.kill('SIGTERM'); } catch (_err) { /* ignore best-effort process termination */ }
|
|
97
97
|
reject(new Error(`Remote tar extraction failed (code ${code}): ${remoteStderr}`));
|
|
98
98
|
} else {
|
|
99
99
|
resolve({ ok: true, direction: 'push', localDir, remoteDir });
|
|
@@ -101,12 +101,12 @@ export class TarSyncService {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
stream.on('error', (err) => {
|
|
104
|
-
try { localTar.kill('SIGTERM'); } catch (
|
|
104
|
+
try { localTar.kill('SIGTERM'); } catch (_err) { /* ignore best-effort process termination */ }
|
|
105
105
|
reject(err);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
localTar.on('error', (err) => {
|
|
109
|
-
try { stream.destroy(); } catch (
|
|
109
|
+
try { stream.destroy(); } catch (_err) { /* ignore best-effort stream cleanup */ }
|
|
110
110
|
reject(err);
|
|
111
111
|
});
|
|
112
112
|
});
|
package/lib/tools.js
CHANGED
|
@@ -1,7 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { shellQuote } from './tar-sync-service.js';
|
|
1
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
4
|
import { renderHostTable } from './host-projection.js';
|
|
3
5
|
import { filterClusterProfiles, runCluster } from './cluster-service.js';
|
|
4
6
|
|
|
7
|
+
|
|
8
|
+
export function isWithinWorkspace(normWorkspace, candidatePath) {
|
|
9
|
+
const normCandidate = path.posix.normalize(candidatePath);
|
|
10
|
+
const prefix = normWorkspace.endsWith('/') ? normWorkspace : normWorkspace + '/';
|
|
11
|
+
return normCandidate === normWorkspace || normCandidate.startsWith(prefix);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveWorkspacePath(workspace, targetPath) {
|
|
15
|
+
if (!workspace || typeof workspace !== 'string' || !workspace.trim()) {
|
|
16
|
+
throw new Error('Remote workspace is not configured for active profile');
|
|
17
|
+
}
|
|
18
|
+
const trimmedWorkspace = workspace.trim();
|
|
19
|
+
if (trimmedWorkspace.includes('\0') || !path.posix.isAbsolute(trimmedWorkspace)) {
|
|
20
|
+
throw new Error('Remote workspace must be an absolute path starting with "/"');
|
|
21
|
+
}
|
|
22
|
+
if (!targetPath || typeof targetPath !== 'string' || targetPath.includes('\0')) {
|
|
23
|
+
throw new Error('Path must be a non-empty string without null bytes');
|
|
24
|
+
}
|
|
25
|
+
const normWorkspace = path.posix.normalize(trimmedWorkspace);
|
|
26
|
+
const resolved = path.posix.isAbsolute(targetPath)
|
|
27
|
+
? path.posix.normalize(targetPath)
|
|
28
|
+
: path.posix.resolve(normWorkspace, targetPath);
|
|
29
|
+
|
|
30
|
+
if (!isWithinWorkspace(normWorkspace, resolved)) {
|
|
31
|
+
throw new Error(`Access denied: path "${targetPath}" is outside configured remoteWorkspace "${workspace}"`);
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function verifyWorkspaceRealpath(remoteFs, profile, workspace, resolvedPath, targetPath) {
|
|
37
|
+
if (!remoteFs || typeof remoteFs.realpath !== 'function') {
|
|
38
|
+
return resolvedPath;
|
|
39
|
+
}
|
|
40
|
+
const normWorkspace = path.posix.normalize(workspace.trim());
|
|
41
|
+
let realWorkspace = normWorkspace;
|
|
42
|
+
try {
|
|
43
|
+
const resolvedWs = await remoteFs.realpath(profile, normWorkspace);
|
|
44
|
+
if (resolvedWs && typeof resolvedWs === 'string') {
|
|
45
|
+
realWorkspace = path.posix.normalize(resolvedWs);
|
|
46
|
+
}
|
|
47
|
+
} catch (_err) {
|
|
48
|
+
// Fallback to normalized configured workspace if workspace root itself cannot be resolved yet
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let checkPath = resolvedPath;
|
|
52
|
+
let realCandidate = null;
|
|
53
|
+
try {
|
|
54
|
+
realCandidate = await remoteFs.realpath(profile, checkPath);
|
|
55
|
+
} catch (_err) {
|
|
56
|
+
// For non-existent leaf targets (e.g., write / mkdir), verify the parent directory realpath
|
|
57
|
+
const parentPath = path.posix.dirname(checkPath);
|
|
58
|
+
try {
|
|
59
|
+
const realParent = await remoteFs.realpath(profile, parentPath);
|
|
60
|
+
if (realParent && typeof realParent === 'string') {
|
|
61
|
+
realCandidate = path.posix.join(realParent, path.posix.basename(checkPath));
|
|
62
|
+
}
|
|
63
|
+
} catch (_parentErr) {
|
|
64
|
+
realCandidate = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (realCandidate && typeof realCandidate === 'string') {
|
|
69
|
+
const normReal = path.posix.normalize(realCandidate);
|
|
70
|
+
if (!isWithinWorkspace(realWorkspace, normReal)) {
|
|
71
|
+
throw new Error(`Access denied: symlink target "${normReal}" for path "${targetPath}" escapes configured remoteWorkspace "${workspace}"`);
|
|
72
|
+
}
|
|
73
|
+
return normReal;
|
|
74
|
+
}
|
|
75
|
+
return resolvedPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
5
78
|
export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunnelService, getActiveProfile, getProfileById, dockerService, diagnoseService, envService, tarSyncService, listProfiles = null) {
|
|
6
79
|
if (!ctx.tools || typeof ctx.tools.register !== 'function') {
|
|
7
80
|
return;
|
|
@@ -9,7 +82,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
9
82
|
|
|
10
83
|
const genericOutput = {
|
|
11
84
|
schema: { type: 'object', additionalProperties: true },
|
|
12
|
-
render: (_args, value) =>
|
|
85
|
+
render: (_args, value) => [{
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
88
|
+
}]
|
|
13
89
|
};
|
|
14
90
|
|
|
15
91
|
// 1. remote_exec
|
|
@@ -68,28 +144,38 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
68
144
|
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
69
145
|
const { action, path: targetPath, content, recursive, bypassCache } = args;
|
|
70
146
|
|
|
147
|
+
const lexicalPath = resolveWorkspacePath(profile.remoteWorkspace, targetPath);
|
|
148
|
+
const normWs = path.posix.normalize(profile.remoteWorkspace.trim());
|
|
149
|
+
if (action === 'remove' && lexicalPath === normWs) {
|
|
150
|
+
throw new Error('Cannot remove root workspace directory');
|
|
151
|
+
}
|
|
152
|
+
const resolvedPath = await verifyWorkspaceRealpath(remoteFs, profile, profile.remoteWorkspace, lexicalPath, targetPath);
|
|
153
|
+
if (action === 'remove' && resolvedPath === normWs) {
|
|
154
|
+
throw new Error('Cannot remove root workspace directory');
|
|
155
|
+
}
|
|
156
|
+
|
|
71
157
|
switch (action) {
|
|
72
158
|
case 'read': {
|
|
73
|
-
const data = await remoteFs.readFile(profile,
|
|
159
|
+
const data = await remoteFs.readFile(profile, resolvedPath, 'utf8');
|
|
74
160
|
return { content: data };
|
|
75
161
|
}
|
|
76
162
|
case 'write': {
|
|
77
163
|
if (content === undefined) throw new Error('Content is required for write action');
|
|
78
|
-
const res = await remoteFs.writeFile(profile,
|
|
164
|
+
const res = await remoteFs.writeFile(profile, resolvedPath, content, 'utf8');
|
|
79
165
|
return res;
|
|
80
166
|
}
|
|
81
167
|
case 'stat': {
|
|
82
|
-
return await remoteFs.stat(profile,
|
|
168
|
+
return await remoteFs.stat(profile, resolvedPath, Boolean(bypassCache));
|
|
83
169
|
}
|
|
84
170
|
case 'list': {
|
|
85
|
-
const entries = await remoteFs.listDir(profile,
|
|
171
|
+
const entries = await remoteFs.listDir(profile, resolvedPath, Boolean(bypassCache));
|
|
86
172
|
return { entries };
|
|
87
173
|
}
|
|
88
174
|
case 'mkdir': {
|
|
89
|
-
return await remoteFs.mkdir(profile,
|
|
175
|
+
return await remoteFs.mkdir(profile, resolvedPath);
|
|
90
176
|
}
|
|
91
177
|
case 'remove': {
|
|
92
|
-
return await remoteFs.remove(profile,
|
|
178
|
+
return await remoteFs.remove(profile, resolvedPath, recursive);
|
|
93
179
|
}
|
|
94
180
|
default:
|
|
95
181
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -216,18 +302,32 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
216
302
|
const profile = getActiveProfile();
|
|
217
303
|
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
218
304
|
|
|
219
|
-
const
|
|
305
|
+
const ALLOWED_ACTIONS = ['status', 'start', 'stop', 'restart', 'reload', 'enable', 'disable', 'logs'];
|
|
306
|
+
if (!args.action || !ALLOWED_ACTIONS.includes(args.action)) {
|
|
307
|
+
throw new Error(`Invalid service action: "${args.action}". Allowed actions: ${ALLOWED_ACTIONS.join(', ')}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const mgr = (args.manager || 'systemd').toLowerCase();
|
|
311
|
+
if (!['systemd', 'pm2'].includes(mgr)) {
|
|
312
|
+
throw new Error(`Invalid service manager: "${args.manager}". Allowed managers: systemd, pm2`);
|
|
313
|
+
}
|
|
314
|
+
|
|
220
315
|
const svc = args.service;
|
|
221
|
-
|
|
316
|
+
if (!svc || typeof svc !== 'string' || svc.trim() === '') {
|
|
317
|
+
throw new Error('Service name is required');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const lines = Math.max(1, Math.min(10000, parseInt(args.lines, 10) || 40));
|
|
321
|
+
const safeSvc = shellQuote(svc.trim());
|
|
222
322
|
|
|
223
323
|
let cmd = '';
|
|
224
324
|
if (mgr === 'pm2') {
|
|
225
|
-
if (args.action === 'logs') cmd = `pm2 logs ${
|
|
226
|
-
else cmd = `pm2 ${args.action} ${
|
|
325
|
+
if (args.action === 'logs') cmd = `pm2 logs ${safeSvc} --lines ${lines} --nostream`;
|
|
326
|
+
else cmd = `pm2 ${args.action} ${safeSvc}`;
|
|
227
327
|
} else {
|
|
228
|
-
if (args.action === 'logs') cmd = `journalctl -u ${
|
|
229
|
-
else if (args.action === 'status') cmd = `systemctl status ${
|
|
230
|
-
else cmd = `sudo systemctl ${args.action} ${
|
|
328
|
+
if (args.action === 'logs') cmd = `journalctl -u ${safeSvc} -n ${lines} --no-pager`;
|
|
329
|
+
else if (args.action === 'status') cmd = `systemctl status ${safeSvc} --no-pager`;
|
|
330
|
+
else cmd = `sudo systemctl ${args.action} ${safeSvc} || systemctl ${args.action} ${safeSvc}`;
|
|
231
331
|
}
|
|
232
332
|
|
|
233
333
|
const res = await sshService.exec(profile, cmd, { cleanAnsi: true });
|
|
@@ -303,7 +403,7 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
303
403
|
},
|
|
304
404
|
key: {
|
|
305
405
|
type: 'string',
|
|
306
|
-
description: 'Environment variable key (required for set)'
|
|
406
|
+
description: 'Environment variable key (optional for read to retrieve a specific variable; required for set)'
|
|
307
407
|
},
|
|
308
408
|
value: {
|
|
309
409
|
type: 'string',
|
|
@@ -316,7 +416,32 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
316
416
|
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
317
417
|
|
|
318
418
|
if (args.action === 'read') {
|
|
319
|
-
|
|
419
|
+
const res = await envService.readEnv(profile, args.filePath);
|
|
420
|
+
if (args.key) {
|
|
421
|
+
const entry = (res.entries || []).find((e) => e.key === args.key);
|
|
422
|
+
const val = entry ? entry.value : (res.variables ? res.variables[args.key] : undefined);
|
|
423
|
+
return {
|
|
424
|
+
filePath: res.filePath || args.filePath,
|
|
425
|
+
key: args.key,
|
|
426
|
+
exists: entry !== undefined || (res.variables && res.variables[args.key] !== undefined),
|
|
427
|
+
value: val !== undefined ? val : null
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const keys = res.entries
|
|
431
|
+
? res.entries.filter((e) => e.key).map((e) => e.key)
|
|
432
|
+
: (res.variables ? Object.keys(res.variables) : []);
|
|
433
|
+
const sanitizedEntries = (res.entries || []).map((e) => ({
|
|
434
|
+
key: e.key,
|
|
435
|
+
isComment: Boolean(e.isComment),
|
|
436
|
+
hasValue: Boolean(e.value),
|
|
437
|
+
value: e.value ? '••••••••' : ''
|
|
438
|
+
}));
|
|
439
|
+
return {
|
|
440
|
+
filePath: res.filePath || args.filePath,
|
|
441
|
+
keys,
|
|
442
|
+
entries: sanitizedEntries,
|
|
443
|
+
note: 'Values are masked to prevent secret leakage into LLM context. Pass "key" argument to read a specific variable.'
|
|
444
|
+
};
|
|
320
445
|
} else if (args.action === 'set') {
|
|
321
446
|
if (!args.key) throw new Error('key is required for set action');
|
|
322
447
|
return await envService.setEnvVar(profile, args.filePath, args.key, args.value || '');
|
|
@@ -360,7 +485,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
360
485
|
},
|
|
361
486
|
output: {
|
|
362
487
|
schema: { type: 'object', additionalProperties: true },
|
|
363
|
-
render: (_args, value) =>
|
|
488
|
+
render: (_args, value) => [{
|
|
489
|
+
type: 'text',
|
|
490
|
+
text: value && value.table ? value.table : ''
|
|
491
|
+
}]
|
|
364
492
|
},
|
|
365
493
|
async execute(args) {
|
|
366
494
|
const profiles = typeof listProfiles === 'function' ? (listProfiles() || []) : [];
|
package/lib/updater-service.js
CHANGED
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Host-side one-click updater for @goodandready/dsh-remote-workspace.
|
|
@@ -206,21 +205,25 @@ export async function status(options, target) {
|
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
|
|
208
|
+
export function buildInstallArgs(target, packageSpec, options = {}) {
|
|
209
|
+
// Note: Do NOT add --config.minimumReleaseAge=0 per issue #214 / dsh-plugin-guard#65
|
|
210
|
+
return [
|
|
211
|
+
target.cliEntry,
|
|
212
|
+
'plugin',
|
|
213
|
+
'--profile',
|
|
214
|
+
target.profileName,
|
|
215
|
+
'add',
|
|
216
|
+
packageSpec,
|
|
217
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
209
221
|
async function installExact(target, packageSpec, options) {
|
|
210
222
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
211
223
|
await new Promise((resolvePromise, reject) => {
|
|
212
224
|
const child = spawn(
|
|
213
225
|
process.execPath,
|
|
214
|
-
|
|
215
|
-
target.cliEntry,
|
|
216
|
-
'plugin',
|
|
217
|
-
'--profile',
|
|
218
|
-
target.profileName,
|
|
219
|
-
'add',
|
|
220
|
-
'--config.minimumReleaseAge=0',
|
|
221
|
-
packageSpec,
|
|
222
|
-
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
223
|
-
],
|
|
226
|
+
buildInstallArgs(target, packageSpec, options),
|
|
224
227
|
{
|
|
225
228
|
cwd: target.profileDir,
|
|
226
229
|
windowsHide: true,
|
package/lib/vault-service.js
CHANGED
|
@@ -2,9 +2,17 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Resolve DSH home directory, respecting custom DSH_HOME environment variable.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveDshHome() {
|
|
9
|
+
return process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* VaultService manages credentials (password, privateKey, passphrase)
|
|
7
|
-
* stored in an isolated, secure .env file
|
|
14
|
+
* stored in an isolated, secure .env file with owner-only (0600) permissions
|
|
15
|
+
* instead of DSH profiles JSON.
|
|
8
16
|
*/
|
|
9
17
|
export class VaultService {
|
|
10
18
|
constructor(customPath = null) {
|
|
@@ -12,8 +20,8 @@ export class VaultService {
|
|
|
12
20
|
if (customPath) {
|
|
13
21
|
this.envPath = customPath;
|
|
14
22
|
} else {
|
|
15
|
-
const
|
|
16
|
-
const secretsDir = path.join(
|
|
23
|
+
const dshHome = resolveDshHome();
|
|
24
|
+
const secretsDir = path.join(dshHome, 'secrets');
|
|
17
25
|
try {
|
|
18
26
|
if (!fs.existsSync(secretsDir)) {
|
|
19
27
|
fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
|
|
@@ -36,7 +44,7 @@ export class VaultService {
|
|
|
36
44
|
ensureEnvFile() {
|
|
37
45
|
try {
|
|
38
46
|
if (!fs.existsSync(this.envPath)) {
|
|
39
|
-
fs.writeFileSync(this.envPath, '# DSH Remote Workspace
|
|
47
|
+
fs.writeFileSync(this.envPath, '# DSH Remote Workspace Secrets Vault (Owner-Only Permissions 0600)\n', { mode: 0o600, encoding: 'utf8' });
|
|
40
48
|
} else {
|
|
41
49
|
// Ensure file permissions are restricted to owner only
|
|
42
50
|
try {
|
|
@@ -53,6 +61,7 @@ export class VaultService {
|
|
|
53
61
|
getPermissionStatus() {
|
|
54
62
|
return {
|
|
55
63
|
secure: !this.permissionError,
|
|
64
|
+
protection: 'owner-only-permissions',
|
|
56
65
|
error: this.permissionError,
|
|
57
66
|
path: this.envPath
|
|
58
67
|
};
|
|
@@ -109,7 +118,7 @@ export class VaultService {
|
|
|
109
118
|
|
|
110
119
|
writeAll(store) {
|
|
111
120
|
this.ensureEnvFile();
|
|
112
|
-
const lines = ['# DSH Remote Workspace Secrets Vault -
|
|
121
|
+
const lines = ['# DSH Remote Workspace Secrets Vault (Owner-Only Permissions 0600)', ''];
|
|
113
122
|
for (const [key, val] of Object.entries(store)) {
|
|
114
123
|
if (val === undefined || val === null || val === '') continue;
|
|
115
124
|
const strVal = String(val);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-remote-workspace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"README.ru.md",
|
|
18
18
|
"README.zh.md",
|
|
19
|
-
"LICENSE"
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"build": "tsup",
|
package/lib/workspace-mount.js
DELETED