typescript-virtual-container 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +871 -1231
  2. package/benchmark-results.txt +21 -21
  3. package/biome.json +9 -0
  4. package/dist/SSHMimic/index.d.ts +19 -2
  5. package/dist/SSHMimic/index.d.ts.map +1 -1
  6. package/dist/SSHMimic/index.js +127 -15
  7. package/dist/VirtualFileSystem/index.d.ts +115 -88
  8. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  9. package/dist/VirtualFileSystem/index.js +406 -258
  10. package/dist/VirtualShell/index.d.ts +3 -4
  11. package/dist/VirtualShell/index.d.ts.map +1 -1
  12. package/dist/VirtualShell/index.js +5 -23
  13. package/dist/VirtualUserManager/index.d.ts +41 -3
  14. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  15. package/dist/VirtualUserManager/index.js +83 -21
  16. package/dist/commands/chmod.d.ts +3 -0
  17. package/dist/commands/chmod.d.ts.map +1 -0
  18. package/dist/commands/chmod.js +31 -0
  19. package/dist/commands/cp.d.ts +3 -0
  20. package/dist/commands/cp.d.ts.map +1 -0
  21. package/dist/commands/cp.js +68 -0
  22. package/dist/commands/find.d.ts +3 -0
  23. package/dist/commands/find.d.ts.map +1 -0
  24. package/dist/commands/find.js +48 -0
  25. package/dist/commands/grep.d.ts.map +1 -1
  26. package/dist/commands/grep.js +61 -35
  27. package/dist/commands/head.d.ts +3 -0
  28. package/dist/commands/head.d.ts.map +1 -0
  29. package/dist/commands/head.js +30 -0
  30. package/dist/commands/index.d.ts.map +1 -1
  31. package/dist/commands/index.js +25 -35
  32. package/dist/commands/ln.d.ts +3 -0
  33. package/dist/commands/ln.d.ts.map +1 -0
  34. package/dist/commands/ln.js +42 -0
  35. package/dist/commands/mv.d.ts +3 -0
  36. package/dist/commands/mv.d.ts.map +1 -0
  37. package/dist/commands/mv.js +35 -0
  38. package/dist/commands/tail.d.ts +3 -0
  39. package/dist/commands/tail.d.ts.map +1 -0
  40. package/dist/commands/tail.js +33 -0
  41. package/dist/commands/wc.d.ts +3 -0
  42. package/dist/commands/wc.d.ts.map +1 -0
  43. package/dist/commands/wc.js +48 -0
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/standalone.js +7 -9
  47. package/package.json +7 -3
  48. package/scripts/publish-package.sh +70 -0
  49. package/src/SSHMimic/index.ts +159 -17
  50. package/src/VirtualFileSystem/index.ts +500 -280
  51. package/src/VirtualShell/index.ts +5 -33
  52. package/src/VirtualUserManager/index.ts +92 -26
  53. package/src/commands/chmod.ts +33 -0
  54. package/src/commands/cp.ts +76 -0
  55. package/src/commands/find.ts +61 -0
  56. package/src/commands/grep.ts +54 -38
  57. package/src/commands/head.ts +35 -0
  58. package/src/commands/index.ts +25 -43
  59. package/src/commands/ln.ts +47 -0
  60. package/src/commands/mv.ts +43 -0
  61. package/src/commands/tail.ts +37 -0
  62. package/src/commands/wc.ts +48 -0
  63. package/src/index.ts +1 -0
  64. package/src/standalone.ts +12 -9
  65. package/standalone.js +102 -0
  66. package/standalone.js.map +7 -0
  67. package/tests/bun-test-shim.ts +1 -0
  68. package/tests/sftp.test.ts +115 -191
  69. package/tests/users.test.ts +66 -83
@@ -1,40 +1,40 @@
1
1
  Benchmarking VirtualShell concurrency:
2
2
 
3
3
  Running 1 shells...
4
- Initialized 1 shells in 86ms, RSS 82 MB
5
- Executed shell commands in 2ms, RSS now 82 MB (+0 MB)
4
+ Initialized 1 shells in 50ms, RSS 82 MB
5
+ Executed shell commands in 4ms, RSS now 83 MB (+0 MB)
6
6
 
7
7
  Running 2 shells...
8
- Initialized 2 shells in 2ms, RSS 82 MB
9
- Executed shell commands in 1ms, RSS now 82 MB (+0 MB)
8
+ Initialized 2 shells in 2ms, RSS 83 MB
9
+ Executed shell commands in 0ms, RSS now 83 MB (+0 MB)
10
10
 
11
11
  Running 5 shells...
12
- Initialized 5 shells in 5ms, RSS 82 MB
13
- Executed shell commands in 2ms, RSS now 82 MB (+0 MB)
12
+ Initialized 5 shells in 3ms, RSS 83 MB
13
+ Executed shell commands in 1ms, RSS now 83 MB (+0 MB)
14
14
 
15
15
  Running 10 shells...
16
- Initialized 10 shells in 5ms, RSS 84 MB
17
- Executed shell commands in 3ms, RSS now 84 MB (+1 MB)
16
+ Initialized 10 shells in 4ms, RSS 83 MB
17
+ Executed shell commands in 4ms, RSS now 84 MB (+1 MB)
18
18
 
19
19
  Running 20 shells...
20
- Initialized 20 shells in 12ms, RSS 84 MB
21
- Executed shell commands in 6ms, RSS now 85 MB (+1 MB)
20
+ Initialized 20 shells in 7ms, RSS 84 MB
21
+ Executed shell commands in 5ms, RSS now 85 MB (+0 MB)
22
22
 
23
23
  Running 50 shells...
24
- Initialized 50 shells in 30ms, RSS 87 MB
25
- Executed shell commands in 12ms, RSS now 88 MB (+2 MB)
24
+ Initialized 50 shells in 23ms, RSS 85 MB
25
+ Executed shell commands in 12ms, RSS now 87 MB (+2 MB)
26
26
 
27
27
  Running 100 shells...
28
- Initialized 100 shells in 52ms, RSS 91 MB
29
- Executed shell commands in 28ms, RSS now 92 MB (+2 MB)
28
+ Initialized 100 shells in 36ms, RSS 89 MB
29
+ Executed shell commands in 33ms, RSS now 91 MB (+2 MB)
30
30
 
31
31
  Summary:
32
32
 
33
33
  count init_ms cmd_ms init_rss final_rss
34
- 1 86 2 82 MB 82 MB 0 MB
35
- 2 2 1 82 MB 82 MB 0 MB
36
- 5 5 2 82 MB 82 MB 0 MB
37
- 10 5 3 84 MB 84 MB 1 MB
38
- 20 12 6 84 MB 85 MB 1 MB
39
- 50 30 12 87 MB 88 MB 2 MB
40
- 100 52 28 91 MB 92 MB 2 MB
34
+ 1 50 4 82 MB 83 MB 0 MB
35
+ 2 2 0 83 MB 83 MB 0 MB
36
+ 5 3 1 83 MB 83 MB 0 MB
37
+ 10 4 4 83 MB 84 MB 1 MB
38
+ 20 7 5 84 MB 85 MB 0 MB
39
+ 50 23 12 85 MB 87 MB 2 MB
40
+ 100 36 33 89 MB 91 MB 2 MB
package/biome.json CHANGED
@@ -17,5 +17,14 @@
17
17
  "noNonNullAssertion": "off"
18
18
  }
19
19
  }
20
+ },
21
+ "formatter": {
22
+ "enabled": true,
23
+ "formatWithErrors": false,
24
+ "attributePosition": "auto",
25
+ "indentStyle": "tab",
26
+ "indentWidth": 2,
27
+ "lineWidth": 80,
28
+ "lineEnding": "lf"
20
29
  }
21
30
  }
@@ -5,19 +5,31 @@ declare class SshMimic extends EventEmitter {
5
5
  port: number;
6
6
  server: SshServer | null;
7
7
  private shell;
8
- private shellHostname;
8
+ /** Max failed auth attempts before an IP is temporarily locked. */
9
+ private readonly maxAuthAttempts;
10
+ /** How long (ms) a locked IP must wait before retrying. */
11
+ private readonly lockoutDurationMs;
12
+ private readonly authAttempts;
9
13
  /**
10
14
  * Creates a new SSH mimic server instance.
11
15
  *
12
16
  * @param port TCP port to bind on localhost.
13
17
  * @param hostname Virtual hostname used for the SSH ident and default shell label.
14
18
  * @param shell Optional preconfigured virtual shell instance to reuse.
19
+ * @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
20
+ * @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
15
21
  */
16
- constructor({ port, hostname, shell, }: {
22
+ constructor({ port, hostname, shell, maxAuthAttempts, lockoutDurationMs, }: {
17
23
  port: number;
18
24
  hostname?: string;
19
25
  shell?: VirtualShell;
26
+ maxAuthAttempts?: number;
27
+ lockoutDurationMs?: number;
20
28
  });
29
+ private isLockedOut;
30
+ private recordFailure;
31
+ private recordSuccess;
32
+ private ensureHomeDir;
21
33
  /**
22
34
  * Starts server and initializes virtual filesystem, users, and handlers.
23
35
  *
@@ -28,6 +40,11 @@ declare class SshMimic extends EventEmitter {
28
40
  * Stops server if running.
29
41
  */
30
42
  stop(): void;
43
+ /**
44
+ * Manually clears the rate-limit record for an IP address.
45
+ * Useful in tests or admin tooling.
46
+ */
47
+ clearLockout(ip: string): void;
31
48
  }
32
49
  export { SftpMimic } from "./sftp";
33
50
  export { SshMimic };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAc/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;OAMG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,GAClC,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;KACrB;IASD;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAyHrC;;OAEG;IACI,IAAI,IAAI,IAAI;CASnB;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA0B/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAE5B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAElE;;;;;;;;OAQG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,EAClC,eAAmB,EACnB,iBAA0B,GAC1B,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAYD,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,aAAa;IAcrB;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA6LrC;;OAEG;IACI,IAAI,IAAI,IAAI;IAUnB;;;OAGG;IACI,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAGrC;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -10,28 +10,76 @@ import { loadOrCreateHostKey } from "./hostKey";
10
10
  * This class is exported as `VirtualSshServer` for public API compatibility.
11
11
  * Create an instance, call {@link SshMimic.start}, and stop it with
12
12
  * {@link SshMimic.stop} when your process exits.
13
+ *
14
+ * Features:
15
+ * - Password authentication
16
+ * - Public-key authentication
17
+ * - Per-IP rate limiting / lockout for brute-force protection
18
+ * - Interactive shell sessions
19
+ * - Non-interactive exec sessions
13
20
  */
14
21
  const perf = createPerfLogger("SshMimic");
15
22
  class SshMimic extends EventEmitter {
16
23
  port;
17
24
  server;
18
25
  shell;
19
- shellHostname;
26
+ /** Max failed auth attempts before an IP is temporarily locked. */
27
+ maxAuthAttempts;
28
+ /** How long (ms) a locked IP must wait before retrying. */
29
+ lockoutDurationMs;
30
+ authAttempts = new Map();
20
31
  /**
21
32
  * Creates a new SSH mimic server instance.
22
33
  *
23
34
  * @param port TCP port to bind on localhost.
24
35
  * @param hostname Virtual hostname used for the SSH ident and default shell label.
25
36
  * @param shell Optional preconfigured virtual shell instance to reuse.
37
+ * @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
38
+ * @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
26
39
  */
27
- constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), }) {
40
+ constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), maxAuthAttempts = 5, lockoutDurationMs = 60_000, }) {
28
41
  super();
29
42
  perf.mark("constructor");
30
43
  this.port = port;
31
- this.shellHostname = hostname;
32
44
  this.server = null;
33
45
  this.shell = shell;
46
+ this.maxAuthAttempts = maxAuthAttempts;
47
+ this.lockoutDurationMs = lockoutDurationMs;
48
+ }
49
+ // ── Rate limiting ────────────────────────────────────────────────────────
50
+ isLockedOut(ip) {
51
+ const entry = this.authAttempts.get(ip);
52
+ if (!entry)
53
+ return false;
54
+ if (Date.now() < entry.lockedUntil)
55
+ return true;
56
+ if (entry.lockedUntil > 0) {
57
+ this.authAttempts.delete(ip);
58
+ }
59
+ return false;
60
+ }
61
+ recordFailure(ip) {
62
+ const entry = this.authAttempts.get(ip) ?? { attempts: 0, lockedUntil: 0 };
63
+ entry.attempts += 1;
64
+ if (entry.attempts >= this.maxAuthAttempts) {
65
+ entry.lockedUntil = Date.now() + this.lockoutDurationMs;
66
+ this.emit("auth:lockout", { ip, until: new Date(entry.lockedUntil) });
67
+ }
68
+ this.authAttempts.set(ip, entry);
34
69
  }
70
+ recordSuccess(ip) {
71
+ this.authAttempts.delete(ip);
72
+ }
73
+ // ── Home directory bootstrap ─────────────────────────────────────────────
74
+ ensureHomeDir(authUser) {
75
+ const homePath = `/home/${authUser}`;
76
+ if (!this.shell.vfs.exists(homePath)) {
77
+ this.shell.vfs.mkdir(homePath, 0o755);
78
+ this.shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${this.shell.hostname}\n`);
79
+ void this.shell.vfs.flushMirror();
80
+ }
81
+ }
82
+ // ── Server lifecycle ─────────────────────────────────────────────────────
35
83
  /**
36
84
  * Starts server and initializes virtual filesystem, users, and handlers.
37
85
  *
@@ -41,7 +89,6 @@ class SshMimic extends EventEmitter {
41
89
  perf.mark("start");
42
90
  const shell = this.shell;
43
91
  const privateKey = loadOrCreateHostKey();
44
- // Ensure VirtualShell is fully initialized before accepting connections
45
92
  await shell.ensureInitialized();
46
93
  this.server = new SshServer({
47
94
  hostKeys: [privateKey],
@@ -52,11 +99,34 @@ class SshMimic extends EventEmitter {
52
99
  let sessionId = null;
53
100
  this.emit("client:connect");
54
101
  client.on("authentication", (ctx) => {
55
- shell;
102
+ const candidateUser = ctx.username || "root";
103
+ remoteAddress = ctx.ip ?? remoteAddress;
104
+ // Rate-limit check
105
+ if (this.isLockedOut(remoteAddress)) {
106
+ this.emit("auth:failure", {
107
+ username: candidateUser,
108
+ remoteAddress,
109
+ reason: "lockout",
110
+ });
111
+ ctx.reject();
112
+ return;
113
+ }
114
+ // ── Password auth ──────────────────────────────────────
56
115
  if (ctx.method === "password") {
57
- const candidateUser = ctx.username || "root";
58
- remoteAddress = ctx.ip ?? remoteAddress;
59
- if (!shell.users.verifyPassword(candidateUser, ctx.password ?? "")) {
116
+ if (!shell.users.hasPassword(candidateUser)) {
117
+ console.log(`User ${candidateUser} has no password set, allowing login without verification`);
118
+ authUser = candidateUser;
119
+ sessionId = shell.users.registerSession(authUser, remoteAddress).id;
120
+ this.recordSuccess(remoteAddress);
121
+ this.emit("auth:success", { username: authUser, remoteAddress });
122
+ this.ensureHomeDir(authUser);
123
+ ctx.accept();
124
+ return;
125
+ }
126
+ if (!ctx.password ||
127
+ ctx.password === "" ||
128
+ !shell.users.verifyPassword(candidateUser, ctx.password)) {
129
+ this.recordFailure(remoteAddress);
60
130
  this.emit("auth:failure", {
61
131
  username: candidateUser,
62
132
  remoteAddress,
@@ -66,17 +136,52 @@ class SshMimic extends EventEmitter {
66
136
  }
67
137
  authUser = candidateUser;
68
138
  sessionId = shell.users.registerSession(authUser, remoteAddress).id;
139
+ this.recordSuccess(remoteAddress);
69
140
  this.emit("auth:success", { username: authUser, remoteAddress });
70
- const homePath = `/home/${authUser}`;
71
- if (!shell.vfs.exists(homePath)) {
72
- shell.vfs.mkdir(homePath, 0o755);
73
- shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
74
- void shell.vfs.flushMirror();
75
- }
141
+ this.ensureHomeDir(authUser);
76
142
  ctx.accept();
77
143
  return;
78
144
  }
79
- ctx.reject();
145
+ // ── Public-key auth ────────────────────────────────────
146
+ if (ctx.method === "publickey") {
147
+ const authorizedKeys = shell.users.getAuthorizedKeys(candidateUser);
148
+ if (authorizedKeys.length === 0) {
149
+ // No keys configured — reject cleanly
150
+ ctx.reject();
151
+ return;
152
+ }
153
+ const incomingKey = ctx.key;
154
+ const keyMatches = authorizedKeys.some((k) => k.algo === incomingKey.algo && k.data.equals(incomingKey.data));
155
+ if (!keyMatches) {
156
+ this.recordFailure(remoteAddress);
157
+ this.emit("auth:failure", {
158
+ username: candidateUser,
159
+ remoteAddress,
160
+ method: "publickey",
161
+ });
162
+ ctx.reject();
163
+ return;
164
+ }
165
+ // Key matched — if this is a signature check step, accept
166
+ if (ctx.signature) {
167
+ authUser = candidateUser;
168
+ sessionId = shell.users.registerSession(authUser, remoteAddress).id;
169
+ this.recordSuccess(remoteAddress);
170
+ this.emit("auth:success", {
171
+ username: authUser,
172
+ remoteAddress,
173
+ method: "publickey",
174
+ });
175
+ this.ensureHomeDir(authUser);
176
+ ctx.accept();
177
+ }
178
+ else {
179
+ // Key exists but no signature yet — ssh2 will call again with signature
180
+ ctx.accept();
181
+ }
182
+ return;
183
+ }
184
+ ctx.reject(["password", "publickey"]);
80
185
  });
81
186
  client.on("close", () => {
82
187
  shell.users.unregisterSession(sessionId);
@@ -130,6 +235,13 @@ class SshMimic extends EventEmitter {
130
235
  });
131
236
  }
132
237
  }
238
+ /**
239
+ * Manually clears the rate-limit record for an IP address.
240
+ * Useful in tests or admin tooling.
241
+ */
242
+ clearLockout(ip) {
243
+ this.authAttempts.delete(ip);
244
+ }
133
245
  }
134
246
  export { SftpMimic } from "./sftp";
135
247
  export { SshMimic };
@@ -1,127 +1,154 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { RemoveOptions, VfsNodeStats, WriteFileOptions } from "../types/vfs";
2
+ import type { RemoveOptions, VfsNodeStats, VfsSnapshot, WriteFileOptions } from "../types/vfs";
3
+ /**
4
+ * "memory" — pure in-memory, no disk I/O (default).
5
+ *
6
+ * "fs" — mirrors the VFS tree to a directory on the host filesystem.
7
+ * `snapshotPath` must be set to the directory where the JSON
8
+ * snapshot file will be read/written.
9
+ */
10
+ export type VfsPersistenceMode = "memory" | "fs";
11
+ export interface VfsOptions {
12
+ /**
13
+ * Persistence mode.
14
+ * - `"memory"` (default): no disk access, snapshot via `toSnapshot()`.
15
+ * - `"fs"`: auto-save JSON snapshot to `snapshotPath` on every
16
+ * `flushMirror()` call, and restore from it on `restoreMirror()`.
17
+ */
18
+ mode?: VfsPersistenceMode;
19
+ /**
20
+ * Directory used by `"fs"` mode.
21
+ * The snapshot file will be written to `<snapshotPath>/vfs-snapshot.json`.
22
+ * Required when `mode` is `"fs"`.
23
+ */
24
+ snapshotPath?: string;
25
+ }
26
+ /**
27
+ * In-memory virtual filesystem with optional JSON-snapshot persistence.
28
+ *
29
+ * **Memory mode** (default) — all state lives in a fast recursive tree.
30
+ * Use `toSnapshot()` / `fromSnapshot()` / `importSnapshot()` for serialisation.
31
+ *
32
+ * **FS mode** — same in-memory tree, but `restoreMirror()` loads a JSON
33
+ * snapshot from disk and `flushMirror()` writes it back. This gives you
34
+ * persistent VFS state across process restarts without any real POSIX filesystem
35
+ * semantics leaking through.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // Pure in-memory (default)
40
+ * const vfs = new VirtualFileSystem();
41
+ *
42
+ * // With disk persistence
43
+ * const vfs = new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" });
44
+ * await vfs.restoreMirror(); // load from disk (no-op if no snapshot yet)
45
+ * // ... use vfs ...
46
+ * await vfs.flushMirror(); // persist to disk
47
+ * ```
48
+ */
3
49
  declare class VirtualFileSystem extends EventEmitter {
4
- private readonly mirrorRoot;
5
- private ensureMirrorRoot;
6
- private resolveFsPath;
7
- private detectGzipFile;
8
- private computeDiskUsageBytes;
9
- private renderTreeLines;
10
- /**
11
- * Creates a virtual filesystem instance.
12
- *
13
- * @param baseDir Base directory used to resolve mirror archive location.
14
- */
15
- constructor(baseDir?: string);
50
+ private root;
51
+ private readonly mode;
52
+ private readonly snapshotFile;
53
+ constructor(options?: VfsOptions);
54
+ private makeDir;
55
+ private makeFile;
56
+ private mkdirRecursive;
16
57
  /**
17
- * Restores filesystem state from mirror archive.
58
+ * In `"fs"` mode: reads the JSON snapshot from disk and hydrates the tree.
59
+ * Silently succeeds when the snapshot file does not exist yet.
18
60
  *
19
- * If archive does not exist or cannot be read, creates fresh mirror file.
61
+ * In `"memory"` mode: no-op (kept for API compatibility).
20
62
  */
21
63
  restoreMirror(): Promise<void>;
22
64
  /**
23
- * Persists current filesystem state to mirror archive.
65
+ * In `"fs"` mode: serialises the in-memory tree to a JSON snapshot on disk.
66
+ * The directory is created if it does not exist.
24
67
  *
25
- * No-op when nothing changed and archive already exists.
68
+ * In `"memory"` mode: emits `"mirror:flush"` and returns (no disk write).
26
69
  */
27
70
  flushMirror(): Promise<void>;
28
- /**
29
- * Creates directory and any missing parent directories.
30
- *
31
- * @param targetPath Absolute or relative path to directory.
32
- * @param mode POSIX-like mode bits for new directories.
33
- */
71
+ /** Returns the current persistence mode. */
72
+ getMode(): VfsPersistenceMode;
73
+ /** Returns the snapshot file path used in `"fs"` mode, or `null`. */
74
+ getSnapshotPath(): string | null;
75
+ /** Creates a directory (and any missing parents). */
34
76
  mkdir(targetPath: string, mode?: number): void;
35
77
  /**
36
- * Writes UTF-8 text or binary content into file.
37
- *
78
+ * Writes UTF-8 text or binary content into a file.
38
79
  * Parent directories are created when missing.
39
- *
40
- * @param targetPath Destination file path.
41
- * @param content File content as string or Buffer.
42
- * @param options Optional write behavior (mode, compression).
43
80
  */
44
81
  writeFile(targetPath: string, content: string | Buffer, options?: WriteFileOptions): void;
45
82
  /**
46
- * Reads file content as UTF-8 text.
47
- *
48
- * Compressed files are transparently decompressed.
49
- *
50
- * @param targetPath Path to file.
51
- * @returns UTF-8 string content.
83
+ * Reads file content as a UTF-8 string.
84
+ * Gzip-compressed files are transparently decompressed.
52
85
  */
53
86
  readFile(targetPath: string): string;
54
- /**
55
- * Checks whether node exists at path.
56
- *
57
- * @param targetPath Node path.
58
- * @returns True when file or directory exists.
59
- */
87
+ /** Reads file content as a Buffer (decompresses if needed). */
88
+ readFileRaw(targetPath: string): Buffer;
89
+ /** Returns true when a file or directory exists at path. */
60
90
  exists(targetPath: string): boolean;
61
- /**
62
- * Updates mode bits for file or directory.
63
- *
64
- * @param targetPath Node path.
65
- * @param mode New POSIX-like mode.
66
- */
91
+ /** Updates mode bits on a node. */
67
92
  chmod(targetPath: string, mode: number): void;
68
- /**
69
- * Returns metadata for file or directory.
70
- *
71
- * @param targetPath Node path.
72
- * @returns Typed stat object based on node type.
73
- */
93
+ /** Returns metadata for a file or directory. */
74
94
  stat(targetPath: string): VfsNodeStats;
75
- /**
76
- * Lists direct children names of directory.
77
- *
78
- * @param dirPath Directory path, defaults to root.
79
- * @returns Sorted child names.
80
- */
95
+ /** Lists direct children names of a directory (sorted). */
81
96
  list(dirPath?: string): string[];
82
- /**
83
- * Renders ASCII tree view of directory hierarchy.
84
- *
85
- * @param dirPath Directory path, defaults to root.
86
- * @returns Multi-line tree string.
87
- */
97
+ /** Renders ASCII tree view of a directory hierarchy. */
88
98
  tree(dirPath?: string): string;
99
+ private renderTreeLines;
100
+ /** Computes total stored bytes under a path. */
101
+ getUsageBytes(targetPath?: string): number;
102
+ private computeUsage;
103
+ /** Compresses a file's content with gzip in place. */
104
+ compressFile(targetPath: string): void;
105
+ /** Decompresses a gzip-compressed file in place. */
106
+ decompressFile(targetPath: string): void;
89
107
  /**
90
- * Computes total stored file bytes under a path.
91
- *
92
- * File usage is based on in-memory stored bytes, including compressed
93
- * payload size when files are marked as compressed.
94
- *
95
- * @param targetPath File or directory path to measure, defaults to root.
96
- * @returns Total byte usage for file content under target path.
108
+ * Creates a symbolic link.
109
+ * The link node is stored with mode `0o120777` (POSIX symlink convention).
97
110
  */
98
- getUsageBytes(targetPath?: string): number;
111
+ symlink(targetPath: string, linkPath: string): void;
112
+ /** Returns true when the path is a symbolic link node. */
113
+ isSymlink(targetPath: string): boolean;
99
114
  /**
100
- * Compresses file content with gzip and flags node as compressed.
101
- *
102
- * @param targetPath Path to file.
115
+ * Resolves a symlink chain up to `maxDepth` hops.
116
+ * Throws when the chain is too long (circular links).
103
117
  */
104
- compressFile(targetPath: string): void;
118
+ resolveSymlink(linkPath: string, maxDepth?: number): string;
119
+ /** Removes a file or directory node. */
120
+ remove(targetPath: string, options?: RemoveOptions): void;
121
+ /** Moves or renames a node. */
122
+ move(fromPath: string, toPath: string): void;
105
123
  /**
106
- * Decompresses gzip-compressed file content.
124
+ * Exports the entire filesystem as a JSON-serialisable snapshot.
107
125
  *
108
- * @param targetPath Path to file.
126
+ * Works regardless of the persistence mode. Useful for test fixtures,
127
+ * manual backups, or passing VFS state between processes.
109
128
  */
110
- decompressFile(targetPath: string): void;
129
+ toSnapshot(): VfsSnapshot;
130
+ private serializeDir;
131
+ private serializeFile;
111
132
  /**
112
- * Removes file or directory node.
133
+ * Creates a new `VirtualFileSystem` instance (memory mode) from a snapshot.
113
134
  *
114
- * @param targetPath Path to remove.
115
- * @param options Removal options, including recursive delete.
135
+ * @example
136
+ * ```ts
137
+ * const vfs = VirtualFileSystem.fromSnapshot(savedSnapshot);
138
+ * ```
116
139
  */
117
- remove(targetPath: string, options?: RemoveOptions): void;
140
+ static fromSnapshot(snapshot: VfsSnapshot): VirtualFileSystem;
118
141
  /**
119
- * Moves or renames node to destination path.
142
+ * Replaces the current filesystem state with the content of a snapshot.
143
+ * The persistence mode is preserved.
120
144
  *
121
- * @param fromPath Existing source path.
122
- * @param toPath Destination path.
145
+ * @example
146
+ * ```ts
147
+ * vfs.importSnapshot(savedSnapshot);
148
+ * ```
123
149
  */
124
- move(fromPath: string, toPath: string): void;
150
+ importSnapshot(snapshot: VfsSnapshot): void;
151
+ private deserializeDir;
125
152
  }
126
153
  export default VirtualFileSystem;
127
154
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,MAAM,cAAc,CAAC;AActB,cAAM,iBAAkB,SAAQ,YAAY;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,eAAe;IA4BvB;;;;OAIG;gBACS,OAAO,GAAE,MAAsB;IAM3C;;;;OAIG;IACU,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3C;;;;OAIG;IACU,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMzC;;;;;OAKG;IACI,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG,IAAI;IAa5D;;;;;;;;OAQG;IACI,SAAS,CACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,gBAAqB,GAC5B,IAAI;IAyBP;;;;;;;OAOG;IACI,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAe3C;;;;;OAKG;IACI,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAU1C;;;;;OAKG;IACI,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IASpD;;;;;OAKG;IACI,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY;IAsC7C;;;;;OAKG;IACI,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM,EAAE;IAU5C;;;;;OAKG;IACI,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM;IAY1C;;;;;;;;OAQG;IACI,aAAa,CAAC,UAAU,GAAE,MAAY,GAAG,MAAM;IAStD;;;;OAIG;IACI,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAa7C;;;;OAIG;IACI,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAa/C;;;;;OAKG;IACI,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,IAAI;IA6BpE;;;;;OAKG;IACI,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAuBnD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/index.ts"],"names":[],"mappings":"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"}