typescript-virtual-container 1.4.1 → 1.4.2
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/.vscode/settings.json +2 -0
- package/README.md +5 -1
- package/benchmark-virtualshell.ts +3 -11
- package/builds/self-standalone.js +202 -202
- package/builds/self-standalone.js.map +3 -3
- package/builds/standalone-wo-sftp.js +8 -8
- package/builds/standalone-wo-sftp.js.map +3 -3
- package/builds/standalone.js +42 -42
- package/builds/standalone.js.map +3 -3
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +6 -10
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +4 -4
- package/dist/commands/helpers.js +1 -1
- package/dist/commands/history.js +2 -2
- package/dist/modules/linuxRootfs.d.ts +1 -1
- package/dist/modules/linuxRootfs.d.ts.map +1 -1
- package/dist/modules/linuxRootfs.js +5 -5
- package/dist/self-standalone.js +149 -102
- package/package.json +1 -1
- package/src/VirtualShell/shell.ts +6 -11
- package/src/VirtualUserManager/index.ts +4 -4
- package/src/commands/helpers.ts +1 -1
- package/src/commands/history.ts +2 -2
- package/src/modules/linuxRootfs.ts +6 -6
- package/src/self-standalone.ts +190 -141
- package/tests/helpers.test.ts +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/shell.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,GAAG,CAAC;AAMvD,OAAO,EAGN,KAAK,YAAY,EAEjB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAwBpD,wBAAgB,UAAU,CACzB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,aAAa,oBAAY,EACzB,YAAY,EAAE,YAAY,YAAyB,EACnD,KAAK,EAAE,YAAY,GACjB,IAAI,
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/shell.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,GAAG,CAAC;AAMvD,OAAO,EAGN,KAAK,YAAY,EAEjB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAwBpD,wBAAgB,UAAU,CACzB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,aAAa,oBAAY,EACzB,YAAY,EAAE,YAAY,YAAyB,EACnD,KAAK,EAAE,YAAY,GACjB,IAAI,CA0nBN"}
|
|
@@ -8,7 +8,7 @@ import { buildPrompt } from "../SSHMimic/prompt";
|
|
|
8
8
|
export function startShell(properties, stream, authUser, hostname, sessionId, remoteAddress = "unknown", terminalSize = { cols: 80, rows: 24 }, shell) {
|
|
9
9
|
let lineBuffer = "";
|
|
10
10
|
let cursorPos = 0;
|
|
11
|
-
let history = loadHistory(shell.vfs);
|
|
11
|
+
let history = loadHistory(shell.vfs, authUser);
|
|
12
12
|
let historyIndex = null;
|
|
13
13
|
let historyDraft = "";
|
|
14
14
|
let cwd = `/home/${authUser}`;
|
|
@@ -252,10 +252,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
252
252
|
history = history.slice(history.length - 500);
|
|
253
253
|
}
|
|
254
254
|
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
255
|
-
shell.vfs.writeFile(
|
|
255
|
+
shell.vfs.writeFile(`/home/${authUser}/.bash_history`, data);
|
|
256
256
|
}
|
|
257
257
|
function readLastLogin() {
|
|
258
|
-
const lastlogPath = `/
|
|
258
|
+
const lastlogPath = `/home/${authUser}/.lastlog.json`;
|
|
259
259
|
if (!shell.vfs.exists(lastlogPath)) {
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
@@ -267,11 +267,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
function writeLastLogin(nowIso) {
|
|
270
|
-
const
|
|
271
|
-
if (!shell.vfs.exists(dir)) {
|
|
272
|
-
shell.vfs.mkdir(dir, 0o700);
|
|
273
|
-
}
|
|
274
|
-
const lastlogPath = `${dir}/${authUser}.json`;
|
|
270
|
+
const lastlogPath = `/home/${authUser}/.lastlog`;
|
|
275
271
|
shell.vfs.writeFile(lastlogPath, JSON.stringify({ at: nowIso, from: remoteAddress }));
|
|
276
272
|
}
|
|
277
273
|
function renderLoginBanner() {
|
|
@@ -492,8 +488,8 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
492
488
|
}
|
|
493
489
|
});
|
|
494
490
|
}
|
|
495
|
-
function loadHistory(vfs) {
|
|
496
|
-
const historyPath =
|
|
491
|
+
function loadHistory(vfs, authUser) {
|
|
492
|
+
const historyPath = `/home/${authUser}/.bash_history`;
|
|
497
493
|
if (!vfs.exists(historyPath)) {
|
|
498
494
|
vfs.writeFile(historyPath, "");
|
|
499
495
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualUserManager/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAE1D,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IACjC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACpC,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CAClB;AAYD;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAqBlD,OAAO,CAAC,QAAQ,CAAC,GAAG;IAGpB,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IAvBrC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAwC;IAC3E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualUserManager/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAE1D,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IACjC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACpC,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CAClB;AAYD;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAqBlD,OAAO,CAAC,QAAQ,CAAC,GAAG;IAGpB,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IAvBrC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAwC;IAC3E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkB;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,OAAO,CAAK;IAEpB;;;;;;OAMG;gBAEe,GAAG,EAAE,iBAAiB,EAGtB,mBAAmB,GAAE,OAAc;IAMrD;;;OAGG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAsCxC;;;;;OAKG;IACU,aAAa,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAehB;;;;OAIG;IACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxD;;;;;OAKG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKrD;;;;;OAKG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAU9C;;;;;;;;OAQG;IACI,sBAAsB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAAG,MAAM,GAC1B,IAAI;IAoCP;;;;;;OAMG;IACI,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAsBlE;;;;;OAKG;IACU,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvE;;;;;OAKG;IACI,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMvD;;;;;;OAMG;IACU,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3E;;;;;OAKG;IACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxD;;;;;OAKG;IACI,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAK1C;;;;;OAKG;IACU,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;;;;OAKG;IACU,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D;;;;;;;;;OASG;IACI,eAAe,CACrB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACnB,oBAAoB;IAkBvB;;;;;;OAMG;IACI,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI;IAiBpE;;;;;;;;;;OAUG;IACI,aAAa,CACnB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACnB,IAAI;IAkBP;;;;;;OAMG;IACI,kBAAkB,IAAI,oBAAoB,EAAE;IAOnD;;;;OAIG;IACI,SAAS,IAAI,MAAM,EAAE;IAI5B,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,iBAAiB;YAwBX,OAAO;IA0CrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,YAAY;IAoBpB;;;;;;;OAOG;IACI,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAU7C;;;;;;;;OAQG;IACH;;;;;OAKG;IACI,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,MAAM;IAWxD,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IAEJ;;;;;;OAMG;IACI,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ3E;;;;OAIG;IACI,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD;;;;;OAKG;IACI,iBAAiB,CACvB,QAAQ,EAAE,MAAM,GACd,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAGxC"}
|
|
@@ -18,10 +18,10 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
18
18
|
autoSudoForNewUsers;
|
|
19
19
|
static recordCache = new Map();
|
|
20
20
|
static fastPasswordHash = resolveFastPasswordHash();
|
|
21
|
-
usersPath = "/
|
|
22
|
-
sudoersPath = "/
|
|
23
|
-
quotasPath = "/
|
|
24
|
-
authDirPath = "
|
|
21
|
+
usersPath = "/etc/htpasswd";
|
|
22
|
+
sudoersPath = "/etc/sudoers";
|
|
23
|
+
quotasPath = "/etc/quotas";
|
|
24
|
+
authDirPath = "/.virtual-env-js/.auth";
|
|
25
25
|
users = new Map();
|
|
26
26
|
sudoers = new Set();
|
|
27
27
|
quotas = new Map();
|
package/dist/commands/helpers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
const PROTECTED_PREFIXES = ["
|
|
3
|
+
const PROTECTED_PREFIXES = ["/.virtual-env-js/.auth", "/etc/htpasswd"];
|
|
4
4
|
function normalizeFetchUrl(input) {
|
|
5
5
|
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input)) {
|
|
6
6
|
return input;
|
package/dist/commands/history.js
CHANGED
|
@@ -8,9 +8,9 @@ export const historyCommand = {
|
|
|
8
8
|
description: "Display command history",
|
|
9
9
|
category: "shell",
|
|
10
10
|
params: ["[n]"],
|
|
11
|
-
run: ({ args, shell }) => {
|
|
11
|
+
run: ({ args, shell, authUser }) => {
|
|
12
12
|
// History is persisted in the VFS by the interactive shell
|
|
13
|
-
const histPath =
|
|
13
|
+
const histPath = `/home/${authUser}/.bash_history`;
|
|
14
14
|
if (!shell.vfs.exists(histPath)) {
|
|
15
15
|
return { stdout: "", exitCode: 0 };
|
|
16
16
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Called once during VirtualShell initialization. Idempotent — skips
|
|
7
7
|
* paths that already exist so FS-mode snapshots survive restarts.
|
|
8
8
|
*/
|
|
9
|
-
import type { ShellProperties } from "../VirtualShell";
|
|
10
9
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
10
|
+
import type { ShellProperties } from "../VirtualShell";
|
|
11
11
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
12
12
|
/**
|
|
13
13
|
* Sync `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAgHhE;;;;;GAKG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,GACvB,IAAI,CAsCN;AAgED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAC1B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,oBAAoB,EAAE,GAC/D,IAAI,CAmJN;AAuND;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,eAAe,EACtB,cAAc,EAAE,MAAM,GACpB,IAAI,CAYN"}
|
|
@@ -394,18 +394,18 @@ function bootstrapTmp(vfs) {
|
|
|
394
394
|
// ─── /root ────────────────────────────────────────────────────────────────────
|
|
395
395
|
function bootstrapRoot(vfs) {
|
|
396
396
|
ensureDir(vfs, "/root", 0o700);
|
|
397
|
-
ensureFile(vfs, "/root/.bashrc", `${[
|
|
397
|
+
ensureFile(vfs, "/home/root/.bashrc", `${[
|
|
398
398
|
"# root .bashrc",
|
|
399
399
|
"export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
|
|
400
400
|
"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
401
401
|
"alias ll='ls -la'",
|
|
402
402
|
"alias la='ls -A'",
|
|
403
403
|
].join("\n")}\n`);
|
|
404
|
-
ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
404
|
+
ensureFile(vfs, "/home/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
405
405
|
// Fix: /home/root should map to /root for root user
|
|
406
|
-
if (!vfs.exists("/home/root")) {
|
|
407
|
-
|
|
408
|
-
}
|
|
406
|
+
// if (!vfs.exists("/home/root")) {
|
|
407
|
+
// vfs.symlink("/root", "/home/root");
|
|
408
|
+
// }
|
|
409
409
|
}
|
|
410
410
|
// ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────
|
|
411
411
|
function bootstrapMisc(vfs) {
|
package/dist/self-standalone.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { basename } from "node:path";
|
|
3
4
|
import { stdin, stdout } from "node:process";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
6
|
+
import { getCommandNames } from "./commands/registry";
|
|
5
7
|
import { makeDefaultEnv, runCommand } from "./commands/runtime";
|
|
6
8
|
import { spawnNanoEditorProcess } from "./modules/shellInteractive";
|
|
9
|
+
import { resolvePath } from "./modules/shellRuntime";
|
|
7
10
|
import { buildLoginBanner } from "./SSHMimic/loginBanner";
|
|
8
11
|
import { buildPrompt } from "./SSHMimic/prompt";
|
|
9
12
|
import { VirtualShell } from "./VirtualShell";
|
|
10
13
|
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
11
14
|
const argv = process.argv.slice(2);
|
|
15
|
+
// ── CLI args ──────────────────────────────────────────────────────────────────
|
|
12
16
|
function readUserArg() {
|
|
13
17
|
for (let index = 0; index < argv.length; index += 1) {
|
|
14
18
|
const current = argv[index];
|
|
@@ -30,11 +34,11 @@ const virtualShell = new VirtualShell(hostname, undefined, {
|
|
|
30
34
|
mode: "fs",
|
|
31
35
|
snapshotPath: ".vfs",
|
|
32
36
|
});
|
|
37
|
+
// ── VFS helpers ───────────────────────────────────────────────────────────────
|
|
33
38
|
function readLastLogin(username) {
|
|
34
|
-
const lastlogPath = `/
|
|
35
|
-
if (!virtualShell.vfs.exists(lastlogPath))
|
|
39
|
+
const lastlogPath = `/home/${username}/.lastlog`;
|
|
40
|
+
if (!virtualShell.vfs.exists(lastlogPath))
|
|
36
41
|
return null;
|
|
37
|
-
}
|
|
38
42
|
try {
|
|
39
43
|
return JSON.parse(virtualShell.vfs.readFile(lastlogPath));
|
|
40
44
|
}
|
|
@@ -42,6 +46,63 @@ function readLastLogin(username) {
|
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
function writeLastLogin(username, from) {
|
|
50
|
+
virtualShell.vfs.writeFile(`/home/${username}/.lastlog`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
51
|
+
}
|
|
52
|
+
async function flushVfs() {
|
|
53
|
+
await virtualShell.vfs.flushMirror();
|
|
54
|
+
}
|
|
55
|
+
function loadHistory(authUser) {
|
|
56
|
+
const historyPath = `/home/${authUser}/.bash_history`;
|
|
57
|
+
if (!virtualShell.vfs.exists(historyPath)) {
|
|
58
|
+
virtualShell.vfs.writeFile(historyPath, "");
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return virtualShell.vfs
|
|
62
|
+
.readFile(historyPath)
|
|
63
|
+
.split("\n")
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter((line) => line.length > 0);
|
|
66
|
+
}
|
|
67
|
+
function saveHistory(history, authUser) {
|
|
68
|
+
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
69
|
+
virtualShell.vfs.writeFile(`/home/${authUser}/.bash_history`, data);
|
|
70
|
+
}
|
|
71
|
+
// ── Tab completion ────────────────────────────────────────────────────────────
|
|
72
|
+
function listPathCompletions(vfs, cwd, prefix) {
|
|
73
|
+
const slashIndex = prefix.lastIndexOf("/");
|
|
74
|
+
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
75
|
+
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
76
|
+
const basePath = resolvePath(cwd, dirPart || ".");
|
|
77
|
+
try {
|
|
78
|
+
return vfs
|
|
79
|
+
.list(basePath)
|
|
80
|
+
.filter((e) => !e.startsWith(".") && e.startsWith(namePart))
|
|
81
|
+
.map((e) => {
|
|
82
|
+
const fullPath = path.posix.join(basePath, e);
|
|
83
|
+
const st = vfs.stat(fullPath);
|
|
84
|
+
return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
|
|
85
|
+
})
|
|
86
|
+
.sort();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function makeCompleter(getState) {
|
|
93
|
+
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
94
|
+
return (line, cb) => {
|
|
95
|
+
const { cwd } = getState();
|
|
96
|
+
// Extract the token under/before cursor (last whitespace-separated word)
|
|
97
|
+
const token = line.split(/\s+/).at(-1) ?? "";
|
|
98
|
+
const isFirstToken = line.trimStart() === token;
|
|
99
|
+
const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
|
|
100
|
+
const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
|
|
101
|
+
const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
|
|
102
|
+
cb(null, [hits, token]);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── Hidden password input ─────────────────────────────────────────────────────
|
|
45
106
|
function askHiddenQuestion(rl, promptText) {
|
|
46
107
|
return new Promise((resolve) => {
|
|
47
108
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
@@ -52,10 +113,8 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
52
113
|
let buffer = "";
|
|
53
114
|
const cleanup = () => {
|
|
54
115
|
stdin.off("data", onData);
|
|
55
|
-
if (!wasRawMode)
|
|
116
|
+
if (!wasRawMode)
|
|
56
117
|
stdin.setRawMode(false);
|
|
57
|
-
}
|
|
58
|
-
rl.resume();
|
|
59
118
|
};
|
|
60
119
|
const finish = (value) => {
|
|
61
120
|
cleanup();
|
|
@@ -64,8 +123,8 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
64
123
|
};
|
|
65
124
|
const onData = (chunk) => {
|
|
66
125
|
const input = chunk.toString("utf8");
|
|
67
|
-
for (let
|
|
68
|
-
const ch = input[
|
|
126
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
127
|
+
const ch = input[i];
|
|
69
128
|
if (ch === "\r" || ch === "\n") {
|
|
70
129
|
finish(buffer);
|
|
71
130
|
return;
|
|
@@ -74,46 +133,20 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
74
133
|
buffer = buffer.slice(0, -1);
|
|
75
134
|
continue;
|
|
76
135
|
}
|
|
77
|
-
if (ch >= " ")
|
|
136
|
+
if (ch >= " ")
|
|
78
137
|
buffer += ch;
|
|
79
|
-
}
|
|
80
138
|
}
|
|
81
139
|
};
|
|
140
|
+
// Pause readline so it doesn't eat our raw keystrokes
|
|
82
141
|
rl.pause();
|
|
83
142
|
stdout.write(promptText);
|
|
84
|
-
if (!wasRawMode)
|
|
143
|
+
if (!wasRawMode)
|
|
85
144
|
stdin.setRawMode(true);
|
|
86
|
-
}
|
|
87
145
|
stdin.resume();
|
|
88
146
|
stdin.on("data", onData);
|
|
89
147
|
});
|
|
90
148
|
}
|
|
91
|
-
|
|
92
|
-
const dir = "/virtual-env-js/.lastlog";
|
|
93
|
-
if (!virtualShell.vfs.exists(dir)) {
|
|
94
|
-
virtualShell.vfs.mkdir(dir, 0o700);
|
|
95
|
-
}
|
|
96
|
-
virtualShell.vfs.writeFile(`/virtual-env-js/.lastlog/${username}.json`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
97
|
-
}
|
|
98
|
-
async function flushVfs() {
|
|
99
|
-
await virtualShell.vfs.flushMirror();
|
|
100
|
-
}
|
|
101
|
-
function loadHistory() {
|
|
102
|
-
const historyPath = "/virtual-env-js/.bash_history";
|
|
103
|
-
if (!virtualShell.vfs.exists(historyPath)) {
|
|
104
|
-
virtualShell.vfs.writeFile(historyPath, "");
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
return virtualShell.vfs
|
|
108
|
-
.readFile(historyPath)
|
|
109
|
-
.split("\n")
|
|
110
|
-
.map((line) => line.trim())
|
|
111
|
-
.filter((line) => line.length > 0);
|
|
112
|
-
}
|
|
113
|
-
function saveHistory(history) {
|
|
114
|
-
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
115
|
-
virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
|
|
116
|
-
}
|
|
149
|
+
// ── Session state helper ──────────────────────────────────────────────────────
|
|
117
150
|
function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
118
151
|
let authUser = authUserState;
|
|
119
152
|
let cwd = cwdState;
|
|
@@ -131,21 +164,16 @@ function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
|
131
164
|
}
|
|
132
165
|
return { authUser, cwd };
|
|
133
166
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
168
|
+
virtualShell.addCommand("demo", [], () => ({
|
|
169
|
+
stdout: "This is a demo command. It does nothing useful.",
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
}));
|
|
172
|
+
// ── Main shell ────────────────────────────────────────────────────────────────
|
|
140
173
|
async function runReadlineShell() {
|
|
141
|
-
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
142
174
|
await virtualShell.ensureInitialized();
|
|
143
|
-
let history = loadHistory();
|
|
144
|
-
const rlWithHistory = rl;
|
|
145
|
-
rlWithHistory.history = [...history].reverse();
|
|
146
175
|
const selectedUser = initialUser.trim() || "root";
|
|
147
|
-
|
|
148
|
-
if (!userExists) {
|
|
176
|
+
if (virtualShell.users.getPasswordHash(selectedUser) === null) {
|
|
149
177
|
process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
|
|
150
178
|
process.exit(1);
|
|
151
179
|
}
|
|
@@ -154,10 +182,19 @@ async function runReadlineShell() {
|
|
|
154
182
|
let cwd = `/home/${authUser}`;
|
|
155
183
|
shellEnv.vars.PWD = cwd;
|
|
156
184
|
const remoteAddress = "localhost";
|
|
157
|
-
const terminalSize = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
|
|
186
|
+
let history = loadHistory(authUser);
|
|
187
|
+
// completer reads cwd via closure — always current
|
|
188
|
+
const rl = createInterface({
|
|
189
|
+
input: stdin,
|
|
190
|
+
output: stdout,
|
|
191
|
+
terminal: true,
|
|
192
|
+
completer: makeCompleter(() => ({ cwd })),
|
|
193
|
+
});
|
|
194
|
+
// Sync readline's internal history with our VFS history
|
|
195
|
+
const rlWithHistory = rl;
|
|
196
|
+
rlWithHistory.history = [...history].reverse();
|
|
197
|
+
// ── nano editor ────────────────────────────────────────────────────────────
|
|
161
198
|
async function startNanoEditor(targetPath, initialContent, tempPath) {
|
|
162
199
|
if (virtualShell.vfs.exists(targetPath)) {
|
|
163
200
|
await writeFile(tempPath, initialContent, "utf8");
|
|
@@ -169,20 +206,16 @@ async function runReadlineShell() {
|
|
|
169
206
|
end: () => undefined,
|
|
170
207
|
});
|
|
171
208
|
const wasRawMode = Boolean(stdin.isRaw);
|
|
172
|
-
const forwardInput = (chunk) => {
|
|
173
|
-
editor.stdin.write(chunk);
|
|
174
|
-
};
|
|
209
|
+
const forwardInput = (chunk) => { editor.stdin.write(chunk); };
|
|
175
210
|
stdin.resume();
|
|
176
|
-
if (!wasRawMode)
|
|
211
|
+
if (!wasRawMode)
|
|
177
212
|
stdin.setRawMode(true);
|
|
178
|
-
}
|
|
179
213
|
stdin.on("data", forwardInput);
|
|
180
214
|
await new Promise((resolve) => {
|
|
181
215
|
const cleanup = () => {
|
|
182
216
|
stdin.off("data", forwardInput);
|
|
183
|
-
if (!wasRawMode)
|
|
217
|
+
if (!wasRawMode)
|
|
184
218
|
stdin.setRawMode(false);
|
|
185
|
-
}
|
|
186
219
|
rl.resume();
|
|
187
220
|
};
|
|
188
221
|
editor.on("error", (error) => {
|
|
@@ -199,7 +232,7 @@ async function runReadlineShell() {
|
|
|
199
232
|
await flushVfs();
|
|
200
233
|
}
|
|
201
234
|
catch {
|
|
202
|
-
//
|
|
235
|
+
// save skipped or temp file missing
|
|
203
236
|
}
|
|
204
237
|
await unlink(tempPath).catch(() => undefined);
|
|
205
238
|
stdout.write("\r\n");
|
|
@@ -207,6 +240,7 @@ async function runReadlineShell() {
|
|
|
207
240
|
});
|
|
208
241
|
});
|
|
209
242
|
}
|
|
243
|
+
// ── challenge handlers ─────────────────────────────────────────────────────
|
|
210
244
|
async function handleSudoChallenge(challenge) {
|
|
211
245
|
if (challenge.onPassword) {
|
|
212
246
|
let promptText = challenge.prompt;
|
|
@@ -275,6 +309,7 @@ async function runReadlineShell() {
|
|
|
275
309
|
break;
|
|
276
310
|
}
|
|
277
311
|
}
|
|
312
|
+
// handleCommandResult must be declared before the "line" handler
|
|
278
313
|
async function handleCommandResult(result) {
|
|
279
314
|
if (result.openEditor) {
|
|
280
315
|
await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
|
|
@@ -288,32 +323,26 @@ async function runReadlineShell() {
|
|
|
288
323
|
await handlePasswordChallenge(result.passwordChallenge);
|
|
289
324
|
return;
|
|
290
325
|
}
|
|
326
|
+
if (result.clearScreen) {
|
|
327
|
+
stdout.write("\u001b[2J\u001b[H");
|
|
328
|
+
console.clear();
|
|
329
|
+
}
|
|
291
330
|
if (result.stdout) {
|
|
292
331
|
stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
293
332
|
}
|
|
294
333
|
if (result.stderr) {
|
|
295
334
|
process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
296
335
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
const updatedState = applySessionState(authUser, cwd, result, shellEnv);
|
|
302
|
-
authUser = updatedState.authUser;
|
|
303
|
-
cwd = updatedState.cwd;
|
|
336
|
+
const updated = applySessionState(authUser, cwd, result, shellEnv);
|
|
337
|
+
authUser = updated.authUser;
|
|
338
|
+
cwd = updated.cwd;
|
|
304
339
|
if (result.closeSession) {
|
|
305
340
|
await flushVfs();
|
|
306
341
|
rl.close();
|
|
307
342
|
process.exit(result.exitCode ?? 0);
|
|
308
343
|
}
|
|
309
344
|
}
|
|
310
|
-
|
|
311
|
-
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
312
|
-
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
313
|
-
process.stderr.write("self-standalone: authentication failed\n");
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
345
|
+
// ── Prompt helper ──────────────────────────────────────────────────────────
|
|
317
346
|
const renderPrompt = () => {
|
|
318
347
|
const cwdLabel = cwd === `/home/${authUser}` ? "~" : basename(cwd) || "/";
|
|
319
348
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
@@ -322,50 +351,68 @@ async function runReadlineShell() {
|
|
|
322
351
|
rl.setPrompt(renderPrompt());
|
|
323
352
|
rl.prompt();
|
|
324
353
|
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
process.exit(0);
|
|
335
|
-
})();
|
|
336
|
-
});
|
|
354
|
+
// ── Auth (password gate) ───────────────────────────────────────────────────
|
|
355
|
+
if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
|
|
356
|
+
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
357
|
+
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
358
|
+
process.stderr.write("self-standalone: authentication failed\n");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// ── Login banner ───────────────────────────────────────────────────────────
|
|
337
363
|
stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
|
|
338
364
|
writeLastLogin(authUser, remoteAddress);
|
|
339
365
|
await flushVfs();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
// ── Event-driven line handler (enables completer) ──────────────────────────
|
|
367
|
+
//
|
|
368
|
+
// Key insight: readline's completer only fires when readline itself owns
|
|
369
|
+
// stdin (i.e. rl is not paused). We use the event-driven "line" pattern
|
|
370
|
+
// instead of a while(true)+rl.once("line") loop so readline stays active
|
|
371
|
+
// between commands. We pause only while awaiting async work, then resume
|
|
372
|
+
// immediately before re-prompting so the next Tab press is caught.
|
|
373
|
+
let busy = false;
|
|
374
|
+
rl.on("line", async (inputLine) => {
|
|
375
|
+
if (busy)
|
|
376
|
+
return; // shouldn't happen but guard re-entrancy
|
|
377
|
+
busy = true;
|
|
345
378
|
rl.pause();
|
|
346
|
-
|
|
379
|
+
const trimmed = inputLine.trim();
|
|
380
|
+
if (trimmed.length > 0) {
|
|
347
381
|
history.push(inputLine);
|
|
348
|
-
if (history.length > 500)
|
|
382
|
+
if (history.length > 500)
|
|
349
383
|
history = history.slice(history.length - 500);
|
|
350
|
-
|
|
351
|
-
saveHistory(history);
|
|
384
|
+
saveHistory(history, authUser);
|
|
352
385
|
rlWithHistory.history = [...history].reverse();
|
|
353
386
|
}
|
|
354
387
|
const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
|
|
355
388
|
await handleCommandResult(result);
|
|
356
389
|
await flushVfs();
|
|
357
|
-
|
|
390
|
+
busy = false;
|
|
391
|
+
// Resume before prompt so readline can handle Tab on the next input
|
|
358
392
|
rl.resume();
|
|
359
|
-
|
|
393
|
+
prompt();
|
|
394
|
+
});
|
|
395
|
+
rl.on("SIGINT", () => {
|
|
396
|
+
stdout.write("^C\n");
|
|
397
|
+
rl.write("", { ctrl: true, name: "u" });
|
|
398
|
+
prompt();
|
|
399
|
+
});
|
|
400
|
+
rl.on("close", () => {
|
|
401
|
+
void flushVfs().then(() => {
|
|
402
|
+
console.log("");
|
|
403
|
+
process.exit(0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// Initial prompt — readline is already active, completer live from first keystroke
|
|
407
|
+
prompt();
|
|
360
408
|
}
|
|
361
409
|
runReadlineShell().catch((error) => {
|
|
362
410
|
console.error("Failed to start readline SSH emulation:", error);
|
|
363
411
|
process.exit(1);
|
|
364
412
|
});
|
|
365
413
|
process.on("uncaughtException", (error) => {
|
|
366
|
-
console.
|
|
414
|
+
console.error("Uncaught exception:", error);
|
|
367
415
|
});
|
|
368
416
|
process.on("unhandledRejection", (error, promise) => {
|
|
369
|
-
console.
|
|
370
|
-
console.log(" The error was: ", error);
|
|
417
|
+
console.error("Unhandled rejection at:", promise, "error:", error);
|
|
371
418
|
});
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ export function startShell(
|
|
|
52
52
|
): void {
|
|
53
53
|
let lineBuffer = "";
|
|
54
54
|
let cursorPos = 0;
|
|
55
|
-
let history = loadHistory(shell.vfs);
|
|
55
|
+
let history = loadHistory(shell.vfs, authUser);
|
|
56
56
|
let historyIndex: number | null = null;
|
|
57
57
|
let historyDraft = "";
|
|
58
58
|
let cwd = `/home/${authUser}`;
|
|
@@ -388,11 +388,11 @@ export function startShell(
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
391
|
-
shell.vfs.writeFile(
|
|
391
|
+
shell.vfs.writeFile(`/home/${authUser}/.bash_history`, data);
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
function readLastLogin(): { at: string; from: string } | null {
|
|
395
|
-
const lastlogPath = `/
|
|
395
|
+
const lastlogPath = `/home/${authUser}/.lastlog.json`;
|
|
396
396
|
if (!shell.vfs.exists(lastlogPath)) {
|
|
397
397
|
return null;
|
|
398
398
|
}
|
|
@@ -408,12 +408,7 @@ export function startShell(
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
function writeLastLogin(nowIso: string): void {
|
|
411
|
-
const
|
|
412
|
-
if (!shell.vfs.exists(dir)) {
|
|
413
|
-
shell.vfs.mkdir(dir, 0o700);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const lastlogPath = `${dir}/${authUser}.json`;
|
|
411
|
+
const lastlogPath = `/home/${authUser}/.lastlog`;
|
|
417
412
|
shell.vfs.writeFile(
|
|
418
413
|
lastlogPath,
|
|
419
414
|
JSON.stringify({ at: nowIso, from: remoteAddress }),
|
|
@@ -690,8 +685,8 @@ export function startShell(
|
|
|
690
685
|
});
|
|
691
686
|
}
|
|
692
687
|
|
|
693
|
-
function loadHistory(vfs: VirtualFileSystem): string[] {
|
|
694
|
-
const historyPath =
|
|
688
|
+
function loadHistory(vfs: VirtualFileSystem, authUser: string): string[] {
|
|
689
|
+
const historyPath = `/home/${authUser}/.bash_history`;
|
|
695
690
|
if (!vfs.exists(historyPath)) {
|
|
696
691
|
vfs.writeFile(historyPath, "");
|
|
697
692
|
return [];
|
|
@@ -47,10 +47,10 @@ const perf: PerfLogger = createPerfLogger("VirtualUserManager");
|
|
|
47
47
|
export class VirtualUserManager extends EventEmitter {
|
|
48
48
|
private static readonly recordCache = new Map<string, VirtualUserRecord>();
|
|
49
49
|
private static readonly fastPasswordHash = resolveFastPasswordHash();
|
|
50
|
-
private readonly usersPath = "/
|
|
51
|
-
private readonly sudoersPath = "/
|
|
52
|
-
private readonly quotasPath = "/
|
|
53
|
-
private readonly authDirPath = "
|
|
50
|
+
private readonly usersPath = "/etc/htpasswd";
|
|
51
|
+
private readonly sudoersPath = "/etc/sudoers";
|
|
52
|
+
private readonly quotasPath = "/etc/quotas";
|
|
53
|
+
private readonly authDirPath = "/.virtual-env-js/.auth";
|
|
54
54
|
private readonly users = new Map<string, VirtualUserRecord>();
|
|
55
55
|
private readonly sudoers = new Set<string>();
|
|
56
56
|
private readonly quotas = new Map<string, number>();
|
package/src/commands/helpers.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type VirtualFileSystem from "../VirtualFileSystem";
|
|
|
4
4
|
import type { VirtualPackageManager } from "../VirtualPackageManager";
|
|
5
5
|
import type { VirtualShell } from "../VirtualShell";
|
|
6
6
|
|
|
7
|
-
const PROTECTED_PREFIXES = ["
|
|
7
|
+
const PROTECTED_PREFIXES = ["/.virtual-env-js/.auth", "/etc/htpasswd"] as const;
|
|
8
8
|
|
|
9
9
|
function normalizeFetchUrl(input: string): string {
|
|
10
10
|
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input)) {
|