@paretools/shared 0.8.0 → 0.8.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/dist/index.d.ts CHANGED
@@ -5,5 +5,6 @@ export { assertNoFlagInjection, assertAllowedCommand } from "./validation.js";
5
5
  export { INPUT_LIMITS } from "./limits.js";
6
6
  export { sanitizeErrorOutput } from "./sanitize.js";
7
7
  export { shouldRegisterTool } from "./tool-filter.js";
8
+ export { assertAllowedByPolicy, assertAllowedRoot, assertNoPathQualifiedCommand, } from "./policy.js";
8
9
  export type { ToolOutput } from "./types.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -5,4 +5,5 @@ export { assertNoFlagInjection, assertAllowedCommand } from "./validation.js";
5
5
  export { INPUT_LIMITS } from "./limits.js";
6
6
  export { sanitizeErrorOutput } from "./sanitize.js";
7
7
  export { shouldRegisterTool } from "./tool-filter.js";
8
+ export { assertAllowedByPolicy, assertAllowedRoot, assertNoPathQualifiedCommand, } from "./policy.js";
8
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,GAAG,EAAE,YAAY,EAAmC,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,GAAG,EAAE,YAAY,EAAmC,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Security policy controls — opt-in hardening via environment variables.
3
+ *
4
+ * All controls follow the same global/per-server precedence pattern as
5
+ * tool filtering (see tool-filter.ts):
6
+ *
7
+ * 1. `PARE_{SETTING}` — global, applies to all servers.
8
+ * 2. `PARE_{SERVER}_{SETTING}` — per-server override.
9
+ * 3. No env var → permissive (default, no restriction).
10
+ *
11
+ * Global takes precedence over per-server when both are set.
12
+ *
13
+ * ## Available Controls
14
+ *
15
+ * ### PARE_ALLOWED_COMMANDS / PARE_{SERVER}_ALLOWED_COMMANDS
16
+ * Comma-separated list of allowed command names.
17
+ * When set, only listed commands may be executed.
18
+ * Example: `PARE_PROCESS_ALLOWED_COMMANDS=node,python,make`
19
+ * Example: `PARE_ALLOWED_COMMANDS=node,python,git,npm` (all servers)
20
+ *
21
+ * ### PARE_ALLOWED_ROOTS / PARE_{SERVER}_ALLOWED_ROOTS
22
+ * Comma-separated list of allowed root directories.
23
+ * When set, all path/cwd parameters must be under one of these roots.
24
+ * Example: `PARE_ALLOWED_ROOTS=/home/user/projects,/tmp/builds`
25
+ * Example: `PARE_PROCESS_ALLOWED_ROOTS=/home/user/safe-dir`
26
+ *
27
+ * ### PARE_BUILD_STRICT_PATH
28
+ * When set to "true", the build server rejects path-qualified commands
29
+ * (e.g., `/tmp/evil/npm`) and only allows bare command names resolved via PATH.
30
+ */
31
+ /**
32
+ * Asserts that a command is allowed by the ALLOWED_COMMANDS policy.
33
+ * No-op when the policy is not configured (permissive default).
34
+ *
35
+ * @param command - The command to check (e.g., "node", "python").
36
+ * @param serverName - Server identifier for per-server overrides (e.g., "process").
37
+ * @throws Error if the command is not in the allowlist.
38
+ */
39
+ export declare function assertAllowedByPolicy(command: string, serverName: string): void;
40
+ /**
41
+ * Asserts that a working directory path is under one of the allowed roots.
42
+ * No-op when the policy is not configured (permissive default).
43
+ *
44
+ * @param targetPath - The path/cwd to validate.
45
+ * @param serverName - Server identifier for per-server overrides (e.g., "process").
46
+ * @throws Error if the path is outside all allowed roots.
47
+ */
48
+ export declare function assertAllowedRoot(targetPath: string, serverName: string): void;
49
+ /**
50
+ * Asserts that a command does not contain path separators (strict path mode).
51
+ * Used by build server when PARE_BUILD_STRICT_PATH=true.
52
+ *
53
+ * @param command - The command to check.
54
+ * @throws Error if the command contains `/` or `\`.
55
+ */
56
+ export declare function assertNoPathQualifiedCommand(command: string): void;
57
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA+BH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAmB/E;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAoB9E;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CASlE"}
package/dist/policy.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Security policy controls — opt-in hardening via environment variables.
3
+ *
4
+ * All controls follow the same global/per-server precedence pattern as
5
+ * tool filtering (see tool-filter.ts):
6
+ *
7
+ * 1. `PARE_{SETTING}` — global, applies to all servers.
8
+ * 2. `PARE_{SERVER}_{SETTING}` — per-server override.
9
+ * 3. No env var → permissive (default, no restriction).
10
+ *
11
+ * Global takes precedence over per-server when both are set.
12
+ *
13
+ * ## Available Controls
14
+ *
15
+ * ### PARE_ALLOWED_COMMANDS / PARE_{SERVER}_ALLOWED_COMMANDS
16
+ * Comma-separated list of allowed command names.
17
+ * When set, only listed commands may be executed.
18
+ * Example: `PARE_PROCESS_ALLOWED_COMMANDS=node,python,make`
19
+ * Example: `PARE_ALLOWED_COMMANDS=node,python,git,npm` (all servers)
20
+ *
21
+ * ### PARE_ALLOWED_ROOTS / PARE_{SERVER}_ALLOWED_ROOTS
22
+ * Comma-separated list of allowed root directories.
23
+ * When set, all path/cwd parameters must be under one of these roots.
24
+ * Example: `PARE_ALLOWED_ROOTS=/home/user/projects,/tmp/builds`
25
+ * Example: `PARE_PROCESS_ALLOWED_ROOTS=/home/user/safe-dir`
26
+ *
27
+ * ### PARE_BUILD_STRICT_PATH
28
+ * When set to "true", the build server rejects path-qualified commands
29
+ * (e.g., `/tmp/evil/npm`) and only allows bare command names resolved via PATH.
30
+ */
31
+ import { resolve, normalize } from "node:path";
32
+ /**
33
+ * Reads a policy env var with global/per-server precedence.
34
+ * Global (`PARE_{setting}`) wins over per-server (`PARE_{SERVER}_{setting}`).
35
+ * Returns undefined when neither is set (no restriction).
36
+ */
37
+ function readPolicyVar(serverName, setting) {
38
+ const global = process.env[`PARE_${setting}`];
39
+ if (global !== undefined)
40
+ return global;
41
+ const envKey = `PARE_${serverName.toUpperCase().replace(/-/g, "_")}_${setting}`;
42
+ return process.env[envKey];
43
+ }
44
+ /**
45
+ * Parses a comma-separated env var value into a trimmed Set.
46
+ * Returns undefined if the raw value is undefined or empty.
47
+ */
48
+ function parseList(raw) {
49
+ if (raw === undefined || raw.trim() === "")
50
+ return undefined;
51
+ return new Set(raw
52
+ .split(",")
53
+ .map((s) => s.trim())
54
+ .filter(Boolean));
55
+ }
56
+ /**
57
+ * Asserts that a command is allowed by the ALLOWED_COMMANDS policy.
58
+ * No-op when the policy is not configured (permissive default).
59
+ *
60
+ * @param command - The command to check (e.g., "node", "python").
61
+ * @param serverName - Server identifier for per-server overrides (e.g., "process").
62
+ * @throws Error if the command is not in the allowlist.
63
+ */
64
+ export function assertAllowedByPolicy(command, serverName) {
65
+ const raw = readPolicyVar(serverName, "ALLOWED_COMMANDS");
66
+ const allowed = parseList(raw);
67
+ if (!allowed)
68
+ return; // No policy — everything allowed
69
+ // Extract basename for comparison (handle paths like /usr/bin/node)
70
+ const base = command
71
+ .replace(/\\/g, "/")
72
+ .split("/")
73
+ .pop()
74
+ ?.replace(/\.(cmd|exe|bat|sh)$/i, "") ?? "";
75
+ if (!allowed.has(base) && !allowed.has(command)) {
76
+ throw new Error(`Command "${command}" is not allowed by ALLOWED_COMMANDS policy. ` +
77
+ `Allowed: ${[...allowed].sort().join(", ")}`);
78
+ }
79
+ }
80
+ /**
81
+ * Asserts that a working directory path is under one of the allowed roots.
82
+ * No-op when the policy is not configured (permissive default).
83
+ *
84
+ * @param targetPath - The path/cwd to validate.
85
+ * @param serverName - Server identifier for per-server overrides (e.g., "process").
86
+ * @throws Error if the path is outside all allowed roots.
87
+ */
88
+ export function assertAllowedRoot(targetPath, serverName) {
89
+ const raw = readPolicyVar(serverName, "ALLOWED_ROOTS");
90
+ const roots = parseList(raw);
91
+ if (!roots)
92
+ return; // No policy — all paths allowed
93
+ const normalizedTarget = normalize(resolve(targetPath));
94
+ for (const root of roots) {
95
+ const normalizedRoot = normalize(resolve(root));
96
+ if (normalizedTarget === normalizedRoot ||
97
+ normalizedTarget.startsWith(normalizedRoot + (process.platform === "win32" ? "\\" : "/"))) {
98
+ return; // Path is under an allowed root
99
+ }
100
+ }
101
+ throw new Error(`Path "${targetPath}" is outside allowed roots. ` + `Allowed roots: ${[...roots].join(", ")}`);
102
+ }
103
+ /**
104
+ * Asserts that a command does not contain path separators (strict path mode).
105
+ * Used by build server when PARE_BUILD_STRICT_PATH=true.
106
+ *
107
+ * @param command - The command to check.
108
+ * @throws Error if the command contains `/` or `\`.
109
+ */
110
+ export function assertNoPathQualifiedCommand(command) {
111
+ if (process.env.PARE_BUILD_STRICT_PATH === "true") {
112
+ if (command.includes("/") || command.includes("\\")) {
113
+ throw new Error(`Path-qualified commands are not allowed when PARE_BUILD_STRICT_PATH is enabled. ` +
114
+ `Use a bare command name (e.g., "npm" not "${command}") that resolves via PATH.`);
115
+ }
116
+ }
117
+ }
118
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
package/dist/runner.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface RunOptions {
11
11
  * and `false` elsewhere. Set to `false` for native executables (e.g., git)
12
12
  * whose args contain characters that cmd.exe would misinterpret (like `<>`). */
13
13
  shell?: boolean;
14
+ /** Maximum combined stdout+stderr buffer size in bytes. Defaults to 10 MB. */
15
+ maxBuffer?: number;
14
16
  }
15
17
  /** Result of a command execution, containing the exit code and ANSI-stripped stdout/stderr. */
16
18
  export interface RunResult {
@@ -40,11 +42,14 @@ export interface RunResult {
40
42
  export declare function escapeCmdArg(arg: string): string;
41
43
  /**
42
44
  * Executes a command and returns cleaned output with ANSI codes stripped.
43
- * Uses execFile (not exec) to avoid shell injection. On Windows, shell is
44
- * enabled so that .cmd/.bat wrappers (like npx) can be executed — args are
45
- * still passed as an array so they remain properly escaped.
45
+ * Uses spawn (not exec) to avoid shell injection. The child is spawned in its
46
+ * own process group (`detached: true` on Unix) so that on timeout we can kill
47
+ * the entire group — preventing orphaned grandchild processes.
46
48
  *
47
- * Throws on system-level errors (command not found, permission denied).
49
+ * On Windows, shell is enabled so that .cmd/.bat wrappers (like npx) can be
50
+ * executed — args are still passed as an array so they remain properly escaped.
51
+ *
52
+ * Throws on system-level errors (command not found, permission denied, timeout).
48
53
  * Normal non-zero exit codes are returned in the result, not thrown.
49
54
  */
50
55
  export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAIA,uGAAuG;AACvG,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;mFAE+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;qFAEiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA8EtF"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAIA,uGAAuG;AACvG,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;mFAE+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;qFAEiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6BD,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqChD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA+ItF"}
package/dist/runner.js CHANGED
@@ -1,6 +1,36 @@
1
- import { execFile } from "node:child_process";
1
+ import { spawn, execFile as nodeExecFile } from "node:child_process";
2
2
  import { stripAnsi } from "./ansi.js";
3
3
  import { sanitizeErrorOutput } from "./sanitize.js";
4
+ /**
5
+ * Kills an entire process group (Unix) or process tree (Windows).
6
+ *
7
+ * On Unix, `spawn({ detached: true })` calls `setsid(2)`, making the child
8
+ * a process group leader. Sending a signal to `-pid` reaches every process
9
+ * in that group, including grandchildren.
10
+ *
11
+ * On Windows, `taskkill /T /F` walks the native process tree.
12
+ *
13
+ * ESRCH (process already exited) is silently ignored.
14
+ */
15
+ function killProcessGroup(pid) {
16
+ if (process.platform === "win32") {
17
+ try {
18
+ nodeExecFile("taskkill", ["/pid", String(pid), "/T", "/F"]);
19
+ }
20
+ catch {
21
+ /* best-effort */
22
+ }
23
+ }
24
+ else {
25
+ try {
26
+ process.kill(-pid, "SIGTERM");
27
+ }
28
+ catch (err) {
29
+ if (err.code !== "ESRCH")
30
+ throw err;
31
+ }
32
+ }
33
+ }
4
34
  /**
5
35
  * Escapes a single argument for safe use with cmd.exe on Windows.
6
36
  *
@@ -56,11 +86,14 @@ export function escapeCmdArg(arg) {
56
86
  }
57
87
  /**
58
88
  * Executes a command and returns cleaned output with ANSI codes stripped.
59
- * Uses execFile (not exec) to avoid shell injection. On Windows, shell is
60
- * enabled so that .cmd/.bat wrappers (like npx) can be executed — args are
61
- * still passed as an array so they remain properly escaped.
89
+ * Uses spawn (not exec) to avoid shell injection. The child is spawned in its
90
+ * own process group (`detached: true` on Unix) so that on timeout we can kill
91
+ * the entire group — preventing orphaned grandchild processes.
62
92
  *
63
- * Throws on system-level errors (command not found, permission denied).
93
+ * On Windows, shell is enabled so that .cmd/.bat wrappers (like npx) can be
94
+ * executed — args are still passed as an array so they remain properly escaped.
95
+ *
96
+ * Throws on system-level errors (command not found, permission denied, timeout).
64
97
  * Normal non-zero exit codes are returned in the result, not thrown.
65
98
  */
66
99
  export function run(cmd, args, opts) {
@@ -73,41 +106,101 @@ export function run(cmd, args, opts) {
73
106
  // environment variable expansion and command injection via special chars.
74
107
  // See escapeCmdArg() for details on what is escaped and why.
75
108
  const safeArgs = useShell && process.platform === "win32" ? args.map(escapeCmdArg) : args;
76
- const child = execFile(cmd, safeArgs, {
109
+ const child = spawn(cmd, safeArgs, {
77
110
  cwd: opts?.cwd,
78
- timeout: opts?.timeout ?? 60_000,
79
111
  env: opts?.env ? { ...process.env, ...opts.env } : undefined,
80
- maxBuffer: 10 * 1024 * 1024, // 10 MB
81
112
  shell: useShell,
82
- }, (error, stdout, stderr) => {
83
- if (error) {
84
- const errno = error;
85
- // Unix: direct ENOENT from execFile (no shell wrapping)
86
- if (errno.code === "ENOENT") {
87
- reject(new Error(`Command not found: "${cmd}". Ensure it is installed and available in your PATH.`));
88
- return;
89
- }
90
- if (errno.code === "EACCES" || errno.code === "EPERM") {
91
- reject(new Error(`Permission denied executing "${cmd}": ${errno.message}`));
92
- return;
93
- }
94
- // Timeout: execFile killed the child after the configured timeout.
95
- // Surface this clearly instead of silently returning exitCode 1.
96
- if (error.killed && error.signal) {
97
- reject(new Error(`Command "${cmd}" timed out after ${opts?.timeout ?? 60_000}ms and was killed (${error.signal}).`));
98
- return;
99
- }
100
- // Windows: cmd.exe masks ENOENT — detect via stderr message
101
- const cleanStderr = stripAnsi(stderr);
102
- if (cleanStderr.includes("is not recognized")) {
103
- reject(new Error(`Command not found: "${cmd}". Ensure it is installed and available in your PATH.`));
104
- return;
105
- }
113
+ // Unix: creates a new process group via setsid(2) so we can kill the
114
+ // entire group on timeout (prevents orphaned grandchild processes).
115
+ // Windows: skip — detached on Windows creates a visible console window,
116
+ // not a process group. Use taskkill /T /F instead.
117
+ detached: process.platform !== "win32",
118
+ windowsHide: true,
119
+ stdio: ["pipe", "pipe", "pipe"],
120
+ });
121
+ // --- Buffer accumulation (replaces execFile's maxBuffer) ---
122
+ const maxBuffer = opts?.maxBuffer ?? 10 * 1024 * 1024; // 10 MB
123
+ const stdoutChunks = [];
124
+ const stderrChunks = [];
125
+ let stdoutLen = 0;
126
+ let stderrLen = 0;
127
+ let bufferExceeded = false;
128
+ child.stdout?.on("data", (chunk) => {
129
+ stdoutLen += chunk.length;
130
+ if (stdoutLen + stderrLen > maxBuffer && !bufferExceeded) {
131
+ bufferExceeded = true;
132
+ killProcessGroup(child.pid);
133
+ return;
134
+ }
135
+ if (!bufferExceeded)
136
+ stdoutChunks.push(chunk);
137
+ });
138
+ child.stderr?.on("data", (chunk) => {
139
+ stderrLen += chunk.length;
140
+ if (stdoutLen + stderrLen > maxBuffer && !bufferExceeded) {
141
+ bufferExceeded = true;
142
+ killProcessGroup(child.pid);
143
+ return;
144
+ }
145
+ if (!bufferExceeded)
146
+ stderrChunks.push(chunk);
147
+ });
148
+ // --- Timeout handling (replaces execFile's timeout) ---
149
+ let timedOut = false;
150
+ let timeoutSignal;
151
+ const timeoutMs = opts?.timeout ?? 60_000;
152
+ const timer = setTimeout(() => {
153
+ timedOut = true;
154
+ timeoutSignal = "SIGTERM";
155
+ killProcessGroup(child.pid);
156
+ }, timeoutMs);
157
+ // --- Settlement guard (prevents double-resolve from close + error) ---
158
+ let settled = false;
159
+ // Handle spawn errors (ENOENT, EACCES on Unix — spawn emits these as
160
+ // 'error' events, unlike execFile which passes them to the callback).
161
+ child.on("error", (err) => {
162
+ clearTimeout(timer);
163
+ if (settled)
164
+ return;
165
+ settled = true;
166
+ if (err.code === "ENOENT") {
167
+ reject(new Error(`Command not found: "${cmd}". Ensure it is installed and available in your PATH.`));
168
+ return;
169
+ }
170
+ if (err.code === "EACCES" || err.code === "EPERM") {
171
+ reject(new Error(`Permission denied executing "${cmd}": ${err.message}`));
172
+ return;
173
+ }
174
+ reject(err);
175
+ });
176
+ // Resolve on 'close' (not 'exit') — close fires after ALL stdio streams
177
+ // are flushed, guaranteeing no late data events.
178
+ child.on("close", (code, signal) => {
179
+ clearTimeout(timer);
180
+ if (settled)
181
+ return;
182
+ settled = true;
183
+ const stdout = stripAnsi(Buffer.concat(stdoutChunks).toString("utf-8"));
184
+ const stderr = sanitizeErrorOutput(stripAnsi(Buffer.concat(stderrChunks).toString("utf-8")));
185
+ // Timeout: we killed the process group after the configured timeout.
186
+ if (timedOut) {
187
+ reject(new Error(`Command "${cmd}" timed out after ${timeoutMs}ms and was killed (${timeoutSignal ?? signal ?? "SIGTERM"}).`));
188
+ return;
189
+ }
190
+ // maxBuffer exceeded
191
+ if (bufferExceeded) {
192
+ reject(new Error(`Command "${cmd}" output exceeded maxBuffer (${maxBuffer} bytes) and was killed.`));
193
+ return;
194
+ }
195
+ // Windows: cmd.exe masks ENOENT — detect via stderr message
196
+ if (code !== 0 && stderr.includes("is not recognized")) {
197
+ reject(new Error(`Command not found: "${cmd}". Ensure it is installed and available in your PATH.`));
198
+ return;
106
199
  }
107
200
  resolve({
108
- exitCode: error ? (typeof error.code === "number" ? error.code : 1) : 0,
109
- stdout: stripAnsi(stdout),
110
- stderr: sanitizeErrorOutput(stripAnsi(stderr)),
201
+ exitCode: code ?? 1,
202
+ stdout,
203
+ stderr,
111
204
  });
112
205
  });
113
206
  // Pipe stdin data if provided, then close the stream so the child
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAmBpD;;;;;;;;;;GAUG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,YAAY,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO;gBAAE,MAAM,GAAG,CAAC;QACjE,CAAC;IACH,CAAC;AACH,CAAC;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,sEAAsE;IACtE,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,OAAO,GAAG,GAAG,CAAC;IAElB,yEAAyE;IACzE,uDAAuD;IACvD,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,qEAAqE;QACrE,sEAAsE;QACtE,oEAAoE;QACpE,mEAAmE;QACnE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACzC,iDAAiD;QACjD,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACtC,OAAO,IAAI,OAAO,GAAG,CAAC;IACxB,CAAC;IAED,kEAAkE;IAClE,kEAAkE;IAClE,iCAAiC;IACjC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAEtC,wDAAwD;IACxD,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAEtC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,IAAc,EAAE,IAAiB;IAChE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,6EAA6E;QAC7E,6EAA6E;QAC7E,wDAAwD;QACxD,MAAM,QAAQ,GAAG,IAAI,EAAE,KAAK,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;QAE7D,uEAAuE;QACvE,0EAA0E;QAC1E,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAE1F,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE;YACjC,GAAG,EAAE,IAAI,EAAE,GAAG;YACd,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YAC5D,KAAK,EAAE,QAAQ;YACf,qEAAqE;YACrE,oEAAoE;YACpE,wEAAwE;YACxE,mDAAmD;YACnD,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;YACtC,WAAW,EAAE,IAAI;YACjB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC;QAEH,8DAA8D;QAC9D,MAAM,SAAS,GAAG,IAAI,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;QAC/D,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,cAAc,GAAG,KAAK,CAAC;QAE3B,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC;YAC1B,IAAI,SAAS,GAAG,SAAS,GAAG,SAAS,IAAI,CAAC,cAAc,EAAE,CAAC;gBACzD,cAAc,GAAG,IAAI,CAAC;gBACtB,gBAAgB,CAAC,KAAK,CAAC,GAAI,CAAC,CAAC;gBAC7B,OAAO;YACT,CAAC;YACD,IAAI,CAAC,cAAc;gBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC;YAC1B,IAAI,SAAS,GAAG,SAAS,GAAG,SAAS,IAAI,CAAC,cAAc,EAAE,CAAC;gBACzD,cAAc,GAAG,IAAI,CAAC;gBACtB,gBAAgB,CAAC,KAAK,CAAC,GAAI,CAAC,CAAC;gBAC7B,OAAO;YACT,CAAC;YACD,IAAI,CAAC,cAAc;gBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,yDAAyD;QACzD,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,aAAiC,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,EAAE,OAAO,IAAI,MAAM,CAAC;QAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,QAAQ,GAAG,IAAI,CAAC;YAChB,aAAa,GAAG,SAAS,CAAC;YAC1B,gBAAgB,CAAC,KAAK,CAAC,GAAI,CAAC,CAAC;QAC/B,CAAC,EAAE,SAAS,CAAC,CAAC;QAEd,wEAAwE;QACxE,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,qEAAqE;QACrE,sEAAsE;QACtE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;YAC/C,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YAEf,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC1B,MAAM,CACJ,IAAI,KAAK,CACP,uBAAuB,GAAG,uDAAuD,CAClF,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAClD,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBAC1E,OAAO;YACT,CAAC;YAED,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;QAEH,wEAAwE;QACxE,iDAAiD;QACjD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACjC,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YAEf,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;YACxE,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAE7F,qEAAqE;YACrE,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,CACJ,IAAI,KAAK,CACP,YAAY,GAAG,qBAAqB,SAAS,sBAAsB,aAAa,IAAI,MAAM,IAAI,SAAS,IAAI,CAC5G,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,qBAAqB;YACrB,IAAI,cAAc,EAAE,CAAC;gBACnB,MAAM,CACJ,IAAI,KAAK,CACP,YAAY,GAAG,gCAAgC,SAAS,yBAAyB,CAClF,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,4DAA4D;YAC5D,IAAI,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBACvD,MAAM,CACJ,IAAI,KAAK,CACP,uBAAuB,GAAG,uDAAuD,CAClF,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,OAAO,CAAC;gBACN,QAAQ,EAAE,IAAI,IAAI,CAAC;gBACnB,MAAM;gBACN,MAAM;aACP,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,kEAAkE;QAClE,iEAAiE;QACjE,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC;YACxB,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Sanitizes error output by replacing sensitive filesystem paths with
3
- * home-relative equivalents. This prevents leaking usernames and absolute
4
- * home directory paths in error messages returned to MCP clients.
2
+ * Sanitizes error output by replacing sensitive filesystem paths.
5
3
  *
6
- * Replacements:
4
+ * Default mode (home paths only):
7
5
  * /home/<user>/... → ~/...
8
6
  * /Users/<user>/... → ~/...
9
7
  * /root/... → ~/...
10
8
  * C:\Users\<user>\... → ~\...
9
+ *
10
+ * Broad mode (PARE_SANITIZE_ALL_PATHS=true):
11
+ * Also redacts other absolute paths outside home directories:
12
+ * /etc/..., /var/..., /opt/..., C:\Program Files\..., etc.
13
+ * These are replaced with <redacted-path>/basename to preserve
14
+ * the filename while hiding directory structure.
11
15
  */
12
16
  export declare function sanitizeErrorOutput(text: string): string;
13
17
  //# sourceMappingURL=sanitize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcxD"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BxD"}
package/dist/sanitize.js CHANGED
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Sanitizes error output by replacing sensitive filesystem paths with
3
- * home-relative equivalents. This prevents leaking usernames and absolute
4
- * home directory paths in error messages returned to MCP clients.
2
+ * Sanitizes error output by replacing sensitive filesystem paths.
5
3
  *
6
- * Replacements:
4
+ * Default mode (home paths only):
7
5
  * /home/<user>/... → ~/...
8
6
  * /Users/<user>/... → ~/...
9
7
  * /root/... → ~/...
10
8
  * C:\Users\<user>\... → ~\...
9
+ *
10
+ * Broad mode (PARE_SANITIZE_ALL_PATHS=true):
11
+ * Also redacts other absolute paths outside home directories:
12
+ * /etc/..., /var/..., /opt/..., C:\Program Files\..., etc.
13
+ * These are replaced with <redacted-path>/basename to preserve
14
+ * the filename while hiding directory structure.
11
15
  */
12
16
  export function sanitizeErrorOutput(text) {
13
17
  // Unix: /home/<username>/rest → ~/rest
@@ -18,6 +22,19 @@ export function sanitizeErrorOutput(text) {
18
22
  result = result.replace(/\/root\//g, "~/");
19
23
  // Windows: C:\Users\<username>\rest → ~\rest (with escaped or literal backslashes)
20
24
  result = result.replace(/[A-Z]:\\Users\\[^\\:\s]+\\/gi, "~\\");
25
+ // Broad mode: redact remaining absolute paths
26
+ if (process.env.PARE_SANITIZE_ALL_PATHS === "true") {
27
+ // Unix absolute paths not already handled (e.g., /etc/foo/bar → <redacted>/bar)
28
+ result = result.replace(/\/(?:etc|var|opt|usr|tmp|srv|snap|nix)\/[^\s:]+/g, (match) => {
29
+ const basename = match.split("/").pop() ?? "";
30
+ return `<redacted-path>/${basename}`;
31
+ });
32
+ // Windows non-user absolute paths (e.g., C:\Program Files\foo → <redacted>\foo)
33
+ result = result.replace(/[A-Z]:\\(?!Users\\)[^\s:]+/gi, (match) => {
34
+ const basename = match.split("\\").pop() ?? "";
35
+ return `<redacted-path>\\${basename}`;
36
+ });
37
+ }
21
38
  return result;
22
39
  }
23
40
  //# sourceMappingURL=sanitize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,uCAAuC;IACvC,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;IAEtD,yCAAyC;IACzC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;IAErD,4BAA4B;IAC5B,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAE3C,mFAAmF;IACnF,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;IAE/D,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,uCAAuC;IACvC,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;IAEtD,yCAAyC;IACzC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;IAErD,4BAA4B;IAC5B,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAE3C,mFAAmF;IACnF,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;IAE/D,8CAA8C;IAC9C,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,MAAM,EAAE,CAAC;QACnD,gFAAgF;QAChF,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,kDAAkD,EAAE,CAAC,KAAK,EAAE,EAAE;YACpF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAC9C,OAAO,mBAAmB,QAAQ,EAAE,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,gFAAgF;QAChF,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,8BAA8B,EAAE,CAAC,KAAK,EAAE,EAAE;YAChE,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAC/C,OAAO,oBAAoB,QAAQ,EAAE,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Validates that a string argument is safe to pass as a positional argument to a CLI tool.
3
3
  * Prevents flag injection attacks (e.g., passing "--output=/etc/passwd" as a ref name).
4
- * See: CVE-2025-68144, CVE-2025-68145
4
+ * See: [CVE-2025-68144](https://nvd.nist.gov/vuln/detail/CVE-2025-68144), [CVE-2025-68145](https://nvd.nist.gov/vuln/detail/CVE-2025-68145)
5
5
  */
6
6
  export declare function assertNoFlagInjection(value: string, paramName: string): void;
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Validates that a string argument is safe to pass as a positional argument to a CLI tool.
3
3
  * Prevents flag injection attacks (e.g., passing "--output=/etc/passwd" as a ref name).
4
- * See: CVE-2025-68144, CVE-2025-68145
4
+ * See: [CVE-2025-68144](https://nvd.nist.gov/vuln/detail/CVE-2025-68144), [CVE-2025-68145](https://nvd.nist.gov/vuln/detail/CVE-2025-68145)
5
5
  */
6
6
  export function assertNoFlagInjection(value, paramName) {
7
7
  if (value.trimStart().startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paretools/shared",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Shared utilities for Pare MCP servers",
5
5
  "license": "MIT",
6
6
  "keywords": [