@lemoncat7/dsh-ssh 1.1.8 → 1.3.0-alpha.9
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/ARCHITECTURE.md +77 -0
- package/README.md +39 -18
- package/lib/activity-panel.d.ts +17 -0
- package/lib/activity-panel.d.ts.map +1 -0
- package/lib/activity-panel.js +160 -0
- package/lib/activity-panel.js.map +1 -0
- package/lib/adaptive-workspace.d.ts.map +1 -1
- package/lib/adaptive-workspace.js +19 -4
- package/lib/adaptive-workspace.js.map +1 -1
- package/lib/api.d.ts +8 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +306 -14
- package/lib/api.js.map +1 -1
- package/lib/client-api.d.ts +86 -0
- package/lib/client-api.d.ts.map +1 -1
- package/lib/client-api.js +27 -0
- package/lib/client-api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +4070 -21049
- package/lib/client.js.map +4 -4
- package/lib/connector.js +2 -2
- package/lib/connector.js.map +1 -1
- package/lib/credentials.d.ts +9 -0
- package/lib/credentials.d.ts.map +1 -1
- package/lib/credentials.js +16 -0
- package/lib/credentials.js.map +1 -1
- package/lib/domain.d.ts +58 -1
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +47 -0
- package/lib/domain.js.map +1 -1
- package/lib/endpoint-session-manager.d.ts +16 -0
- package/lib/endpoint-session-manager.d.ts.map +1 -0
- package/lib/endpoint-session-manager.js +70 -0
- package/lib/endpoint-session-manager.js.map +1 -0
- package/lib/file-transfer-manager.d.ts +50 -0
- package/lib/file-transfer-manager.d.ts.map +1 -0
- package/lib/file-transfer-manager.js +257 -0
- package/lib/file-transfer-manager.js.map +1 -0
- package/lib/file-transfer-tools.d.ts +6 -0
- package/lib/file-transfer-tools.d.ts.map +1 -0
- package/lib/file-transfer-tools.js +143 -0
- package/lib/file-transfer-tools.js.map +1 -0
- package/lib/file-transfer-workspace.d.ts +10 -0
- package/lib/file-transfer-workspace.d.ts.map +1 -0
- package/lib/file-transfer-workspace.js +299 -0
- package/lib/file-transfer-workspace.js.map +1 -0
- package/lib/ftp-adapter.d.ts +18 -0
- package/lib/ftp-adapter.d.ts.map +1 -0
- package/lib/ftp-adapter.js +240 -0
- package/lib/ftp-adapter.js.map +1 -0
- package/lib/ftp-profile-editor.d.ts +9 -0
- package/lib/ftp-profile-editor.d.ts.map +1 -0
- package/lib/ftp-profile-editor.js +89 -0
- package/lib/ftp-profile-editor.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -2
- package/lib/index.js.map +1 -1
- package/lib/network-dialer.d.ts +12 -0
- package/lib/network-dialer.d.ts.map +1 -0
- package/lib/network-dialer.js +27 -0
- package/lib/network-dialer.js.map +1 -0
- package/lib/profile-editor.d.ts +16 -0
- package/lib/profile-editor.d.ts.map +1 -0
- package/lib/profile-editor.js +143 -0
- package/lib/profile-editor.js.map +1 -0
- package/lib/project-session-dialog.d.ts +15 -0
- package/lib/project-session-dialog.d.ts.map +1 -0
- package/lib/project-session-dialog.js +60 -0
- package/lib/project-session-dialog.js.map +1 -0
- package/lib/proxy.d.ts +3 -3
- package/lib/proxy.d.ts.map +1 -1
- package/lib/proxy.js +11 -7
- package/lib/proxy.js.map +1 -1
- package/lib/remote-file-systems.d.ts +9 -0
- package/lib/remote-file-systems.d.ts.map +1 -0
- package/lib/remote-file-systems.js +23 -0
- package/lib/remote-file-systems.js.map +1 -0
- package/lib/remote-files.d.ts +51 -0
- package/lib/remote-files.d.ts.map +1 -0
- package/lib/remote-files.js +36 -0
- package/lib/remote-files.js.map +1 -0
- package/lib/remote-workspace-tree.d.ts +28 -0
- package/lib/remote-workspace-tree.d.ts.map +1 -0
- package/lib/remote-workspace-tree.js +117 -0
- package/lib/remote-workspace-tree.js.map +1 -0
- package/lib/resizable-split.d.ts +10 -0
- package/lib/resizable-split.d.ts.map +1 -0
- package/lib/resizable-split.js +115 -0
- package/lib/resizable-split.js.map +1 -0
- package/lib/session-access-channel.d.ts +6 -0
- package/lib/session-access-channel.d.ts.map +1 -0
- package/lib/session-access-channel.js +10 -0
- package/lib/session-access-channel.js.map +1 -0
- package/lib/session-access.d.ts +18 -0
- package/lib/session-access.d.ts.map +1 -0
- package/lib/session-access.js +102 -0
- package/lib/session-access.js.map +1 -0
- package/lib/sftp-adapter.d.ts +13 -0
- package/lib/sftp-adapter.d.ts.map +1 -0
- package/lib/sftp-adapter.js +28 -0
- package/lib/sftp-adapter.js.map +1 -0
- package/lib/sftp-client.d.ts +5 -1
- package/lib/sftp-client.d.ts.map +1 -1
- package/lib/sftp-client.js +85 -31
- package/lib/sftp-client.js.map +1 -1
- package/lib/sftp.d.ts +7 -0
- package/lib/sftp.d.ts.map +1 -1
- package/lib/sftp.js +215 -6
- package/lib/sftp.js.map +1 -1
- package/lib/store.d.ts +5 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +83 -3
- package/lib/store.js.map +1 -1
- package/lib/terminal.d.ts +2 -0
- package/lib/terminal.d.ts.map +1 -1
- package/lib/terminal.js +10 -2
- package/lib/terminal.js.map +1 -1
- package/lib/tools.d.ts +3 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +4 -1
- package/lib/tools.js.map +1 -1
- package/lib/ui-components.d.ts +25 -0
- package/lib/ui-components.d.ts.map +1 -0
- package/lib/ui-components.js +27 -0
- package/lib/ui-components.js.map +1 -0
- package/package.json +8 -2
package/lib/api.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { pipeline } from 'node:stream/promises';
|
|
3
3
|
import { HostKeyRequiredError } from './connector.js';
|
|
4
|
-
import { normalizeCredentialEntryDraft, normalizeForwardDraft, normalizeProfileDraft, normalizeProxyEntryDraft, normalizeSecrets, } from './domain.js';
|
|
4
|
+
import { normalizeCredentialEntryDraft, normalizeForwardDraft, normalizeFtpProfileDraft, normalizeProfileDraft, normalizeProxyEntryDraft, normalizeRemoteProjectDraft, normalizeSecrets, } from './domain.js';
|
|
5
5
|
import { setSessionDirectory } from './directory.js';
|
|
6
6
|
import { streamTerminalOutput } from './terminal-stream.js';
|
|
7
7
|
import { listSftpDirectory, openSftpFile, readSftpFilePreview, uploadSftpFile } from './sftp.js';
|
|
8
8
|
import { streamActivityEvents } from './activity-events.js';
|
|
9
9
|
import { listLocalWorkspace, openLocalWorkspaceFile, readLocalWorkspacePreview } from './local-workspace.js';
|
|
10
|
+
import { connectFtpProfile } from './ftp-adapter.js';
|
|
10
11
|
const MAX_BODY_BYTES = 1_048_576;
|
|
11
12
|
const MAX_SFTP_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
12
13
|
export function registerSshApi(webServer, prefix, runtime) {
|
|
@@ -30,7 +31,138 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
30
31
|
const segments = relative ? relative.split('/').map(decodeURIComponent) : [];
|
|
31
32
|
const method = req.method ?? 'GET';
|
|
32
33
|
if (method === 'GET' && segments[0] === 'health')
|
|
33
|
-
return sendJson(res, 200, { ok: true, service: 'dsh-ssh', schemaVersion:
|
|
34
|
+
return sendJson(res, 200, { ok: true, service: 'dsh-ssh', schemaVersion: 5 });
|
|
35
|
+
if (segments[0] === 'file-transfer') {
|
|
36
|
+
if (method === 'GET' && segments[1] === 'endpoints' && segments.length === 2)
|
|
37
|
+
return sendJson(res, 200, runtime.files.endpoints());
|
|
38
|
+
if (method === 'GET' && segments[1] === 'directory' && segments.length === 2) {
|
|
39
|
+
const endpointId = requireText(url.searchParams.get('endpointId'), 'endpointId', 110);
|
|
40
|
+
const paneId = requireText(url.searchParams.get('paneId'), 'paneId', 100);
|
|
41
|
+
return sendJson(res, 200, await runtime.fileSessions.run(paneId, endpointId, session => session.list(url.searchParams.get('path') ?? session.endpoint.initialPath)));
|
|
42
|
+
}
|
|
43
|
+
if (method === 'POST' && segments[1] === 'delete' && segments.length === 2) {
|
|
44
|
+
requireMutationHeader(req);
|
|
45
|
+
const request = parseFileDeleteRequest(await readObject(req));
|
|
46
|
+
await runtime.fileSessions.run(request.paneId, request.endpointId, async (session) => {
|
|
47
|
+
const directory = await session.list(request.directory);
|
|
48
|
+
const available = new Set(directory.entries.map(entry => entry.path));
|
|
49
|
+
for (const path of request.paths)
|
|
50
|
+
if (!available.has(path))
|
|
51
|
+
throw httpError(404, `remote path ${path} is not a direct child of ${directory.path}`);
|
|
52
|
+
for (const path of request.paths)
|
|
53
|
+
await session.remove(path, true);
|
|
54
|
+
});
|
|
55
|
+
return sendJson(res, 204, undefined);
|
|
56
|
+
}
|
|
57
|
+
if (segments[1] === 'jobs') {
|
|
58
|
+
if (method === 'GET' && segments.length === 2)
|
|
59
|
+
return sendJson(res, 200, runtime.transfers.list());
|
|
60
|
+
if (method === 'POST' && segments.length === 2) {
|
|
61
|
+
requireMutationHeader(req);
|
|
62
|
+
const body = await readObject(req);
|
|
63
|
+
return sendJson(res, 202, runtime.transfers.start('ui', parseTransferRequest(body)));
|
|
64
|
+
}
|
|
65
|
+
const jobId = segments[2];
|
|
66
|
+
if (jobId !== undefined && method === 'GET' && segments.length === 3)
|
|
67
|
+
return sendJson(res, 200, runtime.transfers.get(jobId));
|
|
68
|
+
if (jobId !== undefined && method === 'DELETE' && segments.length === 3) {
|
|
69
|
+
requireMutationHeader(req);
|
|
70
|
+
runtime.transfers.cancel(jobId);
|
|
71
|
+
return sendJson(res, 204, undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (segments[0] === 'ftp-profiles') {
|
|
76
|
+
if (method === 'GET' && segments.length === 1)
|
|
77
|
+
return sendJson(res, 200, await ftpProfileViews(runtime));
|
|
78
|
+
if (method === 'POST' && segments[1] === 'test-draft' && segments.length === 2) {
|
|
79
|
+
requireMutationHeader(req);
|
|
80
|
+
const body = await readObject(req);
|
|
81
|
+
const draft = normalizeFtpProfileDraft(body.profile);
|
|
82
|
+
const id = body.profileId === undefined ? createId('ftp-preview') : requiredFtpProfile(runtime.store, requireText(body.profileId, 'profileId', 100)).id;
|
|
83
|
+
const previous = body.profileId === undefined ? {} : await runtime.credentials.readFtp(id);
|
|
84
|
+
const secrets = { ...previous, ...normalizeSecrets(body.secrets) };
|
|
85
|
+
if (!secrets.password && draft.credentialId === undefined)
|
|
86
|
+
throw httpError(400, 'FTP password is required');
|
|
87
|
+
const credentialEntry = draft.credentialId === undefined ? undefined : requiredPasswordCredential(runtime.store, draft.credentialId);
|
|
88
|
+
const password = credentialEntry === undefined ? secrets.password : (await runtime.credentials.readEntry(credentialEntry.id)).password;
|
|
89
|
+
if (!password)
|
|
90
|
+
throw httpError(400, 'FTP password credential is not configured');
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const profile = { ...draft, id, username: credentialEntry?.username ?? draft.username, port: draft.port ?? defaultFtpPort(draft.protocol), proxy: draft.proxy ?? { type: 'none' }, initialPath: draft.initialPath ?? '/', connectTimeoutMs: draft.connectTimeoutMs ?? 15_000, createdAt: now, updatedAt: now };
|
|
93
|
+
const session = await connectFtpProfile(profile, password, runtime.dialer);
|
|
94
|
+
try {
|
|
95
|
+
await session.list(profile.initialPath);
|
|
96
|
+
return sendJson(res, 200, { ok: true });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
session.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (method === 'POST' && segments.length === 1) {
|
|
103
|
+
requireMutationHeader(req);
|
|
104
|
+
const body = await readObject(req);
|
|
105
|
+
const draft = normalizeFtpProfileDraft(body.profile);
|
|
106
|
+
if (draft.credentialId !== undefined)
|
|
107
|
+
requiredPasswordCredential(runtime.store, draft.credentialId);
|
|
108
|
+
const secrets = normalizeSecrets(body.secrets);
|
|
109
|
+
if (draft.credentialId === undefined && !secrets.password)
|
|
110
|
+
throw httpError(400, 'FTP password is required');
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const profile = { ...draft, id: createId('ftp'), port: draft.port ?? defaultFtpPort(draft.protocol), proxy: draft.proxy ?? { type: 'none' }, initialPath: draft.initialPath ?? '/', connectTimeoutMs: draft.connectTimeoutMs ?? 15_000, createdAt: now, updatedAt: now };
|
|
113
|
+
if (profile.credentialId === undefined)
|
|
114
|
+
await runtime.credentials.replaceFtp(profile.id, { password: secrets.password });
|
|
115
|
+
try {
|
|
116
|
+
await runtime.store.update(state => { state.ftpProfiles.push(profile); });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (profile.credentialId === undefined)
|
|
120
|
+
await runtime.credentials.deleteFtp(profile.id).catch(() => { });
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
return sendJson(res, 201, await ftpProfileView(runtime, profile));
|
|
124
|
+
}
|
|
125
|
+
const id = segments[1];
|
|
126
|
+
if (id !== undefined && method === 'PUT' && segments.length === 2) {
|
|
127
|
+
requireMutationHeader(req);
|
|
128
|
+
const previous = requiredFtpProfile(runtime.store, id);
|
|
129
|
+
const body = await readObject(req);
|
|
130
|
+
const draft = normalizeFtpProfileDraft(body.profile);
|
|
131
|
+
if (draft.credentialId !== undefined)
|
|
132
|
+
requiredPasswordCredential(runtime.store, draft.credentialId);
|
|
133
|
+
const secrets = normalizeSecrets(body.secrets);
|
|
134
|
+
const next = { ...previous, ...draft, id, port: draft.port ?? defaultFtpPort(draft.protocol), proxy: draft.proxy ?? { type: 'none' }, initialPath: draft.initialPath ?? '/', connectTimeoutMs: draft.connectTimeoutMs ?? 15_000, createdAt: previous.createdAt, updatedAt: Date.now() };
|
|
135
|
+
if (draft.group === undefined)
|
|
136
|
+
delete next.group;
|
|
137
|
+
if (draft.tlsServerName === undefined)
|
|
138
|
+
delete next.tlsServerName;
|
|
139
|
+
const oldSecrets = await runtime.credentials.readFtp(id);
|
|
140
|
+
if (next.credentialId === undefined && !secrets.password && !oldSecrets.password)
|
|
141
|
+
throw httpError(400, 'FTP password is required when switching to connection-specific credentials');
|
|
142
|
+
if (next.credentialId === undefined && secrets.password)
|
|
143
|
+
await runtime.credentials.writeFtp(id, { password: secrets.password });
|
|
144
|
+
try {
|
|
145
|
+
await runtime.store.update(state => { state.ftpProfiles = state.ftpProfiles.map(profile => profile.id === id ? next : profile); });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
await runtime.credentials.replaceFtp(id, oldSecrets).catch(() => { });
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
if (next.credentialId !== undefined && previous.credentialId === undefined)
|
|
152
|
+
await runtime.credentials.deleteFtp(id).catch(() => { });
|
|
153
|
+
return sendJson(res, 200, await ftpProfileView(runtime, next));
|
|
154
|
+
}
|
|
155
|
+
if (id !== undefined && method === 'DELETE' && segments.length === 2) {
|
|
156
|
+
requireMutationHeader(req);
|
|
157
|
+
requiredFtpProfile(runtime.store, id);
|
|
158
|
+
await runtime.store.update(state => {
|
|
159
|
+
state.ftpProfiles = state.ftpProfiles.filter(profile => profile.id !== id);
|
|
160
|
+
state.injections = state.injections.map(injection => ({ ...injection, fileEndpointIds: injection.fileEndpointIds.filter(endpointId => endpointId !== `ftp:${id}`) }));
|
|
161
|
+
});
|
|
162
|
+
await runtime.credentials.deleteFtp(id);
|
|
163
|
+
return sendJson(res, 204, undefined);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
34
166
|
if (segments[0] === 'vault') {
|
|
35
167
|
if (method === 'GET' && segments.length === 1)
|
|
36
168
|
return sendJson(res, 200, await credentialEntryViews(runtime));
|
|
@@ -76,9 +208,9 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
76
208
|
if (id !== undefined && method === 'DELETE' && segments.length === 2) {
|
|
77
209
|
requireMutationHeader(req);
|
|
78
210
|
requiredCredentialEntry(runtime.store, id);
|
|
79
|
-
const references = runtime.store.profiles().filter(profile => profile.credentialId === id);
|
|
80
|
-
if (references
|
|
81
|
-
throw httpError(409, `credential entry is used by ${references
|
|
211
|
+
const references = runtime.store.profiles().filter(profile => profile.credentialId === id).length + runtime.store.ftpProfiles().filter(profile => profile.credentialId === id).length;
|
|
212
|
+
if (references > 0)
|
|
213
|
+
throw httpError(409, `credential entry is used by ${references} connection(s)`);
|
|
82
214
|
await runtime.store.update(state => { state.credentialEntries = state.credentialEntries.filter(entry => entry.id !== id); });
|
|
83
215
|
await runtime.credentials.deleteEntry(id);
|
|
84
216
|
return sendJson(res, 204, undefined);
|
|
@@ -127,9 +259,9 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
127
259
|
if (id !== undefined && method === 'DELETE' && segments.length === 2) {
|
|
128
260
|
requireMutationHeader(req);
|
|
129
261
|
requiredProxyEntry(runtime.store, id);
|
|
130
|
-
const references = runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === id);
|
|
131
|
-
if (references
|
|
132
|
-
throw httpError(409, `proxy entry is used by ${references
|
|
262
|
+
const references = runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === id).length + runtime.store.ftpProfiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === id).length;
|
|
263
|
+
if (references > 0)
|
|
264
|
+
throw httpError(409, `proxy entry is used by ${references} connection(s)`);
|
|
133
265
|
await runtime.store.update(state => { state.proxyEntries = state.proxyEntries.filter(entry => entry.id !== id); });
|
|
134
266
|
await runtime.credentials.deleteProxyEntry(id);
|
|
135
267
|
return sendJson(res, 204, undefined);
|
|
@@ -178,6 +310,49 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
178
310
|
return sendJson(res, 201, await profileView(runtime, profile));
|
|
179
311
|
}
|
|
180
312
|
const id = segments[1];
|
|
313
|
+
if (id !== undefined && segments[2] === 'projects') {
|
|
314
|
+
requiredProfile(runtime.store, id);
|
|
315
|
+
if (method === 'GET' && segments.length === 3)
|
|
316
|
+
return sendJson(res, 200, runtime.store.remoteProjects(id));
|
|
317
|
+
if (method === 'POST' && segments.length === 3) {
|
|
318
|
+
requireMutationHeader(req);
|
|
319
|
+
const draft = normalizeRemoteProjectDraft((await readObject(req)).project);
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const project = { ...draft, id: createId('project'), profileId: id, createdAt: now, updatedAt: now };
|
|
322
|
+
await runtime.store.update(state => { state.remoteProjects.push(project); });
|
|
323
|
+
return sendJson(res, 201, project);
|
|
324
|
+
}
|
|
325
|
+
const projectId = segments[3];
|
|
326
|
+
if (projectId !== undefined && method === 'PUT' && segments.length === 4) {
|
|
327
|
+
requireMutationHeader(req);
|
|
328
|
+
const previous = requiredRemoteProject(runtime.store, projectId, id);
|
|
329
|
+
const draft = normalizeRemoteProjectDraft((await readObject(req)).project);
|
|
330
|
+
const next = { ...previous, ...draft, updatedAt: Date.now() };
|
|
331
|
+
await runtime.store.update(state => {
|
|
332
|
+
state.remoteProjects = state.remoteProjects.map(project => project.id === projectId ? next : project);
|
|
333
|
+
state.injections = state.injections.map(injection => {
|
|
334
|
+
if (injection.workingProjectIds[previous.profileId] !== projectId || injection.workingDirectories[previous.profileId] !== previous.path)
|
|
335
|
+
return injection;
|
|
336
|
+
return { ...injection, workingDirectories: { ...injection.workingDirectories, [previous.profileId]: next.path } };
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
return sendJson(res, 200, next);
|
|
340
|
+
}
|
|
341
|
+
if (projectId !== undefined && method === 'DELETE' && segments.length === 4) {
|
|
342
|
+
requireMutationHeader(req);
|
|
343
|
+
requiredRemoteProject(runtime.store, projectId, id);
|
|
344
|
+
await runtime.store.update(state => {
|
|
345
|
+
state.remoteProjects = state.remoteProjects.filter(project => project.id !== projectId);
|
|
346
|
+
state.injections = state.injections.map(injection => {
|
|
347
|
+
if (injection.workingProjectIds[id] !== projectId)
|
|
348
|
+
return injection;
|
|
349
|
+
const { [id]: _removed, ...workingProjectIds } = injection.workingProjectIds;
|
|
350
|
+
return { ...injection, workingProjectIds };
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
return sendJson(res, 204, undefined);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
181
356
|
if (id !== undefined && segments[2] === 'sftp') {
|
|
182
357
|
requiredProfile(runtime.store, id);
|
|
183
358
|
const operation = segments[3];
|
|
@@ -236,10 +411,12 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
236
411
|
await Promise.all(related.map(rule => runtime.forwards.stop(rule.id).catch(() => { })));
|
|
237
412
|
await runtime.store.update(state => {
|
|
238
413
|
state.profiles = state.profiles.filter(profile => profile.id !== id);
|
|
414
|
+
state.remoteProjects = (state.remoteProjects ?? []).filter(project => project.profileId !== id);
|
|
239
415
|
state.forwardRules = state.forwardRules.filter(rule => rule.profileId !== id);
|
|
240
416
|
state.injections = state.injections.map(item => {
|
|
241
417
|
const { [id]: _removed, ...workingDirectories } = item.workingDirectories;
|
|
242
|
-
|
|
418
|
+
const { [id]: _removedProject, ...workingProjectIds } = item.workingProjectIds;
|
|
419
|
+
return { ...item, profileIds: item.profileIds.filter(profileId => profileId !== id), fileEndpointIds: (item.fileEndpointIds ?? []).filter(endpointId => endpointId !== `sftp:${id}`), workingDirectories, workingProjectIds };
|
|
243
420
|
});
|
|
244
421
|
});
|
|
245
422
|
await runtime.credentials.delete(id);
|
|
@@ -329,19 +506,33 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
329
506
|
if (sessionId !== undefined && method === 'PUT' && segments.length === 2) {
|
|
330
507
|
requireMutationHeader(req);
|
|
331
508
|
const body = await readObject(req);
|
|
509
|
+
const previous = runtime.store.injection(sessionId);
|
|
332
510
|
const profileIds = parseProfileIds(body.profileIds, runtime.store);
|
|
511
|
+
const fileEndpointIds = body.fileEndpointIds === undefined ? previous?.fileEndpointIds ?? [] : parseFileEndpointIds(body.fileEndpointIds, runtime.files);
|
|
512
|
+
const filePermission = body.filePermission === undefined ? previous?.filePermission ?? 'browse' : body.filePermission === 'browse' ? 'browse' : body.filePermission === 'transfer' ? 'transfer' : undefined;
|
|
513
|
+
if (filePermission === undefined)
|
|
514
|
+
throw httpError(400, 'filePermission must be browse or transfer');
|
|
333
515
|
const permission = body.permission === 'exec' ? 'exec' : body.permission === 'terminal' ? 'terminal' : undefined;
|
|
334
516
|
if (permission === undefined)
|
|
335
517
|
throw httpError(400, 'permission must be exec or terminal');
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const injection = {
|
|
518
|
+
const workingDirectories = parseWorkingDirectories(body.workingDirectories, profileIds);
|
|
519
|
+
const workingProjectIds = parseWorkingProjectIds(body.workingProjectIds, profileIds, runtime.store);
|
|
520
|
+
const injection = {
|
|
521
|
+
sessionId, profileIds, fileEndpointIds, filePermission, requireFileApproval: body.requireFileApproval === undefined ? previous?.requireFileApproval ?? true : body.requireFileApproval !== false,
|
|
522
|
+
permission, requireCommandApproval: body.requireCommandApproval !== false, workingDirectories, workingProjectIds, updatedAt: Date.now(),
|
|
523
|
+
};
|
|
339
524
|
await runtime.store.update(state => { state.injections = [...state.injections.filter(item => item.sessionId !== sessionId), injection]; });
|
|
525
|
+
const revoked = previous?.profileIds.filter(profileId => !profileIds.includes(profileId)) ?? [];
|
|
526
|
+
if (permission !== 'terminal')
|
|
527
|
+
await runtime.aiTerminals.closeOwner(sessionId);
|
|
528
|
+
else
|
|
529
|
+
await Promise.all(revoked.map(profileId => runtime.aiTerminals.closeProfile(sessionId, profileId)));
|
|
340
530
|
return sendJson(res, 200, injection);
|
|
341
531
|
}
|
|
342
532
|
if (sessionId !== undefined && method === 'DELETE' && segments.length === 2) {
|
|
343
533
|
requireMutationHeader(req);
|
|
344
534
|
await runtime.store.update(state => { state.injections = state.injections.filter(item => item.sessionId !== sessionId); });
|
|
535
|
+
await runtime.aiTerminals.closeOwner(sessionId);
|
|
345
536
|
return sendJson(res, 204, undefined);
|
|
346
537
|
}
|
|
347
538
|
}
|
|
@@ -554,14 +745,24 @@ async function credentialEntryViews(runtime) {
|
|
|
554
745
|
async function credentialEntryView(runtime, entry) {
|
|
555
746
|
const credential = await runtime.credentials.describeEntry(entry.id);
|
|
556
747
|
const requiredField = entry.authType === 'password' ? 'password' : 'privateKey';
|
|
557
|
-
return { ...entry, credential: { ...credential, configured: credential.fields.includes(requiredField) }, references: runtime.store.profiles().filter(profile => profile.credentialId === entry.id).length };
|
|
748
|
+
return { ...entry, credential: { ...credential, configured: credential.fields.includes(requiredField) }, references: runtime.store.profiles().filter(profile => profile.credentialId === entry.id).length + runtime.store.ftpProfiles().filter(profile => profile.credentialId === entry.id).length };
|
|
558
749
|
}
|
|
559
750
|
async function proxyEntryViews(runtime) {
|
|
560
751
|
return Promise.all(runtime.store.proxyEntries().map(entry => proxyEntryView(runtime, entry)));
|
|
561
752
|
}
|
|
562
753
|
async function proxyEntryView(runtime, entry) {
|
|
563
754
|
const credential = await runtime.credentials.describeProxyEntry(entry.id);
|
|
564
|
-
return { ...entry, credential, references: runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length };
|
|
755
|
+
return { ...entry, credential, references: runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length + runtime.store.ftpProfiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length };
|
|
756
|
+
}
|
|
757
|
+
async function ftpProfileViews(runtime) { return Promise.all(runtime.store.ftpProfiles().map(profile => ftpProfileView(runtime, profile))); }
|
|
758
|
+
async function ftpProfileView(runtime, profile) {
|
|
759
|
+
if (profile.credentialId !== undefined) {
|
|
760
|
+
const entry = requiredPasswordCredential(runtime.store, profile.credentialId);
|
|
761
|
+
const credential = await runtime.credentials.describeEntry(entry.id);
|
|
762
|
+
return { ...profile, username: entry.username, credential: { ...credential, configured: credential.fields.includes('password'), source: 'vault', entryId: entry.id, entryName: entry.name } };
|
|
763
|
+
}
|
|
764
|
+
const credential = await runtime.credentials.describeFtp(profile.id);
|
|
765
|
+
return { ...profile, credential: { ...credential, configured: credential.fields.includes('password'), source: 'profile' } };
|
|
565
766
|
}
|
|
566
767
|
function requiredProfile(store, id) {
|
|
567
768
|
const profile = store.profile(id);
|
|
@@ -569,6 +770,24 @@ function requiredProfile(store, id) {
|
|
|
569
770
|
throw httpError(404, 'SSH profile was not found');
|
|
570
771
|
return profile;
|
|
571
772
|
}
|
|
773
|
+
function requiredFtpProfile(store, id) {
|
|
774
|
+
const profile = store.ftpProfile(id);
|
|
775
|
+
if (profile === undefined)
|
|
776
|
+
throw httpError(404, 'FTP profile was not found');
|
|
777
|
+
return profile;
|
|
778
|
+
}
|
|
779
|
+
function requiredPasswordCredential(store, id) {
|
|
780
|
+
const entry = requiredCredentialEntry(store, id);
|
|
781
|
+
if (entry.authType !== 'password')
|
|
782
|
+
throw httpError(400, 'FTP connections require a password credential');
|
|
783
|
+
return entry;
|
|
784
|
+
}
|
|
785
|
+
function requiredRemoteProject(store, id, profileId) {
|
|
786
|
+
const project = store.remoteProject(id);
|
|
787
|
+
if (project === undefined || project.profileId !== profileId)
|
|
788
|
+
throw httpError(404, 'remote project was not found');
|
|
789
|
+
return project;
|
|
790
|
+
}
|
|
572
791
|
function requiredForward(store, id) {
|
|
573
792
|
const rule = store.forward(id);
|
|
574
793
|
if (rule === undefined)
|
|
@@ -616,6 +835,79 @@ function parseProfileIds(value, store) {
|
|
|
616
835
|
throw httpError(400, 'profileIds must be an array');
|
|
617
836
|
return [...new Set(value.map(item => requireText(item, 'profileId', 100)))].map(id => requiredProfile(store, id).id);
|
|
618
837
|
}
|
|
838
|
+
function parseWorkingDirectories(value, profileIds) {
|
|
839
|
+
if (value === undefined)
|
|
840
|
+
return {};
|
|
841
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
842
|
+
throw httpError(400, 'workingDirectories must be an object');
|
|
843
|
+
const result = {};
|
|
844
|
+
for (const [profileId, rawPath] of Object.entries(value)) {
|
|
845
|
+
if (!profileIds.includes(profileId))
|
|
846
|
+
continue;
|
|
847
|
+
const path = requireRawText(rawPath, 'working directory', 4096).trim();
|
|
848
|
+
if (path.length > 0)
|
|
849
|
+
result[profileId] = path;
|
|
850
|
+
}
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
function parseWorkingProjectIds(value, profileIds, store) {
|
|
854
|
+
if (value === undefined)
|
|
855
|
+
return {};
|
|
856
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
857
|
+
throw httpError(400, 'workingProjectIds must be an object');
|
|
858
|
+
const result = {};
|
|
859
|
+
for (const [profileId, rawProjectId] of Object.entries(value)) {
|
|
860
|
+
if (!profileIds.includes(profileId))
|
|
861
|
+
continue;
|
|
862
|
+
const projectId = requireText(rawProjectId, 'working project id', 100);
|
|
863
|
+
result[profileId] = requiredRemoteProject(store, projectId, profileId).id;
|
|
864
|
+
}
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
function parseFileEndpointIds(value, files) {
|
|
868
|
+
if (value === undefined)
|
|
869
|
+
return [];
|
|
870
|
+
if (!Array.isArray(value) || value.length > 100)
|
|
871
|
+
throw httpError(400, 'fileEndpointIds must be an array');
|
|
872
|
+
return [...new Set(value.map(item => requireText(item, 'fileEndpointId', 110)))].map(id => {
|
|
873
|
+
if (files.endpoint(id) === undefined)
|
|
874
|
+
throw httpError(404, `file endpoint ${id} was not found`);
|
|
875
|
+
return id;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function parseTransferRequest(value) {
|
|
879
|
+
const conflict = value.conflictPolicy;
|
|
880
|
+
if (conflict !== 'fail' && conflict !== 'skip' && conflict !== 'overwrite' && conflict !== 'rename')
|
|
881
|
+
throw httpError(400, 'invalid conflictPolicy');
|
|
882
|
+
if (!Array.isArray(value.sourcePaths))
|
|
883
|
+
throw httpError(400, 'sourcePaths must be an array');
|
|
884
|
+
return {
|
|
885
|
+
sourceEndpointId: requireText(value.sourceEndpointId, 'sourceEndpointId', 110),
|
|
886
|
+
sourcePaths: value.sourcePaths.map(path => requireRawText(path, 'source path', 4096)),
|
|
887
|
+
destinationEndpointId: requireText(value.destinationEndpointId, 'destinationEndpointId', 110),
|
|
888
|
+
destinationDirectory: requireRawText(value.destinationDirectory, 'destinationDirectory', 4096),
|
|
889
|
+
conflictPolicy: conflict,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function parseFileDeleteRequest(value) {
|
|
893
|
+
if (!Array.isArray(value.paths) || value.paths.length < 1 || value.paths.length > 100)
|
|
894
|
+
throw httpError(400, 'paths must contain 1-100 entries');
|
|
895
|
+
const paths = [...new Set(value.paths.map(path => requireRawText(path, 'remote path', 4096)))];
|
|
896
|
+
for (const path of paths)
|
|
897
|
+
assertDeletableRemotePath(path);
|
|
898
|
+
return {
|
|
899
|
+
paneId: requireText(value.paneId, 'paneId', 100),
|
|
900
|
+
endpointId: requireText(value.endpointId, 'endpointId', 110),
|
|
901
|
+
directory: requireRawText(value.directory, 'directory', 4096),
|
|
902
|
+
paths,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function assertDeletableRemotePath(value) {
|
|
906
|
+
const normalized = value.trim().replaceAll('\\', '/').replace(/\/+$/, '') || '/';
|
|
907
|
+
if (normalized === '/' || normalized === '.' || normalized === '~' || /^[A-Za-z]:$/.test(normalized))
|
|
908
|
+
throw httpError(400, 'refusing to delete a remote filesystem root');
|
|
909
|
+
}
|
|
910
|
+
function defaultFtpPort(protocol) { return protocol === 'ftps-implicit' ? 990 : 21; }
|
|
619
911
|
function createId(prefix) { return `${prefix}-${randomBytes(10).toString('hex')}`; }
|
|
620
912
|
function assertSameOrigin(req) {
|
|
621
913
|
const origin = req.headers.origin;
|