@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.
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ /**
6
+ * VaultService manages credentials (password, privateKey, passphrase)
7
+ * stored in an isolated, secure .env file instead of DSH profiles JSON.
8
+ */
9
+ export class VaultService {
10
+ constructor(customPath = null) {
11
+ if (customPath) {
12
+ this.envPath = customPath;
13
+ } else {
14
+ const homeDir = os.homedir();
15
+ const secretsDir = path.join(homeDir, '.dsh', 'secrets');
16
+ try {
17
+ if (!fs.existsSync(secretsDir)) {
18
+ fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
19
+ }
20
+ } catch (_) {}
21
+ this.envPath = path.join(secretsDir, 'dsh-remote-workspace.env');
22
+ }
23
+ this.ensureEnvFile();
24
+ }
25
+
26
+ ensureEnvFile() {
27
+ try {
28
+ if (!fs.existsSync(this.envPath)) {
29
+ fs.writeFileSync(this.envPath, '# DSH Remote Workspace Encrypted Secrets Vault\n', { mode: 0o600, encoding: 'utf8' });
30
+ } else {
31
+ // Ensure file permissions are restricted to owner only
32
+ try { fs.chmodSync(this.envPath, 0o600); } catch (_) {}
33
+ }
34
+ } catch (_) {}
35
+ }
36
+
37
+ /**
38
+ * Parse .env file into key-value map
39
+ */
40
+ readAll() {
41
+ this.ensureEnvFile();
42
+ try {
43
+ const content = fs.readFileSync(this.envPath, 'utf8');
44
+ const lines = content.split('\n');
45
+ const store = {};
46
+ let currentKey = null;
47
+ let currentValue = '';
48
+ let isMultiline = false;
49
+
50
+ for (const line of lines) {
51
+ if (!isMultiline) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed || trimmed.startsWith('#')) continue;
54
+ const eqIdx = line.indexOf('=');
55
+ if (eqIdx === -1) continue;
56
+ const key = line.slice(0, eqIdx).trim();
57
+ let val = line.slice(eqIdx + 1);
58
+
59
+ if (val.startsWith('"') && !val.endsWith('"')) {
60
+ isMultiline = true;
61
+ currentKey = key;
62
+ currentValue = val.slice(1) + '\n';
63
+ } else if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) {
64
+ store[key] = val.slice(1, -1).replace(/\\n/g, '\n').replace(/\\r/g, '\r');
65
+ } else {
66
+ store[key] = val;
67
+ }
68
+ } else {
69
+ if (line.endsWith('"')) {
70
+ currentValue += line.slice(0, -1);
71
+ store[currentKey] = currentValue.replace(/\\n/g, '\n').replace(/\\r/g, '\r');
72
+ isMultiline = false;
73
+ currentKey = null;
74
+ currentValue = '';
75
+ } else {
76
+ currentValue += line + '\n';
77
+ }
78
+ }
79
+ }
80
+ return store;
81
+ } catch (err) {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ writeAll(store) {
87
+ this.ensureEnvFile();
88
+ const lines = ['# DSH Remote Workspace Secrets Vault - Auto Generated', ''];
89
+ for (const [key, val] of Object.entries(store)) {
90
+ if (val === undefined || val === null || val === '') continue;
91
+ const strVal = String(val);
92
+ if (strVal.includes('\n') || strVal.includes('"') || strVal.includes('=')) {
93
+ const escaped = strVal.replace(/"/g, '\\"').replace(/\r/g, '\\r').replace(/\n/g, '\\n');
94
+ lines.push(`${key}="${escaped}"`);
95
+ } else {
96
+ lines.push(`${key}=${strVal}`);
97
+ }
98
+ }
99
+ lines.push('');
100
+ fs.writeFileSync(this.envPath, lines.join('\n'), { mode: 0o600, encoding: 'utf8' });
101
+ }
102
+
103
+ keyPrefix(profileId) {
104
+ const cleanId = profileId.replace(/[^a-zA-Z0-9_]/g, '_').toUpperCase();
105
+ return `DRW_${cleanId}`;
106
+ }
107
+
108
+ getProfileSecrets(profileId) {
109
+ if (!profileId) return {};
110
+ const store = this.readAll();
111
+ const prefix = this.keyPrefix(profileId);
112
+ return {
113
+ password: store[`${prefix}_PASSWORD`] || '',
114
+ privateKey: store[`${prefix}_PRIVATE_KEY`] || '',
115
+ passphrase: store[`${prefix}_PASSPHRASE`] || ''
116
+ };
117
+ }
118
+
119
+ setProfileSecrets(profileId, secrets = {}) {
120
+ if (!profileId) return;
121
+ const store = this.readAll();
122
+ const prefix = this.keyPrefix(profileId);
123
+
124
+ if (secrets.password !== undefined) {
125
+ if (secrets.password) store[`${prefix}_PASSWORD`] = secrets.password;
126
+ else delete store[`${prefix}_PASSWORD`];
127
+ }
128
+ if (secrets.privateKey !== undefined) {
129
+ if (secrets.privateKey) store[`${prefix}_PRIVATE_KEY`] = secrets.privateKey;
130
+ else delete store[`${prefix}_PRIVATE_KEY`];
131
+ }
132
+ if (secrets.passphrase !== undefined) {
133
+ if (secrets.passphrase) store[`${prefix}_PASSPHRASE`] = secrets.passphrase;
134
+ else delete store[`${prefix}_PASSPHRASE`];
135
+ }
136
+
137
+ this.writeAll(store);
138
+ }
139
+
140
+ deleteProfileSecrets(profileId) {
141
+ if (!profileId) return;
142
+ const store = this.readAll();
143
+ const prefix = this.keyPrefix(profileId);
144
+ delete store[`${prefix}_PASSWORD`];
145
+ delete store[`${prefix}_PRIVATE_KEY`];
146
+ delete store[`${prefix}_PASSPHRASE`];
147
+ this.writeAll(store);
148
+ }
149
+
150
+ /**
151
+ * Enrich profile with secrets from .env vault before connecting
152
+ */
153
+ hydrateProfile(profile) {
154
+ if (!profile || !profile.id) return profile;
155
+ const secrets = this.getProfileSecrets(profile.id);
156
+ return {
157
+ ...profile,
158
+ password: secrets.password || profile.password || '',
159
+ privateKey: secrets.privateKey || profile.privateKey || '',
160
+ passphrase: secrets.passphrase || profile.passphrase || ''
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Strip secrets for public config/UI serialization
166
+ */
167
+ sanitizeProfile(profile) {
168
+ if (!profile) return profile;
169
+ const secrets = this.getProfileSecrets(profile.id);
170
+ const hasPassword = Boolean(secrets.password || profile.password);
171
+ const hasPrivateKey = Boolean(secrets.privateKey || profile.privateKey);
172
+ const hasPassphrase = Boolean(secrets.passphrase || profile.passphrase);
173
+
174
+ return {
175
+ ...profile,
176
+ password: hasPassword ? '••••••••' : '',
177
+ privateKey: hasPrivateKey ? '••••••••' : '',
178
+ passphrase: hasPassphrase ? '••••••••' : '',
179
+ hasStoredPassword: hasPassword,
180
+ hasStoredPrivateKey: hasPrivateKey,
181
+ hasStoredPassphrase: hasPassphrase
182
+ };
183
+ }
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-remote-workspace",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",