mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
Node 24 refuses execFile of npm.cmd without a shell (nodejs/node#52554), so embedded-service install (9Router/CLIProxy) failed with spawn EINVAL on Windows. runNpm now enables shell on win32 only; to stay Hard-Rule-#13 safe under a shell, the install --prefix is passed via npm_config_prefix (env) instead of an argv path (survives spaces), and the user-supplied version is constrained by SERVICE_VERSION_PATTERN at the route boundary.
This commit is contained in:
committed by
GitHub
parent
e9c739184e
commit
29bdb8dfde
@@ -1,11 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { install, InstallResult } from "@/lib/services/installers/ninerouter";
|
||||
import { InstallError } from "@/lib/services/installers/utils";
|
||||
import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const BodySchema = z.object({
|
||||
version: z.string().optional().default("latest"),
|
||||
version: z
|
||||
.string()
|
||||
.regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed")
|
||||
.optional()
|
||||
.default("latest"),
|
||||
});
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { install, InstallResult } from "@/lib/services/installers/cliproxy";
|
||||
import { InstallError } from "@/lib/services/installers/utils";
|
||||
import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const BodySchema = z.object({
|
||||
version: z.string().optional().default("latest"),
|
||||
version: z
|
||||
.string()
|
||||
.regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed")
|
||||
.optional()
|
||||
.default("latest"),
|
||||
});
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
|
||||
@@ -77,16 +77,10 @@ export async function install(version = "latest"): Promise<InstallResult> {
|
||||
}
|
||||
|
||||
await runNpm(
|
||||
[
|
||||
"install",
|
||||
`${NINEROUTER_PACKAGE}@${version}`,
|
||||
"--omit=dev",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefix",
|
||||
NINEROUTER_INSTALL_DIR,
|
||||
],
|
||||
{ cwd: NINEROUTER_INSTALL_DIR }
|
||||
["install", `${NINEROUTER_PACKAGE}@${version}`, "--omit=dev", "--no-audit", "--no-fund"],
|
||||
// `--prefix` is passed via `prefix` (→ npm_config_prefix env) instead of an
|
||||
// argv path so an install dir with spaces survives the Windows shell (#5379).
|
||||
{ cwd: NINEROUTER_INSTALL_DIR, prefix: NINEROUTER_INSTALL_DIR }
|
||||
);
|
||||
|
||||
const installedVersion = await getInstalledVersion();
|
||||
|
||||
@@ -73,10 +73,68 @@ function classifyError(
|
||||
return new InstallError(raw, `Falha na instalação: ${raw}`, 500);
|
||||
}
|
||||
|
||||
/** Runs npm with the given args array. Never uses shell interpolation. */
|
||||
/**
|
||||
* Validates a user-supplied service version (npm dist-tag or semver). Constrained
|
||||
* to letters, digits and `. _ + -`, with a leading alphanumeric, so the value can
|
||||
* never carry shell metacharacters once `runNpm` runs under a shell on Windows
|
||||
* (see `buildNpmExecOptions`). Accepts `latest`, `next`, `1.2.3`, `1.2.3-beta.1`,
|
||||
* `1.2.3+build.5`; rejects `latest && calc`, `$(id)`, spaces, leading `-`, etc.
|
||||
*/
|
||||
export const SERVICE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
||||
|
||||
export interface NpmExecOptions {
|
||||
cwd?: string;
|
||||
timeout: number;
|
||||
env: NodeJS.ProcessEnv;
|
||||
maxBuffer: number;
|
||||
shell?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `execFile` options for {@link runNpm}.
|
||||
*
|
||||
* On Windows, npm is `npm.cmd` (a batch wrapper). Node 24 refuses to `execFile`
|
||||
* a `.cmd` without a shell (nodejs/node#52554 — manifests as `spawn EINVAL`, see
|
||||
* issue #5379), so we enable `shell` on win32 only.
|
||||
*
|
||||
* Enabling the shell means the shell — not `execFile` — splits the command line,
|
||||
* so NO runtime value may be interpolated into argv (Hard Rule #13). The install
|
||||
* prefix (a DATA_DIR path that can legitimately contain spaces, e.g.
|
||||
* `C:\Users\John Doe\.omniroute\…`) is therefore exported as the
|
||||
* `npm_config_prefix` environment variable — npm's documented env form of
|
||||
* `--prefix` — never as an argv entry. With the prefix moved to the environment
|
||||
* and the version constrained by {@link SERVICE_VERSION_PATTERN}, every remaining
|
||||
* argv entry is a static, metacharacter-free flag.
|
||||
*/
|
||||
export function buildNpmExecOptions(
|
||||
platform: NodeJS.Platform,
|
||||
options: { cwd?: string; timeoutMs: number; prefix?: string }
|
||||
): NpmExecOptions {
|
||||
const env: NodeJS.ProcessEnv = { ...process.env };
|
||||
if (options.prefix) {
|
||||
env.npm_config_prefix = options.prefix;
|
||||
}
|
||||
const execOptions: NpmExecOptions = {
|
||||
cwd: options.cwd,
|
||||
timeout: options.timeoutMs,
|
||||
env,
|
||||
maxBuffer: 10 * 1024 * 1024, // 10 MB for npm output
|
||||
};
|
||||
if (platform === "win32") {
|
||||
execOptions.shell = true;
|
||||
}
|
||||
return execOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs npm with the given args array. Never uses shell interpolation: argv holds
|
||||
* only static flags, and any install prefix is passed via `options.prefix`
|
||||
* (exported as `npm_config_prefix`), not as an argv path. See
|
||||
* {@link buildNpmExecOptions} for the Windows/Node-24 shell handling.
|
||||
*/
|
||||
export function runNpm(
|
||||
args: string[],
|
||||
options: { cwd?: string; timeoutMs?: number } = {}
|
||||
options: { cwd?: string; timeoutMs?: number; prefix?: string } = {}
|
||||
): Promise<NpmRunResult> {
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
// On Windows, npm is npm.cmd; on Unix it's npm.
|
||||
@@ -86,12 +144,11 @@ export function runNpm(
|
||||
execFile(
|
||||
npmBin,
|
||||
args,
|
||||
{
|
||||
buildNpmExecOptions(process.platform, {
|
||||
cwd: options.cwd,
|
||||
timeout: timeoutMs,
|
||||
env: process.env,
|
||||
maxBuffer: 10 * 1024 * 1024, // 10 MB for npm output
|
||||
},
|
||||
timeoutMs,
|
||||
prefix: options.prefix,
|
||||
}),
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const classified = classifyError(
|
||||
|
||||
Reference in New Issue
Block a user