rastack 0.0.40 → 0.0.41

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.41](https://github.com/theserverkid/reactapistack/compare/v0.0.40...v0.0.41) (2026-07-12)
6
+
5
7
  ### [0.0.40](https://github.com/theserverkid/reactapistack/compare/v0.0.39...v0.0.40) (2026-07-12)
6
8
 
7
9
 
@@ -56,6 +56,12 @@ const path = __importStar(require("path"));
56
56
  const update_1 = require("./update");
57
57
  /** The npm executable for this platform (`npm.cmd` on Windows, else `npm`). */
58
58
  const NPM = (0, update_1.npmBinary)();
59
+ /**
60
+ * Whether to launch npm through a shell. Required on Windows, where a bare
61
+ * `.cmd` spawn is rejected with `EINVAL` since the Node CVE-2024-27980 fix;
62
+ * false on POSIX so the direct spawn stays injection-free.
63
+ */
64
+ const NPM_SHELL = (0, update_1.npmSpawnShell)();
59
65
  /** The installed rastack package directory (…/rastack), one up from dist/. */
60
66
  const PKG_DIR = path.join(__dirname, "..");
61
67
  const PKG_JSON = path.join(PKG_DIR, "package.json");
@@ -73,6 +79,7 @@ function npmCapture(args) {
73
79
  try {
74
80
  return (0, child_process_1.execFileSync)(NPM, args, {
75
81
  stdio: ["ignore", "pipe", "pipe"],
82
+ shell: NPM_SHELL,
76
83
  })
77
84
  .toString()
78
85
  .trim();
@@ -118,10 +125,12 @@ function main(argv) {
118
125
  }
119
126
  console.log(`\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`);
120
127
  try {
121
- // Re-runs the developer's own npm to update this global package — no
122
- // untrusted input, and execFileSync (no shell) avoids any injection.
128
+ // Re-runs the developer's own npm to update this global package. On Windows
129
+ // the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
130
+ // caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
131
+ // and every other arg is a fixed literal, so nothing injectable reaches it.
123
132
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
124
- (0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env });
133
+ (0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
125
134
  }
126
135
  catch (err) {
127
136
  console.error(`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${(0, update_1.npmInstallArgs)(flags.version)[2]}\n ${err.message}`);
package/dist/update.d.ts CHANGED
@@ -23,14 +23,35 @@ export interface UpdateFlags {
23
23
  * 0.0.40`). Unknown values default to `latest`.
24
24
  */
25
25
  export declare function parseUpdateFlags(argv: string[]): UpdateFlags;
26
+ /**
27
+ * Constrain the update target to a safe npm version or dist-tag. The Windows
28
+ * update path runs npm through a shell (see `npmSpawnShell`), so the target —
29
+ * the one caller-supplied token that reaches the command line — must not be
30
+ * able to carry shell metacharacters. A valid npm spec starts alphanumeric and
31
+ * uses only `A-Za-z0-9._-` (e.g. `latest`, `next`, `0.0.40`, `1.2.3-beta.1`);
32
+ * anything else falls back to `latest`.
33
+ */
34
+ export declare function sanitizeVersion(version: string | undefined): string;
26
35
  /**
27
36
  * The npm executable to spawn for a platform. On Windows npm is a `.cmd` shim,
28
37
  * not a real executable, so `execFile("npm", …)` (no shell) fails with
29
38
  * `spawnSync npm ENOENT` — the direct-spawn path only resolves real binaries,
30
39
  * never `PATHEXT` scripts. Spawn `npm.cmd` there; POSIX keeps plain `npm`.
31
40
  * (`aws` is unaffected — it installs as `aws.exe`, a real executable.)
41
+ *
42
+ * The `.cmd` must still be launched through a shell — see `npmSpawnShell`.
32
43
  */
33
44
  export declare function npmBinary(platform?: string): string;
45
+ /**
46
+ * Whether npm must be spawned through a shell on this platform. Since the Node
47
+ * fix for CVE-2024-27980 (18.20.2 / 20.12.2 / 21.7.2), spawning a `.cmd` shim
48
+ * like `npm.cmd` *without* a shell is rejected with `spawnSync npm.cmd EINVAL`
49
+ * — the very failure a Windows/PowerShell `rastack update` hits. cmd.exe knows
50
+ * how to run a `.cmd`, so Windows spawns through the shell; POSIX keeps the
51
+ * direct, shell-free spawn (npm is a real script resolved on PATH there, and no
52
+ * shell means no injection surface).
53
+ */
54
+ export declare function npmSpawnShell(platform?: string): boolean;
34
55
  /** The npm package spec to install for a target version. */
35
56
  export declare function installSpec(version: string): string;
36
57
  /** The `npm <args>` (after the `npm` binary) that installs the target globally. */
package/dist/update.js CHANGED
@@ -12,7 +12,9 @@
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.parseUpdateFlags = parseUpdateFlags;
15
+ exports.sanitizeVersion = sanitizeVersion;
15
16
  exports.npmBinary = npmBinary;
17
+ exports.npmSpawnShell = npmSpawnShell;
16
18
  exports.installSpec = installSpec;
17
19
  exports.npmInstallArgs = npmInstallArgs;
18
20
  exports.globalPrefixFromPackageDir = globalPrefixFromPackageDir;
@@ -39,26 +41,53 @@ function parseUpdateFlags(argv) {
39
41
  break;
40
42
  case "--version":
41
43
  case "--to":
42
- version = argv[++i] ?? "latest";
44
+ version = sanitizeVersion(argv[++i]);
43
45
  break;
44
46
  default:
45
47
  // A bare, non-flag token is taken as the target version.
46
48
  if (!a.startsWith("-"))
47
- version = a;
49
+ version = sanitizeVersion(a);
48
50
  }
49
51
  }
50
52
  return { version, check, dryRun };
51
53
  }
54
+ /**
55
+ * Constrain the update target to a safe npm version or dist-tag. The Windows
56
+ * update path runs npm through a shell (see `npmSpawnShell`), so the target —
57
+ * the one caller-supplied token that reaches the command line — must not be
58
+ * able to carry shell metacharacters. A valid npm spec starts alphanumeric and
59
+ * uses only `A-Za-z0-9._-` (e.g. `latest`, `next`, `0.0.40`, `1.2.3-beta.1`);
60
+ * anything else falls back to `latest`.
61
+ */
62
+ function sanitizeVersion(version) {
63
+ return version && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version)
64
+ ? version
65
+ : "latest";
66
+ }
52
67
  /**
53
68
  * The npm executable to spawn for a platform. On Windows npm is a `.cmd` shim,
54
69
  * not a real executable, so `execFile("npm", …)` (no shell) fails with
55
70
  * `spawnSync npm ENOENT` — the direct-spawn path only resolves real binaries,
56
71
  * never `PATHEXT` scripts. Spawn `npm.cmd` there; POSIX keeps plain `npm`.
57
72
  * (`aws` is unaffected — it installs as `aws.exe`, a real executable.)
73
+ *
74
+ * The `.cmd` must still be launched through a shell — see `npmSpawnShell`.
58
75
  */
59
76
  function npmBinary(platform = process.platform) {
60
77
  return platform === "win32" ? "npm.cmd" : "npm";
61
78
  }
79
+ /**
80
+ * Whether npm must be spawned through a shell on this platform. Since the Node
81
+ * fix for CVE-2024-27980 (18.20.2 / 20.12.2 / 21.7.2), spawning a `.cmd` shim
82
+ * like `npm.cmd` *without* a shell is rejected with `spawnSync npm.cmd EINVAL`
83
+ * — the very failure a Windows/PowerShell `rastack update` hits. cmd.exe knows
84
+ * how to run a `.cmd`, so Windows spawns through the shell; POSIX keeps the
85
+ * direct, shell-free spawn (npm is a real script resolved on PATH there, and no
86
+ * shell means no injection surface).
87
+ */
88
+ function npmSpawnShell(platform = process.platform) {
89
+ return platform === "win32";
90
+ }
62
91
  /** The npm package spec to install for a target version. */
63
92
  function installSpec(version) {
64
93
  return !version || version === "latest" ? "rastack" : `rastack@${version}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -24,6 +24,7 @@ import {
24
24
  globalPrefixFromPackageDir,
25
25
  npmBinary,
26
26
  npmInstallArgs,
27
+ npmSpawnShell,
27
28
  parseUpdateFlags,
28
29
  updateAvailable,
29
30
  } from "./update";
@@ -31,6 +32,13 @@ import {
31
32
  /** The npm executable for this platform (`npm.cmd` on Windows, else `npm`). */
32
33
  const NPM = npmBinary();
33
34
 
35
+ /**
36
+ * Whether to launch npm through a shell. Required on Windows, where a bare
37
+ * `.cmd` spawn is rejected with `EINVAL` since the Node CVE-2024-27980 fix;
38
+ * false on POSIX so the direct spawn stays injection-free.
39
+ */
40
+ const NPM_SHELL = npmSpawnShell();
41
+
34
42
  /** The installed rastack package directory (…/rastack), one up from dist/. */
35
43
  const PKG_DIR = path.join(__dirname, "..");
36
44
  const PKG_JSON = path.join(PKG_DIR, "package.json");
@@ -49,6 +57,7 @@ function npmCapture(args: string[]): string | null {
49
57
  try {
50
58
  return execFileSync(NPM, args, {
51
59
  stdio: ["ignore", "pipe", "pipe"],
60
+ shell: NPM_SHELL,
52
61
  })
53
62
  .toString()
54
63
  .trim();
@@ -103,10 +112,12 @@ function main(argv: string[]): void {
103
112
  `\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`,
104
113
  );
105
114
  try {
106
- // Re-runs the developer's own npm to update this global package — no
107
- // untrusted input, and execFileSync (no shell) avoids any injection.
115
+ // Re-runs the developer's own npm to update this global package. On Windows
116
+ // the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
117
+ // caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
118
+ // and every other arg is a fixed literal, so nothing injectable reaches it.
108
119
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
109
- execFileSync(NPM, args, { stdio: "inherit", env });
120
+ execFileSync(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
110
121
  } catch (err) {
111
122
  console.error(
112
123
  `\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${npmInstallArgs(flags.version)[2]}\n ${(err as Error).message}`,
package/src/update.ts CHANGED
@@ -40,27 +40,56 @@ export function parseUpdateFlags(argv: string[]): UpdateFlags {
40
40
  break;
41
41
  case "--version":
42
42
  case "--to":
43
- version = argv[++i] ?? "latest";
43
+ version = sanitizeVersion(argv[++i]);
44
44
  break;
45
45
  default:
46
46
  // A bare, non-flag token is taken as the target version.
47
- if (!a.startsWith("-")) version = a;
47
+ if (!a.startsWith("-")) version = sanitizeVersion(a);
48
48
  }
49
49
  }
50
50
  return { version, check, dryRun };
51
51
  }
52
52
 
53
+ /**
54
+ * Constrain the update target to a safe npm version or dist-tag. The Windows
55
+ * update path runs npm through a shell (see `npmSpawnShell`), so the target —
56
+ * the one caller-supplied token that reaches the command line — must not be
57
+ * able to carry shell metacharacters. A valid npm spec starts alphanumeric and
58
+ * uses only `A-Za-z0-9._-` (e.g. `latest`, `next`, `0.0.40`, `1.2.3-beta.1`);
59
+ * anything else falls back to `latest`.
60
+ */
61
+ export function sanitizeVersion(version: string | undefined): string {
62
+ return version && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version)
63
+ ? version
64
+ : "latest";
65
+ }
66
+
53
67
  /**
54
68
  * The npm executable to spawn for a platform. On Windows npm is a `.cmd` shim,
55
69
  * not a real executable, so `execFile("npm", …)` (no shell) fails with
56
70
  * `spawnSync npm ENOENT` — the direct-spawn path only resolves real binaries,
57
71
  * never `PATHEXT` scripts. Spawn `npm.cmd` there; POSIX keeps plain `npm`.
58
72
  * (`aws` is unaffected — it installs as `aws.exe`, a real executable.)
73
+ *
74
+ * The `.cmd` must still be launched through a shell — see `npmSpawnShell`.
59
75
  */
60
76
  export function npmBinary(platform: string = process.platform): string {
61
77
  return platform === "win32" ? "npm.cmd" : "npm";
62
78
  }
63
79
 
80
+ /**
81
+ * Whether npm must be spawned through a shell on this platform. Since the Node
82
+ * fix for CVE-2024-27980 (18.20.2 / 20.12.2 / 21.7.2), spawning a `.cmd` shim
83
+ * like `npm.cmd` *without* a shell is rejected with `spawnSync npm.cmd EINVAL`
84
+ * — the very failure a Windows/PowerShell `rastack update` hits. cmd.exe knows
85
+ * how to run a `.cmd`, so Windows spawns through the shell; POSIX keeps the
86
+ * direct, shell-free spawn (npm is a real script resolved on PATH there, and no
87
+ * shell means no injection surface).
88
+ */
89
+ export function npmSpawnShell(platform: string = process.platform): boolean {
90
+ return platform === "win32";
91
+ }
92
+
64
93
  /** The npm package spec to install for a target version. */
65
94
  export function installSpec(version: string): string {
66
95
  return !version || version === "latest" ? "rastack" : `rastack@${version}`;
@@ -4,7 +4,9 @@ import {
4
4
  installSpec,
5
5
  npmBinary,
6
6
  npmInstallArgs,
7
+ npmSpawnShell,
7
8
  parseUpdateFlags,
9
+ sanitizeVersion,
8
10
  updateAvailable,
9
11
  } from "../src/update";
10
12
 
@@ -56,6 +58,39 @@ describe("update — npm spec + args", () => {
56
58
  expect(npmBinary("linux")).toBe("npm");
57
59
  expect(npmBinary("darwin")).toBe("npm");
58
60
  });
61
+
62
+ it("runs npm through a shell only on Windows", () => {
63
+ // A `.cmd` shim can't be spawned directly since the Node CVE-2024-27980 fix
64
+ // (spawnSync npm.cmd EINVAL) — it must go through cmd.exe. POSIX spawns npm
65
+ // directly so there is no shell and no injection surface.
66
+ expect(npmSpawnShell("win32")).toBe(true);
67
+ expect(npmSpawnShell("linux")).toBe(false);
68
+ expect(npmSpawnShell("darwin")).toBe(false);
69
+ });
70
+ });
71
+
72
+ describe("update — version sanitization", () => {
73
+ it("keeps valid semver, dist-tags, and pinned versions", () => {
74
+ expect(sanitizeVersion("latest")).toBe("latest");
75
+ expect(sanitizeVersion("next")).toBe("next");
76
+ expect(sanitizeVersion("0.0.40")).toBe("0.0.40");
77
+ expect(sanitizeVersion("1.2.3-beta.1")).toBe("1.2.3-beta.1");
78
+ });
79
+
80
+ it("falls back to latest for empty or shell-unsafe input", () => {
81
+ expect(sanitizeVersion(undefined)).toBe("latest");
82
+ expect(sanitizeVersion("")).toBe("latest");
83
+ // Shell metacharacters must never reach the Windows shell-spawned npm.
84
+ expect(sanitizeVersion("0.0.40; rm -rf /")).toBe("latest");
85
+ expect(sanitizeVersion("$(whoami)")).toBe("latest");
86
+ expect(sanitizeVersion("a b")).toBe("latest");
87
+ });
88
+
89
+ it("is applied when parsing the bare positional and --version", () => {
90
+ expect(parseUpdateFlags(["0.0.40; evil"]).version).toBe("latest");
91
+ expect(parseUpdateFlags(["--version", "&& evil"]).version).toBe("latest");
92
+ expect(parseUpdateFlags(["0.0.40"]).version).toBe("0.0.40");
93
+ });
59
94
  });
60
95
 
61
96
  describe("update — global prefix derivation", () => {