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.
- package/README.md +871 -1231
- package/benchmark-results.txt +21 -21
- package/biome.json +9 -0
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +127 -15
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +406 -258
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +5 -23
- package/dist/VirtualUserManager/index.d.ts +41 -3
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +83 -21
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +31 -0
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +68 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +48 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +61 -35
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +30 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +25 -35
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +42 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +35 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +33 -0
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/standalone.js +7 -9
- package/package.json +7 -3
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/index.ts +159 -17
- package/src/VirtualFileSystem/index.ts +500 -280
- package/src/VirtualShell/index.ts +5 -33
- package/src/VirtualUserManager/index.ts +92 -26
- package/src/commands/chmod.ts +33 -0
- package/src/commands/cp.ts +76 -0
- package/src/commands/find.ts +61 -0
- package/src/commands/grep.ts +54 -38
- package/src/commands/head.ts +35 -0
- package/src/commands/index.ts +25 -43
- package/src/commands/ln.ts +47 -0
- package/src/commands/mv.ts +43 -0
- package/src/commands/tail.ts +37 -0
- package/src/commands/wc.ts +48 -0
- package/src/index.ts +1 -0
- package/src/standalone.ts +12 -9
- package/standalone.js +102 -0
- package/standalone.js.map +7 -0
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +66 -83
package/benchmark-results.txt
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
Benchmarking VirtualShell concurrency:
|
|
2
2
|
|
|
3
3
|
Running 1 shells...
|
|
4
|
-
Initialized 1 shells in
|
|
5
|
-
Executed shell commands in
|
|
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
|
|
9
|
-
Executed shell commands in
|
|
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
|
|
13
|
-
Executed shell commands in
|
|
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
|
|
17
|
-
Executed shell commands in
|
|
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
|
|
21
|
-
Executed shell commands in
|
|
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
|
|
25
|
-
Executed shell commands in 12ms, RSS now
|
|
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
|
|
29
|
-
Executed shell commands in
|
|
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
|
|
35
|
-
2 2
|
|
36
|
-
5
|
|
37
|
-
10
|
|
38
|
-
20
|
|
39
|
-
50
|
|
40
|
-
100
|
|
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
package/dist/SSHMimic/index.d.ts
CHANGED
|
@@ -5,19 +5,31 @@ declare class SshMimic extends EventEmitter {
|
|
|
5
5
|
port: number;
|
|
6
6
|
server: SshServer | null;
|
|
7
7
|
private shell;
|
|
8
|
-
|
|
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;
|
|
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"}
|
package/dist/SSHMimic/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
|
|
8
|
-
private
|
|
9
|
-
private
|
|
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
|
-
*
|
|
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
|
-
*
|
|
61
|
+
* In `"memory"` mode: no-op (kept for API compatibility).
|
|
20
62
|
*/
|
|
21
63
|
restoreMirror(): Promise<void>;
|
|
22
64
|
/**
|
|
23
|
-
*
|
|
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
|
-
*
|
|
68
|
+
* In `"memory"` mode: emits `"mirror:flush"` and returns (no disk write).
|
|
26
69
|
*/
|
|
27
70
|
flushMirror(): Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
124
|
+
* Exports the entire filesystem as a JSON-serialisable snapshot.
|
|
107
125
|
*
|
|
108
|
-
*
|
|
126
|
+
* Works regardless of the persistence mode. Useful for test fixtures,
|
|
127
|
+
* manual backups, or passing VFS state between processes.
|
|
109
128
|
*/
|
|
110
|
-
|
|
129
|
+
toSnapshot(): VfsSnapshot;
|
|
130
|
+
private serializeDir;
|
|
131
|
+
private serializeFile;
|
|
111
132
|
/**
|
|
112
|
-
*
|
|
133
|
+
* Creates a new `VirtualFileSystem` instance (memory mode) from a snapshot.
|
|
113
134
|
*
|
|
114
|
-
* @
|
|
115
|
-
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const vfs = VirtualFileSystem.fromSnapshot(savedSnapshot);
|
|
138
|
+
* ```
|
|
116
139
|
*/
|
|
117
|
-
|
|
140
|
+
static fromSnapshot(snapshot: VfsSnapshot): VirtualFileSystem;
|
|
118
141
|
/**
|
|
119
|
-
*
|
|
142
|
+
* Replaces the current filesystem state with the content of a snapshot.
|
|
143
|
+
* The persistence mode is preserved.
|
|
120
144
|
*
|
|
121
|
-
* @
|
|
122
|
-
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* vfs.importSnapshot(savedSnapshot);
|
|
148
|
+
* ```
|
|
123
149
|
*/
|
|
124
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAU3C,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EAIX,gBAAgB,EAChB,MAAM,cAAc,CAAC;AAItB;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEjD,MAAM,WAAW,UAAU;IAC1B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,cAAM,iBAAkB,SAAQ,YAAY;IAC3C,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;gBAEjC,OAAO,GAAE,UAAe;IAqBpC,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,cAAc;IAwBtB;;;;;OAKG;IACU,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3C;;;;;OAKG;IACU,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAazC,4CAA4C;IACrC,OAAO,IAAI,kBAAkB;IAIpC,qEAAqE;IAC9D,eAAe,IAAI,MAAM,GAAG,IAAI;IAMvC,qDAAqD;IAC9C,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG,IAAI;IAiB5D;;;OAGG;IACI,SAAS,CACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,gBAAqB,GAC5B,IAAI;IAuCP;;;OAGG;IACI,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAY3C,+DAA+D;IACxD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAY9C,4DAA4D;IACrD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAS1C,mCAAmC;IAC5B,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpD,gDAAgD;IACzC,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY;IA6B7C,2DAA2D;IACpD,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM,EAAE;IAS5C,wDAAwD;IACjD,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM;IAU1C,OAAO,CAAC,eAAe;IAqBvB,gDAAgD;IACzC,aAAa,CAAC,UAAU,GAAE,MAAY,GAAG,MAAM;IAItD,OAAO,CAAC,YAAY;IASpB,sDAAsD;IAC/C,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAY7C,oDAAoD;IAC7C,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAY/C;;;OAGG;IACI,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IA2B1D,0DAA0D;IACnD,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAS7C;;;OAGG;IACI,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM;IAsB7D,wCAAwC;IACjC,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,IAAI;IAsBpE,+BAA+B;IACxB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IA8BnD;;;;;OAKG;IACI,UAAU,IAAI,WAAW;IAIhC,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,aAAa;IAYrB;;;;;;;OAOG;WACW,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,iBAAiB;IAMpE;;;;;;;;OAQG;IACI,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKlD,OAAO,CAAC,cAAc;CAkCtB;AAED,eAAe,iBAAiB,CAAC"}
|