typescript-virtual-container 1.2.4 → 1.2.6

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