typescript-virtual-container 1.2.4 → 1.2.5

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 (66) hide show
  1. package/README.md +868 -1245
  2. package/benchmark-results.txt +21 -21
  3. package/dist/SSHMimic/index.d.ts +19 -2
  4. package/dist/SSHMimic/index.d.ts.map +1 -1
  5. package/dist/SSHMimic/index.js +116 -20
  6. package/dist/VirtualFileSystem/index.d.ts +115 -88
  7. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  8. package/dist/VirtualFileSystem/index.js +406 -258
  9. package/dist/VirtualShell/index.d.ts +3 -4
  10. package/dist/VirtualShell/index.d.ts.map +1 -1
  11. package/dist/VirtualShell/index.js +4 -6
  12. package/dist/VirtualUserManager/index.d.ts +25 -0
  13. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  14. package/dist/VirtualUserManager/index.js +33 -0
  15. package/dist/commands/chmod.d.ts +3 -0
  16. package/dist/commands/chmod.d.ts.map +1 -0
  17. package/dist/commands/chmod.js +31 -0
  18. package/dist/commands/cp.d.ts +3 -0
  19. package/dist/commands/cp.d.ts.map +1 -0
  20. package/dist/commands/cp.js +68 -0
  21. package/dist/commands/find.d.ts +3 -0
  22. package/dist/commands/find.d.ts.map +1 -0
  23. package/dist/commands/find.js +48 -0
  24. package/dist/commands/grep.d.ts.map +1 -1
  25. package/dist/commands/grep.js +61 -35
  26. package/dist/commands/head.d.ts +3 -0
  27. package/dist/commands/head.d.ts.map +1 -0
  28. package/dist/commands/head.js +30 -0
  29. package/dist/commands/index.d.ts.map +1 -1
  30. package/dist/commands/index.js +25 -35
  31. package/dist/commands/ln.d.ts +3 -0
  32. package/dist/commands/ln.d.ts.map +1 -0
  33. package/dist/commands/ln.js +42 -0
  34. package/dist/commands/mv.d.ts +3 -0
  35. package/dist/commands/mv.d.ts.map +1 -0
  36. package/dist/commands/mv.js +35 -0
  37. package/dist/commands/tail.d.ts +3 -0
  38. package/dist/commands/tail.d.ts.map +1 -0
  39. package/dist/commands/tail.js +33 -0
  40. package/dist/commands/wc.d.ts +3 -0
  41. package/dist/commands/wc.d.ts.map +1 -0
  42. package/dist/commands/wc.js +48 -0
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/package.json +5 -2
  46. package/scripts/publish-package.sh +70 -0
  47. package/src/SSHMimic/index.ts +143 -28
  48. package/src/VirtualFileSystem/index.ts +500 -280
  49. package/src/VirtualShell/index.ts +4 -6
  50. package/src/VirtualUserManager/index.ts +41 -0
  51. package/src/commands/chmod.ts +33 -0
  52. package/src/commands/cp.ts +76 -0
  53. package/src/commands/find.ts +61 -0
  54. package/src/commands/grep.ts +54 -38
  55. package/src/commands/head.ts +35 -0
  56. package/src/commands/index.ts +25 -43
  57. package/src/commands/ln.ts +47 -0
  58. package/src/commands/mv.ts +43 -0
  59. package/src/commands/tail.ts +37 -0
  60. package/src/commands/wc.ts +48 -0
  61. package/src/index.ts +1 -0
  62. package/standalone.js +62 -52
  63. package/standalone.js.map +4 -4
  64. package/tests/bun-test-shim.ts +1 -0
  65. package/tests/sftp.test.ts +115 -191
  66. package/tests/users.test.ts +66 -83
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `typescript-virtual-container`
2
2
 
3
- > Scalable SSH/SFTP server with a virtual filesystem and typed programmatic API for testing, automation, and interactive shell scripting in TypeScript/JavaScript.
3
+ > Pure in-memory SSH/SFTP server with a virtual filesystem and typed programmatic API for testing, automation, honeypots, and interactive shell scripting in TypeScript/JavaScript.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/typescript-virtual-container.svg)](https://www.npmjs.com/package/typescript-virtual-container)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -17,6 +17,14 @@
17
17
  - [Quick Start](#quick-start)
18
18
  - [Architecture Overview](#architecture-overview)
19
19
  - [API Reference](#api-reference)
20
+ - [VirtualSshServer](#virtualsshserver)
21
+ - [VirtualSftpServer](#virtualsftpserver)
22
+ - [VirtualShell](#virtualshell)
23
+ - [VirtualFileSystem](#virtualfilesystem)
24
+ - [VirtualUserManager](#virtualusermanager)
25
+ - [HoneyPot](#honeypot)
26
+ - [SshClient](#sshclient-programmatic-api)
27
+ - [Key Types](#key-types)
20
28
  - [Usage Examples](#usage-examples)
21
29
  - [Built-in Commands](#built-in-commands)
22
30
  - [Configuration](#configuration)
@@ -24,7 +32,6 @@
24
32
  - [Types & TypeScript](#types--typescript)
25
33
  - [FAQ](#faq)
26
34
  - [Troubleshooting](#troubleshooting)
27
- - [Migration Guide](#migration-guide)
28
35
  - [Contributing](#contributing)
29
36
  - [Security](#security)
30
37
  - [Support](#support)
@@ -32,43 +39,55 @@
32
39
  - [Roadmap](#roadmap)
33
40
  - [Changelog](#changelog)
34
41
 
42
+ ---
43
+
35
44
  ## Overview
36
45
 
37
46
  `typescript-virtual-container` is a lightweight, fully-typed SSH/SFTP runtime written in TypeScript that provides:
38
47
 
48
+ - **Pure in-memory filesystem**: No disk I/O at runtime. All state lives in a fast recursive in-memory tree. Use JSON snapshots for optional persistence.
39
49
  - **SSH + SFTP Protocol Support**: Serve SSH shell/exec sessions and SFTP file operations on configurable ports.
40
- - **Virtual Filesystem**: Fast developer workflow backed by a mirror directory under `.vfs/mirror`, with optional gzip compression and programmatic access.
41
- - **User Management**: Create, authenticate, and manage virtual users with strict password hashing (scrypt) and sudo-like privilege elevation.
50
+ - **Password & public-key authentication**: Register SSH public keys per user alongside (or instead of) password auth.
51
+ - **Rate limiting / brute-force protection**: Configurable per-IP lockout after N failed auth attempts.
52
+ - **User Management**: Create, authenticate, and manage virtual users with scrypt password hashing, sudo-like privilege elevation, and optional per-user disk quotas.
42
53
  - **Programmatic Shell API**: Execute shell commands and query filesystem state directly from TypeScript without SSH overhead.
43
54
  - **Event-Driven Architecture**: All core classes extend `EventEmitter` for lifecycle and operation tracking. Listen to auth events, filesystem operations, session lifecycle, and command execution for auditing and integration.
44
55
  - **Security Auditing**: Built-in `HoneyPot` utility for comprehensive activity logging, event tracking, statistics collection, and anomaly detection across all components.
45
- - **Built-in Commands**: `ls`, `cd`, `pwd`, `cat`, `mkdir`, `touch`, `rm`, `tree`, `whoami`, `hostname`, `who`, `sudo`, `su`, `adduser`, `deluser`, `nano` (text editor), `curl`, `wget`, and a growing set of additional commands. Not everything is implemented yet, and shell compatibility is still being expanded.
56
+ - **40+ Built-in Commands**: `ls`, `cd`, `cat`, `cp`, `mv`, `ln`, `find`, `grep`, `wc`, `head`, `tail`, `chmod`, `mkdir`, `touch`, `rm`, `tree`, `nano`, `curl`, `wget`, `sudo`, `su`, `adduser`, `deluser`, and more. Shell compatibility is still being expanded.
46
57
  - **Full TypeScript Support**: Complete JSDoc coverage, exported types, and first-class async/await for all operations.
47
58
 
59
+ ---
60
+
48
61
  ## What This Is / What This Is Not
49
62
 
50
63
  ### What This Is
51
64
 
52
- - A virtual shell runtime written in TypeScript.
53
- - A virtual environment with its own virtual filesystem, user management, and command runtime.
65
+ - A virtual shell runtime written in TypeScript with a **pure in-memory filesystem**.
66
+ - A virtual environment with its own filesystem, user management, and command runtime.
54
67
  - A practical tool for deterministic testing, automation pipelines, and SSH-like workflows without running real containers.
68
+ - A honeypot framework for capturing and auditing attacker behavior.
55
69
 
56
70
  ### What This Is Not
57
71
 
58
72
  - Not a fully isolated container runtime.
59
- - Not a security sandbox.
73
+ - Not a security sandbox — the VFS does not sandbox host filesystem access by spawned child processes (e.g. `wget`, `curl` delegate to the host binary).
60
74
  - Not a kernel-level isolation boundary (unlike Docker/VM-based isolation).
61
75
 
62
76
  This project emulates shell behavior for developer workflows. It is designed for realism and productivity, not hard security isolation.
63
77
 
78
+ ---
79
+
64
80
  ## Why This Package
65
81
 
66
82
  This package is designed for teams that need a realistic SSH-like runtime without spinning up real containers or VMs.
67
83
 
68
- - **Deterministic test environments**: Repeatable state for CI pipelines and integration tests.
84
+ - **Zero disk footprint by default**: The VFS operates entirely in memory. Opt into JSON snapshot persistence when you need it.
85
+ - **Deterministic test environments**: Repeatable state for CI pipelines and integration tests. Build a fixture snapshot once, hydrate for each test.
69
86
  - **Low operational overhead**: No Docker daemon, no kernel namespaces, no privileged setup.
70
87
  - **Fast feedback loops**: Programmatic API for command execution and filesystem assertions.
71
- - **Developer-friendly internals**: Typed APIs, clear boundaries, and composable building blocks.
88
+ - **Developer-friendly internals**: Typed APIs, clear boundaries, composable building blocks, and full JSDoc.
89
+
90
+ ---
72
91
 
73
92
  ## Installation
74
93
 
@@ -86,12 +105,23 @@ bun add typescript-virtual-container
86
105
 
87
106
  ```bash
88
107
  git clone https://github.com/itsrealfortune/typescript-virtual-container/
89
- cd virtual-env-js
108
+ cd typescript-virtual-container
90
109
  bun install
91
- bun format # Format code per Biome
92
- bun check # Lint and typecheck
110
+ bun format # Format code per Biome
111
+ bun check # Lint and typecheck
112
+ bun run build
93
113
  ```
94
114
 
115
+ ### Standalone (zero install)
116
+
117
+ To quickly try a standalone demo:
118
+
119
+ ```bash
120
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/standalone.js -o standalone.js && node standalone.js && rm -f standalone.js
121
+ ```
122
+
123
+ ---
124
+
95
125
  ## Compatibility
96
126
 
97
127
  - **Node.js**: Recommended `>=18`
@@ -101,6 +131,8 @@ bun check # Lint and typecheck
101
131
 
102
132
  The virtual filesystem and shell behavior are intentionally portable and do not depend on host-specific POSIX syscalls.
103
133
 
134
+ ---
135
+
104
136
  ## Quick Start
105
137
 
106
138
  ### Running an SSH Server
@@ -108,23 +140,21 @@ The virtual filesystem and shell behavior are intentionally portable and do not
108
140
  ```typescript
109
141
  import { VirtualSshServer } from "typescript-virtual-container";
110
142
 
111
- // Create server on port 2222
112
- const ssh = new VirtualSshServer({
113
- port: 2222,
114
- hostname: "my-container"
143
+ const ssh = new VirtualSshServer({
144
+ port: 2222,
145
+ hostname: "my-container",
115
146
  });
116
147
 
117
- // Start server
118
148
  await ssh.start();
119
149
  console.log("SSH server listening on :2222");
120
150
 
121
- // Connect externally via SSH
122
- // ssh root@localhost -p 2222 (password: "root")
151
+ // Connect externally:
152
+ // ssh root@localhost -p 2222
153
+ // root has no password by default — login is allowed without verification.
123
154
 
124
- // Graceful shutdown
125
155
  process.on("SIGTERM", () => {
126
- ssh.stop();
127
- process.exit(0);
156
+ ssh.stop();
157
+ process.exit(0);
128
158
  });
129
159
  ```
130
160
 
@@ -135,17 +165,8 @@ import { VirtualSftpServer, VirtualShell, VirtualSshServer } from "typescript-vi
135
165
 
136
166
  const shell = new VirtualShell("my-container");
137
167
 
138
- const ssh = new VirtualSshServer({
139
- port: 2222,
140
- hostname: "my-container",
141
- shell,
142
- });
143
-
144
- const sftp = new VirtualSftpServer({
145
- port: 2223,
146
- hostname: "my-container",
147
- shell,
148
- });
168
+ const ssh = new VirtualSshServer({ port: 2222, hostname: "my-container", shell });
169
+ const sftp = new VirtualSftpServer({ port: 2223, hostname: "my-container", shell });
149
170
 
150
171
  await ssh.start();
151
172
  await sftp.start();
@@ -156,18 +177,16 @@ console.log("SSH on :2222, SFTP on :2223");
156
177
  ### Using the Programmatic Client API
157
178
 
158
179
  ```typescript
159
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
180
+ import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
160
181
 
161
182
  const shell = new VirtualShell("typescript-vm");
162
- const ssh = new VirtualSshServer({ port: 2222, shell });
183
+ const ssh = new VirtualSshServer({ port: 2222, shell });
163
184
  await ssh.start();
164
185
 
165
- // Create authenticated client for specific user
166
186
  const client = new SshClient(shell, "root");
167
187
 
168
- // Execute commands programmatically
169
188
  const list = await client.ls("/home");
170
- console.log("stdout:", list.stdout); // Directory listing
189
+ console.log("stdout:", list.stdout);
171
190
 
172
191
  const result = await client.pwd();
173
192
  console.log("Current dir:", result.stdout);
@@ -183,109 +202,100 @@ await client.writeFile("output.txt", "Hello, World!");
183
202
  ssh.stop();
184
203
  ```
185
204
 
186
- ## Architecture Overview
187
-
188
- <!-- ### Core Components
205
+ ---
189
206
 
190
- ```
191
- ┌─────────────────────────────────────────────┐
192
- │ SSH Server (SshMimic) │
193
- │ Listens on :port, handles auth & sessions │
194
- └──────────────┬──────────────────────────────┘
195
-
196
- ┌───────┴────────┬──────────────┐
197
- │ │ │
198
- ┌──────┴──────┐ ┌─────┴─────┐ ┌────┴─────┐
199
- │ VirtualFileSystem │ VirtualUserManager │ Command Runtime
200
- │ In-mem FS w/ persist │ Auth & Sudoers │ Shell/Exec Mode
201
- └──────────────┘ └──────────┘ └──────────┘
202
-
203
- │ Backed by disk
204
- │ .vfs/mirror
205
- └──────────────────────────────────┘
206
- ``` -->
207
+ ## Architecture Overview
207
208
 
208
209
  ### Execution Modes
209
210
 
210
- 1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, prompt, TTY resizing.
211
- 2. **SSH Exec Mode**: Non-interactive command execution (e.g., `ssh user@host "ls -la"`).
211
+ 1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, prompt, history, TTY resizing.
212
+ 2. **SSH Exec Mode**: Non-interactive command execution (e.g. `ssh user@host "ls -la"`).
212
213
  3. **SFTP Mode**: Remote file operations (`readdir`, `stat`, `readFile`, `writeFile`, `mkdir`, `rename`, etc.) with home-directory confinement.
213
- 4. **Programmatic Mode**: Direct TypeScript API via `SshClient`, no SSH protocol overhead.
214
-
215
- ### Persistence
216
-
217
- - Filesystem state is stored under `.vfs/mirror` inside the configured `basePath`
218
- - Users/passwords stored in virtual paths `/virtual-env-js/.auth/htpasswd` and `/virtual-env-js/.auth/sudoers`
219
- - `restoreMirror()` and `flushMirror()` are lightweight compatibility hooks for initialization boundaries
214
+ 4. **Programmatic Mode**: Direct TypeScript API via `SshClient` no SSH protocol overhead.
215
+
216
+ ```
217
+ ┌──────────────────────────────────────────────────────────────────────────┐
218
+ │ SshMimic (VirtualSshServer) SftpMimic (VirtualSftpServer) │
219
+ │ password auth · publickey auth SFTP protocol handlers │
220
+ │ per-IP rate limiting / lockout home-dir confinement │
221
+ └─────────────────────────┬────────────────────────────────────────────────┘
222
+
223
+ ┌──────────▼──────────┐
224
+ │ VirtualShell │
225
+ │ pipeline parser │
226
+ │ command executor │
227
+ │ session manager │
228
+ └──┬──────────────┬───┘
229
+ │ │
230
+ ┌────────────▼───┐ ┌─────▼───────────────┐
231
+ │VirtualFileSystem│ │ VirtualUserManager │
232
+ │ in-memory tree │ │ scrypt · sudoers │
233
+ │ gzip · symlinks │ │ publickey auth │
234
+ │ snapshot I/O │ │ quotas · sessions │
235
+ │ mode:memory|fs │ └─────────────────────-┘
236
+ └─────────────────┘
237
+
238
+ ┌────────────▼────────────┐
239
+ │ HoneyPot │
240
+ │ audit log · stats │
241
+ │ anomaly detection │
242
+ └─────────────────────────┘
243
+ ```
220
244
 
221
245
  ---
222
246
 
223
247
  ## API Reference
224
248
 
225
- ### SshMimic (SSH Server)
249
+ ### `VirtualSshServer`
226
250
 
227
- Main SSH server class, exported as `VirtualSshServer` in the package entrypoint. It wires the virtual shell runtime into ssh2 sessions and manages authentication/session handlers.
251
+ Main SSH server class. Wires the virtual shell runtime into `ssh2` sessions and manages authentication and session handlers.
228
252
 
229
253
  #### Constructor
230
254
 
231
255
  ```typescript
232
- new SshMimic(options: {
233
- port: number; // TCP port to bind on localhost
234
- hostname?: string; // Virtual hostname (default: "typescript-vm")
235
- shell?: VirtualShell; // Optional preconfigured shell instance
256
+ new VirtualSshServer({
257
+ port: number; // TCP port to bind
258
+ hostname?: string; // Virtual hostname (default: "typescript-vm")
259
+ shell?: VirtualShell; // Optional shared shell instance (share state with SFTP)
260
+ maxAuthAttempts?: number; // Max failed auth per IP before lockout (default: 5)
261
+ lockoutDurationMs?: number; // Lockout duration in ms (default: 60_000)
236
262
  })
237
263
  ```
238
264
 
239
- - `hostname` controls the SSH ident label and the default hostname used by a generated shell.
240
- - If `shell` is omitted, the server creates `new VirtualShell(hostname)` for you.
265
+ If `shell` is omitted, the server creates `new VirtualShell(hostname)` internally.
241
266
 
242
267
  **Example:**
243
268
 
244
269
  ```typescript
245
- const virtualShell = new VirtualShell("my-lab", {
246
- kernel: "1.0.0+itsrealfortune+1-amd64",
247
- os: "Fortune GNU/Linux x64",
248
- arch: "x86_64",
249
- }, "./data");
250
- const ssh = new SshMimic({
251
- port: 2222,
252
- hostname: "my-lab",
253
- shell: virtualShell
270
+ const shell = new VirtualShell("my-lab", {
271
+ kernel: "1.0.0+itsrealfortune+1-amd64",
272
+ os: "Fortune GNU/Linux x64",
273
+ arch: "x86_64",
254
274
  });
255
- ```
256
-
257
- #### Methods
258
-
259
- ##### `async start(): Promise<number>`
260
-
261
- Initializes virtual filesystem, user manager, and starts listening for SSH connections.
262
275
 
263
- - **Returns**: Bound port number
264
- - **Throws**: Error if port not available or initialization fails
265
-
266
- ```typescript
267
- const port = await ssh.start();
268
- console.log(`Listening on ${port}`);
276
+ const ssh = new VirtualSshServer({ port: 2222, hostname: "my-lab", shell });
269
277
  ```
270
278
 
271
- ##### `stop(): void`
272
-
273
- Cleanly closes server and all active connections.
279
+ #### Methods
274
280
 
275
- ```typescript
276
- ssh.stop();
277
- ```
281
+ | Method | Description |
282
+ |--------|-------------|
283
+ | `start(): Promise<number>` | Initialize VFS, users, start listening. Returns bound port. |
284
+ | `stop(): void` | Gracefully close server and all active connections. |
285
+ | `clearLockout(ip: string): void` | Manually lift a rate-limit lockout for an IP. |
286
+ | `getVfs(): VirtualFileSystem \| null` | Access VFS instance (null before start). |
287
+ | `getUsers(): VirtualUserManager \| null` | Access user manager (null before start). |
288
+ | `getHostname(): string` | Returns configured hostname. |
278
289
 
279
290
  #### Events
280
291
 
281
- `SshMimic` extends `EventEmitter` and emits the following events:
282
-
283
292
  | Event | Data | Description |
284
293
  |-------|------|-------------|
285
294
  | `start` | `{ port: number }` | Server started and listening |
286
295
  | `stop` | — | Server stopped |
287
- | `auth:success` | `{ username: string; remoteAddress: string }` | User authenticated |
288
- | `auth:failure` | `{ username: string; remoteAddress: string }` | Auth failed for user |
296
+ | `auth:success` | `{ username, remoteAddress, method? }` | User authenticated |
297
+ | `auth:failure` | `{ username, remoteAddress, reason?, method? }` | Auth failed |
298
+ | `auth:lockout` | `{ ip, until: Date }` | IP locked out after too many failures |
289
299
  | `client:connect` | — | New SSH client connected |
290
300
  | `client:disconnect` | `{ user: string }` | SSH client disconnected |
291
301
 
@@ -293,942 +303,557 @@ ssh.stop();
293
303
 
294
304
  ```typescript
295
305
  ssh.on("auth:success", ({ username, remoteAddress }) => {
296
- console.log(`[SSH] User ${username} authenticated from ${remoteAddress}`);
306
+ console.log(`[SSH] ${username} authenticated from ${remoteAddress}`);
297
307
  });
298
308
 
299
- ssh.on("auth:failure", ({ username }) => {
300
- console.log(`[SSH] Auth failed for user ${username}`);
309
+ ssh.on("auth:lockout", ({ ip, until }) => {
310
+ console.warn(`[SSH] ${ip} locked until ${until}`);
301
311
  });
302
312
  ```
303
313
 
304
- ##### `getVfs(): VirtualFileSystem | null`
305
-
306
- Returns the virtual filesystem instance. Null if server not started.
307
-
308
- ```typescript
309
- const vfs = ssh.getVfs();
310
- if (vfs) {
311
- const content = vfs.readFile("/etc/hosts");
312
- }
313
- ```
314
-
315
- ##### `getUsers(): VirtualUserManager | null`
316
-
317
- Returns the user manager instance. Null if server not started.
318
-
319
- ```typescript
320
- const users = ssh.getUsers();
321
- const sessions = users.listActiveSessions();
322
- ```
323
-
324
- ##### `getHostname(): string`
325
-
326
- Returns configured server hostname.
327
-
328
- ```typescript
329
- console.log(`Server name: ${ssh.getHostname()}`);
330
- ```
331
-
332
314
  ---
333
315
 
334
- ### SftpMimic (SFTP Server)
316
+ ### `VirtualSftpServer`
335
317
 
336
- SFTP server class, exported as `VirtualSftpServer` in the package entrypoint. It can run with a shared `VirtualShell` (recommended) or with explicit `vfs + users` dependencies.
318
+ SFTP server class. Can share a `VirtualShell` with `VirtualSshServer` (recommended) or accept explicit `vfs` + `users` dependencies.
337
319
 
338
320
  #### Constructor
339
321
 
340
322
  ```typescript
341
- new SftpMimic(options: {
342
- port: number;
343
- hostname?: string;
344
- shell?: VirtualShell;
345
- vfs?: VirtualFileSystem;
346
- users?: VirtualUserManager;
323
+ new VirtualSftpServer({
324
+ port: number;
325
+ hostname?: string;
326
+ shell?: VirtualShell; // share state with SSH server
327
+ vfs?: VirtualFileSystem; // explicit if no shell
328
+ users?: VirtualUserManager; // explicit if no shell
347
329
  })
348
330
  ```
349
331
 
350
- - If `shell` is provided, SFTP reuses the same users/filesystem state as SSH.
351
- - If `shell` is omitted, pass `vfs` and `users` explicitly.
352
-
353
332
  #### Methods
354
333
 
355
- ##### `async start(): Promise<number>`
356
-
357
- Starts the SFTP server and returns the bound port (useful with `port: 0`).
358
-
359
- ```typescript
360
- const sftp = new SftpMimic({ port: 0, shell });
361
- const boundPort = await sftp.start();
362
- console.log(`SFTP listening on ${boundPort}`);
363
- ```
364
-
365
- ##### `stop(): void`
366
-
367
- Stops the SFTP server.
368
-
369
- ```typescript
370
- sftp.stop();
371
- ```
334
+ | Method | Description |
335
+ |--------|-------------|
336
+ | `start(): Promise<number>` | Start SFTP server, returns bound port. |
337
+ | `stop(): void` | Stop SFTP server. |
372
338
 
373
339
  #### Behavior Notes
374
340
 
375
341
  - Supports `password` and `keyboard-interactive` authentication.
376
342
  - Resolves relative SFTP paths from `/home/<user>`.
377
- - Confines all SFTP operations to `/home/<user>` and blocks traversal attempts outside the user home.
343
+ - Confines all SFTP operations to `/home/<user>` blocks traversal attempts.
378
344
  - Unsupported operations (`READLINK`, `SYMLINK`) return `OP_UNSUPPORTED`.
379
345
 
380
- #### Events
381
-
382
- `SftpMimic` extends `EventEmitter` and emits the following events:
383
-
384
- | Event | Data | Description |
385
- |-------|------|-------------|
386
- | `start` | `{ port: number }` | SFTP server started and listening |
387
- | `stop` | | SFTP server stopped |
388
- | `auth:success` | `{ username: string; remoteAddress: string }` | User authenticated for SFTP |
389
- | `auth:failure` | `{ username: string; remoteAddress: string }` | SFTP auth failed for user |
390
- | `client:connect` | — | New SFTP client connected |
391
- | `client:disconnect` | `{ user: string }` | SFTP client disconnected |
392
-
393
- **Example:**
394
-
395
- ```typescript
396
- sftp.on("auth:success", ({ username }) => {
397
- console.log(`[SFTP] User ${username} authenticated`);
398
- });
399
- ```
400
-
401
- ---
402
-
403
- ### SshClient (Programmatic Shell API)
404
-
405
- Execute shell commands against a `VirtualShell` instance without SSH overhead. Maintains connection state (current working directory) across calls.
406
-
407
- #### Constructor
408
-
409
- ```typescript
410
- new SshClient(shell: VirtualShell, username: string)
411
- ```
412
-
413
- - **shell**: Parent virtual shell instance
414
- - **username**: User to authenticate as (no password required)
415
-
416
- **Example:**
417
-
418
- ```typescript
419
- const shell = new VirtualShell("typescript-vm");
420
- const client = new SshClient(shell, "alice");
421
- ```
422
-
423
- #### Methods
424
-
425
- ##### `async exec(command: string): Promise<CommandResult>`
426
-
427
- Raw command execution. Returns structured output.
428
-
429
- ```typescript
430
- const result = await client.exec("echo hello && exit 42");
431
- console.log(result.stdout); // "hello"
432
- console.log(result.exitCode); // 42
433
- ```
434
-
435
- ##### `async ls(path?: string): Promise<CommandResult>`
436
-
437
- Lists directory contents. Defaults to current directory.
438
-
439
- ```typescript
440
- const result = await client.ls("/tmp");
441
- // result.stdout contains formatted listing
442
- ```
443
-
444
- ##### `async pwd(): Promise<CommandResult>`
445
-
446
- Prints current working directory.
447
-
448
- ```typescript
449
- const result = await client.pwd();
450
- console.log("cwd:", result.stdout); // "/home/alice"
451
- ```
452
-
453
- ##### `async cd(path: string): Promise<CommandResult>`
454
-
455
- Changes working directory. Updates internal state on success.
456
-
457
- ```typescript
458
- const result = await client.cd("/var/log");
459
- // Internal cwd now "/var/log"
460
-
461
- const result2 = await client.ls(); // Listed from /var/log
462
- ```
463
-
464
- ##### `async cat(path: string): Promise<CommandResult>`
465
-
466
- Reads file content via command.
467
-
468
- ```typescript
469
- const result = await client.cat("/etc/hostname");
470
- console.log(result.stdout);
471
- ```
472
-
473
- ##### `async mkdir(path: string, recursive?: boolean): Promise<CommandResult>`
474
-
475
- Creates directory. Set `recursive=true` for `-p` flag.
476
-
477
- ```typescript
478
- await client.mkdir("/tmp/nested/dirs", true);
479
- ```
480
-
481
- ##### `async touch(path: string): Promise<CommandResult>`
482
-
483
- Creates empty file.
484
-
485
- ```typescript
486
- await client.touch("/tmp/marker.txt");
487
- ```
488
-
489
- ##### `async rm(path: string, recursive?: boolean): Promise<CommandResult>`
490
-
491
- Removes file or directory. Set `recursive=true` for `-r` flag.
492
-
493
- ```typescript
494
- await client.rm("/tmp/old", true); // rm -r /tmp/old
495
- ```
496
-
497
- ##### `async readFile(path: string): Promise<CommandResult>`
498
-
499
- Reads file content directly from VFS (programmatic, no shell).
500
-
501
- ```typescript
502
- const result = await client.readFile("/etc/hostname");
503
- console.log(result.stdout); // File content
504
- if (result.exitCode !== 0) console.error(result.stderr);
505
- ```
506
-
507
- ##### `async writeFile(path: string, content: string): Promise<CommandResult>`
508
-
509
- Writes file content directly to VFS (programmatic, no shell).
510
-
511
- ```typescript
512
- await client.writeFile("/tmp/config.txt", "port=8080\nhost=localhost");
513
- ```
514
-
515
- ##### `async tree(path?: string): Promise<CommandResult>`
516
-
517
- Renders ASCII directory tree.
518
-
519
- ```typescript
520
- const result = await client.tree("/home");
521
- console.log(result.stdout);
522
- ```
523
-
524
- ##### `async whoami(): Promise<CommandResult>`
525
-
526
- Shows authenticated user.
527
-
528
- ```typescript
529
- const result = await client.whoami();
530
- console.log(result.stdout); // "alice" (or user passed to constructor)
531
- ```
532
-
533
- ##### `async hostname(): Promise<CommandResult>`
534
-
535
- Shows server hostname.
536
-
537
- ```typescript
538
- const result = await client.hostname();
539
- ```
540
-
541
- ##### `async who(): Promise<CommandResult>`
542
-
543
- Lists active user sessions.
544
-
545
- ```typescript
546
- const result = await client.who();
547
- console.log(result.stdout); // Active sessions
548
- ```
549
-
550
- ##### `getCwd(): string`
551
-
552
- Returns current working directory (local state, no I/O).
553
-
554
- ```typescript
555
- await client.cd("/tmp");
556
- console.log(client.getCwd()); // "/tmp"
557
- ```
558
-
559
- ##### `getUsername(): string`
560
-
561
- Returns authenticated username (local state, no I/O).
562
-
563
- ```typescript
564
- console.log(client.getUsername()); // Username from constructor
565
- ```
566
-
567
- ---
568
-
569
- ### VirtualShell
570
-
571
- Encapsulates shell execution primitives used by the SSH runtime for command dispatch, interactive sessions, and the programmatic client.
572
-
573
- #### ShellProperties
574
-
575
- ```typescript
576
- interface ShellProperties {
577
- kernel: string;
578
- os: "Fortune GNU/Linux x64";
579
- arch: "x86_64";
580
- }
581
-
582
- const defaultShellProperties: ShellProperties;
583
- ```
584
-
585
- - `kernel` is displayed in shell/system information output.
586
- - `os` and `arch` are fixed labels used by the shell runtime.
587
-
588
- #### Constructor
589
-
590
- ```typescript
591
- new VirtualShell(
592
- hostname: string,
593
- properties?: ShellProperties,
594
- basePath?: string,
595
- )
596
- ```
597
-
598
- - **hostname**: Hostname injected into command context and prompt behavior.
599
- - **properties**: Optional shell metadata. Defaults to `defaultShellProperties`.
600
- - **basePath**: Optional directory used to resolve `.vfs/mirror` and auth storage (defaults to `.`).
601
-
602
- **Example:**
603
-
604
- ```typescript
605
- const shell = new VirtualShell("typescript-vm", {
606
- kernel: "1.0.0+itsrealfortune+1-amd64",
607
- os: "Fortune GNU/Linux x64",
608
- arch: "x86_64",
609
- }, "./data");
610
- ```
611
-
612
- #### Methods
613
-
614
- ##### `addCommand(name: string, params: string[], callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>): void`
615
-
616
- Registers a custom command at runtime.
617
-
618
- ```typescript
619
- shell.addCommand("hello", [], () => ({ stdout: "hello", exitCode: 0 }));
620
- ```
621
-
622
- ##### `executeCommand(rawInput: string, authUser: string, cwd: string): void`
623
-
624
- Runs one command input in shell mode for a given user and working directory.
625
-
626
- ```typescript
627
- shell.executeCommand("ls -la", "root", "/home/root");
628
- ```
629
-
630
- ##### `startInteractiveSession(stream: ShellStream, authUser: string, sessionId: string | null, remoteAddress: string, terminalSize: { cols: number; rows: number }): void`
631
-
632
- Starts an interactive shell session over a shell stream.
633
-
634
- ```typescript
635
- shell.startInteractiveSession(
636
- stream,
637
- "root",
638
- sessionId,
639
- "127.0.0.1",
640
- { cols: 120, rows: 30 },
641
- );
642
- ```
643
-
644
- #### Events
645
-
646
- `VirtualShell` extends `EventEmitter` and emits the following events:
647
-
648
- | Event | Data | Description |
649
- |-------|------|-------------|
650
- | `initialized` | — | Shell initialization complete |
651
- | `command` | `{ command: string; user: string; cwd: string }` | Command executed |
652
- | `session:start` | `{ user: string; sessionId: string \| null; remoteAddress: string }` | Interactive session started |
653
-
654
- **Example:**
655
-
656
- ```typescript
657
- shell.on("command", ({ command, user, cwd }) => {
658
- console.log(`[SHELL] User ${user} executed: ${command} (cwd: ${cwd})`);
659
- });
660
-
661
- shell.on("session:start", ({ user, remoteAddress }) => {
662
- console.log(`[SHELL] Session started for ${user} from ${remoteAddress}`);
663
- });
664
- ```
665
-
666
- ---
667
-
668
- ### VirtualFileSystem
669
-
670
- Virtual filesystem abstraction backed by a mirror directory on disk, with optional gzip compression per file.
671
-
672
- #### Constructor
673
-
674
- ```typescript
675
- new VirtualFileSystem(baseDir?: string)
676
- ```
677
-
678
- - **baseDir**: Directory used for the `.vfs/mirror` root (default: current working directory)
679
-
680
- ```typescript
681
- const vfs = new VirtualFileSystem("./container-data");
682
- // Mirror root at ./container-data/.vfs/mirror
683
- ```
684
-
685
- #### Methods
686
-
687
- #### Events
688
-
689
- `VirtualFileSystem` extends `EventEmitter` and emits the following events:
690
-
691
- | Event | Data | Description |
692
- |-------|------|-------------|
693
- | `file:read` | `{ path: string; size: number }` | File read |
694
- | `file:write` | `{ path: string; size: number }` | File written |
695
- | `dir:create` | `{ path: string; mode: number }` | Directory created |
696
- | `mirror:flush` | — | Mirror persisted to disk |
697
-
698
- **Example:**
699
-
700
- ```typescript
701
- vfs.on("file:write", ({ path, size }) => {
702
- console.log(`[VFS] File written: ${path} (${size} bytes)`);
703
- });
704
-
705
- vfs.on("dir:create", ({ path, mode }) => {
706
- console.log(`[VFS] Directory created: ${path} (mode: ${mode.toString(8)})`);
707
- });
708
- ```
709
-
710
- ##### `async restoreMirror(): Promise<void>`
711
-
712
- Ensures mirror directory structure exists and is ready for operations.
713
-
714
- ```typescript
715
- await vfs.restoreMirror();
716
- ```
717
-
718
- ##### `async flushMirror(): Promise<void>`
719
-
720
- Compatibility hook to finalize mirror boundary operations.
721
-
722
- ```typescript
723
- // After file modifications...
724
- await vfs.flushMirror();
725
- ```
726
-
727
- ##### `mkdir(path: string, mode?: number): void`
728
-
729
- Creates directory and any missing parents. Throws if parent is a file.
730
-
731
- ```typescript
732
- vfs.mkdir("/home/user/.ssh", 0o700);
733
- ```
734
-
735
- ##### `writeFile(path: string, content: string | Buffer, options?: WriteFileOptions): void`
736
-
737
- Writes file content. Creates parent directories if missing.
738
-
739
- - **options.mode**: POSIX file mode (default: 0o644)
740
- - **options.compress**: Store as gzip (default: false)
741
-
742
- ```typescript
743
- vfs.writeFile("/etc/app.conf", "debug=true\n", { compress: true });
744
- ```
745
-
746
- ##### `readFile(path: string): string`
747
-
748
- Reads file as UTF-8 string. Transparently decompresses if needed.
749
-
750
- ```typescript
751
- const content = vfs.readFile("/etc/app.conf");
752
- ```
753
-
754
- ##### `exists(path: string): boolean`
755
-
756
- Checks node existence (file or directory).
757
-
758
- ```typescript
759
- if (!vfs.exists("/var/log")) {
760
- vfs.mkdir("/var/log");
761
- }
762
- ```
763
-
764
- ##### `stat(path: string): VfsNodeStats`
765
-
766
- Returns metadata (type, size, dates, mode, etc.).
767
-
768
- ```typescript
769
- const stats = vfs.stat("/etc/hostname");
770
- if (stats.type === "file") {
771
- console.log(`File size: ${stats.size} bytes`);
772
- }
773
- ```
774
-
775
- ##### `list(dirPath?: string): string[]`
776
-
777
- Lists child names in directory (sorted). Throws if path not a directory.
778
-
779
- ```typescript
780
- const files = vfs.list("/home");
781
- // ["alice", "bob", "root"]
782
- ```
783
-
784
- ##### `tree(dirPath?: string): string`
785
-
786
- Renders ASCII tree view of directory hierarchy.
787
-
788
- ```typescript
789
- console.log(vfs.tree("/home"));
790
- ```
791
-
792
- ##### `chmod(path: string, mode: number): void`
793
-
794
- Updates file/dir permissions.
795
-
796
- ```typescript
797
- vfs.chmod("/tmp/script.sh", 0o755);
798
- ```
799
-
800
- ##### `remove(path: string, options?: RemoveOptions): void`
801
-
802
- Removes file or directory. Throws if directory not empty unless `recursive: true`.
803
-
804
- ```typescript
805
- vfs.remove("/tmp/old", { recursive: true });
806
- ```
807
-
808
- ##### `move(fromPath: string, toPath: string): void`
809
-
810
- Renames or moves node. Throws if destination exists.
811
-
812
- ```typescript
813
- vfs.move("/var/tmp", "/var/backup");
814
- ```
815
-
816
- ##### `compressFile(path: string): void`
817
-
818
- Gzip-compresses file content and marks as compressed.
819
-
820
- ```typescript
821
- vfs.compressFile("/var/log/app.log");
822
- ```
823
-
824
- ##### `decompressFile(path: string): void`
825
-
826
- Decompresses file content (inverse of `compressFile`).
827
-
828
- ```typescript
829
- vfs.decompressFile("/var/log/app.log");
830
- ```
831
-
832
- **Example:**
833
-
834
- ```typescript
835
- vfs.on("file:write", ({ path, size }) => {
836
- console.log(`[VFS] File written: ${path} (${size} bytes)`);
837
- });
838
-
839
- vfs.on("dir:create", ({ path, mode }) => {
840
- console.log(`[VFS] Directory created: ${path} (mode: ${mode.toString(8)})`);
841
- });
842
- ```
346
+ #### Events
347
+
348
+ | Event | Data | Description |
349
+ |-------|------|-------------|
350
+ | `start` | `{ port: number }` | SFTP server started |
351
+ | `stop` | — | SFTP server stopped |
352
+ | `auth:success` | `{ username, remoteAddress }` | User authenticated |
353
+ | `auth:failure` | `{ username, remoteAddress }` | Auth failed |
354
+ | `client:connect` | | New SFTP client connected |
355
+ | `client:disconnect` | `{ user: string }` | SFTP client disconnected |
843
356
 
844
357
  ---
845
358
 
846
- ### VirtualUserManager
359
+ ### `VirtualShell`
847
360
 
848
- User authentication, password hashing (scrypt), sudo privilege management, and session tracking.
361
+ Coordinates the virtual filesystem, user manager, and command runtime. Used by both SSH servers and the programmatic `SshClient`.
849
362
 
850
363
  #### Constructor
851
364
 
852
365
  ```typescript
853
- new VirtualUserManager(vfs: VirtualFileSystem, defaultRootPassword?: string, autoSudoForNewUsers?: boolean)
366
+ new VirtualShell(
367
+ hostname: string,
368
+ properties?: ShellProperties,
369
+ vfsOptions?: VfsOptions,
370
+ )
854
371
  ```
855
372
 
856
- - **vfs**: Virtual filesystem (for auth data persistence)
857
- - **defaultRootPassword**: Root password used when root is created (default: "root")
858
- - **autoSudoForNewUsers**: When true, new users are added to sudoers automatically (default: `true` unless `SSH_MIMIC_AUTO_SUDO_NEW_USERS` disables it)
373
+ - **hostname**: Injected into command context and prompt.
374
+ - **properties**: Optional shell metadata shown in `uname`-like output. Defaults to `defaultShellProperties`.
375
+ - **vfsOptions**: Optional VFS persistence options see [VirtualFileSystem](#virtualfilesystem).
859
376
 
860
377
  ```typescript
861
- const users = new VirtualUserManager(vfs, "SecureRootPass123");
378
+ interface ShellProperties {
379
+ kernel: string; // e.g. "1.0.0+itsrealfortune+1-amd64"
380
+ os: string; // e.g. "Fortune GNU/Linux x64"
381
+ arch: string; // e.g. "x86_64"
382
+ }
862
383
  ```
863
384
 
864
- #### Methods
865
-
866
- ##### `async initialize(): Promise<void>`
867
-
868
- Loads users/sudoers from disk, ensures root exists, and initializes sessions.
385
+ **Example:**
869
386
 
870
387
  ```typescript
871
- await users.initialize();
388
+ const shell = new VirtualShell("typescript-vm", {
389
+ kernel: "1.0.0+itsrealfortune+1-amd64",
390
+ os: "Fortune GNU/Linux x64",
391
+ arch: "x86_64",
392
+ }, {
393
+ mode: "fs",
394
+ snapshotPath: "./data",
395
+ });
872
396
  ```
873
397
 
874
- ##### `verifyPassword(username: string, password: string): boolean`
875
-
876
- Checks plaintext password against hashed record.
877
-
878
- ```typescript
879
- if (users.verifyPassword("alice", "password123")) {
880
- console.log("Auth OK");
881
- }
882
- ```
398
+ #### Methods
883
399
 
884
- ##### `async addUser(username: string, password: string): Promise<void>`
400
+ | Method | Description |
401
+ |--------|-------------|
402
+ | `ensureInitialized(): Promise<void>` | Await this before using the shell programmatically. |
403
+ | `addCommand(name, params, callback)` | Register a custom shell command. |
404
+ | `executeCommand(rawInput, authUser, cwd)` | Run a raw command string. |
405
+ | `startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize)` | Start an SSH interactive session. |
406
+ | `writeFileAsUser(authUser, path, content)` | Write a file with quota enforcement. |
407
+ | `getVfs(): VirtualFileSystem \| null` | Access the VFS instance. |
408
+ | `getUsers(): VirtualUserManager \| null` | Access the user manager. |
409
+ | `getHostname(): string` | Returns the configured hostname. |
885
410
 
886
- Creates new user with home directory.
411
+ **Custom command example:**
887
412
 
888
413
  ```typescript
889
- await users.addUser("bob", "bob_password");
890
- // ~/bob created, added to sudoers
414
+ shell.addCommand("greet", ["[name]"], ({ args, authUser }) => {
415
+ const name = args[0] ?? authUser;
416
+ return { stdout: `Hello, ${name}!`, exitCode: 0 };
417
+ });
418
+ // Inside the shell: greet world → Hello, world!
891
419
  ```
892
420
 
893
- ##### `async deleteUser(username: string): Promise<void>`
421
+ #### Events
894
422
 
895
- Removes user. Cannot delete root.
423
+ | Event | Data | Description |
424
+ |-------|------|-------------|
425
+ | `initialized` | — | Shell initialization complete |
426
+ | `command` | `{ command, user, cwd }` | A command was executed |
427
+ | `session:start` | `{ user, sessionId, remoteAddress }` | Interactive session started |
896
428
 
897
- ```typescript
898
- await users.deleteUser("bob");
899
- ```
429
+ ---
900
430
 
901
- ##### `isSudoer(username: string): boolean`
431
+ ### `VirtualFileSystem`
902
432
 
903
- Checks sudo access.
433
+ Pure in-memory virtual filesystem. All state lives in a recursive `Map`-based tree — no host filesystem access at runtime.
904
434
 
435
+ Two persistence modes are available via the `VfsOptions` constructor argument:
905
436
 
906
437
  ```typescript
907
- if (users.isSudoer("alice")) {
908
- console.log("alice can use sudo");
909
- }
910
- ```
911
-
912
- ##### `async addSudoer(username: string): Promise<void>`
438
+ // Default — pure in-memory, zero disk I/O
439
+ const vfs = new VirtualFileSystem();
440
+ const vfs = new VirtualFileSystem({ mode: "memory" });
913
441
 
914
- Grants sudo privileges to user.
915
-
916
- ```typescript
917
- await users.addSudoer("charlie");
442
+ // FS mode — JSON snapshot auto-saved to disk on flushMirror()
443
+ const vfs = new VirtualFileSystem({
444
+ mode: "fs",
445
+ snapshotPath: "./data", // writes ./data/vfs-snapshot.json
446
+ });
447
+ await vfs.restoreMirror(); // load from disk (silent no-op if no file yet)
448
+ // ... use vfs ...
449
+ await vfs.flushMirror(); // persist to disk
918
450
  ```
919
451
 
920
- ##### `async removeSudoer(username: string): Promise<void>`
452
+ Both modes expose exactly the same API. The tree always lives in memory; `"fs"` mode adds a JSON round-trip on `restoreMirror` / `flushMirror`.
921
453
 
922
- Revokes sudo privileges. Cannot remove root.
454
+ #### Constructor
923
455
 
924
456
  ```typescript
925
- await users.removeSudoer("charlie");
457
+ interface VfsOptions {
458
+ mode?: "memory" | "fs"; // default: "memory"
459
+ snapshotPath?: string; // required when mode is "fs"
460
+ }
461
+
462
+ new VirtualFileSystem(options?: VfsOptions)
926
463
  ```
927
464
 
928
- ##### `async setQuotaBytes(username: string, maxBytes: number): Promise<void>`
465
+ #### Methods
929
466
 
930
- Sets an optional per-user quota (bytes) for writes under `/home/<username>`.
467
+ | Method | Description |
468
+ |--------|-------------|
469
+ | `mkdir(path, mode?)` | Create directory and any missing parents. |
470
+ | `writeFile(path, content, options?)` | Write file (creates parent dirs). `options.compress` stores as gzip; `options.mode` sets POSIX mode bits. |
471
+ | `readFile(path): string` | Read file as UTF-8. Transparently decompresses gzip files. |
472
+ | `readFileRaw(path): Buffer` | Read file as Buffer (decompresses if needed). |
473
+ | `exists(path): boolean` | Test whether a file or directory exists. |
474
+ | `stat(path): VfsNodeStats` | Returns file/directory metadata. |
475
+ | `list(path?): string[]` | List direct children of a directory (sorted). |
476
+ | `tree(path?): string` | Render ASCII directory tree. |
477
+ | `move(from, to)` | Move or rename a node. Throws if destination exists. |
478
+ | `remove(path, options?)` | Delete file or directory. `options.recursive` required for non-empty dirs. |
479
+ | `chmod(path, mode)` | Update POSIX mode bits. |
480
+ | `compressFile(path)` | Gzip-compress file content in place. |
481
+ | `decompressFile(path)` | Gunzip file content in place. |
482
+ | `symlink(target, linkPath)` | Create a symbolic link (mode `0o120777`). |
483
+ | `isSymlink(path): boolean` | Returns true if the path is a symlink node. |
484
+ | `resolveSymlink(path, maxDepth?): string` | Resolve symlink chain to real path (default max 8 hops). |
485
+ | `getUsageBytes(path?): number` | Total stored bytes under a path. |
486
+ | `getMode(): VfsPersistenceMode` | Returns `"memory"` or `"fs"`. |
487
+ | `getSnapshotPath(): string \| null` | Snapshot file path in `"fs"` mode, or null. |
488
+ | `toSnapshot(): VfsSnapshot` | Export the whole tree as a JSON-serialisable snapshot. |
489
+ | `importSnapshot(snapshot)` | Replace current state from a snapshot (preserves mode). |
490
+ | `restoreMirror(): Promise<void>` | Load from disk (`"fs"` mode) / no-op (`"memory"` mode). |
491
+ | `flushMirror(): Promise<void>` | Save to disk (`"fs"` mode) / emit `mirror:flush` (`"memory"` mode). |
492
+ | `VirtualFileSystem.fromSnapshot(snapshot)` | **Static.** Create a new memory-mode instance from a snapshot. |
931
493
 
932
- ```typescript
933
- await users.setQuotaBytes("alice", 5 * 1024 * 1024); // 5 MB
934
- ```
494
+ #### Events
935
495
 
936
- ##### `async clearQuota(username: string): Promise<void>`
496
+ | Event | Data | Description |
497
+ |-------|------|-------------|
498
+ | `file:write` | `{ path, size }` | File written |
499
+ | `file:read` | `{ path, size }` | File read |
500
+ | `dir:create` | `{ path, mode }` | Directory created |
501
+ | `node:remove` | `{ path }` | File or directory deleted |
502
+ | `symlink:create` | `{ link, target }` | Symlink created |
503
+ | `snapshot:import` | — | `importSnapshot()` called |
504
+ | `snapshot:restore` | `{ path }` | Restored from disk (fs mode) |
505
+ | `mirror:flush` | `{ path? }` | Flushed (path present in fs mode) |
937
506
 
938
- Removes quota limit for a user.
507
+ **Example:**
939
508
 
940
509
  ```typescript
941
- await users.clearQuota("alice");
942
- ```
510
+ vfs.on("file:write", ({ path, size }) => {
511
+ console.log(`[VFS] Written: ${path} (${size} bytes)`);
512
+ });
943
513
 
944
- ##### `getQuotaBytes(username: string): number | null`
514
+ vfs.on("dir:create", ({ path, mode }) => {
515
+ console.log(`[VFS] Dir created: ${path} (mode: ${mode.toString(8)})`);
516
+ });
517
+ ```
945
518
 
946
- Returns configured quota in bytes, or `null` if unlimited.
519
+ #### Memory mode manual snapshot persistence
947
520
 
948
521
  ```typescript
949
- console.log(users.getQuotaBytes("alice"));
950
- ```
522
+ import { VirtualFileSystem } from "typescript-virtual-container";
523
+ import { writeFileSync, readFileSync } from "node:fs";
951
524
 
952
- ##### `getUsageBytes(username: string): number`
525
+ const vfs = new VirtualFileSystem(); // mode: "memory"
526
+ vfs.writeFile("/etc/config.json", JSON.stringify({ debug: true }));
953
527
 
954
- Returns current stored usage in bytes under `/home/<username>`.
528
+ // Export to disk manually
529
+ writeFileSync("vfs-snapshot.json", JSON.stringify(vfs.toSnapshot()));
955
530
 
956
- ```typescript
957
- console.log(users.getUsageBytes("alice"));
531
+ // Restore into a new instance
532
+ const snapshot = JSON.parse(readFileSync("vfs-snapshot.json", "utf8"));
533
+ const restored = VirtualFileSystem.fromSnapshot(snapshot);
534
+ console.log(restored.readFile("/etc/config.json")); // {"debug":true}
958
535
  ```
959
536
 
960
- ##### `assertWriteWithinQuota(username: string, targetPath: string, nextContent: string | Buffer): void`
961
-
962
- Validates a write operation against quota rules; throws when projected usage exceeds quota.
537
+ #### FS mode automatic persistence across restarts
963
538
 
964
539
  ```typescript
965
- users.assertWriteWithinQuota("alice", "/home/alice/data.txt", "payload");
966
- ```
967
-
968
- ##### `registerSession(username: string, remoteAddress: string): VirtualActiveSession`
540
+ import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
969
541
 
970
- Creates active session (called on SSH auth). Returns session descriptor with UUID, tty, start time.
542
+ const shell = new VirtualShell("my-vm", undefined, {
543
+ mode: "fs",
544
+ snapshotPath: "./vfs-data",
545
+ });
971
546
 
972
- ```typescript
973
- const session = users.registerSession("alice", "192.168.1.100");
974
- console.log(session.id); // UUID
547
+ const ssh = new VirtualSshServer({ port: 2222, shell });
548
+ await ssh.start();
549
+ // VFS is restored from ./vfs-data/vfs-snapshot.json on start (if it exists).
550
+ // flushMirror() is called after each write, persisting state to disk automatically.
975
551
  ```
976
552
 
977
- ##### `unregisterSession(sessionId: string | null): void`
978
-
979
- Closes session. Safe to call with null.
553
+ ---
980
554
 
981
- ```typescript
982
- users.unregisterSession(sessionId);
983
- ```
555
+ ### `VirtualUserManager`
984
556
 
985
- ##### `updateSession(sessionId: string | null, username: string, remoteAddress: string): void`
557
+ Manages virtual users, password hashing (scrypt), sudo privileges, per-user storage quotas, SSH public keys, and active session tracking.
986
558
 
987
- Updates session metadata (used for su/sudo).
559
+ #### Constructor
988
560
 
989
561
  ```typescript
990
- users.updateSession(sessionId, "root", "192.168.1.100");
562
+ new VirtualUserManager(
563
+ vfs: VirtualFileSystem,
564
+ autoSudoForNewUsers?: boolean, // default: true
565
+ )
991
566
  ```
992
567
 
993
- ##### `listActiveSessions(): VirtualActiveSession[]`
568
+ - Auth data is stored inside the VFS at protected paths under `/virtual-env-js/.auth/`.
569
+ - `autoSudoForNewUsers`: when true, newly created users are automatically added to sudoers.
994
570
 
995
- Returns snapshot of active sessions (sorted by start time).
571
+ #### Methods
996
572
 
997
- ```typescript
998
- const sessions = users.listActiveSessions();
999
- sessions.forEach(s => {
1000
- console.log(`${s.username}@${s.remoteAddress} on ${s.tty}`);
1001
- });
1002
- ```
573
+ | Method | Description |
574
+ |--------|-------------|
575
+ | `initialize(): Promise<void>` | Load users/sudoers from VFS, ensure root exists. Call once on startup. |
576
+ | `verifyPassword(username, password): boolean` | Check plaintext password against stored hash. |
577
+ | `hasPassword(username): boolean` | Returns true if a password is set for the user. |
578
+ | `hashPassword(password): string` | Hash a password using the configured algorithm. |
579
+ | `addUser(username, password): Promise<void>` | Create user with home directory. |
580
+ | `deleteUser(username): Promise<void>` | Delete user. Cannot delete root. |
581
+ | `isSudoer(username): boolean` | Check if user has sudo privileges. |
582
+ | `addSudoer(username): Promise<void>` | Grant sudo privileges. |
583
+ | `removeSudoer(username): Promise<void>` | Revoke sudo privileges. Cannot remove root. |
584
+ | `setQuotaBytes(username, maxBytes): Promise<void>` | Set per-user write quota (bytes under `/home/<user>`). |
585
+ | `clearQuota(username): Promise<void>` | Remove quota limit. |
586
+ | `getQuotaBytes(username): number \| null` | Returns quota in bytes, or null if unlimited. |
587
+ | `getUsageBytes(username): number` | Returns current usage in bytes under `/home/<user>`. |
588
+ | `assertWriteWithinQuota(username, path, content)` | Throws if the write would exceed the user's quota. |
589
+ | `addAuthorizedKey(username, algo, data)` | Register an SSH public key for the user. |
590
+ | `getAuthorizedKeys(username)` | Returns the list of authorized keys for a user. |
591
+ | `removeAuthorizedKeys(username)` | Revoke all authorized keys for a user. |
592
+ | `registerSession(username, remoteAddress): VirtualActiveSession` | Start session tracking, returns session descriptor. |
593
+ | `unregisterSession(sessionId): void` | End session. Safe to call with null. |
594
+ | `updateSession(sessionId, username, remoteAddress): void` | Update session metadata (used by `su`/`sudo`). |
595
+ | `listActiveSessions(): VirtualActiveSession[]` | Returns all active sessions sorted by start time. |
1003
596
 
1004
597
  #### Events
1005
598
 
1006
- `VirtualUserManager` extends `EventEmitter` and emits the following events:
1007
-
1008
599
  | Event | Data | Description |
1009
600
  |-------|------|-------------|
1010
- | `initialized` | — | User manager initialization complete, root user ready |
1011
- | `user:add` | `{ username: string }` | New user created |
1012
- | `user:delete` | `{ username: string }` | User deleted |
1013
- | `session:register` | `{ sessionId: string; username: string; remoteAddress: string }` | Session registered (user logged in) |
1014
- | `session:unregister` | `{ sessionId: string; username: string }` | Session unregistered (user logged out) |
601
+ | `initialized` | — | User manager ready, root account ensured |
602
+ | `user:add` | `{ username }` | New user created |
603
+ | `user:delete` | `{ username }` | User deleted |
604
+ | `key:add` | `{ username, algo }` | Public key added |
605
+ | `key:remove` | `{ username }` | Public keys removed |
606
+ | `session:register` | `{ sessionId, username, remoteAddress }` | Session started |
607
+ | `session:unregister` | `{ sessionId, username }` | Session ended |
1015
608
 
1016
609
  **Example:**
1017
610
 
1018
611
  ```typescript
1019
612
  users.on("user:add", ({ username }) => {
1020
- console.log(`[USERS] User created: ${username}`);
613
+ console.log(`[USERS] Created: ${username}`);
1021
614
  });
1022
615
 
1023
616
  users.on("session:register", ({ sessionId, username, remoteAddress }) => {
1024
- console.log(`[USERS] Session ${sessionId}: ${username} from ${remoteAddress}`);
1025
- });
1026
-
1027
- users.on("session:unregister", ({ sessionId, username }) => {
1028
- console.log(`[USERS] Session ${sessionId} (${username}) closed`);
617
+ console.log(`[USERS] Session ${sessionId}: ${username} from ${remoteAddress}`);
1029
618
  });
1030
619
  ```
1031
620
 
1032
621
  ---
1033
622
 
1034
- ### HoneyPot (Auditing & Event Tracking)
623
+ ### `HoneyPot`
1035
624
 
1036
- Comprehensive security auditing and event tracking utility. Attaches to all core components (VirtualShell, VirtualFileSystem, VirtualUserManager, SshMimic, SftpMimic) to log activity, track statistics, and detect anomalies.
625
+ Comprehensive security auditing and event tracking utility. Attaches listeners to all core components to log activity, track statistics, and detect anomalies.
1037
626
 
1038
627
  #### Constructor
1039
628
 
1040
629
  ```typescript
1041
- new HoneyPot(maxLogSize?: number)
1042
- ```
1043
-
1044
- - **maxLogSize**: Maximum audit log entries to retain (default: 10000)
1045
-
1046
- ```typescript
1047
- const honeypot = new HoneyPot(5000); // Keep last 5000 audit entries
630
+ new HoneyPot(maxLogSize?: number) // default: 10000
1048
631
  ```
1049
632
 
1050
633
  #### Methods
1051
634
 
1052
- ##### `attach(shell: VirtualShell, vfs: VirtualFileSystem, users: VirtualUserManager, ssh?: SshMimic, sftp?: SftpMimic): void`
1053
-
1054
- Attaches honeypot listeners to all provided event emitters. This wires up all audit tracking across the entire virtual environment.
1055
-
1056
- ```typescript
1057
- honeypot.attach(shell, vfs, users, ssh, sftp);
1058
- // All components now emit events to honeypot
635
+ | Method | Description |
636
+ |--------|-------------|
637
+ | `attach(shell, vfs, users, ssh?, sftp?)` | Subscribe to all event sources. |
638
+ | `getAuditLog(type?, source?): AuditLogEntry[]` | Full log, optionally filtered by event type and/or source component. |
639
+ | `getStats(): Readonly<HoneyPotStats>` | Aggregated activity counters. |
640
+ | `getRecent(limit?): AuditLogEntry[]` | Most recent entries in reverse chronological order. |
641
+ | `detectAnomalies()` | Analyze patterns returns `{ type, severity, message }[]`. |
642
+ | `reset()` | Clear audit log and reset all stat counters. |
643
+ | `exportJson(): string` | Serialise full log + stats to a JSON string. |
644
+
645
+ #### HoneyPotStats fields
646
+
647
+ ```typescript
648
+ interface HoneyPotStats {
649
+ authAttempts: number;
650
+ authSuccesses: number;
651
+ authFailures: number;
652
+ commands: number;
653
+ fileWrites: number;
654
+ fileReads: number;
655
+ sessionStarts: number;
656
+ sessionEnds: number;
657
+ userCreated: number;
658
+ userDeleted: number;
659
+ clientConnects: number;
660
+ clientDisconnects: number;
661
+ }
1059
662
  ```
1060
663
 
1061
- ##### `getAuditLog(type?: string, source?: string): AuditLogEntry[]`
1062
-
1063
- Returns audit log entries with optional filtering by event type or source component.
664
+ #### Audit Log Entry
1064
665
 
1065
666
  ```typescript
1066
- // All entries
1067
- const allLogs = honeypot.getAuditLog();
1068
-
1069
- // Only auth events
1070
- const authLogs = honeypot.getAuditLog("auth:failure");
1071
-
1072
- // Only SshMimic events
1073
- const sshLogs = honeypot.getAuditLog(undefined, "SshMimic");
1074
-
1075
- // Combine filters
1076
- const sshAuthLogs = honeypot.getAuditLog("auth:success", "SshMimic");
667
+ interface AuditLogEntry {
668
+ timestamp: string; // ISO-8601
669
+ type: string; // e.g. "auth:failure", "file:write"
670
+ source: string; // e.g. "SshMimic", "VirtualFileSystem"
671
+ details: Record<string, unknown>; // event-specific payload
672
+ }
1077
673
  ```
1078
674
 
1079
- ##### `getStats(): Readonly<HoneyPotStats>`
1080
-
1081
- Returns current activity statistics snapshot.
675
+ #### Example
1082
676
 
1083
677
  ```typescript
1084
- const stats = honeypot.getStats();
1085
- console.log(`Auth attempts: ${stats.authAttempts}`);
1086
- console.log(`Auth successes: ${stats.authSuccesses}`);
1087
- console.log(`Auth failures: ${stats.authFailures}`);
1088
- console.log(`Commands executed: ${stats.commands}`);
1089
- console.log(`File writes: ${stats.fileWrites}`);
1090
- console.log(`File reads: ${stats.fileReads}`);
1091
- console.log(`Sessions started: ${stats.sessionStarts}`);
1092
- console.log(`Sessions ended: ${stats.sessionEnds}`);
1093
- console.log(`Users created: ${stats.userCreated}`);
1094
- console.log(`Users deleted: ${stats.userDeleted}`);
1095
- console.log(`Client connects: ${stats.clientConnects}`);
1096
- console.log(`Client disconnects: ${stats.clientDisconnects}`);
1097
- ```
1098
-
1099
- ##### `getRecent(limit?: number): AuditLogEntry[]`
678
+ import { HoneyPot, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1100
679
 
1101
- Returns most recent audit entries in reverse chronological order.
680
+ const shell = new VirtualShell("honeypot");
681
+ const ssh = new VirtualSshServer({ port: 2222, shell });
682
+ const hp = new HoneyPot(50_000);
1102
683
 
1103
- ```typescript
1104
- const last50 = honeypot.getRecent(50);
1105
- last50.forEach(entry => {
1106
- console.log(`${entry.timestamp} | ${entry.source} | ${entry.type}`);
1107
- console.log(`Details:`, entry.details);
1108
- });
1109
- ```
684
+ await ssh.start();
685
+ hp.attach(shell, shell.vfs, shell.users, ssh);
1110
686
 
1111
- ##### `detectAnomalies(): Array<{ type: string; severity: "low" | "medium" | "high"; message: string }>`
687
+ // Filter audit log
688
+ const failures = hp.getAuditLog("auth:failure");
689
+ failures.forEach(e => console.log(e.details.username, e.details.remoteAddress));
1112
690
 
1113
- Analyzes activity patterns and detects potential security issues.
691
+ // Detect anomalies
692
+ hp.detectAnomalies().forEach(a =>
693
+ console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
694
+ );
1114
695
 
1115
- ```typescript
1116
- const anomalies = honeypot.detectAnomalies();
1117
- anomalies.forEach(anomaly => {
1118
- console.log(`[${anomaly.severity.toUpperCase()}] ${anomaly.type}`);
1119
- console.log(` ${anomaly.message}`);
696
+ // Export on shutdown
697
+ process.on("SIGINT", () => {
698
+ require("fs").writeFileSync("audit.json", hp.exportJson());
699
+ process.exit(0);
1120
700
  });
1121
701
  ```
1122
702
 
1123
- Detects:
703
+ **`detectAnomalies` detects:**
1124
704
  - High authentication failure rates
1125
705
  - Excessive authentication failures
1126
706
  - Unusual command execution volume
1127
707
  - Unusual file write volume
1128
708
 
1129
- ##### `reset(): void`
709
+ ---
1130
710
 
1131
- Clears audit log and resets all statistics counters.
711
+ ### `SshClient` (Programmatic API)
1132
712
 
1133
- ```typescript
1134
- honeypot.reset(); // Fresh start
1135
- ```
713
+ Execute shell commands against a `VirtualShell` without SSH protocol overhead. Maintains working-directory state across calls.
1136
714
 
1137
- #### Audit Log Entry Structure
715
+ #### Constructor
1138
716
 
1139
717
  ```typescript
1140
- interface AuditLogEntry {
1141
- timestamp: string; // ISO-8601 timestamp
1142
- type: string; // Event type (e.g., "auth:success", "file:write")
1143
- source: string; // Event source component
1144
- details: Record<string, unknown>; // Event-specific data
1145
- }
718
+ new SshClient(shell: VirtualShell, username: string)
1146
719
  ```
1147
720
 
1148
- #### Example Usage
721
+ No password required — the client authenticates by username only.
722
+
723
+ #### Methods
1149
724
 
1150
- See [Example 8: Security Auditing with HoneyPot](#example-8-security-auditing-with-honeypot) in Usage Examples.
725
+ | Method | Description |
726
+ |--------|-------------|
727
+ | `exec(command): Promise<CommandResult>` | Run arbitrary raw command string. |
728
+ | `ls(path?)` | List directory (default: cwd). |
729
+ | `pwd()` | Print current working directory. |
730
+ | `cd(path)` | Change directory. Updates internal cwd state on success. |
731
+ | `cat(path)` | Read file content via `cat` command. |
732
+ | `readFile(path)` | Read file directly from VFS (programmatic, no shell parse). |
733
+ | `writeFile(path, content)` | Write file directly to VFS (programmatic). |
734
+ | `mkdir(path, recursive?)` | Create directory. `recursive=true` adds `-p`. |
735
+ | `touch(path)` | Create empty file. |
736
+ | `rm(path, recursive?)` | Remove file or directory. `recursive=true` adds `-r`. |
737
+ | `tree(path?)` | Render ASCII directory tree. |
738
+ | `whoami()` | Print current user. |
739
+ | `hostname()` | Print server hostname. |
740
+ | `who()` | List active sessions. |
741
+ | `getCwd(): string` | Returns current working directory (local, no I/O). |
742
+ | `getUsername(): string` | Returns authenticated username. |
1151
743
 
1152
- ---
744
+ **Example:**
1153
745
 
1154
- ### Demo: Standalone Version
746
+ ```typescript
747
+ const shell = new VirtualShell("typescript-vm");
748
+ const client = new SshClient(shell, "alice");
1155
749
 
1156
- To quickly try out a standalone version of the project, you can use the following command:
750
+ await client.mkdir("/home/alice/projects", true);
751
+ await client.cd("/home/alice/projects");
1157
752
 
1158
- ```bash
1159
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/standalone.js -o standalone.js && node standalone.js && rm -f standalone.js
1160
- ```
753
+ console.log(client.getCwd()); // /home/alice/projects
1161
754
 
1162
- This will:
1163
- 1. Download the standalone script.
1164
- 2. Execute it using Node.js.
1165
- 3. Clean up by removing the script after execution.
755
+ await client.writeFile("notes.txt", "Work in progress");
756
+ const list = await client.ls();
757
+ console.log(list.stdout); // notes.txt
1166
758
 
1167
- Enjoy exploring the standalone features of the project!
759
+ const read = await client.readFile("notes.txt");
760
+ console.log(read.stdout); // Work in progress
761
+ ```
1168
762
 
1169
763
  ---
1170
764
 
1171
765
  ### Key Types
1172
766
 
1173
- #### CommandResult
767
+ #### `CommandResult`
1174
768
 
1175
- Response from command execution (shell or programmatic).
769
+ Returned by all command executions (shell or programmatic).
1176
770
 
1177
771
  ```typescript
1178
772
  interface CommandResult {
1179
- stdout?: string; // Standard output
1180
- stderr?: string; // Standard error
1181
- exitCode?: number; // Exit code (default: 0)
1182
- nextCwd?: string; // Updated cwd (used by cd command)
1183
- clearScreen?: boolean; // Request terminal clear
1184
- closeSession?: boolean; // Request session close
1185
- switchUser?: string; // User change request (su/sudo)
1186
- openEditor?: NanoEditorSession; // Text editor launch
1187
- openHtop?: boolean; // System monitor launch
1188
- sudoChallenge?: SudoChallenge; // Sudo password challenge
773
+ stdout?: string; // Standard output
774
+ stderr?: string; // Standard error
775
+ exitCode?: number; // Exit code (default: 0)
776
+ nextCwd?: string; // Updated cwd (set by cd command)
777
+ clearScreen?: boolean; // Request terminal clear
778
+ closeSession?: boolean; // Request session close
779
+ switchUser?: string; // User switch (su/sudo)
780
+ openEditor?: NanoEditorSession; // Nano editor launch
781
+ openHtop?: boolean; // htop launch
782
+ sudoChallenge?: SudoChallenge; // Sudo password challenge
1189
783
  }
1190
784
  ```
1191
785
 
1192
- #### VfsNodeStats
1193
-
1194
- File/directory metadata.
786
+ #### `VfsNodeStats`
1195
787
 
1196
788
  ```typescript
1197
789
  type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
1198
790
 
1199
791
  interface VfsFileNode {
1200
- type: "file";
1201
- name: string;
1202
- path: string;
1203
- mode: number; // POSIX mode bits
1204
- size: number; // Byte length
1205
- compressed: boolean; // Is gzip compressed?
1206
- createdAt: Date;
1207
- updatedAt: Date;
792
+ type: "file";
793
+ name: string;
794
+ path: string;
795
+ mode: number; // POSIX mode bits
796
+ size: number; // Byte length (compressed size when compressed=true)
797
+ compressed: boolean;
798
+ createdAt: Date;
799
+ updatedAt: Date;
1208
800
  }
1209
801
 
1210
802
  interface VfsDirectoryNode {
1211
- type: "directory";
1212
- name: string;
1213
- path: string;
1214
- mode: number;
1215
- childrenCount: number;
1216
- createdAt: Date;
1217
- updatedAt: Date;
803
+ type: "directory";
804
+ name: string;
805
+ path: string;
806
+ mode: number;
807
+ childrenCount: number;
808
+ createdAt: Date;
809
+ updatedAt: Date;
1218
810
  }
1219
811
  ```
1220
812
 
1221
- #### VirtualActiveSession
1222
-
1223
- Active SSH/programmatic session descriptor.
813
+ #### `VirtualActiveSession`
1224
814
 
1225
815
  ```typescript
1226
816
  interface VirtualActiveSession {
1227
- id: string; // UUID
1228
- username: string;
1229
- tty: string; // e.g., "pts/0"
1230
- remoteAddress: string; // Client IP or label
1231
- startedAt: string; // ISO-8601 timestamp
817
+ id: string; // UUID
818
+ username: string;
819
+ tty: string; // e.g. "pts/0"
820
+ remoteAddress: string; // Client IP or label
821
+ startedAt: string; // ISO-8601
822
+ }
823
+ ```
824
+
825
+ #### `VfsSnapshot`
826
+
827
+ ```typescript
828
+ interface VfsSnapshot {
829
+ root: VfsSnapshotDirectoryNode;
830
+ }
831
+ // VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode
832
+ // File nodes store content as base64 in contentBase64.
833
+ ```
834
+
835
+ #### `ShellModule`
836
+
837
+ Contract for custom command plugins:
838
+
839
+ ```typescript
840
+ interface ShellModule {
841
+ name: string;
842
+ params: string[];
843
+ aliases?: string[];
844
+ run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
845
+ }
846
+
847
+ interface CommandContext {
848
+ authUser: string;
849
+ hostname: string;
850
+ activeSessions: VirtualActiveSession[];
851
+ rawInput: string;
852
+ mode: "shell" | "exec";
853
+ args: string[];
854
+ stdin?: string;
855
+ cwd: string;
856
+ shell: VirtualShell;
1232
857
  }
1233
858
  ```
1234
859
 
@@ -1238,72 +863,52 @@ interface VirtualActiveSession {
1238
863
 
1239
864
  ### Example 1: Basic SSH Server
1240
865
 
1241
- Minimal server startup that accepts SSH connections:
1242
-
1243
866
  ```typescript
1244
867
  import { VirtualSshServer } from "typescript-virtual-container";
1245
868
 
1246
- const ssh = new VirtualSshServer({
1247
- port: 2222,
1248
- hostname: "lab-environment"
1249
- });
1250
-
869
+ const ssh = new VirtualSshServer({ port: 2222, hostname: "lab-environment" });
1251
870
  await ssh.start();
1252
- console.log("SSH server ready. Connect via: ssh root@localhost -p 2222");
871
+ console.log("SSH server ready. Connect: ssh root@localhost -p 2222");
1253
872
 
1254
- // Keep running (e.g., in cloud deployment)
1255
- process.on("SIGINT", () => {
1256
- ssh.stop();
1257
- process.exit(0);
1258
- });
873
+ process.on("SIGINT", () => { ssh.stop(); process.exit(0); });
1259
874
  ```
1260
875
 
1261
- **External SSH connection:**
1262
-
1263
876
  ```bash
1264
877
  ssh root@localhost -p 2222
1265
- # Password: root
1266
878
  # $ whoami
1267
879
  # root
1268
- # $
1269
880
  ```
1270
881
 
1271
882
  ---
1272
883
 
1273
884
  ### Example 2: Programmatic File Operations
1274
885
 
1275
- Create, read, modify files without SSH:
1276
-
1277
886
  ```typescript
1278
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
887
+ import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1279
888
 
1280
- const shell = new VirtualShell("typescript-vm");
1281
- const ssh = new VirtualSshServer({ port: 2222, shell });
889
+ const shell = new VirtualShell("typescript-vm");
890
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1282
891
  await ssh.start();
1283
892
 
1284
893
  const client = new SshClient(shell, "root");
1285
894
 
1286
- // Create structure
1287
895
  await client.mkdir("/app/config", true);
1288
896
  await client.mkdir("/app/logs", true);
1289
897
 
1290
- // Write config
1291
898
  await client.writeFile("/app/config/settings.json", JSON.stringify({
1292
- environment: "dev",
1293
- port: 8080,
1294
- debug: true
899
+ environment: "dev",
900
+ port: 8080,
901
+ debug: true,
1295
902
  }, null, 2));
1296
903
 
1297
- // Read it back
1298
904
  const result = await client.readFile("/app/config/settings.json");
1299
905
  console.log("Config:", result.stdout);
1300
906
 
1301
- // List directory
1302
907
  const list = await client.ls("/app");
1303
908
  console.log(list.stdout);
1304
909
 
1305
- // Verify tree
1306
- console.log(await client.tree("/app"));
910
+ const tree = await client.tree("/app");
911
+ console.log(tree.stdout);
1307
912
 
1308
913
  ssh.stop();
1309
914
  ```
@@ -1312,360 +917,340 @@ ssh.stop();
1312
917
 
1313
918
  ### Example 3: Multi-User Environment
1314
919
 
1315
- Create users, manage permissions, session tracking:
1316
-
1317
920
  ```typescript
1318
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
921
+ import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1319
922
 
1320
923
  const shell = new VirtualShell("typescript-vm");
1321
- const ssh = new VirtualSshServer({ port: 2222, shell });
924
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1322
925
  await ssh.start();
1323
926
 
1324
927
  const users = ssh.getUsers()!;
1325
928
 
1326
- // Create users
1327
929
  await users.addUser("alice", "alice123");
1328
930
  await users.addUser("bob", "bob456");
1329
- console.log("Created users: alice, bob");
1330
931
 
1331
- // Grant sudo to alice only
932
+ // Alice has sudo; Bob does not
1332
933
  await users.removeSudoer("bob");
1333
- await users.addSudoer("alice");
1334
934
 
1335
- // Alice: High privilege
935
+ // Set a 5 MB quota for bob
936
+ await users.setQuotaBytes("bob", 5 * 1024 * 1024);
937
+
1336
938
  const alice = new SshClient(shell, "alice");
1337
939
  await alice.writeFile("/etc/important.conf", "secret=yes");
1338
940
 
1339
- // Bob: Regular user
1340
941
  const bob = new SshClient(shell, "bob");
1341
942
  const result = await bob.cat("/etc/important.conf");
1342
- console.log("Bob read file:", result.stderr);
943
+ console.log("Bob read file:", result.stderr); // permission denied
1343
944
 
1344
945
  ssh.stop();
1345
946
  ```
1346
947
 
1347
948
  ---
1348
949
 
1349
- ### Example 4: Persistent State
950
+ ### Example 4: Persistent State across Restarts
1350
951
 
1351
- Save filesystem state between runs:
952
+ #### Memory mode (manual)
1352
953
 
1353
954
  ```typescript
1354
- import { VirtualSshServer, VirtualShell } from "typescript-virtual-container";
955
+ import { VirtualFileSystem } from "typescript-virtual-container";
956
+ import { writeFileSync, readFileSync } from "node:fs";
1355
957
 
1356
- // First run: Initialize
1357
- const shell1 = new VirtualShell("typescript-vm", undefined, "./container");
1358
- const ssh1 = new VirtualSshServer({
1359
- port: 2222,
1360
- shell: shell1
1361
- });
1362
- await ssh1.start();
1363
- const vfs1 = ssh1.getVfs()!;
958
+ const vfs = new VirtualFileSystem();
959
+ vfs.writeFile("/data/report.txt", "Baseline data");
960
+
961
+ // Persist
962
+ writeFileSync("snapshot.json", JSON.stringify(vfs.toSnapshot()));
963
+
964
+ // Later, in a new process
965
+ const snapshot = JSON.parse(readFileSync("snapshot.json", "utf8"));
966
+ const restored = VirtualFileSystem.fromSnapshot(snapshot);
967
+ console.log(restored.readFile("/data/report.txt")); // Baseline data
968
+ ```
1364
969
 
1365
- vfs1.mkdir("/data", 0o777);
1366
- vfs1.writeFile("/data/report.txt", "Baseline data");
1367
- await vfs1.flushMirror();
1368
- ssh1.stop();
970
+ #### FS mode (automatic)
1369
971
 
1370
- console.log("State available under ./container/.vfs/mirror");
972
+ ```typescript
973
+ import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1371
974
 
1372
- // Later: Reload and continue
1373
- const shell2 = new VirtualShell("typescript-vm", undefined, "./container");
1374
- const ssh2 = new VirtualSshServer({
1375
- port: 2223,
1376
- shell: shell2
975
+ const shell = new VirtualShell("my-vm", undefined, {
976
+ mode: "fs",
977
+ snapshotPath: "./container-data",
1377
978
  });
1378
- await ssh2.start();
1379
- const vfs2 = ssh2.getVfs()!;
1380
- await vfs2.restoreMirror();
1381
979
 
1382
- const content = vfs2.readFile("/data/report.txt");
1383
- console.log("Restored:", content);
980
+ const ssh = new VirtualSshServer({ port: 2222, shell });
981
+ await ssh.start();
982
+ // Snapshot is auto-restored on start and auto-written on each flushMirror() call.
1384
983
 
1385
- ssh2.stop();
984
+ process.on("SIGTERM", () => { ssh.stop(); process.exit(0); });
1386
985
  ```
1387
986
 
1388
987
  ---
1389
988
 
1390
- ### Example 5: CI/CD Automation
1391
-
1392
- Simulate filesystem changes and verify outcomes:
989
+ ### Example 5: Public-key authentication
1393
990
 
1394
991
  ```typescript
1395
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
1396
-
1397
- async function testDeployment() {
1398
- const shell = new VirtualShell("typescript-vm");
1399
- const ssh = new VirtualSshServer({ port: 2222, shell });
1400
- await ssh.start();
1401
-
1402
- const client = new SshClient(shell, "root");
992
+ import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
993
+ import { readFileSync } from "node:fs";
1403
994
 
1404
- // Pre-deployment: Set up base structure
1405
- await client.mkdir("/srv/app", true);
1406
- await client.writeFile("/srv/app/package.json", '{"name":"myapp"}');
995
+ const shell = new VirtualShell("secure-vm");
996
+ await shell.ensureInitialized();
1407
997
 
1408
- // Simulate deployment: Write new version
1409
- await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
998
+ await shell.users.addUser("alice", "fallback-password");
1410
999
 
1411
- // Verify deployment: Read and validate
1412
- const appContent = await client.readFile("/srv/app/app.js");
1413
- if (appContent.stdout.includes("v2.0")) {
1414
- console.log(" Deployment verified");
1415
- } else {
1416
- console.error("✗ Deployment failed");
1417
- }
1000
+ // Parse from ~/.ssh/id_ed25519.pub
1001
+ const pubLine = readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, "utf8").trim();
1002
+ const [algo, b64] = pubLine.split(" ");
1003
+ shell.users.addAuthorizedKey("alice", algo, Buffer.from(b64, "base64"));
1418
1004
 
1419
- ssh.stop();
1420
- }
1421
-
1422
- testDeployment().catch(console.error);
1005
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1006
+ await ssh.start();
1007
+ // ssh -i ~/.ssh/id_ed25519 alice@localhost -p 2222
1423
1008
  ```
1424
1009
 
1425
1010
  ---
1426
1011
 
1427
- ### Example 6: Complex Navigation
1428
-
1429
- Simulate shell workflows:
1012
+ ### Example 6: Rate Limiting
1430
1013
 
1431
1014
  ```typescript
1432
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
1015
+ const ssh = new VirtualSshServer({
1016
+ port: 2222,
1017
+ maxAuthAttempts: 3, // lock after 3 consecutive failures
1018
+ lockoutDurationMs: 300_000, // 5-minute lockout
1019
+ });
1433
1020
 
1434
- const shell = new VirtualShell("typescript-vm");
1435
- const ssh = new VirtualSshServer({ port: 2222, shell });
1436
- await ssh.start();
1021
+ ssh.on("auth:lockout", ({ ip, until }) => {
1022
+ console.warn(`[SSH] ${ip} locked until ${until}`);
1023
+ });
1437
1024
 
1438
- const client = new SshClient(shell, "root");
1025
+ ssh.on("auth:failure", ({ username, remoteAddress }) => {
1026
+ console.warn(`[SSH] Failed login: ${username} from ${remoteAddress}`);
1027
+ });
1028
+
1029
+ // Manually lift a lockout (e.g. in an admin endpoint)
1030
+ ssh.clearLockout("192.168.1.100");
1031
+ ```
1439
1032
 
1440
- // Create nested structure
1441
- await client.mkdir("/home/user/projects/myapp/src", true);
1442
- await client.cd("/home/user/projects");
1033
+ ---
1443
1034
 
1444
- console.log(client.getCwd()); // "/home/user/projects"
1035
+ ### Example 7: CI/CD Automation & Assertions
1445
1036
 
1446
- // Navigate deeper
1447
- await client.cd("myapp/src");
1448
- console.log(client.getCwd()); // "/home/user/projects/myapp/src"
1037
+ ```typescript
1038
+ import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1449
1039
 
1450
- // Create files in new location
1451
- await client.writeFile("main.ts", "export function main() {}");
1452
- await client.writeFile("utils.ts", "export function util() {}");
1040
+ async function testDeployment() {
1041
+ const shell = new VirtualShell("typescript-vm");
1042
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1043
+ await ssh.start();
1453
1044
 
1454
- // List current
1455
- const srcFiles = await client.ls();
1456
- console.log(srcFiles.stdout); // main.ts, utils.ts
1045
+ const client = new SshClient(shell, "root");
1457
1046
 
1458
- // Navigate up (relative paths)
1459
- await client.cd("..");
1460
- console.log(client.getCwd()); // "/home/user/projects/myapp"
1047
+ await client.mkdir("/srv/app", true);
1048
+ await client.writeFile("/srv/app/package.json", '{"name":"myapp","version":"1.0.0"}');
1461
1049
 
1462
- const appTree = await client.tree();
1463
- console.log(appTree.stdout);
1050
+ // Simulate deployment
1051
+ await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
1464
1052
 
1465
- ssh.stop();
1053
+ const appContent = await client.readFile("/srv/app/app.js");
1054
+ if (appContent.stdout.includes("v2.0")) {
1055
+ console.log("✓ Deployment verified");
1056
+ } else {
1057
+ throw new Error("✗ Deployment failed");
1058
+ }
1059
+
1060
+ ssh.stop();
1061
+ }
1062
+
1063
+ testDeployment().catch(console.error);
1466
1064
  ```
1467
1065
 
1468
1066
  ---
1469
1067
 
1470
- ### Example 7: Error Handling
1471
-
1472
- Graceful error handling in programmatic workflows:
1068
+ ### Example 8: Snapshot-Based Test Fixtures
1473
1069
 
1474
1070
  ```typescript
1475
- import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
1071
+ import { VirtualFileSystem } from "typescript-virtual-container";
1072
+ import type { VfsSnapshot } from "typescript-virtual-container";
1476
1073
 
1477
- const shell = new VirtualShell("typescript-vm");
1478
- const ssh = new VirtualSshServer({ port: 2222, shell });
1479
- await ssh.start();
1074
+ function buildFixture(): VfsSnapshot {
1075
+ const vfs = new VirtualFileSystem();
1076
+ vfs.mkdir("/app/config");
1077
+ vfs.writeFile("/app/config/settings.json", JSON.stringify({ env: "test" }));
1078
+ vfs.writeFile("/app/README.md", "# My App");
1079
+ return vfs.toSnapshot();
1080
+ }
1480
1081
 
1481
- const client = new SshClient(shell, "root");
1082
+ const FIXTURE = buildFixture();
1482
1083
 
1483
- // Try read non-existent file
1484
- const result = await client.readFile("/etc/nonexistent.conf");
1485
- if (result.exitCode !== 0) {
1486
- console.error("Read error:", result.stderr);
1487
- }
1084
+ // Each test gets a clean, isolated VFS from the same fixture
1085
+ test("reads config file", () => {
1086
+ const vfs = VirtualFileSystem.fromSnapshot(FIXTURE);
1087
+ const content = JSON.parse(vfs.readFile("/app/config/settings.json"));
1088
+ expect(content.env).toBe("test");
1089
+ });
1090
+ ```
1488
1091
 
1489
- // Try change to non-existent directory
1490
- const cdResult = await client.cd("/invalid/path");
1491
- if (cdResult.exitCode !== 0) {
1492
- console.error("Invalid path");
1493
- }
1092
+ ---
1494
1093
 
1495
- // Try remove root
1496
- const rmResult = await client.rm("/", true);
1497
- console.log("Remove root:", rmResult.stderr); // Error
1094
+ ### Example 9: Symlinks
1498
1095
 
1499
- ssh.stop();
1096
+ ```typescript
1097
+ const vfs = new VirtualFileSystem();
1098
+ vfs.mkdir("/usr/local/bin");
1099
+ vfs.writeFile("/opt/myapp/bin/app", "#!/bin/sh\necho hello");
1100
+ vfs.symlink("/opt/myapp/bin/app", "/usr/local/bin/app");
1101
+
1102
+ console.log(vfs.isSymlink("/usr/local/bin/app")); // true
1103
+ console.log(vfs.resolveSymlink("/usr/local/bin/app")); // /opt/myapp/bin/app
1500
1104
  ```
1501
1105
 
1502
1106
  ---
1503
1107
 
1504
- ### Example 8: Security Auditing with HoneyPot
1505
-
1506
- Track all system activity, detect anomalies, and maintain security audit logs:
1108
+ ### Example 10: Security Auditing with HoneyPot
1507
1109
 
1508
1110
  ```typescript
1509
1111
  import {
1510
- VirtualSshServer,
1511
- VirtualShell,
1512
- SshClient,
1513
- HoneyPot,
1112
+ HoneyPot,
1113
+ SshClient,
1114
+ VirtualShell,
1115
+ VirtualSshServer,
1514
1116
  } from "typescript-virtual-container";
1515
1117
 
1516
1118
  const shell = new VirtualShell("typescript-vm");
1517
- const ssh = new VirtualSshServer({ port: 2222, shell });
1119
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1518
1120
  await ssh.start();
1519
1121
 
1520
1122
  const users = ssh.getUsers()!;
1521
- const vfs = ssh.getVfs()!;
1123
+ const vfs = ssh.getVfs()!;
1522
1124
 
1523
- // Initialize honeypot with 5000-entry log limit
1524
- const honeypot = new HoneyPot(5000);
1525
- honeypot.attach(shell, vfs, users, ssh);
1125
+ const hp = new HoneyPot(5000);
1126
+ hp.attach(shell, vfs, users, ssh);
1526
1127
 
1527
- // Create users
1528
1128
  await users.addUser("alice", "alice123");
1529
1129
  await users.addUser("bob", "bob456");
1530
1130
 
1531
- // Simulate activity
1532
1131
  const alice = new SshClient(shell, "alice");
1533
1132
  await alice.mkdir("/home/alice/projects", true);
1534
1133
  await alice.writeFile("/home/alice/projects/app.txt", "My application");
1535
1134
  await alice.ls("/home/alice/projects");
1536
1135
 
1537
1136
  const bob = new SshClient(shell, "bob");
1538
- // Bob tries invalid operations
1539
- await bob.readFile("/etc/shadow"); // Will fail
1540
- await bob.writeFile("/etc/passwd", "hacked"); // Will fail
1541
-
1542
- // Collect stats
1543
- const stats = honeypot.getStats();
1544
- console.log("\n=== Activity Summary ===");
1545
- console.log(`Auth attempts: ${stats.authAttempts}`);
1546
- console.log(`Auth successes: ${stats.authSuccesses}`);
1547
- console.log(`Auth failures: ${stats.authFailures}`);
1548
- console.log(`Commands executed: ${stats.commands}`);
1549
- console.log(`File writes: ${stats.fileWrites}`);
1550
- console.log(`File reads: ${stats.fileReads}`);
1551
- console.log(`Sessions active: ${stats.sessionStarts}`);
1552
- console.log(`Users created: ${stats.userCreated}`);
1553
-
1554
- // Get recent events
1555
- console.log("\n=== Last 5 Events ===");
1556
- honeypot.getRecent(5).forEach((entry) => {
1557
- console.log(`[${entry.timestamp}] ${entry.source} -> ${entry.type}`);
1558
- console.log(` Details: ${JSON.stringify(entry.details, null, 2)}`);
1559
- });
1137
+ await bob.readFile("/etc/shadow"); // will fail
1138
+ await bob.writeFile("/etc/passwd", ""); // will fail
1139
+
1140
+ // Stats
1141
+ const stats = hp.getStats();
1142
+ console.log(`Auth attempts: ${stats.authAttempts}`);
1143
+ console.log(`Commands run: ${stats.commands}`);
1144
+ console.log(`File writes: ${stats.fileWrites}`);
1145
+
1146
+ // Recent events
1147
+ hp.getRecent(5).forEach(e =>
1148
+ console.log(`[${e.timestamp}] ${e.source} ${e.type}`)
1149
+ );
1560
1150
 
1561
- // Detect anomalies
1562
- console.log("\n=== Security Analysis ===");
1563
- const anomalies = honeypot.detectAnomalies();
1564
- if (anomalies.length > 0) {
1565
- anomalies.forEach((anomaly) => {
1566
- console.log(
1567
- `[${anomaly.severity.toUpperCase()}] ${anomaly.type}: ${anomaly.message}`,
1568
- );
1569
- });
1570
- } else {
1571
- console.log("No anomalies detected");
1572
- }
1151
+ // Anomaly detection
1152
+ hp.detectAnomalies().forEach(a =>
1153
+ console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
1154
+ );
1573
1155
 
1574
- // Filter audit log by event type
1575
- console.log("\n=== Auth Failures ===");
1576
- const authFailures = honeypot.getAuditLog("auth:failure");
1577
- authFailures.forEach((entry) => {
1578
- console.log(
1579
- ` ${entry.details.username} from ${entry.details.remoteAddress}`,
1580
- );
1581
- });
1156
+ // Filter by type and source
1157
+ const authFailures = hp.getAuditLog("auth:failure");
1158
+ const sshEvents = hp.getAuditLog(undefined, "SshMimic");
1159
+ console.log(`Auth failures: ${authFailures.length}`);
1160
+ console.log(`SSH events: ${sshEvents.length}`);
1161
+
1162
+ ssh.stop();
1163
+ ```
1164
+
1165
+ ---
1166
+
1167
+ ### Example 11: Error Handling
1582
1168
 
1583
- // Filter by source component
1584
- console.log("\n=== All SSH Events ===");
1585
- const sshEvents = honeypot.getAuditLog(undefined, "SshMimic");
1586
- console.log(` Total SSH events: ${sshEvents.length}`);
1169
+ ```typescript
1170
+ import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1171
+
1172
+ const shell = new VirtualShell("typescript-vm");
1173
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1174
+ await ssh.start();
1175
+
1176
+ const client = new SshClient(shell, "root");
1177
+
1178
+ const r1 = await client.readFile("/etc/nonexistent.conf");
1179
+ if (r1.exitCode !== 0) console.error("Read error:", r1.stderr);
1587
1180
 
1588
- // Export full audit log (for external storage/analysis)
1589
- const fullAuditLog = honeypot.getAuditLog();
1590
- console.log(`\nTotal audit entries: ${fullAuditLog.length}`);
1181
+ const r2 = await client.cd("/invalid/path");
1182
+ if (r2.exitCode !== 0) console.error("cd failed");
1591
1183
 
1592
- // Optional: Reset for next test phase
1593
- // honeypot.reset();
1184
+ const r3 = await client.rm("/", true);
1185
+ console.log("Remove root:", r3.stderr); // Cannot remove root directory.
1594
1186
 
1595
1187
  ssh.stop();
1596
1188
  ```
1597
1189
 
1598
- **Output example:**
1190
+ ---
1599
1191
 
1600
- ```
1601
- [AUDIT] 2026-04-16T10:30:45.123Z | SshMimic | start { port: 2222 }
1602
- [AUDIT] 2026-04-16T10:30:46.234Z | VirtualUserManager | user:add { username: 'alice' }
1603
- [AUDIT] 2026-04-16T10:30:47.345Z | VirtualUserManager | user:add { username: 'bob' }
1604
- [AUDIT] 2026-04-16T10:30:48.456Z | VirtualShell | command { command: 'mkdir /home/alice/projects', user: 'alice', cwd: '/home/alice' }
1605
- [AUDIT] 2026-04-16T10:30:49.567Z | VirtualFileSystem | dir:create { path: '/home/alice/projects', mode: 16877 }
1606
- [AUDIT] 2026-04-16T10:30:50.678Z | VirtualShell | command { command: 'writeFile /home/alice/projects/app.txt', user: 'alice', cwd: '/home/alice' }
1607
-
1608
- === Activity Summary ===
1609
- Auth attempts: 2
1610
- Auth successes: 2
1611
- Auth failures: 0
1612
- Commands executed: 8
1613
- File writes: 1
1614
- File reads: 2
1615
- Sessions active: 2
1616
- Users created: 2
1617
-
1618
- === Last 5 Events ===
1619
- [2026-04-16T10:30:50.678Z] VirtualShell -> command
1620
- Details: { command: 'ls /home/alice/projects', user: 'alice', cwd: '/home/alice/projects' }
1621
-
1622
- === Security Analysis ===
1623
- No anomalies detected
1624
-
1625
- === All SSH Events ===
1626
- Total SSH events: 4
1192
+ ### Example 12: Concurrent Clients
1193
+
1194
+ ```typescript
1195
+ const shell = new VirtualShell("typescript-vm");
1196
+ const client1 = new SshClient(shell, "alice");
1197
+ const client2 = new SshClient(shell, "bob");
1198
+
1199
+ const [r1, r2] = await Promise.all([
1200
+ client1.writeFile("/tmp/alice.txt", "Alice's data"),
1201
+ client2.writeFile("/tmp/bob.txt", "Bob's data"),
1202
+ ]);
1627
1203
  ```
1628
1204
 
1629
1205
  ---
1630
1206
 
1631
1207
  ## Built-in Commands
1632
1208
 
1633
- The following commands are currently registered and available in both SSH shell mode and via `SshClient.exec()`. Some flags and edge-case behavior are still being expanded for shell compatibility.
1634
-
1635
- | Command | Purpose | Notes |
1636
- |---------|---------|-------|
1637
- | `adduser <name> <pass>` | Create user | Root only |
1638
- | `cat <path>` | Read file | Displays content |
1639
- | `cd <path>` | Change directory | Updates client cwd |
1640
- | `clear` | Clear screen | No args |
1641
- | `curl <url>` | Fetch URL | Mock implementation |
1642
- | `deluser <name>` | Delete user | Root only, not root |
1643
- | `echo <text...>` | Print text | Supports shell-like argument output |
1644
- | `env` | List environment variables | Shell environment view |
1645
- | `exit [code]` | Close session | Optional exit code |
1646
- | `export NAME=VALUE` | Set/export environment variable | Persists in shell env |
1647
- | `grep <pattern> [path]` | Search for text | Simplified grep behavior |
1648
- | `help` | List commands | No args |
1649
- | `hostname` | Server hostname | No args |
1650
- | `htop` | System monitor | Mock display |
1651
- | `pwd` | Print working directory | No args |
1652
- | `ls [path]` | List directory | Defaults to `.` |
1653
- | `mkdir [-p] <path>` | Create directory | `-p` for parents |
1654
- | `nano <path>` | Text editor | Interactive mode |
1655
- | `neofetch` | Show system summary | Mock display |
1656
- | `touch <path>` | Create empty file | Updates timestamps |
1657
- | `rm [-r] <path>` | Remove file/dir | `-r` for recursive |
1658
- | `set` | Show shell options/variables | Simplified behavior |
1659
- | `sh <script>` | Run shell script | Simplified execution model |
1660
- | `su <user>` | Switch user | Requires password/sudo |
1661
- | `sudo [-i] <cmd>` | Elevation | Requires sudoer status |
1662
- | `tree [path]` | ASCII tree view | Defaults to `.` |
1663
- | `unset <name>` | Remove environment variable | Shell environment update |
1664
- | `wget <url>` | Download | Mock implementation |
1665
- | `who` | Active sessions | No args |
1666
- | `whoami` | Current user | No args |
1667
-
1668
- Commands can be added via the VirtualShell addCommand() method for custom behavior.
1209
+ All commands are available in SSH shell mode and via `SshClient.exec()`.
1210
+
1211
+ | Command | Flags | Description |
1212
+ |---------|-------|-------------|
1213
+ | `adduser <name> <pass>` | | Create user (root only) |
1214
+ | `cat <path>` | | Print file contents |
1215
+ | `cd <path>` | | Change directory |
1216
+ | `chmod <mode> <file>` | | Change file permissions (octal) |
1217
+ | `clear` | | Clear terminal screen |
1218
+ | `cp <src> <dest>` | `-r` | Copy file or directory |
1219
+ | `curl <url>` | | Fetch URL (delegates to host binary) |
1220
+ | `deluser <name>` | | Delete user (root only, not root) |
1221
+ | `echo <text...>` | | Print text |
1222
+ | `env` | | List environment variables |
1223
+ | `exit [code]` | | Close session |
1224
+ | `export NAME=VALUE` | | Set/export shell variable |
1225
+ | `find [path]` | `-name <pat>` `-type f\|d` | Search for files |
1226
+ | `grep <pattern> [files]` | `-i` `-v` `-n` `-r` | Search file content |
1227
+ | `head [files]` | `-n <N>` | First N lines (default 10) |
1228
+ | `help` | | List all commands |
1229
+ | `hostname` | | Print hostname |
1230
+ | `htop` | | System monitor (mock) |
1231
+ | `ln <target> <link>` | `-s` | Create hard or symbolic link |
1232
+ | `ls [path]` | `-l` | List directory |
1233
+ | `mkdir <path>` | `-p` | Create directory |
1234
+ | `mv <src> <dest>` | | Move or rename |
1235
+ | `nano <path>` | | Interactive text editor |
1236
+ | `neofetch` | | System summary (mock) |
1237
+ | `passwd [user]` | | Change password |
1238
+ | `pwd` | | Print working directory |
1239
+ | `rm <path>` | `-r` | Remove file or directory |
1240
+ | `set` | | Show shell variables |
1241
+ | `sh <script>` | | Run shell script |
1242
+ | `su [user]` | | Switch user |
1243
+ | `sudo <cmd>` | `-i` | Run as root |
1244
+ | `tail [files]` | `-n <N>` | Last N lines (default 10) |
1245
+ | `touch <path>` | | Create/update file |
1246
+ | `tree [path]` | | ASCII directory tree |
1247
+ | `unset <name>` | | Remove shell variable |
1248
+ | `wc [files]` | `-l` `-w` `-c` | Line/word/byte count |
1249
+ | `wget <url>` | | Download file (delegates to host binary) |
1250
+ | `who` | | List active sessions |
1251
+ | `whoami` | | Print current user |
1252
+
1253
+ Custom commands can be added via `shell.addCommand()`.
1669
1254
 
1670
1255
  ---
1671
1256
 
@@ -1673,28 +1258,46 @@ Commands can be added via the VirtualShell addCommand() method for custom behavi
1673
1258
 
1674
1259
  ### Environment Variables
1675
1260
 
1676
- - **`SSH_MIMIC_HOSTNAME`**: Override server hostname at startup (default: "typescript-vm")
1677
- - **`SSH_MIMIC_AUTO_SUDO_NEW_USERS`**: Control whether new users are added to sudoers automatically (default: enabled). Set to `0`, `false`, `no`, or `off` to disable.
1678
-
1679
- **Note:** By default, no password is set for the root user or any new users during the first initialization. Ensure to configure user passwords manually if required.
1261
+ | Variable | Default | Description |
1262
+ |----------|---------|-------------|
1263
+ | `SSH_MIMIC_FAST_PASSWORD_HASH` | `""` | Use SHA-256 instead of scrypt (faster, less secure — dev only). Set to `1` or `true`. |
1264
+ | `SSH_MIMIC_AUTO_SUDO_NEW_USERS` | `"true"` | Auto-grant sudo to new users. Set to `0`, `false`, `no`, or `off` to disable. |
1265
+ | `DEV_MODE` | `""` | Enable performance logging. |
1266
+ | `RENDER_PERF` | `""` | Enable render performance logging. |
1680
1267
 
1681
1268
  **Example:**
1682
1269
 
1683
1270
  ```bash
1684
- export SSH_MIMIC_HOSTNAME=production-lab
1271
+ export SSH_MIMIC_FAST_PASSWORD_HASH=1
1685
1272
  export SSH_MIMIC_AUTO_SUDO_NEW_USERS=false
1686
- npm run start
1273
+ node server.js
1687
1274
  ```
1688
1275
 
1689
- ### Runtime Options
1276
+ ### Runtime Options Summary
1690
1277
 
1691
1278
  ```typescript
1692
- const shell = new VirtualShell("my-container", undefined, "./data");
1693
- const ssh = new VirtualSshServer({
1694
- port: 2222, // Required
1695
- hostname: "my-container", // Optional
1696
- shell // Optional, prebuilt shell instance
1697
- });
1279
+ // VirtualShell
1280
+ new VirtualShell(
1281
+ hostname,
1282
+ properties?, // kernel, os, arch strings
1283
+ vfsOptions?, // { mode: "memory"|"fs", snapshotPath?: string }
1284
+ )
1285
+
1286
+ // VirtualSshServer
1287
+ new VirtualSshServer({
1288
+ port,
1289
+ hostname?,
1290
+ shell?,
1291
+ maxAuthAttempts?, // default: 5
1292
+ lockoutDurationMs?, // default: 60_000
1293
+ })
1294
+
1295
+ // VirtualSftpServer
1296
+ new VirtualSftpServer({
1297
+ port,
1298
+ hostname?,
1299
+ shell?, // or: vfs + users separately
1300
+ })
1698
1301
  ```
1699
1302
 
1700
1303
  ---
@@ -1703,43 +1306,41 @@ const ssh = new VirtualSshServer({
1703
1306
 
1704
1307
  ### Benchmarking
1705
1308
 
1706
- Use the built-in benchmark script to measure initialization and command throughput under concurrent shell loads:
1309
+ Use the built-in benchmark script:
1707
1310
 
1708
1311
  ```bash
1709
1312
  bun ./benchmark-virtualshell.ts
1710
1313
  ```
1711
1314
 
1712
1315
  The benchmark reports:
1713
-
1714
- - shell initialization time by concurrency level
1715
- - command execution time across all active shells
1316
+ - Shell initialization time by concurrency level
1317
+ - Command execution time across active shells
1716
1318
  - RSS memory growth during the run
1717
1319
 
1718
- Recent baseline runs show strong startup behavior up to 100 concurrent shells, and the runtime is designed to scale up to **1000 environments very easily** for testing and automation workloads.
1320
+ Recent baselines show strong startup behavior up to 100 concurrent shells. The runtime is designed to scale easily to **1000+ parallel environments** for testing and automation workloads.
1719
1321
 
1720
1322
  ### Concurrency
1721
1323
 
1722
- - SSH server handles multiple concurrent connections (event-driven)
1723
- - Programmatic `SshClient` is synchronous (executes sequentially per instance)
1724
- - Create multiple client instances for parallel operations
1725
- - Horizontal shell instantiation (`new VirtualShell(...)`) is intended for high-volume scenarios, including large test matrices and multi-tenant simulation batches
1324
+ - SSH server is event-driven and handles multiple concurrent connections.
1325
+ - `SshClient` is sequential per instance create multiple instances for parallel operations.
1326
+ - Each `VirtualShell` instance is fully independent (separate VFS, users, state).
1726
1327
 
1727
- ### Scalability Notes
1328
+ ### Performance Tips
1728
1329
 
1729
- - Use a dedicated `basePath` per isolated environment to parallelize safely
1730
- - Reuse long-lived shell instances when you need low-latency command bursts
1731
- - Keep performance logging enabled in development (`DEV_MODE=1` or `RENDER_PERF=1`) to locate hotspots quickly
1330
+ - Use `SSH_MIMIC_FAST_PASSWORD_HASH=1` in test environments to skip scrypt overhead.
1331
+ - Reuse long-lived shell instances for low-latency command bursts.
1332
+ - Keep `DEV_MODE=1` enabled only during development (adds logging overhead).
1732
1333
 
1733
- **Example:**
1334
+ **Parallel clients example:**
1734
1335
 
1735
1336
  ```typescript
1736
- const shell = new VirtualShell("typescript-vm");
1337
+ const shell = new VirtualShell("typescript-vm");
1737
1338
  const client1 = new SshClient(shell, "alice");
1738
1339
  const client2 = new SshClient(shell, "bob");
1739
1340
 
1740
- const [result1, result2] = await Promise.all([
1741
- client1.writeFile("/tmp/alice.txt", "..."),
1742
- client2.writeFile("/tmp/bob.txt", "...")
1341
+ const [r1, r2] = await Promise.all([
1342
+ client1.writeFile("/tmp/alice.txt", "..."),
1343
+ client2.writeFile("/tmp/bob.txt", "..."),
1743
1344
  ]);
1744
1345
  ```
1745
1346
 
@@ -1751,145 +1352,167 @@ Full TypeScript support with exported types:
1751
1352
 
1752
1353
  ```typescript
1753
1354
  import type {
1754
- CommandResult,
1755
- VirtualActiveSession,
1756
- VfsNodeStats,
1757
- VfsFileNode,
1758
- VfsDirectoryNode,
1759
- SudoChallenge
1355
+ // Persistence
1356
+ VfsOptions,
1357
+ VfsPersistenceMode,
1358
+ // Filesystem
1359
+ VfsSnapshot,
1360
+ VfsNodeStats,
1361
+ VfsFileNode,
1362
+ VfsDirectoryNode,
1363
+ WriteFileOptions,
1364
+ RemoveOptions,
1365
+ // Commands
1366
+ CommandContext,
1367
+ CommandResult,
1368
+ CommandMode,
1369
+ CommandOutcome,
1370
+ ShellModule,
1371
+ SudoChallenge,
1372
+ NanoEditorSession,
1373
+ // Audit
1374
+ AuditLogEntry,
1375
+ HoneyPotStats,
1376
+ // Streams
1377
+ ShellStream,
1378
+ ExecStream,
1760
1379
  } from "typescript-virtual-container";
1761
-
1762
- async function processResult(r: CommandResult) {
1763
- if (r.exitCode === 0 && r.stdout) {
1764
- console.log("Success:", r.stdout);
1765
- } else if (r.stderr) {
1766
- console.error("Error:", r.stderr);
1767
- }
1768
- }
1769
1380
  ```
1770
1381
 
1771
1382
  ---
1772
1383
 
1773
1384
  ## FAQ
1774
1385
 
1775
- ### Is this a real container runtime?
1386
+ **Is this a real container runtime?**
1387
+ No. It emulates SSH sessions, users, and filesystem behavior in a virtual runtime. Ideal for testing, simulations, and automation where full OS isolation is not required.
1776
1388
 
1777
- No. It emulates SSH sessions, users, and filesystem behavior in a virtual runtime. It is ideal for testing, simulations, and automation workflows where full OS isolation is not required.
1389
+ **Can I use this in production?**
1390
+ You can use it in production-like automation contexts (sandboxed command runners, test harnesses, training environments, honeypots). It is not a security boundary like a real container/VM. Shell command fidelity is still being expanded.
1778
1391
 
1779
- ### Can I use this in production?
1392
+ **Does the VFS touch the host filesystem?**
1393
+ In the default `"memory"` mode: no, all data lives in memory. In `"fs"` mode, it reads/writes a single JSON file (`vfs-snapshot.json`) inside the configured `snapshotPath` directory. No other host paths are accessed.
1780
1394
 
1781
- You can use it in production-like automation contexts (sandboxed command runners, test harnesses, training environments), but it is not a security boundary like a real container/VM. And at the moment, all commands are not implemented with full fidelity, so it may not be suitable for all production use cases.
1395
+ **Does data persist between restarts?**
1396
+ Only if you explicitly use `"fs"` mode or call `toSnapshot()` / `fromSnapshot()` manually. Memory mode is ephemeral.
1782
1397
 
1783
- ### Does data persist between restarts?
1398
+ **Can I run multiple isolated shells?**
1399
+ Yes. Each `new VirtualShell(...)` creates a completely independent VFS and user manager.
1784
1400
 
1785
- Yes, when using a stable `basePath`. Files are stored under `.vfs/mirror`.
1401
+ **Are custom commands shared between shell instances?**
1402
+ No. Custom commands registered with `shell.addCommand()` are instance-local.
1786
1403
 
1787
- ### Is networking fully implemented for curl/wget?
1404
+ **Is networking fully implemented for curl/wget?**
1405
+ `curl` and `wget` delegate to the host binaries. They are intended for realistic workflows, not full GNU tooling parity.
1788
1406
 
1789
- `curl` and `wget` are command-layer implementations intended for realistic workflows, not full parity with GNU tooling.
1407
+ **Can I create custom commands?**
1408
+ Yes — use `shell.addCommand()` or implement the `ShellModule` interface directly.
1790
1409
 
1791
- ### Can I create custom commands?
1410
+ **Is SFTP fully supported?**
1411
+ Core SFTP operations (open, read, write, stat, mkdir, remove, rename) are implemented. Some optional operations (extended attributes, symlinks) return `OP_UNSUPPORTED`.
1792
1412
 
1793
- Yes. Commands are modular and can be extended in the command runtime layer to fit project-specific use cases.
1413
+ **Can I use this for honeypot deployments?**
1414
+ Yes — that is one of its primary use-cases. Use `HoneyPot.attach()` to capture all activity, configure `maxAuthAttempts` to throttle scanners, and export audit logs on shutdown.
1794
1415
 
1795
1416
  ---
1796
1417
 
1797
1418
  ## Troubleshooting
1798
1419
 
1799
- ### Port Already in Use
1800
-
1801
- ```
1802
- Error: listen EADDRINUSE :::2222
1803
- ```
1804
-
1805
- **Solution**: Use a different port
1806
-
1420
+ **`Error: listen EADDRINUSE :::2222`**
1421
+ The port is already in use. Use a different port or stop the existing process.
1807
1422
  ```typescript
1808
1423
  const ssh = new VirtualSshServer({ port: 3333 });
1809
1424
  ```
1810
1425
 
1811
- ### SSH Authentication Failed
1812
-
1813
- **Causes**: Server not started, wrong password, SSH client not found
1814
-
1815
- **Solution**:
1816
-
1817
- ```typescript
1818
- process.env.SSH_MIMIC_ROOT_PASSWORD = "your-password";
1819
- await ssh.start();
1820
- ```
1426
+ **SSH authentication always fails**
1427
+ - Check the password (root has no password by default — any login is accepted).
1428
+ - If you set a password, verify it with `users.verifyPassword(username, password)`.
1429
+ - Check if the IP is rate-limited: call `ssh.clearLockout(ip)`.
1821
1430
 
1822
- ### File Not Found Errors
1431
+ **Auth always fails with "lockout"**
1432
+ Call `ssh.clearLockout(ip)` or increase `maxAuthAttempts`. In tests, use `maxAuthAttempts: Infinity`.
1823
1433
 
1824
- **Cause**: Directory doesn't exist
1434
+ **`Error: Too many levels of symbolic links`**
1435
+ A symlink chain exceeds 8 hops. Check for circular links or pass a larger `maxDepth` to `resolveSymlink()`.
1825
1436
 
1826
- **Solution**: Create directories first
1437
+ **`Command 'xyz' not found` (exit code 127)**
1438
+ The command is not registered. Register it with `shell.addCommand()` or use `SshClient.exec()` with a handler.
1827
1439
 
1440
+ **File not found errors**
1441
+ Create the parent directory first:
1828
1442
  ```typescript
1829
- const vfs = ssh.getVfs();
1830
1443
  vfs.mkdir("/home/alice", 0o755);
1444
+ vfs.writeFile("/home/alice/file.txt", "content");
1831
1445
  ```
1832
1446
 
1833
- ### Filesystem State Not Persisted
1834
-
1835
- **Cause**: `flushMirror()` not called
1836
-
1837
- **Solution**:
1838
-
1447
+ **`snapshotPath` is required error**
1448
+ You set `mode: "fs"` without providing `snapshotPath`:
1839
1449
  ```typescript
1840
- await ssh.getVfs().flushMirror();
1450
+ new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" });
1841
1451
  ```
1842
1452
 
1843
1453
  ---
1844
1454
 
1845
1455
  ## Contributing
1846
1456
 
1847
- 1. Fork repository
1848
- 2. Create feature branch: `git checkout -b feat/my-feature`
1849
- 3. Make changes and add tests
1850
- 4. Format & lint: `bun format && bun check`
1851
- 5. Push and open PR
1457
+ 1. Fork the repository.
1458
+ 2. Create a feature branch: `git checkout -b feat/my-feature`
1459
+ 3. Make changes and add tests.
1460
+ 4. Format and lint: `bun format && bun check`
1461
+ 5. Push and open a PR.
1852
1462
 
1853
- **Code Quality**:
1854
- - Biome formatting (opinionated)
1855
- - Full TypeScript (no `any`)
1856
- - JSDoc comments on public API
1857
- - Async/await (no callbacks)
1463
+ **Code quality standards:**
1464
+ - Biome formatting (opinionated, enforced by CI)
1465
+ - Full TypeScript no `any`
1466
+ - JSDoc comments on all public API surface
1467
+ - Async/await throughout — no callbacks
1468
+ - Tests for new commands and VFS behavior
1858
1469
 
1859
1470
  ---
1860
1471
 
1861
1472
  ## Security
1862
1473
 
1863
- - Passwords are hashed with `scrypt` in the virtual auth store.
1864
- - Root account is always protected and cannot be deleted.
1865
- - Sudo privileges are explicit and persisted in sudoers data.
1866
- - Protect the root password in production by setting `SSH_MIMIC_ROOT_PASSWORD`; otherwise startup logs a generated ephemeral password.
1867
- - Disable `SSH_MIMIC_AUTO_SUDO_NEW_USERS` when you want newly created users to stay unprivileged by default.
1868
- - This project is not intended to provide kernel-level or process-level isolation.
1474
+ - Passwords are hashed with `scrypt` by default (N=32768, r=8, p=1), with a random per-user salt.
1475
+ - Root account always exists and cannot be deleted.
1476
+ - Sudo privileges are explicit and stored in the VFS under `/virtual-env-js/.auth/sudoers`.
1477
+ - Per-IP rate limiting prevents automated brute-force attacks on the SSH server.
1478
+ - This project does **not** provide kernel-level or process-level isolation.
1479
+ - Do **not** expose a running instance to the public internet without understanding the risks — the virtual shell allows arbitrary command execution within the virtual environment.
1869
1480
 
1870
- If you discover a vulnerability, avoid public disclosure in issues and contact maintainers privately first.
1481
+ If you discover a vulnerability, avoid public disclosure in GitHub Issues. Contact maintainers privately first — see `SECURITY.md`.
1871
1482
 
1872
1483
  ---
1873
1484
 
1874
1485
  ## Support
1875
1486
 
1876
1487
  - Open an issue for bugs, regressions, or feature requests.
1877
- - Include Node/Bun version, package version, and a minimal reproduction.
1878
- - For API questions, include the exact command sequence and expected vs actual result.
1488
+ - Include your Node.js/Bun version, package version, and a minimal reproduction.
1489
+ - For API questions, include the exact call sequence plus expected vs. actual behavior.
1879
1490
 
1880
1491
  ---
1881
1492
 
1882
1493
  ## License
1883
1494
 
1884
- MIT License. See LICENSE file for details.
1495
+ MIT see [LICENSE](./LICENSE).
1885
1496
 
1886
1497
  ---
1887
1498
 
1888
1499
  ## Roadmap
1889
1500
 
1890
1501
  - [x] Custom command plugin API
1891
- - [x] Optional per-user quotas for virtual filesystem usage
1892
- - [x] Improved shell compatibility for complex piping and redirection
1893
- - [ ] Snapshot diff tooling for test assertions
1502
+ - [x] Optional per-user storage quotas
1503
+ - [x] Improved shell compatibility (pipelines, redirections)
1504
+ - [x] Pure in-memory VFS with snapshot import/export
1505
+ - [x] Symlinks (`ln -s`, `isSymlink`, `resolveSymlink`)
1506
+ - [x] SSH public-key authentication
1507
+ - [x] Per-IP rate limiting and lockout
1508
+ - [x] New commands: `cp`, `mv`, `ln`, `find`, `wc`, `head`, `tail`, `chmod`
1894
1509
  - [x] Structured event hooks (session open/close, file write, sudo challenge)
1895
- - [ ] WebSocket-based remote shell client (experimental)
1510
+ - [ ] Snapshot diff tooling for test assertions
1511
+ - [ ] WebSocket-based remote shell client (experimental)
1512
+ - [ ] Shell scripting: `if`/`for`/`while` constructs
1513
+
1514
+ ---
1515
+
1516
+ ## Changelog
1517
+
1518
+ See [CHANGELOG.md](./CHANGELOG.md).