typescript-virtual-container 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +462 -44
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +35 -21
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts +202 -0
  14. package/dist/VirtualPackageManager/index.d.ts.map +1 -0
  15. package/dist/VirtualPackageManager/index.js +825 -0
  16. package/dist/VirtualShell/index.d.ts +93 -12
  17. package/dist/VirtualShell/index.d.ts.map +1 -1
  18. package/dist/VirtualShell/index.js +95 -13
  19. package/dist/VirtualShell/shell.d.ts.map +1 -1
  20. package/dist/VirtualShell/shell.js +3 -1
  21. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  22. package/dist/VirtualUserManager/index.d.ts +52 -20
  23. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  24. package/dist/VirtualUserManager/index.js +54 -20
  25. package/dist/commands/adduser.d.ts +6 -0
  26. package/dist/commands/adduser.d.ts.map +1 -1
  27. package/dist/commands/adduser.js +6 -0
  28. package/dist/commands/alias.d.ts +9 -0
  29. package/dist/commands/alias.d.ts.map +1 -0
  30. package/dist/commands/alias.js +63 -0
  31. package/dist/commands/apt.d.ts +9 -0
  32. package/dist/commands/apt.d.ts.map +1 -0
  33. package/dist/commands/apt.js +205 -0
  34. package/dist/commands/awk.d.ts +11 -0
  35. package/dist/commands/awk.d.ts.map +1 -1
  36. package/dist/commands/awk.js +15 -2
  37. package/dist/commands/base64.d.ts +5 -0
  38. package/dist/commands/base64.d.ts.map +1 -1
  39. package/dist/commands/base64.js +9 -1
  40. package/dist/commands/cat.d.ts +5 -0
  41. package/dist/commands/cat.d.ts.map +1 -1
  42. package/dist/commands/cat.js +35 -8
  43. package/dist/commands/cd.d.ts +5 -0
  44. package/dist/commands/cd.d.ts.map +1 -1
  45. package/dist/commands/cd.js +5 -0
  46. package/dist/commands/chmod.d.ts +5 -0
  47. package/dist/commands/chmod.d.ts.map +1 -1
  48. package/dist/commands/chmod.js +57 -3
  49. package/dist/commands/command-helpers.d.ts +78 -4
  50. package/dist/commands/command-helpers.d.ts.map +1 -1
  51. package/dist/commands/command-helpers.js +78 -4
  52. package/dist/commands/cp.d.ts +5 -0
  53. package/dist/commands/cp.d.ts.map +1 -1
  54. package/dist/commands/cp.js +5 -0
  55. package/dist/commands/curl.d.ts +5 -0
  56. package/dist/commands/curl.d.ts.map +1 -1
  57. package/dist/commands/curl.js +106 -26
  58. package/dist/commands/cut.d.ts +5 -0
  59. package/dist/commands/cut.d.ts.map +1 -1
  60. package/dist/commands/cut.js +8 -1
  61. package/dist/commands/date.d.ts +5 -0
  62. package/dist/commands/date.d.ts.map +1 -1
  63. package/dist/commands/date.js +7 -1
  64. package/dist/commands/declare.d.ts +3 -0
  65. package/dist/commands/declare.d.ts.map +1 -0
  66. package/dist/commands/declare.js +39 -0
  67. package/dist/commands/diff.d.ts +5 -0
  68. package/dist/commands/diff.d.ts.map +1 -1
  69. package/dist/commands/diff.js +5 -0
  70. package/dist/commands/dpkg.d.ts +9 -0
  71. package/dist/commands/dpkg.d.ts.map +1 -0
  72. package/dist/commands/dpkg.js +161 -0
  73. package/dist/commands/du.d.ts.map +1 -1
  74. package/dist/commands/du.js +8 -2
  75. package/dist/commands/echo.d.ts +5 -0
  76. package/dist/commands/echo.d.ts.map +1 -1
  77. package/dist/commands/echo.js +33 -12
  78. package/dist/commands/env.d.ts +5 -0
  79. package/dist/commands/env.d.ts.map +1 -1
  80. package/dist/commands/env.js +11 -1
  81. package/dist/commands/exit.d.ts +5 -0
  82. package/dist/commands/exit.d.ts.map +1 -1
  83. package/dist/commands/exit.js +12 -2
  84. package/dist/commands/export.d.ts.map +1 -1
  85. package/dist/commands/export.js +3 -1
  86. package/dist/commands/find.d.ts +5 -0
  87. package/dist/commands/find.d.ts.map +1 -1
  88. package/dist/commands/find.js +5 -0
  89. package/dist/commands/free.d.ts +8 -0
  90. package/dist/commands/free.d.ts.map +1 -0
  91. package/dist/commands/free.js +43 -0
  92. package/dist/commands/grep.d.ts +5 -0
  93. package/dist/commands/grep.d.ts.map +1 -1
  94. package/dist/commands/grep.js +12 -2
  95. package/dist/commands/gzip.d.ts +5 -0
  96. package/dist/commands/gzip.d.ts.map +1 -1
  97. package/dist/commands/gzip.js +18 -2
  98. package/dist/commands/head.d.ts +5 -0
  99. package/dist/commands/head.d.ts.map +1 -1
  100. package/dist/commands/head.js +5 -0
  101. package/dist/commands/help.d.ts.map +1 -1
  102. package/dist/commands/help.js +98 -45
  103. package/dist/commands/helpers.d.ts +3 -0
  104. package/dist/commands/helpers.d.ts.map +1 -1
  105. package/dist/commands/helpers.js +3 -0
  106. package/dist/commands/history.d.ts +8 -0
  107. package/dist/commands/history.d.ts.map +1 -0
  108. package/dist/commands/history.js +26 -0
  109. package/dist/commands/hostname.d.ts +5 -0
  110. package/dist/commands/hostname.d.ts.map +1 -1
  111. package/dist/commands/hostname.js +5 -0
  112. package/dist/commands/id.d.ts.map +1 -1
  113. package/dist/commands/id.js +4 -1
  114. package/dist/commands/index.d.ts +2 -10
  115. package/dist/commands/index.d.ts.map +1 -1
  116. package/dist/commands/index.js +2 -231
  117. package/dist/commands/ls.d.ts.map +1 -1
  118. package/dist/commands/ls.js +6 -3
  119. package/dist/commands/lsb-release.d.ts +3 -0
  120. package/dist/commands/lsb-release.d.ts.map +1 -0
  121. package/dist/commands/lsb-release.js +56 -0
  122. package/dist/commands/man.d.ts +3 -0
  123. package/dist/commands/man.d.ts.map +1 -0
  124. package/dist/commands/man.js +155 -0
  125. package/dist/commands/nano.js +1 -1
  126. package/dist/commands/neofetch.d.ts.map +1 -1
  127. package/dist/commands/neofetch.js +6 -1
  128. package/dist/commands/node.d.ts +9 -0
  129. package/dist/commands/node.d.ts.map +1 -0
  130. package/dist/commands/node.js +316 -0
  131. package/dist/commands/npm.d.ts +19 -0
  132. package/dist/commands/npm.d.ts.map +1 -0
  133. package/dist/commands/npm.js +109 -0
  134. package/dist/commands/ping.d.ts.map +1 -1
  135. package/dist/commands/ping.js +7 -2
  136. package/dist/commands/printf.d.ts +3 -0
  137. package/dist/commands/printf.d.ts.map +1 -0
  138. package/dist/commands/printf.js +113 -0
  139. package/dist/commands/ps.d.ts.map +1 -1
  140. package/dist/commands/ps.js +30 -6
  141. package/dist/commands/python.d.ts +30 -0
  142. package/dist/commands/python.d.ts.map +1 -0
  143. package/dist/commands/python.js +2058 -0
  144. package/dist/commands/read.d.ts +3 -0
  145. package/dist/commands/read.d.ts.map +1 -0
  146. package/dist/commands/read.js +34 -0
  147. package/dist/commands/registry.d.ts +8 -0
  148. package/dist/commands/registry.d.ts.map +1 -0
  149. package/dist/commands/registry.js +229 -0
  150. package/dist/commands/runtime.d.ts +6 -0
  151. package/dist/commands/runtime.d.ts.map +1 -0
  152. package/dist/commands/runtime.js +280 -0
  153. package/dist/commands/sed.d.ts.map +1 -1
  154. package/dist/commands/sed.js +11 -3
  155. package/dist/commands/set.d.ts.map +1 -1
  156. package/dist/commands/set.js +9 -3
  157. package/dist/commands/sh.d.ts.map +1 -1
  158. package/dist/commands/sh.js +69 -30
  159. package/dist/commands/shift.d.ts +5 -0
  160. package/dist/commands/shift.d.ts.map +1 -0
  161. package/dist/commands/shift.js +52 -0
  162. package/dist/commands/sleep.d.ts.map +1 -1
  163. package/dist/commands/sort.d.ts.map +1 -1
  164. package/dist/commands/sort.js +4 -2
  165. package/dist/commands/source.d.ts +3 -0
  166. package/dist/commands/source.d.ts.map +1 -0
  167. package/dist/commands/source.js +34 -0
  168. package/dist/commands/sudo.js +1 -1
  169. package/dist/commands/tar.d.ts.map +1 -1
  170. package/dist/commands/tar.js +11 -3
  171. package/dist/commands/tee.d.ts.map +1 -1
  172. package/dist/commands/tee.js +8 -6
  173. package/dist/commands/test.d.ts +3 -0
  174. package/dist/commands/test.d.ts.map +1 -0
  175. package/dist/commands/test.js +114 -0
  176. package/dist/commands/tr.d.ts.map +1 -1
  177. package/dist/commands/tr.js +3 -1
  178. package/dist/commands/true.d.ts +4 -0
  179. package/dist/commands/true.d.ts.map +1 -0
  180. package/dist/commands/true.js +14 -0
  181. package/dist/commands/type.d.ts +3 -0
  182. package/dist/commands/type.d.ts.map +1 -0
  183. package/dist/commands/type.js +34 -0
  184. package/dist/commands/uname.d.ts.map +1 -1
  185. package/dist/commands/uname.js +4 -1
  186. package/dist/commands/uniq.d.ts.map +1 -1
  187. package/dist/commands/uptime.d.ts +3 -0
  188. package/dist/commands/uptime.d.ts.map +1 -0
  189. package/dist/commands/uptime.js +43 -0
  190. package/dist/commands/wget.d.ts.map +1 -1
  191. package/dist/commands/wget.js +92 -96
  192. package/dist/commands/which.d.ts +3 -0
  193. package/dist/commands/which.d.ts.map +1 -0
  194. package/dist/commands/which.js +32 -0
  195. package/dist/commands/xargs.d.ts.map +1 -1
  196. package/dist/commands/xargs.js +1 -1
  197. package/dist/index.d.ts +15 -11
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js +9 -8
  200. package/dist/modules/linuxRootfs.d.ts +41 -0
  201. package/dist/modules/linuxRootfs.d.ts.map +1 -0
  202. package/dist/modules/linuxRootfs.js +440 -0
  203. package/dist/modules/neofetch.d.ts.map +1 -1
  204. package/dist/modules/neofetch.js +1 -0
  205. package/dist/standalone-wo-sftp.d.ts +2 -0
  206. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  207. package/dist/standalone-wo-sftp.js +30 -0
  208. package/dist/utils/expand.d.ts +50 -0
  209. package/dist/utils/expand.d.ts.map +1 -0
  210. package/dist/utils/expand.js +183 -0
  211. package/dist/utils/vfsDiff.d.ts +90 -0
  212. package/dist/utils/vfsDiff.d.ts.map +1 -0
  213. package/dist/utils/vfsDiff.js +177 -0
  214. package/package.json +3 -1
  215. package/src/SSHMimic/exec.ts +10 -1
  216. package/src/SSHMimic/executor.ts +105 -21
  217. package/src/SSHMimic/index.ts +49 -15
  218. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  219. package/src/VirtualFileSystem/index.ts +78 -28
  220. package/src/VirtualPackageManager/index.ts +979 -0
  221. package/src/VirtualShell/index.ts +133 -14
  222. package/src/VirtualShell/shell.ts +23 -3
  223. package/src/VirtualShell/shellParser.ts +134 -36
  224. package/src/VirtualUserManager/index.ts +62 -22
  225. package/src/commands/adduser.ts +6 -0
  226. package/src/commands/alias.ts +64 -0
  227. package/src/commands/apt.ts +228 -0
  228. package/src/commands/awk.ts +20 -6
  229. package/src/commands/base64.ts +13 -2
  230. package/src/commands/cat.ts +40 -8
  231. package/src/commands/cd.ts +5 -0
  232. package/src/commands/chmod.ts +53 -3
  233. package/src/commands/command-helpers.ts +78 -4
  234. package/src/commands/cp.ts +5 -0
  235. package/src/commands/curl.ts +118 -33
  236. package/src/commands/cut.ts +8 -1
  237. package/src/commands/date.ts +7 -1
  238. package/src/commands/declare.ts +44 -0
  239. package/src/commands/diff.ts +17 -3
  240. package/src/commands/dpkg.ts +180 -0
  241. package/src/commands/du.ts +17 -5
  242. package/src/commands/echo.ts +41 -12
  243. package/src/commands/env.ts +11 -1
  244. package/src/commands/exit.ts +12 -2
  245. package/src/commands/export.ts +3 -1
  246. package/src/commands/find.ts +5 -0
  247. package/src/commands/free.ts +47 -0
  248. package/src/commands/grep.ts +12 -2
  249. package/src/commands/gzip.ts +28 -4
  250. package/src/commands/head.ts +5 -0
  251. package/src/commands/help.ts +121 -47
  252. package/src/commands/helpers.ts +8 -0
  253. package/src/commands/history.ts +34 -0
  254. package/src/commands/hostname.ts +5 -0
  255. package/src/commands/id.ts +4 -1
  256. package/src/commands/index.ts +9 -255
  257. package/src/commands/ls.ts +6 -3
  258. package/src/commands/lsb-release.ts +58 -0
  259. package/src/commands/man.ts +166 -0
  260. package/src/commands/nano.ts +1 -1
  261. package/src/commands/neofetch.ts +6 -1
  262. package/src/commands/node.ts +341 -0
  263. package/src/commands/npm.ts +132 -0
  264. package/src/commands/ping.ts +10 -3
  265. package/src/commands/printf.ts +112 -0
  266. package/src/commands/ps.ts +40 -6
  267. package/src/commands/python.ts +2229 -0
  268. package/src/commands/read.ts +41 -0
  269. package/src/commands/registry.ts +244 -0
  270. package/src/commands/runtime.ts +353 -0
  271. package/src/commands/sed.ts +27 -9
  272. package/src/commands/set.ts +9 -3
  273. package/src/commands/sh.ts +170 -44
  274. package/src/commands/shift.ts +53 -0
  275. package/src/commands/sleep.ts +2 -1
  276. package/src/commands/sort.ts +10 -6
  277. package/src/commands/source.ts +47 -0
  278. package/src/commands/sudo.ts +1 -1
  279. package/src/commands/tar.ts +28 -7
  280. package/src/commands/tee.ts +7 -1
  281. package/src/commands/test.ts +135 -0
  282. package/src/commands/tr.ts +3 -1
  283. package/src/commands/true.ts +17 -0
  284. package/src/commands/type.ts +43 -0
  285. package/src/commands/uname.ts +5 -1
  286. package/src/commands/uniq.ts +8 -2
  287. package/src/commands/uptime.ts +49 -0
  288. package/src/commands/wget.ts +105 -119
  289. package/src/commands/which.ts +37 -0
  290. package/src/commands/xargs.ts +11 -2
  291. package/src/index.ts +27 -18
  292. package/src/modules/linuxRootfs.ts +642 -0
  293. package/src/modules/neofetch.ts +1 -0
  294. package/src/standalone-wo-sftp.ts +38 -0
  295. package/src/utils/expand.ts +238 -0
  296. package/src/utils/vfsDiff.ts +275 -0
  297. package/standalone-wo-sftp.js +507 -0
  298. package/standalone-wo-sftp.js.map +7 -0
  299. package/standalone.js +486 -109
  300. package/standalone.js.map +4 -4
  301. package/tests/bun-test-shim.ts +9 -1
  302. package/tests/command-helpers.test.ts +1 -5
  303. package/tests/new-features.test.ts +1036 -0
  304. package/tests/parser-executor.test.ts +27 -27
  305. package/tests/sftp.test.ts +122 -42
  306. package/tests/users.test.ts +23 -5
  307. package/CHANGELOG.md +0 -150
@@ -1,16 +1,42 @@
1
1
  import { EventEmitter } from "node:events";
2
- import { createCustomCommand, registerCommand, runCommand } from "../commands";
2
+ import { createCustomCommand, registerCommand } from "../commands/registry";
3
+ import { runCommand } from "../commands/runtime";
4
+ import {
5
+ bootstrapLinuxRootfs,
6
+ refreshProc,
7
+ syncEtcPasswd,
8
+ } from "../modules/linuxRootfs";
3
9
  import type { CommandContext, CommandResult } from "../types/commands";
4
10
  import type { ShellStream } from "../types/streams";
5
11
  import type { PerfLogger } from "../utils/perfLogger";
6
12
  import { createPerfLogger } from "../utils/perfLogger";
7
13
  import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
14
+ import { VirtualPackageManager } from "../VirtualPackageManager";
8
15
  import { VirtualUserManager } from "../VirtualUserManager";
9
16
  import { startShell } from "./shell";
10
17
 
18
+ /**
19
+ * Virtual machine identity strings surfaced by system-info commands
20
+ * (`uname`, `neofetch`, `lsb_release`, `/proc/version`, `/etc/os-release`).
21
+ *
22
+ * Pass this as the second argument to `new VirtualShell()` to customise the
23
+ * distro name, kernel version, and CPU architecture reported inside the shell.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const shell = new VirtualShell("my-vm", {
28
+ * kernel: "6.1.0+custom-amd64",
29
+ * os: "Acme GNU/Linux x64",
30
+ * arch: "x86_64",
31
+ * });
32
+ * ```
33
+ */
11
34
  export interface ShellProperties {
35
+ /** Kernel version string (e.g. `"1.0.0+itsrealfortune+1-amd64"`). */
12
36
  kernel: string;
37
+ /** Full OS description (e.g. `"Fortune GNU/Linux x64"`). */
13
38
  os: string;
39
+ /** CPU architecture label (e.g. `"x86_64"`, `"aarch64"`). */
14
40
  arch: string;
15
41
  }
16
42
 
@@ -32,16 +58,40 @@ function resolveAutoSudoForNewUsers(): boolean {
32
58
  }
33
59
 
34
60
  /**
35
- * Coordinates the virtual filesystem, user manager, and command runtime.
61
+ * Coordinates the virtual filesystem, user manager, package manager, and
62
+ * command runtime for a single isolated shell environment.
36
63
  *
37
- * Instances are used both by the SSH server facade and by the programmatic
38
- * client API.
64
+ * Each instance owns its own VFS tree, user database, package registry, and
65
+ * session state — multiple instances are fully independent.
66
+ *
67
+ * Instances are consumed both by the SSH/SFTP server facades and directly via
68
+ * the programmatic `SshClient` API.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const shell = new VirtualShell("my-vm");
73
+ * await shell.ensureInitialized();
74
+ * const client = new SshClient(shell, "root");
75
+ * const result = await client.exec("uname -a");
76
+ * ```
77
+ *
78
+ * @fires VirtualShell#initialized Emitted once the VFS and users are ready.
79
+ * @fires VirtualShell#command Emitted after every command execution.
80
+ * @fires VirtualShell#session:start Emitted when an interactive session opens.
39
81
  */
40
82
  class VirtualShell extends EventEmitter {
83
+ /** Backing virtual filesystem — use for direct path operations. */
41
84
  vfs: VirtualFileSystem;
85
+ /** Virtual user database — use for auth, quotas, and session tracking. */
42
86
  users: VirtualUserManager;
87
+ /** APT/dpkg package manager backed by the built-in package registry. */
88
+ packageManager: VirtualPackageManager;
89
+ /** Hostname shown in the shell prompt and SSH ident string. */
43
90
  hostname: string;
91
+ /** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
44
92
  properties: ShellProperties;
93
+ /** Unix ms timestamp of shell creation — used by `uptime` and `/proc/uptime`. */
94
+ startTime: number;
45
95
  private initialized: Promise<void>;
46
96
 
47
97
  /**
@@ -60,17 +110,27 @@ class VirtualShell extends EventEmitter {
60
110
  perf.mark("constructor");
61
111
  this.hostname = hostname;
62
112
  this.properties = properties || defaultShellProperties;
113
+ this.startTime = Date.now();
63
114
  this.vfs = new VirtualFileSystem(vfsOptions ?? {});
64
115
  this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
116
+ this.packageManager = new VirtualPackageManager(this.vfs, this.users);
65
117
 
66
118
  // Store references to avoid TypeScript "used before assigned" errors
67
119
  const vfs = this.vfs;
68
120
  const users = this.users;
121
+ const pm = this.packageManager;
122
+ const shellProps = this.properties;
123
+ const shellHostname = this.hostname;
124
+ const startTime = this.startTime;
69
125
 
70
126
  // Initialize both VFS mirror and users, ensuring all is ready before auth
71
127
  this.initialized = (async () => {
72
128
  await vfs.restoreMirror();
73
129
  await users.initialize();
130
+ // Bootstrap Linux rootfs (idempotent)
131
+ bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
132
+ // Load installed packages from dpkg status
133
+ pm.load();
74
134
  this.emit("initialized");
75
135
  })();
76
136
  }
@@ -105,11 +165,16 @@ class VirtualShell extends EventEmitter {
105
165
  }
106
166
 
107
167
  /**
108
- * Executes a command line string in the context of this shell instance.
168
+ * Executes a raw command line string programmatically.
169
+ *
170
+ * Supports the full shell operator set (`&&`, `||`, `;`, `|`, `>`, `<`,
171
+ * `$(cmd)`) and alias expansion. The result is emitted via the
172
+ * `"command"` event but not returned — use `SshClient.exec()` for a
173
+ * result-returning wrapper.
109
174
  *
110
- * @param rawInput
111
- * @param authUser
112
- * @param cwd
175
+ * @param rawInput Unparsed command line (e.g. `"ls -la /tmp"`).
176
+ * @param authUser Username to run the command as.
177
+ * @param cwd Current working directory for path resolution.
113
178
  */
114
179
  executeCommand(rawInput: string, authUser: string, cwd: string): void {
115
180
  perf.mark("executeCommand");
@@ -118,14 +183,19 @@ class VirtualShell extends EventEmitter {
118
183
  }
119
184
 
120
185
  /**
121
- * Starts an interactive session with the shell.
186
+ * Attaches an interactive PTY session to this shell instance.
122
187
  *
123
- * @param stream The stream for the interactive session.
124
- * @param authUser The authenticated user for the session.
125
- * @param sessionId The ID of the session.
126
- * @param remoteAddress The address of the remote client.
188
+ * Called internally by `SshMimic` when a client opens a shell channel.
189
+ * The session reads from `stream` (user keystrokes) and writes back ANSI
190
+ * output. History, `.bashrc` sourcing, and Ctrl+W/Ctrl+U line editing are
191
+ * handled automatically.
192
+ *
193
+ * @param stream Bidirectional SSH channel stream.
194
+ * @param authUser Authenticated username bound to this session.
195
+ * @param sessionId Stable session UUID (used for `who` output), or `null`.
196
+ * @param remoteAddress IP or hostname of the connecting client.
197
+ * @param terminalSize Initial terminal dimensions in columns and rows.
127
198
  */
128
-
129
199
  startInteractiveSession(
130
200
  stream: ShellStream,
131
201
  authUser: string,
@@ -146,6 +216,55 @@ class VirtualShell extends EventEmitter {
146
216
  terminalSize,
147
217
  this,
148
218
  );
219
+ // Refresh /proc/<pid> and /proc/self after session is registered
220
+ this.refreshProcSessions();
221
+ }
222
+
223
+ /**
224
+ * Refreshes the `/proc` virtual filesystem with current system state.
225
+ *
226
+ * Updates `/proc/uptime`, `/proc/meminfo`, `/proc/cpuinfo`,
227
+ * `/proc/version`, `/proc/loadavg`, `/proc/self`, and per-session
228
+ * `/proc/<pid>` entries from live session and host data.
229
+ *
230
+ * Called automatically during `bootstrapLinuxRootfs`. Call again before
231
+ * reading `/proc` files for up-to-date values.
232
+ */
233
+ public refreshProcFs(): void {
234
+ refreshProc(
235
+ this.vfs,
236
+ this.properties,
237
+ this.hostname,
238
+ this.startTime,
239
+ this.users.listActiveSessions(),
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Updates only the session-dependent `/proc` entries (`/proc/<pid>`,
245
+ * `/proc/self`). Cheaper than a full `refreshProcFs()` — call this
246
+ * whenever a session is registered or unregistered.
247
+ */
248
+ public refreshProcSessions(): void {
249
+ refreshProc(
250
+ this.vfs,
251
+ this.properties,
252
+ this.hostname,
253
+ this.startTime,
254
+ this.users.listActiveSessions(),
255
+ );
256
+ }
257
+
258
+ /**
259
+ * Syncs `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the current
260
+ * `VirtualUserManager` state.
261
+ *
262
+ * Called automatically during `bootstrapLinuxRootfs`. Call again after
263
+ * `users.addUser()`, `users.deleteUser()`, or `users.addSudoer()` to keep
264
+ * the classic Unix credential files in sync with the user manager.
265
+ */
266
+ public syncPasswd(): void {
267
+ syncEtcPasswd(this.vfs, this.users);
149
268
  }
150
269
 
151
270
  /**
@@ -73,9 +73,20 @@ export function startShell(
73
73
  for (const line of bashrc.split("\n")) {
74
74
  const l = line.trim();
75
75
  if (!l || l.startsWith("#")) continue;
76
- await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
76
+ await runCommand(
77
+ l,
78
+ authUser,
79
+ hostname,
80
+ "shell",
81
+ cwd,
82
+ shell,
83
+ undefined,
84
+ shellEnv,
85
+ );
77
86
  }
78
- } catch { /* ignore bashrc errors */ }
87
+ } catch {
88
+ /* ignore bashrc errors */
89
+ }
79
90
  }
80
91
  })();
81
92
 
@@ -585,7 +596,16 @@ export function startShell(
585
596
 
586
597
  if (line.length > 0) {
587
598
  const result = await Promise.resolve(
588
- runCommand(line, authUser, hostname, "shell", cwd, shell, undefined, shellEnv),
599
+ runCommand(
600
+ line,
601
+ authUser,
602
+ hostname,
603
+ "shell",
604
+ cwd,
605
+ shell,
606
+ undefined,
607
+ shellEnv,
608
+ ),
589
609
  );
590
610
 
591
611
  pushHistory(line);
@@ -1,4 +1,10 @@
1
- import type { Pipeline, PipelineCommand, Script, Statement, LogicalOp } from "../types/pipeline";
1
+ import type {
2
+ Pipeline,
3
+ PipelineCommand,
4
+ Script,
5
+ Statement,
6
+ LogicalOp,
7
+ } from "../types/pipeline";
2
8
 
3
9
  // ── Public API ───────────────────────────────────────────────────────────────
4
10
 
@@ -56,8 +62,9 @@ export function expandToken(
56
62
  token = token.replace(/\$#/g, "0");
57
63
 
58
64
  // ${VAR:-default} and ${VAR:+value}
59
- token = token.replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, name, def) =>
60
- env[name] ?? def,
65
+ token = token.replace(
66
+ /\$\{([^}:]+):-([^}]*)\}/g,
67
+ (_, name, def) => env[name] ?? def,
61
68
  );
62
69
  token = token.replace(/\$\{([^}:]+):\+([^}]*)\}/g, (_, name, val) =>
63
70
  env[name] ? val : "",
@@ -67,8 +74,9 @@ export function expandToken(
67
74
  token = token.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
68
75
 
69
76
  // $VAR (greedy: match longest valid identifier)
70
- token = token.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) =>
71
- env[name] ?? "",
77
+ token = token.replace(
78
+ /\$([A-Za-z_][A-Za-z0-9_]*)/g,
79
+ (_, name) => env[name] ?? "",
72
80
  );
73
81
 
74
82
  return token;
@@ -120,7 +128,10 @@ function parseStatements(input: string): Statement[] {
120
128
  return statements;
121
129
  }
122
130
 
123
- interface Segment { text: string; op?: LogicalOp }
131
+ interface Segment {
132
+ text: string;
133
+ op?: LogicalOp;
134
+ }
124
135
 
125
136
  function splitByLogicalOps(input: string): Segment[] {
126
137
  const segments: Segment[] = [];
@@ -139,19 +150,61 @@ function splitByLogicalOps(input: string): Segment[] {
139
150
  const ch = input[i]!;
140
151
  const ch2 = input.slice(i, i + 2);
141
152
 
142
- if ((ch === '"' || ch === "'") && !inQ) { inQ = true; qChar = ch; current += ch; i++; continue; }
143
- if (inQ && ch === qChar) { inQ = false; current += ch; i++; continue; }
144
- if (inQ) { current += ch; i++; continue; }
153
+ if ((ch === '"' || ch === "'") && !inQ) {
154
+ inQ = true;
155
+ qChar = ch;
156
+ current += ch;
157
+ i++;
158
+ continue;
159
+ }
160
+ if (inQ && ch === qChar) {
161
+ inQ = false;
162
+ current += ch;
163
+ i++;
164
+ continue;
165
+ }
166
+ if (inQ) {
167
+ current += ch;
168
+ i++;
169
+ continue;
170
+ }
145
171
 
146
- if (ch === "(") { depth++; current += ch; i++; continue; }
147
- if (ch === ")") { depth--; current += ch; i++; continue; }
148
- if (depth > 0) { current += ch; i++; continue; }
172
+ if (ch === "(") {
173
+ depth++;
174
+ current += ch;
175
+ i++;
176
+ continue;
177
+ }
178
+ if (ch === ")") {
179
+ depth--;
180
+ current += ch;
181
+ i++;
182
+ continue;
183
+ }
184
+ if (depth > 0) {
185
+ current += ch;
186
+ i++;
187
+ continue;
188
+ }
149
189
 
150
- if (ch2 === "&&") { flush("&&"); i += 2; continue; }
151
- if (ch2 === "||") { flush("||"); i += 2; continue; }
152
- if (ch === ";") { flush(";"); i++; continue; }
190
+ if (ch2 === "&&") {
191
+ flush("&&");
192
+ i += 2;
193
+ continue;
194
+ }
195
+ if (ch2 === "||") {
196
+ flush("||");
197
+ i += 2;
198
+ continue;
199
+ }
200
+ if (ch === ";") {
201
+ flush(";");
202
+ i++;
203
+ continue;
204
+ }
153
205
 
154
- current += ch; i++;
206
+ current += ch;
207
+ i++;
155
208
  }
156
209
  flush();
157
210
  return segments;
@@ -170,13 +223,26 @@ function splitByPipe(input: string): string[] {
170
223
 
171
224
  for (let i = 0; i < input.length; i++) {
172
225
  const ch = input[i]!;
173
- if ((ch === '"' || ch === "'") && !inQ) { inQ = true; qChar = ch; current += ch; continue; }
174
- if (inQ && ch === qChar) { inQ = false; current += ch; continue; }
175
- if (inQ) { current += ch; continue; }
226
+ if ((ch === '"' || ch === "'") && !inQ) {
227
+ inQ = true;
228
+ qChar = ch;
229
+ current += ch;
230
+ continue;
231
+ }
232
+ if (inQ && ch === qChar) {
233
+ inQ = false;
234
+ current += ch;
235
+ continue;
236
+ }
237
+ if (inQ) {
238
+ current += ch;
239
+ continue;
240
+ }
176
241
 
177
242
  // || was already consumed at statement level, bare | is pipe
178
243
  if (ch === "|" && input[i + 1] !== "|") {
179
- if (!current.trim()) throw new Error("Syntax error near unexpected token '|'");
244
+ if (!current.trim())
245
+ throw new Error("Syntax error near unexpected token '|'");
180
246
  tokens.push(current.trim());
181
247
  current = "";
182
248
  } else {
@@ -185,7 +251,8 @@ function splitByPipe(input: string): string[] {
185
251
  }
186
252
 
187
253
  const tail = current.trim();
188
- if (!tail && tokens.length > 0) throw new Error("Syntax error near unexpected token '|'");
254
+ if (!tail && tokens.length > 0)
255
+ throw new Error("Syntax error near unexpected token '|'");
189
256
  if (tail) tokens.push(tail);
190
257
  return tokens;
191
258
  }
@@ -204,19 +271,27 @@ function parseCommandWithRedirections(token: string): PipelineCommand {
204
271
  const part = parts[i]!;
205
272
  if (part === "<") {
206
273
  i++;
207
- if (i >= parts.length) throw new Error("Syntax error: expected filename after <");
274
+ if (i >= parts.length)
275
+ throw new Error("Syntax error: expected filename after <");
208
276
  inputFile = parts[i];
209
277
  i++;
210
278
  } else if (part === ">>") {
211
279
  i++;
212
- if (i >= parts.length) throw new Error("Syntax error: expected filename after >>");
213
- outputFile = parts[i]; appendOutput = true; i++;
280
+ if (i >= parts.length)
281
+ throw new Error("Syntax error: expected filename after >>");
282
+ outputFile = parts[i];
283
+ appendOutput = true;
284
+ i++;
214
285
  } else if (part === ">") {
215
286
  i++;
216
- if (i >= parts.length) throw new Error("Syntax error: expected filename after >");
217
- outputFile = parts[i]; appendOutput = false; i++;
287
+ if (i >= parts.length)
288
+ throw new Error("Syntax error: expected filename after >");
289
+ outputFile = parts[i];
290
+ appendOutput = false;
291
+ i++;
218
292
  } else {
219
- cmdParts.push(part); i++;
293
+ cmdParts.push(part);
294
+ i++;
220
295
  }
221
296
  }
222
297
 
@@ -236,26 +311,49 @@ function tokenizeCommand(input: string): string[] {
236
311
  const next = input[i + 1];
237
312
 
238
313
  if ((ch === '"' || ch === "'") && !inQ) {
239
- inQ = true; qChar = ch; i++; continue;
314
+ inQ = true;
315
+ qChar = ch;
316
+ i++;
317
+ continue;
240
318
  }
241
319
  if (inQ && ch === qChar) {
242
- inQ = false; qChar = ""; i++; continue;
320
+ inQ = false;
321
+ qChar = "";
322
+ i++;
323
+ continue;
324
+ }
325
+ if (inQ) {
326
+ current += ch;
327
+ i++;
328
+ continue;
243
329
  }
244
- if (inQ) { current += ch; i++; continue; }
245
330
 
246
331
  if (ch === " ") {
247
- if (current) { tokens.push(current); current = ""; }
248
- i++; continue;
332
+ if (current) {
333
+ tokens.push(current);
334
+ current = "";
335
+ }
336
+ i++;
337
+ continue;
249
338
  }
250
339
 
251
340
  if ((ch === ">" || ch === "<") && !inQ) {
252
- if (current) { tokens.push(current); current = ""; }
253
- if (ch === ">" && next === ">") { tokens.push(">>"); i += 2; }
254
- else { tokens.push(ch); i++; }
341
+ if (current) {
342
+ tokens.push(current);
343
+ current = "";
344
+ }
345
+ if (ch === ">" && next === ">") {
346
+ tokens.push(">>");
347
+ i += 2;
348
+ } else {
349
+ tokens.push(ch);
350
+ i++;
351
+ }
255
352
  continue;
256
353
  }
257
354
 
258
- current += ch; i++;
355
+ current += ch;
356
+ i++;
259
357
  }
260
358
  if (current) tokens.push(current);
261
359
  return tokens;
@@ -290,10 +290,11 @@ export class VirtualUserManager extends EventEmitter {
290
290
  }
291
291
 
292
292
  /**
293
- * Updates password for an existing user account.
293
+ * Updates the password for an existing user account.
294
294
  *
295
295
  * @param username Username to update.
296
- * @param password New plaintext password.
296
+ * @param password New plaintext password (must be non-empty).
297
+ * @throws When the user does not exist or the password is empty.
297
298
  */
298
299
  public async setPassword(username: string, password: string): Promise<void> {
299
300
  perf.mark("setPassword");
@@ -309,9 +310,10 @@ export class VirtualUserManager extends EventEmitter {
309
310
  }
310
311
 
311
312
  /**
312
- * Deletes existing non-root user account.
313
+ * Deletes an existing non-root user account and revokes sudo access.
313
314
  *
314
315
  * @param username Username to remove.
316
+ * @throws When `username` is `"root"` or the user does not exist.
315
317
  */
316
318
  public async deleteUser(username: string): Promise<void> {
317
319
  perf.mark("deleteUser");
@@ -343,9 +345,10 @@ export class VirtualUserManager extends EventEmitter {
343
345
  }
344
346
 
345
347
  /**
346
- * Grants sudo access to existing user.
348
+ * Grants sudo privileges to an existing user.
347
349
  *
348
350
  * @param username Username to promote.
351
+ * @throws When the user does not exist.
349
352
  */
350
353
  public async addSudoer(username: string): Promise<void> {
351
354
  perf.mark("addSudoer");
@@ -359,9 +362,10 @@ export class VirtualUserManager extends EventEmitter {
359
362
  }
360
363
 
361
364
  /**
362
- * Revokes sudo access from user.
365
+ * Revokes sudo privileges from a user. Root cannot be demoted.
363
366
  *
364
367
  * @param username Username to demote.
368
+ * @throws When `username` is `"root"`.
365
369
  */
366
370
  public async removeSudoer(username: string): Promise<void> {
367
371
  perf.mark("removeSudoer");
@@ -375,11 +379,14 @@ export class VirtualUserManager extends EventEmitter {
375
379
  }
376
380
 
377
381
  /**
378
- * Registers active session and allocates tty id.
382
+ * Registers a new active session and allocates a virtual TTY identifier.
379
383
  *
380
- * @param username Session username.
381
- * @param remoteAddress Session source address.
382
- * @returns Registered session descriptor.
384
+ * Called by the SSH server when a client is authenticated. The returned
385
+ * descriptor is visible in `who` output and `listActiveSessions()`.
386
+ *
387
+ * @param username Authenticated username bound to the session.
388
+ * @param remoteAddress IP address or hostname of the connecting client.
389
+ * @returns The newly created `VirtualActiveSession` descriptor.
383
390
  */
384
391
  public registerSession(
385
392
  username: string,
@@ -403,9 +410,11 @@ export class VirtualUserManager extends EventEmitter {
403
410
  }
404
411
 
405
412
  /**
406
- * Unregisters active session when connection closes.
413
+ * Removes an active session record when the connection closes.
414
+ *
415
+ * Safe to call with a `null` or `undefined` session ID — it will be a no-op.
407
416
  *
408
- * @param sessionId Session identifier; ignored when nullish.
417
+ * @param sessionId Session UUID returned by `registerSession()`, or nullish.
409
418
  */
410
419
  public unregisterSession(sessionId: string | null | undefined): void {
411
420
  perf.mark("unregisterSession");
@@ -425,11 +434,15 @@ export class VirtualUserManager extends EventEmitter {
425
434
  }
426
435
 
427
436
  /**
428
- * Updates username/address metadata for existing session.
437
+ * Updates the username and remote address metadata for an active session.
429
438
  *
430
- * @param sessionId Session identifier; ignored when nullish.
431
- * @param username New username value.
432
- * @param remoteAddress New remote address value.
439
+ * Called internally by `su` and `sudo` when the effective user changes
440
+ * within a session. Silently ignored when the session ID is nullish or
441
+ * unknown.
442
+ *
443
+ * @param sessionId Session UUID to update, or nullish for no-op.
444
+ * @param username New effective username.
445
+ * @param remoteAddress New remote address (usually unchanged).
433
446
  */
434
447
  public updateSession(
435
448
  sessionId: string | null | undefined,
@@ -454,9 +467,11 @@ export class VirtualUserManager extends EventEmitter {
454
467
  }
455
468
 
456
469
  /**
457
- * Lists active sessions sorted by start time.
470
+ * Returns a snapshot of all currently active sessions, sorted by start time.
471
+ *
472
+ * Used by `who`, `ps`, `uptime`, and the `HoneyPot` auditor.
458
473
  *
459
- * @returns Snapshot of active session descriptors.
474
+ * @returns Array of `VirtualActiveSession` descriptors.
460
475
  */
461
476
  public listActiveSessions(): VirtualActiveSession[] {
462
477
  perf.mark("listActiveSessions");
@@ -465,6 +480,15 @@ export class VirtualUserManager extends EventEmitter {
465
480
  );
466
481
  }
467
482
 
483
+ /**
484
+ * Returns a sorted list of all registered usernames.
485
+ *
486
+ * @returns Array of username strings sorted alphabetically.
487
+ */
488
+ public listUsers(): string[] {
489
+ return Array.from(this.users.keys()).sort();
490
+ }
491
+
468
492
  private loadFromVfs(): void {
469
493
  this.users.clear();
470
494
 
@@ -610,6 +634,14 @@ export class VirtualUserManager extends EventEmitter {
610
634
  return record;
611
635
  }
612
636
 
637
+ /**
638
+ * Returns `true` when the user has a non-empty password set.
639
+ *
640
+ * A user with no password (or whose password hash matches the empty-string
641
+ * hash) is allowed to authenticate without a credential check.
642
+ *
643
+ * @param username Target username.
644
+ */
613
645
  public hasPassword(username: string): boolean {
614
646
  perf.mark("hasPassword");
615
647
  if (this.getPasswordHash(username) === this.hashPassword("")) {
@@ -620,10 +652,13 @@ export class VirtualUserManager extends EventEmitter {
620
652
  }
621
653
 
622
654
  /**
623
- * Hashes plaintext password with per-user salt using scrypt.
655
+ * Hashes a plaintext password using scrypt (or SHA-256 in fast-hash mode).
624
656
  *
625
- * @param password Plaintext password.
626
- * @returns Hex-encoded password hash.
657
+ * Set `SSH_MIMIC_FAST_PASSWORD_HASH=1` to switch to SHA-256 for test
658
+ * environments where scrypt latency is undesirable.
659
+ *
660
+ * @param password Plaintext password string.
661
+ * @returns Hex-encoded hash string.
627
662
  */
628
663
  public hashPassword(password: string): string {
629
664
  if (VirtualUserManager.fastPasswordHash) {
@@ -648,7 +683,10 @@ export class VirtualUserManager extends EventEmitter {
648
683
  throw new Error("invalid password");
649
684
  }
650
685
  }
651
- private readonly authorizedKeys = new Map<string, Array<{ algo: string; data: Buffer }>>();
686
+ private readonly authorizedKeys = new Map<
687
+ string,
688
+ Array<{ algo: string; data: Buffer }>
689
+ >();
652
690
 
653
691
  /**
654
692
  * Adds an SSH public key for a user, enabling public-key authentication.
@@ -681,7 +719,9 @@ export class VirtualUserManager extends EventEmitter {
681
719
  *
682
720
  * @param username Target user.
683
721
  */
684
- public getAuthorizedKeys(username: string): Array<{ algo: string; data: Buffer }> {
722
+ public getAuthorizedKeys(
723
+ username: string,
724
+ ): Array<{ algo: string; data: Buffer }> {
685
725
  return this.authorizedKeys.get(username) ?? [];
686
726
  }
687
727
  }