typescript-virtual-container 1.5.11 → 1.6.1

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 (132) hide show
  1. package/README.md +236 -456
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/Honeypot/index.d.ts +9 -0
  4. package/dist/Honeypot/index.js +57 -0
  5. package/dist/SSHMimic/exec.d.ts +4 -0
  6. package/dist/SSHMimic/exec.js +4 -0
  7. package/dist/SSHMimic/executor.d.ts +10 -1
  8. package/dist/SSHMimic/executor.js +18 -8
  9. package/dist/SSHMimic/hostKey.d.ts +5 -0
  10. package/dist/SSHMimic/hostKey.js +5 -0
  11. package/dist/SSHMimic/loginBanner.d.ts +7 -0
  12. package/dist/SSHMimic/loginBanner.js +4 -0
  13. package/dist/SSHMimic/loginFormat.d.ts +4 -0
  14. package/dist/SSHMimic/loginFormat.js +4 -0
  15. package/dist/SSHMimic/prompt.d.ts +9 -0
  16. package/dist/SSHMimic/prompt.js +9 -0
  17. package/dist/SSHMimic/scp.d.ts +18 -0
  18. package/dist/SSHMimic/scp.js +14 -0
  19. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
  20. package/dist/VirtualFileSystem/binaryPack.js +32 -10
  21. package/dist/VirtualFileSystem/index.d.ts +29 -0
  22. package/dist/VirtualFileSystem/index.js +126 -5
  23. package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
  24. package/dist/VirtualFileSystem/journal.d.ts +10 -4
  25. package/dist/VirtualFileSystem/journal.js +12 -2
  26. package/dist/VirtualFileSystem/path.d.ts +23 -1
  27. package/dist/VirtualFileSystem/path.js +23 -3
  28. package/dist/VirtualPackageManager/index.js +1 -1
  29. package/dist/VirtualShell/index.d.ts +3 -0
  30. package/dist/VirtualShell/index.js +12 -3
  31. package/dist/VirtualUserManager/index.d.ts +20 -1
  32. package/dist/VirtualUserManager/index.js +52 -15
  33. package/dist/commands/bc.d.ts +5 -0
  34. package/dist/commands/bc.js +5 -0
  35. package/dist/commands/cat.js +2 -2
  36. package/dist/commands/chgrp.d.ts +7 -0
  37. package/dist/commands/chgrp.js +42 -0
  38. package/dist/commands/chown.d.ts +7 -0
  39. package/dist/commands/chown.js +79 -0
  40. package/dist/commands/cp.js +4 -3
  41. package/dist/commands/dd.d.ts +7 -0
  42. package/dist/commands/dd.js +60 -0
  43. package/dist/commands/declare.js +0 -2
  44. package/dist/commands/expr.d.ts +7 -0
  45. package/dist/commands/expr.js +63 -0
  46. package/dist/commands/fun.d.ts +5 -0
  47. package/dist/commands/fun.js +5 -1
  48. package/dist/commands/help.d.ts +5 -0
  49. package/dist/commands/help.js +5 -0
  50. package/dist/commands/helpers.d.ts +43 -0
  51. package/dist/commands/helpers.js +61 -0
  52. package/dist/commands/id.d.ts +5 -0
  53. package/dist/commands/id.js +5 -0
  54. package/dist/commands/ip.d.ts +1 -0
  55. package/dist/commands/ip.js +50 -23
  56. package/dist/commands/jobs.js +43 -9
  57. package/dist/commands/kill.d.ts +1 -0
  58. package/dist/commands/kill.js +13 -5
  59. package/dist/commands/last.js +1 -1
  60. package/dist/commands/ln.d.ts +5 -0
  61. package/dist/commands/ln.js +5 -0
  62. package/dist/commands/ls.d.ts +5 -0
  63. package/dist/commands/ls.js +19 -4
  64. package/dist/commands/lsb-release.js +1 -1
  65. package/dist/commands/man.d.ts +5 -0
  66. package/dist/commands/man.js +5 -0
  67. package/dist/commands/manuals-bundle.js +242 -0
  68. package/dist/commands/miscutils.d.ts +43 -0
  69. package/dist/commands/miscutils.js +233 -0
  70. package/dist/commands/mkdir.js +3 -2
  71. package/dist/commands/mv.js +4 -3
  72. package/dist/commands/netcat.d.ts +7 -0
  73. package/dist/commands/netcat.js +64 -0
  74. package/dist/commands/nice.d.ts +7 -0
  75. package/dist/commands/nice.js +22 -0
  76. package/dist/commands/nohup.d.ts +7 -0
  77. package/dist/commands/nohup.js +18 -0
  78. package/dist/commands/ping.d.ts +2 -1
  79. package/dist/commands/ping.js +46 -8
  80. package/dist/commands/procUtils.d.ts +13 -0
  81. package/dist/commands/procUtils.js +72 -0
  82. package/dist/commands/pwd.d.ts +5 -0
  83. package/dist/commands/pwd.js +5 -0
  84. package/dist/commands/python.js +0 -4
  85. package/dist/commands/read.js +0 -1
  86. package/dist/commands/registry.d.ts +37 -0
  87. package/dist/commands/registry.js +73 -0
  88. package/dist/commands/rm.js +3 -2
  89. package/dist/commands/runtime.d.ts +47 -1
  90. package/dist/commands/runtime.js +60 -5
  91. package/dist/commands/sh.d.ts +5 -0
  92. package/dist/commands/sh.js +5 -0
  93. package/dist/commands/stat.js +3 -2
  94. package/dist/commands/strace.js +0 -1
  95. package/dist/commands/sysinfo.d.ts +19 -0
  96. package/dist/commands/sysinfo.js +73 -0
  97. package/dist/commands/test.d.ts +5 -0
  98. package/dist/commands/test.js +5 -0
  99. package/dist/commands/textutils.d.ts +25 -0
  100. package/dist/commands/textutils.js +171 -0
  101. package/dist/commands/top.d.ts +7 -0
  102. package/dist/commands/top.js +54 -0
  103. package/dist/commands/touch.js +6 -2
  104. package/dist/commands/tr.d.ts +5 -0
  105. package/dist/commands/tr.js +5 -0
  106. package/dist/commands/w.js +1 -1
  107. package/dist/commands/which.d.ts +5 -0
  108. package/dist/commands/which.js +5 -0
  109. package/dist/index.d.ts +10 -2
  110. package/dist/index.js +4 -0
  111. package/dist/modules/VirtualNetworkManager.d.ts +123 -0
  112. package/dist/modules/VirtualNetworkManager.js +201 -0
  113. package/dist/modules/linuxRootfs.d.ts +4 -3
  114. package/dist/modules/linuxRootfs.js +115 -74
  115. package/dist/modules/neofetch.d.ts +2 -0
  116. package/dist/modules/neofetch.js +3 -2
  117. package/dist/modules/pacmanGame.d.ts +2 -0
  118. package/dist/modules/pacmanGame.js +1 -0
  119. package/dist/modules/shellInteractive.d.ts +2 -0
  120. package/dist/modules/shellInteractive.js +2 -0
  121. package/dist/modules/shellRuntime.d.ts +7 -0
  122. package/dist/modules/shellRuntime.js +6 -0
  123. package/dist/modules/webTermRenderer.js +0 -7
  124. package/dist/types/commands.d.ts +1 -1
  125. package/dist/types/vfs.d.ts +8 -0
  126. package/dist/utils/argv.d.ts +22 -3
  127. package/dist/utils/argv.js +22 -3
  128. package/dist/utils/perfLogger.d.ts +10 -2
  129. package/dist/utils/perfLogger.js +7 -14
  130. package/dist/utils/shellSession.d.ts +35 -0
  131. package/dist/utils/shellSession.js +35 -0
  132. package/package.json +1 -1
@@ -32,6 +32,15 @@ export class HoneyPot {
32
32
  clientDisconnects: 0,
33
33
  shellFreezes: 0,
34
34
  shellThaws: 0,
35
+ keysAdded: 0,
36
+ keysRemoved: 0,
37
+ snapshotsRestored: 0,
38
+ snapshotsImported: 0,
39
+ mounts: 0,
40
+ unmounts: 0,
41
+ symlinksCreated: 0,
42
+ nodesRemoved: 0,
43
+ authLockouts: 0,
35
44
  };
36
45
  maxLogSize;
37
46
  /** Reference kept so VFS events can ping the shell's idle manager. */
@@ -114,6 +123,33 @@ export class HoneyPot {
114
123
  vfs.on("mirror:flush", () => {
115
124
  this.log("VirtualFileSystem", "mirror:flush", {});
116
125
  });
126
+ vfs.on("snapshot:restore", (data) => {
127
+ this.stats.snapshotsRestored++;
128
+ this.log("VirtualFileSystem", "snapshot:restore", data);
129
+ });
130
+ vfs.on("snapshot:import", (data) => {
131
+ this.stats.snapshotsImported++;
132
+ this.log("VirtualFileSystem", "snapshot:import", data);
133
+ });
134
+ vfs.on("mount", (data) => {
135
+ this.stats.mounts++;
136
+ this._shell?.pingIdle();
137
+ this.log("VirtualFileSystem", "mount", data);
138
+ });
139
+ vfs.on("unmount", (data) => {
140
+ this.stats.unmounts++;
141
+ this.log("VirtualFileSystem", "unmount", data);
142
+ });
143
+ vfs.on("symlink:create", (data) => {
144
+ this.stats.symlinksCreated++;
145
+ this._shell?.pingIdle();
146
+ this.log("VirtualFileSystem", "symlink:create", data);
147
+ });
148
+ vfs.on("node:remove", (data) => {
149
+ this.stats.nodesRemoved++;
150
+ this._shell?.pingIdle();
151
+ this.log("VirtualFileSystem", "node:remove", data);
152
+ });
117
153
  }
118
154
  /**
119
155
  * Attaches to VirtualUserManager events.
@@ -137,6 +173,14 @@ export class HoneyPot {
137
173
  this.stats.sessionEnds++;
138
174
  this.log("VirtualUserManager", "session:unregister", data);
139
175
  });
176
+ users.on("key:add", (data) => {
177
+ this.stats.keysAdded++;
178
+ this.log("VirtualUserManager", "key:add", data);
179
+ });
180
+ users.on("key:remove", (data) => {
181
+ this.stats.keysRemoved++;
182
+ this.log("VirtualUserManager", "key:remove", data);
183
+ });
140
184
  }
141
185
  /**
142
186
  * Attaches to SshMimic events.
@@ -158,6 +202,10 @@ export class HoneyPot {
158
202
  this.stats.authFailures++;
159
203
  this.log("SshMimic", "auth:failure", data);
160
204
  });
205
+ ssh.on("auth:lockout", (data) => {
206
+ this.stats.authLockouts++;
207
+ this.log("SshMimic", "auth:lockout", data);
208
+ });
161
209
  ssh.on("client:connect", () => {
162
210
  this.stats.clientConnects++;
163
211
  this.log("SshMimic", "client:connect", {});
@@ -259,6 +307,15 @@ export class HoneyPot {
259
307
  clientDisconnects: 0,
260
308
  shellFreezes: 0,
261
309
  shellThaws: 0,
310
+ keysAdded: 0,
311
+ keysRemoved: 0,
312
+ snapshotsRestored: 0,
313
+ snapshotsImported: 0,
314
+ mounts: 0,
315
+ unmounts: 0,
316
+ symlinksCreated: 0,
317
+ nodesRemoved: 0,
318
+ authLockouts: 0,
262
319
  };
263
320
  }
264
321
  /**
@@ -1,3 +1,7 @@
1
1
  import type { ExecStream } from "../types/streams";
2
2
  import type { VirtualShell } from "../VirtualShell";
3
+ /**
4
+ * Handles SSH exec channel requests. Runs the given command in a non-interactive
5
+ * shell session and writes stdout/stderr to the stream, then signals exit.
6
+ */
3
7
  export declare function runExec(stream: ExecStream, cmd: string, authUser: string, hostname: string, shell: VirtualShell): void;
@@ -5,6 +5,10 @@ function toTtyLines(text) {
5
5
  .replace(/\r/g, "\n")
6
6
  .replace(/\n/g, "\r\n");
7
7
  }
8
+ /**
9
+ * Handles SSH exec channel requests. Runs the given command in a non-interactive
10
+ * shell session and writes stdout/stderr to the stream, then signals exit.
11
+ */
8
12
  export function runExec(stream, cmd, authUser, hostname, shell) {
9
13
  Promise.resolve(runCommand(cmd, authUser, hostname, "exec", userHome(authUser), shell, undefined, makeDefaultEnv(authUser, hostname)))
10
14
  .then((result) => {
@@ -1,5 +1,14 @@
1
1
  import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
2
2
  import type { Pipeline, Statement } from "../types/pipeline";
3
3
  import type { VirtualShell } from "../VirtualShell";
4
+ /**
5
+ * Executes a list of shell statements sequentially, respecting `&&`, `||`, and `;`
6
+ * operators. Accumulates stdout across statements and tracks cwd changes.
7
+ */
4
8
  export declare function executeStatements(statements: Statement[], authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
5
- export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env?: ShellEnv): Promise<CommandResult>;
9
+ /**
10
+ * Executes a shell pipeline of commands connected by pipes. Handles redirections,
11
+ * input/output files, and stderr redirects. Delegates to the single-command or
12
+ * chained-pipeline executor based on command count.
13
+ */
14
+ export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env?: ShellEnv, abortController?: AbortController): Promise<CommandResult>;
@@ -1,6 +1,10 @@
1
1
  import { runCommandDirect } from "../commands";
2
2
  import { resolvePath } from "../commands/helpers";
3
3
  // ── Script executor (handles &&/||/;) ────────────────────────────────────────
4
+ /**
5
+ * Executes a list of shell statements sequentially, respecting `&&`, `||`, and `;`
6
+ * operators. Accumulates stdout across statements and tracks cwd changes.
7
+ */
4
8
  export async function executeStatements(statements, authUser, hostname, mode, cwd, shell, env) {
5
9
  let last = { exitCode: 0 };
6
10
  const accumulatedStdout = [];
@@ -9,9 +13,9 @@ export async function executeStatements(statements, authUser, hostname, mode, cw
9
13
  while (i < statements.length) {
10
14
  const stmt = statements[i];
11
15
  if (stmt.background) {
12
- // Fire-and-forget: do not await. The MAX_CALL_DEPTH guard in runtime.ts
13
- // prevents runaway recursion (fork bombs, etc.).
14
- void executePipeline(stmt.pipeline, authUser, hostname, mode, currentCwd, shell, env);
16
+ // Background job: fire with AbortController so kill can cancel it.
17
+ const ac = new AbortController();
18
+ executePipeline(stmt.pipeline, authUser, hostname, "background", currentCwd, shell, env, ac);
15
19
  last = { exitCode: 0 };
16
20
  env.lastExitCode = 0;
17
21
  i++;
@@ -58,18 +62,23 @@ export async function executeStatements(statements, authUser, hostname, mode, cw
58
62
  return { ...last, stdout: merged || last.stdout, nextCwd: currentCwd !== cwd ? currentCwd : undefined };
59
63
  }
60
64
  // ── Pipeline executor ─────────────────────────────────────────────────────────
61
- export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell, env) {
65
+ /**
66
+ * Executes a shell pipeline of commands connected by pipes. Handles redirections,
67
+ * input/output files, and stderr redirects. Delegates to the single-command or
68
+ * chained-pipeline executor based on command count.
69
+ */
70
+ export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell, env, abortController) {
62
71
  if (!pipeline.isValid)
63
72
  return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
64
73
  if (pipeline.commands.length === 0)
65
74
  return { exitCode: 0 };
66
75
  const shellEnv = env ?? { vars: {}, lastExitCode: 0 };
67
76
  if (pipeline.commands.length === 1) {
68
- return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv);
77
+ return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv, abortController);
69
78
  }
70
79
  return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell, shellEnv);
71
80
  }
72
- async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env) {
81
+ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env, abortController) {
73
82
  let stdin;
74
83
  if (cmd.inputFile) {
75
84
  const inputPath = resolvePath(cwd, cmd.inputFile);
@@ -83,7 +92,8 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
83
92
  };
84
93
  }
85
94
  }
86
- const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, stdin, env);
95
+ const isBackground = mode === "background";
96
+ const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, stdin, env, isBackground, abortController);
87
97
  if (cmd.outputFile) {
88
98
  const outputPath = resolvePath(cwd, cmd.outputFile);
89
99
  const output = result.stdout || "";
@@ -149,7 +159,7 @@ async function executePipelineChain(commands, authUser, hostname, mode, cwd, she
149
159
  } })();
150
160
  shell.writeFileAsUser(authUser, sp, cmd.stderrAppend ? ex + effectiveResult.stderr : effectiveResult.stderr);
151
161
  }
152
- catch { }
162
+ catch { /* best-effort stderr write */ }
153
163
  }
154
164
  if (i === commands.length - 1 && cmd.outputFile) {
155
165
  const outputPath = resolvePath(cwd, cmd.outputFile);
@@ -1 +1,6 @@
1
+ /**
2
+ * Loads an existing PEM-encoded RSA host key from `.ssh-mimic/host_rsa` under
3
+ * the given base directory, or generates a new 2048-bit key pair and persists
4
+ * it to disk. Returns the private key in PEM format.
5
+ */
1
6
  export declare function loadOrCreateHostKey(baseDir?: string): string;
@@ -1,6 +1,11 @@
1
1
  import { generateKeyPairSync } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
+ /**
5
+ * Loads an existing PEM-encoded RSA host key from `.ssh-mimic/host_rsa` under
6
+ * the given base directory, or generates a new 2048-bit key pair and persists
7
+ * it to disk. Returns the private key in PEM format.
8
+ */
4
9
  export function loadOrCreateHostKey(baseDir = process.cwd()) {
5
10
  const hostKeyPath = resolve(baseDir, ".ssh-mimic", "host_rsa");
6
11
  if (existsSync(hostKeyPath)) {
@@ -1,6 +1,13 @@
1
1
  import type { ShellProperties } from "../VirtualShell";
2
+ /**
3
+ * Tracks the timestamp and origin of the user's last login for the login banner.
4
+ */
2
5
  export interface LoginBannerState {
3
6
  at: string;
4
7
  from: string;
5
8
  }
9
+ /**
10
+ * Builds the SSH login banner displaying OS info, warranty notice, and the
11
+ * last login timestamp and origin.
12
+ */
6
13
  export declare function buildLoginBanner(hostname: string, properties: ShellProperties, lastLogin: LoginBannerState | null): string;
@@ -1,4 +1,8 @@
1
1
  import { formatLoginDate } from "./loginFormat";
2
+ /**
3
+ * Builds the SSH login banner displaying OS info, warranty notice, and the
4
+ * last login timestamp and origin.
5
+ */
2
6
  export function buildLoginBanner(hostname, properties, lastLogin) {
3
7
  const lines = [
4
8
  `Linux ${hostname} ${properties.kernel} ${properties.arch}`,
@@ -1 +1,5 @@
1
+ /**
2
+ * Formats a Date object into the SSH last-login display format:
3
+ * e.g. "Mon Jan 02 15:04:05 2024"
4
+ */
1
5
  export declare function formatLoginDate(date: Date): string;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Formats a Date object into the SSH last-login display format:
3
+ * e.g. "Mon Jan 02 15:04:05 2024"
4
+ */
1
5
  export function formatLoginDate(date) {
2
6
  const weekday = date.toLocaleString("en-US", { weekday: "short" });
3
7
  const month = date.toLocaleString("en-US", { month: "short" });
@@ -1,2 +1,11 @@
1
+ /**
2
+ * Expands a PS1 template string by replacing escape sequences (\\u, \\h, \\w,
3
+ * \\$, etc.) with the corresponding user, host, and directory values.
4
+ */
1
5
  export declare function expandPs1(ps1: string, user: string, host: string, cwd: string, readlineMode?: boolean): string;
6
+ /**
7
+ * Builds the complete shell prompt string. If a PS1 template is provided it is
8
+ * expanded via expandPs1; otherwise a traditional `[user@host cwd]$` prompt
9
+ * with ANSI color codes is returned.
10
+ */
2
11
  export declare function buildPrompt(user: string, host: string, cwdName: string, ps1?: string, fullCwd?: string, readlineMode?: boolean): string;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Expands a PS1 template string by replacing escape sequences (\\u, \\h, \\w,
3
+ * \\$, etc.) with the corresponding user, host, and directory values.
4
+ */
1
5
  export function expandPs1(ps1, user, host, cwd, readlineMode = false) {
2
6
  const home = user === "root" ? "/root" : `/home/${user}`;
3
7
  const withTilde = cwd === home ? "~"
@@ -16,6 +20,11 @@ export function expandPs1(ps1, user, host, cwd, readlineMode = false) {
16
20
  .replace(/\\n/g, "\n")
17
21
  .replace(/\\\\/g, "\\");
18
22
  }
23
+ /**
24
+ * Builds the complete shell prompt string. If a PS1 template is provided it is
25
+ * expanded via expandPs1; otherwise a traditional `[user@host cwd]$` prompt
26
+ * with ANSI color codes is returned.
27
+ */
19
28
  export function buildPrompt(user, host, cwdName, ps1, fullCwd, readlineMode = false) {
20
29
  if (ps1)
21
30
  return expandPs1(ps1, user, host, fullCwd ?? cwdName, readlineMode);
@@ -18,6 +18,10 @@
18
18
  * Acknowledgement byte: \0 = ok, \1 = warning, \2 = error.
19
19
  */
20
20
  import type { VirtualShell } from "../VirtualShell";
21
+ /**
22
+ * Minimal stream interface representing an SSH channel for SCP transfers.
23
+ * Mirrors the ssh2 ServerChannel subset used by the SCP protocol.
24
+ */
21
25
  interface ScpStream {
22
26
  write(data: Buffer | string): boolean;
23
27
  on(event: "data", listener: (chunk: Buffer) => void): this;
@@ -28,7 +32,21 @@ interface ScpStream {
28
32
  exit(code: number): void;
29
33
  end(): void;
30
34
  }
35
+ /**
36
+ * Handles SCP sink mode (receiving files from the client). Parses the SCP wire
37
+ * protocol control messages (C, D, E, T) and writes file data into the virtual
38
+ * filesystem.
39
+ */
31
40
  export declare function runScpSink(stream: ScpStream, destArg: string, authUser: string, shell: VirtualShell, recursive: boolean): void;
41
+ /**
42
+ * Handles SCP source mode (sending files to the client). Traverses the virtual
43
+ * filesystem and streams file contents using the SCP wire protocol control
44
+ * messages (C, D, E) in response to client acknowledgements.
45
+ */
32
46
  export declare function runScpSource(stream: ScpStream, srcArg: string, _authUser: string, shell: VirtualShell, recursive: boolean): void;
47
+ /**
48
+ * Handles SCP protocol transfer by parsing the exec arguments to determine
49
+ * sink (upload) or source (download) mode and delegating accordingly.
50
+ */
33
51
  export declare function handleScp(stream: ScpStream, rawArgs: string[], authUser: string, shell: VirtualShell): void;
34
52
  export {};
@@ -39,6 +39,11 @@ function parseArgs(args) {
39
39
  };
40
40
  }
41
41
  // ── Sink mode (upload: client → server) ──────────────────────────────────────
42
+ /**
43
+ * Handles SCP sink mode (receiving files from the client). Parses the SCP wire
44
+ * protocol control messages (C, D, E, T) and writes file data into the virtual
45
+ * filesystem.
46
+ */
42
47
  export function runScpSink(stream, destArg, authUser, shell, recursive) {
43
48
  // Buffered reader that handles arbitrary chunk boundaries
44
49
  let buf = Buffer.alloc(0);
@@ -166,6 +171,11 @@ export function runScpSink(stream, destArg, authUser, shell, recursive) {
166
171
  });
167
172
  }
168
173
  // ── Source mode (download: server → client) ───────────────────────────────────
174
+ /**
175
+ * Handles SCP source mode (sending files to the client). Traverses the virtual
176
+ * filesystem and streams file contents using the SCP wire protocol control
177
+ * messages (C, D, E) in response to client acknowledgements.
178
+ */
169
179
  export function runScpSource(stream, srcArg, _authUser, shell, recursive) {
170
180
  const srcPath = resolvePath("/", srcArg);
171
181
  if (!shell.vfs.exists(srcPath)) {
@@ -266,6 +276,10 @@ export function runScpSource(stream, srcArg, _authUser, shell, recursive) {
266
276
  // (processAcks will call sendNext on first ack)
267
277
  }
268
278
  // ── Entry point ───────────────────────────────────────────────────────────────
279
+ /**
280
+ * Handles SCP protocol transfer by parsing the exec arguments to determine
281
+ * sink (upload) or source (download) mode and delegating accordingly.
282
+ */
269
283
  export function handleScp(stream, rawArgs, authUser, shell) {
270
284
  const { sink, source, recursive, target } = parseArgs(rawArgs);
271
285
  if (!sink && !source) {
@@ -7,13 +7,15 @@
7
7
  *
8
8
  * File header:
9
9
  * [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
10
- * [1] version = 0x01
10
+ * [1] version = 0x02
11
11
  *
12
12
  * Node (recursive):
13
13
  * [1] type = 0x01 (file) | 0x02 (directory)
14
14
  * [2] name length (uint16)
15
15
  * [N] name bytes (utf8)
16
16
  * [4] mode (uint32)
17
+ * [4] uid (uint32)
18
+ * [4] gid (uint32)
17
19
  * [8] createdAt ms (float64)
18
20
  * [8] updatedAt ms (float64)
19
21
  *
@@ -49,7 +51,9 @@ export declare function forkDirTree(base: InternalDirectoryNode): InternalDirect
49
51
  */
50
52
  export declare function decodeVfs(buf: Buffer): InternalDirectoryNode;
51
53
  /**
52
- * Returns true if `buf` looks like a VFS binary snapshot (starts with magic bytes).
53
- * Used to auto-detect format when loading from disk.
54
+ * Checks whether `buf` starts with the VFS binary magic bytes, indicating a valid
55
+ * binary snapshot produced by {@link encodeVfs}.
56
+ * @param buf - The buffer to inspect.
57
+ * @returns `true` if the buffer begins with the VFS magic header (`"VFS!"`).
54
58
  */
55
59
  export declare function isBinarySnapshot(buf: Buffer): boolean;
@@ -7,13 +7,15 @@
7
7
  *
8
8
  * File header:
9
9
  * [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
10
- * [1] version = 0x01
10
+ * [1] version = 0x02
11
11
  *
12
12
  * Node (recursive):
13
13
  * [1] type = 0x01 (file) | 0x02 (directory)
14
14
  * [2] name length (uint16)
15
15
  * [N] name bytes (utf8)
16
16
  * [4] mode (uint32)
17
+ * [4] uid (uint32)
18
+ * [4] gid (uint32)
17
19
  * [8] createdAt ms (float64)
18
20
  * [8] updatedAt ms (float64)
19
21
  *
@@ -31,7 +33,7 @@
31
33
  * Binary pack : ~1.00 MB + ~40 bytes/node header → ~27% smaller, no string parsing
32
34
  */
33
35
  const MAGIC = Buffer.from([0x56, 0x46, 0x53, 0x21]); // "VFS!"
34
- const VERSION = 0x01;
36
+ const VERSION = 0x02;
35
37
  const TYPE_FILE = 0x01;
36
38
  const TYPE_DIR = 0x02;
37
39
  // ── Encoder ───────────────────────────────────────────────────────────────────
@@ -79,6 +81,8 @@ function encodeNode(enc, node) {
79
81
  enc.writeUint8(TYPE_FILE);
80
82
  enc.writeString(f.name);
81
83
  enc.writeUint32(f.mode);
84
+ enc.writeUint32(f.uid);
85
+ enc.writeUint32(f.gid);
82
86
  enc.writeFloat64(f.createdAt);
83
87
  enc.writeFloat64(f.updatedAt);
84
88
  enc.writeUint8(f.compressed ? 0x01 : 0x00);
@@ -90,6 +94,8 @@ function encodeNode(enc, node) {
90
94
  enc.writeUint8(TYPE_FILE);
91
95
  enc.writeString(s.name);
92
96
  enc.writeUint32(s.mode);
97
+ enc.writeUint32(s.uid);
98
+ enc.writeUint32(s.gid);
93
99
  enc.writeFloat64(s.createdAt);
94
100
  enc.writeFloat64(s.updatedAt);
95
101
  enc.writeUint8(0x00); // not compressed
@@ -100,6 +106,8 @@ function encodeNode(enc, node) {
100
106
  enc.writeUint8(TYPE_DIR);
101
107
  enc.writeString(d.name);
102
108
  enc.writeUint32(d.mode);
109
+ enc.writeUint32(d.uid);
110
+ enc.writeUint32(d.gid);
103
111
  enc.writeFloat64(d.createdAt);
104
112
  enc.writeFloat64(d.updatedAt);
105
113
  const children = Object.values(d.children);
@@ -160,10 +168,12 @@ class Decoder {
160
168
  return this.buf.length - this.pos;
161
169
  }
162
170
  }
163
- function decodeNode(dec) {
171
+ function decodeNode(dec, includeUidGid) {
164
172
  const type = dec.readUint8();
165
173
  const name = internName(dec.readString());
166
174
  const mode = dec.readUint32();
175
+ const uid = includeUidGid ? dec.readUint32() : 0;
176
+ const gid = includeUidGid ? dec.readUint32() : 0;
167
177
  const createdAt = dec.readFloat64();
168
178
  const updatedAt = dec.readFloat64();
169
179
  if (type === TYPE_FILE) {
@@ -173,6 +183,8 @@ function decodeNode(dec) {
173
183
  type: "file",
174
184
  name,
175
185
  mode,
186
+ uid,
187
+ gid,
176
188
  createdAt,
177
189
  updatedAt,
178
190
  compressed,
@@ -183,13 +195,15 @@ function decodeNode(dec) {
183
195
  const count = dec.readUint32();
184
196
  const children = Object.create(null);
185
197
  for (let i = 0; i < count; i++) {
186
- const child = decodeNode(dec);
198
+ const child = decodeNode(dec, includeUidGid);
187
199
  children[child.name] = child;
188
200
  }
189
201
  return {
190
202
  type: "directory",
191
203
  name,
192
204
  mode,
205
+ uid,
206
+ gid,
193
207
  createdAt,
194
208
  updatedAt,
195
209
  children,
@@ -227,6 +241,8 @@ export function forkDirTree(base) {
227
241
  type: "directory",
228
242
  name: base.name,
229
243
  mode: base.mode,
244
+ uid: base.uid,
245
+ gid: base.gid,
230
246
  createdAt: base.createdAt,
231
247
  updatedAt: base.updatedAt,
232
248
  children,
@@ -246,18 +262,24 @@ export function decodeVfs(buf) {
246
262
  throw new Error("[VFS binary] Invalid magic — not a VFS binary snapshot");
247
263
  }
248
264
  const dec = new Decoder(buf);
249
- // skip magic (4) + version (1)
250
- for (let i = 0; i < 5; i++)
251
- dec.readUint8();
252
- const root = decodeNode(dec);
265
+ // skip magic (4)
266
+ dec.readUint8();
267
+ dec.readUint8();
268
+ dec.readUint8();
269
+ dec.readUint8();
270
+ const version = dec.readUint8();
271
+ const includeUidGid = version >= 0x02;
272
+ const root = decodeNode(dec, includeUidGid);
253
273
  if (root.type !== "directory") {
254
274
  throw new Error("[VFS binary] Root node must be a directory");
255
275
  }
256
276
  return root;
257
277
  }
258
278
  /**
259
- * Returns true if `buf` looks like a VFS binary snapshot (starts with magic bytes).
260
- * Used to auto-detect format when loading from disk.
279
+ * Checks whether `buf` starts with the VFS binary magic bytes, indicating a valid
280
+ * binary snapshot produced by {@link encodeVfs}.
281
+ * @param buf - The buffer to inspect.
282
+ * @returns `true` if the buffer begins with the VFS magic header (`"VFS!"`).
261
283
  */
262
284
  export function isBinarySnapshot(buf) {
263
285
  return buf.length >= 4 && buf.slice(0, 4).equals(MAGIC);
@@ -88,6 +88,12 @@ declare class VirtualFileSystem extends EventEmitter {
88
88
  private readonly mounts;
89
89
  /** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
90
90
  private _sortedMounts;
91
+ /** Read hooks: path prefix → callback invoked before reading any file under that prefix. */
92
+ private readonly readHooks;
93
+ /** Sorted read hook prefixes (longest-first) for matching. */
94
+ private _sortedReadHooks;
95
+ /** Re-entrancy guard for read hooks — prevents infinite loop when hook triggers another read. */
96
+ private _inReadHook;
91
97
  /** True when running in a browser environment (no host FS access). */
92
98
  private static readonly isBrowser;
93
99
  constructor(options?: VfsOptions);
@@ -225,6 +231,15 @@ declare class VirtualFileSystem extends EventEmitter {
225
231
  hostPath: string;
226
232
  readOnly: boolean;
227
233
  }>;
234
+ /**
235
+ * Register a callback that is invoked before any read under `prefix`.
236
+ * Used by /proc to refresh dynamic content on every access.
237
+ */
238
+ onBeforeRead(prefix: string, cb: () => void): void;
239
+ /** Remove a previously registered read hook. */
240
+ offBeforeRead(prefix: string): void;
241
+ /** Invoke any matching read hook for `normalizedPath`. */
242
+ private _triggerReadHook;
228
243
  /**
229
244
  * If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
230
245
  * `relPath` is the path relative to the mount's host directory.
@@ -248,6 +263,20 @@ declare class VirtualFileSystem extends EventEmitter {
248
263
  exists(targetPath: string): boolean;
249
264
  /** Updates mode bits on a node. */
250
265
  chmod(targetPath: string, mode: number): void;
266
+ /** Changes ownership (uid/gid) of a file or directory. */
267
+ chown(targetPath: string, uid: number, gid: number): void;
268
+ /** Returns the uid and gid of a node. */
269
+ getOwner(targetPath: string): {
270
+ uid: number;
271
+ gid: number;
272
+ };
273
+ /**
274
+ * POSIX-style access check: does `uid`/`gid` have `want` permission on `targetPath`?
275
+ * `want` is a bitmask of R_OK (4), W_OK (2), X_OK (1).
276
+ * Root (uid === 0) is granted everything except X_OK without at least one x bit set.
277
+ * Returns true when access is granted.
278
+ */
279
+ checkAccess(targetPath: string, uid: number, gid: number, want: number): boolean;
251
280
  /** Returns metadata for a file or directory. */
252
281
  stat(targetPath: string): VfsNodeStats;
253
282
  /**