typescript-virtual-container 1.1.3 → 1.1.4

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.
Files changed (38) hide show
  1. package/dist/SSHMimic/exec.d.ts.map +1 -1
  2. package/dist/SSHMimic/exec.js +8 -2
  3. package/dist/SSHMimic/index.d.ts +1 -0
  4. package/dist/SSHMimic/index.d.ts.map +1 -1
  5. package/dist/SSHMimic/index.js +9 -3
  6. package/dist/SSHMimic/sftp.d.ts +46 -0
  7. package/dist/SSHMimic/sftp.d.ts.map +1 -0
  8. package/dist/SSHMimic/sftp.js +576 -0
  9. package/dist/VirtualFileSystem/index.d.ts +6 -4
  10. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  11. package/dist/VirtualFileSystem/index.js +144 -153
  12. package/dist/VirtualShell/index.d.ts +6 -0
  13. package/dist/VirtualShell/index.d.ts.map +1 -1
  14. package/dist/VirtualShell/index.js +16 -4
  15. package/dist/VirtualShell/shell.d.ts.map +1 -1
  16. package/dist/VirtualShell/shell.js +7 -0
  17. package/dist/VirtualUserManager/index.d.ts +1 -0
  18. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  19. package/dist/VirtualUserManager/index.js +15 -0
  20. package/dist/commands/exit.d.ts.map +1 -1
  21. package/dist/commands/exit.js +1 -0
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/standalone.js +10 -1
  26. package/package.json +1 -1
  27. package/src/SSHMimic/exec.ts +18 -12
  28. package/src/SSHMimic/index.ts +16 -7
  29. package/src/SSHMimic/sftp.ts +833 -0
  30. package/src/VirtualFileSystem/index.ts +158 -188
  31. package/src/VirtualShell/index.ts +19 -8
  32. package/src/VirtualShell/shell.ts +7 -0
  33. package/src/VirtualUserManager/index.ts +20 -0
  34. package/src/commands/exit.ts +1 -0
  35. package/src/index.ts +2 -1
  36. package/src/standalone.ts +11 -1
  37. package/tests/sftp.test.ts +319 -0
  38. package/tests/ssh-exec.test.ts +45 -0
@@ -0,0 +1,319 @@
1
+ /// <reference types="bun" />
2
+ import { describe, expect, test } from "bun:test";
3
+ import { rmSync } from "node:fs";
4
+ import type { FileEntryWithStats, SFTPWrapper } from "ssh2";
5
+ import { Client } from "ssh2";
6
+ import { SftpMimic } from "../src/SSHMimic/sftp";
7
+ import VirtualFileSystem from "../src/VirtualFileSystem";
8
+ import { VirtualUserManager } from "../src/VirtualUserManager";
9
+
10
+ function makeTempBasePath(): string {
11
+ return `./temp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
12
+ }
13
+
14
+ function connectSftp(port: number): Promise<{ client: Client; sftp: SFTPWrapper }> {
15
+ return new Promise((resolve, reject) => {
16
+ const client = new Client();
17
+ client.on("ready", () => {
18
+ client.sftp((err, sftp) => {
19
+ if (err) {
20
+ client.end();
21
+ reject(err);
22
+ return;
23
+ }
24
+ resolve({ client, sftp });
25
+ });
26
+ });
27
+
28
+ client.on("error", reject);
29
+ client.connect({
30
+ host: "127.0.0.1",
31
+ port,
32
+ username: "root",
33
+ password: "root",
34
+ hostVerifier: () => true,
35
+ });
36
+ });
37
+ }
38
+
39
+ function connectSftpWithUser(
40
+ port: number,
41
+ username: string,
42
+ password: string,
43
+ ): Promise<{ client: Client; sftp: SFTPWrapper }> {
44
+ return new Promise((resolve, reject) => {
45
+ const client = new Client();
46
+ client.on("ready", () => {
47
+ client.sftp((err, sftp) => {
48
+ if (err) {
49
+ client.end();
50
+ reject(err);
51
+ return;
52
+ }
53
+ resolve({ client, sftp });
54
+ });
55
+ });
56
+
57
+ client.on("error", reject);
58
+ client.connect({
59
+ host: "127.0.0.1",
60
+ port,
61
+ username,
62
+ password,
63
+ hostVerifier: () => true,
64
+ });
65
+ });
66
+ }
67
+
68
+ describe("SftpMimic", () => {
69
+ test("authenticates with VirtualUserManager and serves files from the VirtualFileSystem", async () => {
70
+ const tempBasePath = makeTempBasePath();
71
+ const vfs = new VirtualFileSystem(tempBasePath);
72
+ const users = new VirtualUserManager(vfs, "root");
73
+
74
+ try {
75
+ await users.initialize();
76
+
77
+ const rootPath = "/home/root";
78
+ if (!vfs.exists(rootPath)) {
79
+ vfs.mkdir(rootPath, 0o755);
80
+ }
81
+ vfs.writeFile(`${rootPath}/TEST.txt`, "hello world");
82
+
83
+ const server = new SftpMimic({
84
+ port: 0,
85
+ hostname: "test-sftp",
86
+ vfs,
87
+ users,
88
+ });
89
+ const port = await server.start();
90
+
91
+ const { client, sftp } = await connectSftp(port);
92
+ const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
93
+ sftp.readdir(
94
+ "/home/root",
95
+ (err?: Error | null, list?: FileEntryWithStats[]) => {
96
+ if (err) {
97
+ reject(err);
98
+ return;
99
+ }
100
+ resolve(list || []);
101
+ },
102
+ );
103
+ });
104
+
105
+ expect(list.map((entry) => entry.filename)).toContain("TEST.txt");
106
+
107
+ const content = await new Promise<string>((resolve, reject) => {
108
+ sftp.readFile(
109
+ "/home/root/TEST.txt",
110
+ "utf8",
111
+ (err?: Error | null, data?: Buffer) => {
112
+ if (err) {
113
+ reject(err);
114
+ return;
115
+ }
116
+ resolve((data || Buffer.alloc(0)).toString("utf8"));
117
+ },
118
+ );
119
+ });
120
+
121
+ expect(content).toBe("hello world");
122
+ client.end();
123
+ server.stop();
124
+ } finally {
125
+ rmSync(tempBasePath, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test("blocks path traversal attempts outside home directory", async () => {
130
+ const tempBasePath = makeTempBasePath();
131
+ const vfs = new VirtualFileSystem(tempBasePath);
132
+ const users = new VirtualUserManager(vfs, "root");
133
+
134
+ try {
135
+ await users.initialize();
136
+
137
+ const rootPath = "/home/root";
138
+ if (!vfs.exists(rootPath)) {
139
+ vfs.mkdir(rootPath, 0o755);
140
+ }
141
+
142
+ const server = new SftpMimic({
143
+ port: 0,
144
+ hostname: "test-sftp",
145
+ vfs,
146
+ users,
147
+ });
148
+ const port = await server.start();
149
+
150
+ const { client, sftp } = await connectSftp(port);
151
+
152
+ // Try to read /etc/passwd (outside home directory) - should fail with PERMISSION_DENIED
153
+ const traversalAttempt = await new Promise<Error | null>((resolve) => {
154
+ sftp.stat(
155
+ "/etc/passwd",
156
+ (err?: Error | null) => {
157
+ resolve(err ?? null);
158
+ },
159
+ );
160
+ });
161
+
162
+ expect(traversalAttempt).not.toBeNull();
163
+ expect(traversalAttempt?.message).toContain("Permission denied");
164
+
165
+ // Try to access /home/root which should work
166
+ const homeAccess = await new Promise<FileEntryWithStats[]>((
167
+ resolve,
168
+ reject,
169
+ ) => {
170
+ sftp.readdir(
171
+ "/home/root",
172
+ (err?: Error | null, list?: FileEntryWithStats[]) => {
173
+ if (err) {
174
+ reject(err);
175
+ return;
176
+ }
177
+ resolve(list || []);
178
+ },
179
+ );
180
+ });
181
+
182
+ expect(homeAccess).toBeDefined();
183
+
184
+ // Try to go up with ../ - should fail
185
+ const upTraversalAttempt = await new Promise<Error | null>((resolve) => {
186
+ sftp.readdir(
187
+ "/home/root/../../etc",
188
+ (err?: Error | null) => {
189
+ resolve(err ?? null);
190
+ },
191
+ );
192
+ });
193
+
194
+ expect(upTraversalAttempt).not.toBeNull();
195
+
196
+ client.end();
197
+ server.stop();
198
+ } finally {
199
+ rmSync(tempBasePath, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test("auto-creates current system user on initialization", async () => {
204
+ // Use a unique temp directory for this test to avoid VFS sharing
205
+ const tempPath = `./temp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
206
+ const vfs = new VirtualFileSystem(tempPath);
207
+ const users = new VirtualUserManager(vfs, "testpass");
208
+ await users.initialize();
209
+
210
+ // Verify that the current system user was created
211
+ const currentUser = process.env.USER || process.env.USERNAME;
212
+ if (currentUser && currentUser !== "root") {
213
+ // Should be able to verify password with the default password (testpass)
214
+ const passwordValid = users.verifyPassword(currentUser, "testpass");
215
+ expect(passwordValid).toBe(true);
216
+
217
+ // Home directory should exist
218
+ const homePath = `/home/${currentUser}`;
219
+ expect(vfs.exists(homePath)).toBe(true);
220
+
221
+ // README.txt should exist in home
222
+ const readmePath = `${homePath}/README.txt`;
223
+ expect(vfs.exists(readmePath)).toBe(true);
224
+ }
225
+
226
+ // Cleanup
227
+ const fs = await import("node:fs");
228
+ fs.rmSync(tempPath, { recursive: true, force: true });
229
+ });
230
+
231
+ test("allows system user to authenticate and access SFTP", async () => {
232
+ const tempBasePath = makeTempBasePath();
233
+ const vfs = new VirtualFileSystem(tempBasePath);
234
+ const users = new VirtualUserManager(vfs, "root");
235
+
236
+ try {
237
+ await users.initialize();
238
+
239
+ // Verify system user was created with the default password
240
+ const currentUser = process.env.USER || process.env.USERNAME;
241
+ if (!currentUser || currentUser === "root") {
242
+ // Skip test if we can't determine current user
243
+ return;
244
+ }
245
+
246
+ // Ensure a deterministic password for environments where user data may persist.
247
+ await users.setPassword(currentUser, "root");
248
+
249
+ const server = new SftpMimic({
250
+ port: 0,
251
+ hostname: "test-sftp",
252
+ vfs,
253
+ users,
254
+ });
255
+ const port = await server.start();
256
+
257
+ // Connect as the system user (which was auto-created during initialize)
258
+ const { client, sftp } = await connectSftpWithUser(
259
+ port,
260
+ currentUser,
261
+ "root",
262
+ );
263
+
264
+ // User should be able to list their home directory
265
+ const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
266
+ sftp.readdir(
267
+ `/home/${currentUser}`,
268
+ (err?: Error | null, list?: FileEntryWithStats[]) => {
269
+ if (err) {
270
+ reject(err);
271
+ return;
272
+ }
273
+ resolve(list || []);
274
+ },
275
+ );
276
+ });
277
+
278
+ // README.txt should be in the home directory
279
+ expect(list.map((e) => e.filename)).toContain("README.txt");
280
+
281
+ // Create a file as the system user
282
+ await new Promise<void>((resolve, reject) => {
283
+ sftp.writeFile(
284
+ `/home/${currentUser}/test-file.txt`,
285
+ Buffer.from("WinSCP test"),
286
+ (err?: Error | null) => {
287
+ if (err) {
288
+ reject(err);
289
+ return;
290
+ }
291
+ resolve();
292
+ },
293
+ );
294
+ });
295
+
296
+ // Read the file back
297
+ const content = await new Promise<string>((resolve, reject) => {
298
+ sftp.readFile(
299
+ `/home/${currentUser}/test-file.txt`,
300
+ "utf8",
301
+ (err?: Error | null, data?: Buffer) => {
302
+ if (err) {
303
+ reject(err);
304
+ return;
305
+ }
306
+ resolve((data || Buffer.alloc(0)).toString("utf8"));
307
+ },
308
+ );
309
+ });
310
+
311
+ expect(content).toBe("WinSCP test");
312
+
313
+ client.end();
314
+ server.stop();
315
+ } finally {
316
+ rmSync(tempBasePath, { recursive: true, force: true });
317
+ }
318
+ });
319
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { VirtualShell } from "../src";
3
+ import { runExec } from "../src/SSHMimic/exec";
4
+
5
+ describe("SSH exec inline commands", () => {
6
+ test("runExec sends stdout, stderr, and exit code for inline commands", async () => {
7
+ const shell = new VirtualShell("localhost");
8
+ const stdout: string[] = [];
9
+ const stderr: string[] = [];
10
+ let exitCode: number = -1;
11
+
12
+ const stream = {
13
+ write(data: string) {
14
+ stdout.push(data);
15
+ },
16
+ stderr: {
17
+ write(data: string) {
18
+ stderr.push(data);
19
+ },
20
+ },
21
+ exit(code: number) {
22
+ exitCode = code;
23
+ },
24
+ end() {
25
+ return undefined;
26
+ },
27
+ };
28
+
29
+ await shell.ensureInitialized();
30
+
31
+ const endPromise = new Promise<void>((resolve) => {
32
+ stream.end = () => {
33
+ resolve();
34
+ return;
35
+ };
36
+ });
37
+
38
+ runExec(stream as never, "echo hello", "root", "localhost", shell);
39
+ await endPromise;
40
+
41
+ expect(stdout.join("")).toContain("hello");
42
+ expect(stderr.join("")).toBe("");
43
+ expect(exitCode).toBe(0);
44
+ });
45
+ });