@tbrandenburg/node-red-cli 0.2.3

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.
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Parsing, validation, and userDir resolution for the
5
+ * `--node-modules <name[@version]>[,...]` / `--user-dir [path]` CLI options.
6
+ *
7
+ * Actual disk-diffing and npm install logic lives in `./node-modules-install`
8
+ * to keep this file focused and under the repo's soft per-file LOC limit.
9
+ */
10
+
11
+ const os = require("node:os");
12
+ const path = require("node:path");
13
+
14
+ /** Simplified but strict npm package name validation (no external dep). */
15
+ const NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
16
+
17
+ /** Loose semver validation: major[.minor[.patch]] with optional -pre/+build, or a dist-tag word. */
18
+ const VERSION_RE = /^[a-zA-Z0-9][a-zA-Z0-9.+_-]*$/;
19
+
20
+ const DEFAULT_DENY_PATTERNS = [
21
+ /\.\./, // path traversal
22
+ /^\./, // hidden/relative
23
+ /:\/\//, // URLs
24
+ /\s/, // whitespace
25
+ /^file:/i
26
+ ];
27
+
28
+ /** Extra deny patterns configurable via NODE_RED_CLI_DENY_MODULES (comma-separated exact names or *-globs). */
29
+ function envDenyPatterns() {
30
+ const raw = process.env.NODE_RED_CLI_DENY_MODULES;
31
+ if (!raw) return [];
32
+ return raw
33
+ .split(",")
34
+ .map((entry) => entry.trim())
35
+ .filter(Boolean)
36
+ .map((entry) => new RegExp(`^${entry.split("*").map(escapeRegExp).join(".*")}$`));
37
+ }
38
+
39
+ function escapeRegExp(value) {
40
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
+ }
42
+
43
+ /**
44
+ * Checks `name` against the built-in security denylist (path traversal,
45
+ * URLs, whitespace, `file:` refs) plus any patterns configured via
46
+ * `NODE_RED_CLI_DENY_MODULES`.
47
+ */
48
+ function isDenied(name) {
49
+ return [...DEFAULT_DENY_PATTERNS, ...envDenyPatterns()].some((pattern) => pattern.test(name));
50
+ }
51
+
52
+ /**
53
+ * Splits a single `--node-modules` value into its `name[@version]` entries
54
+ * (comma-separated), also merging multiple repeated `--node-modules`
55
+ * occurrences into one flat list.
56
+ */
57
+ function splitEntries(values) {
58
+ const list = Array.isArray(values) ? values : [values];
59
+ return list.flatMap((value) => String(value).split(","));
60
+ }
61
+
62
+ /**
63
+ * Parses one `name[@version]` entry. Handles scoped package names
64
+ * (`@scope/name` or `@scope/name@version`) by only splitting on the last
65
+ * `@` when it isn't the entry's leading scope marker.
66
+ */
67
+ function parseEntry(raw) {
68
+ const entry = raw.trim();
69
+ if (entry.length === 0) {
70
+ throw new Error("invalid --node-modules entry: empty module name");
71
+ }
72
+
73
+ const at = entry.startsWith("@") ? entry.indexOf("@", 1) : entry.indexOf("@");
74
+ const name = at === -1 ? entry : entry.slice(0, at);
75
+ const version = at === -1 ? undefined : entry.slice(at + 1);
76
+
77
+ if (version === "") {
78
+ throw new Error(`invalid --node-modules entry '${entry}': missing version after '@'`);
79
+ }
80
+ if (!NAME_RE.test(name)) {
81
+ throw new Error(`invalid --node-modules entry '${entry}': '${name}' is not a valid npm package name`);
82
+ }
83
+ if (version !== undefined && !VERSION_RE.test(version)) {
84
+ throw new Error(`invalid --node-modules entry '${entry}': '${version}' is not a valid version`);
85
+ }
86
+ if (isDenied(name)) {
87
+ throw new Error(`invalid --node-modules entry '${entry}': module '${name}' is not allowed`);
88
+ }
89
+
90
+ return { name, version };
91
+ }
92
+
93
+ /**
94
+ * Parses and validates the full `--node-modules` option value(s) into a
95
+ * deduplicated list of `{ name, version }` entries. Throws a clear `Error`
96
+ * on any malformed entry, before any npm process is spawned.
97
+ */
98
+ function parseNodeModulesParam(values) {
99
+ const modules = splitEntries(values).map(parseEntry);
100
+
101
+ const seen = new Set();
102
+ for (const { name } of modules) {
103
+ if (seen.has(name)) {
104
+ throw new Error(`invalid --node-modules value: module '${name}' is declared more than once`);
105
+ }
106
+ seen.add(name);
107
+ }
108
+ return modules;
109
+ }
110
+
111
+ /**
112
+ * Default persistent userDir used when `--user-dir` is passed with no
113
+ * explicit path (bare flag): `$XDG_CACHE_HOME/node-red-cli`, falling back
114
+ * to `~/.cache/node-red-cli`.
115
+ */
116
+ function defaultCacheDir() {
117
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
118
+ return path.join(base, "node-red-cli");
119
+ }
120
+
121
+ /**
122
+ * Resolves the `--user-dir` CLI option into an absolute persistent path, or
123
+ * `undefined` when the option wasn't given at all (ephemeral mode).
124
+ * `--user-dir` (bare, no value) resolves to the default cache dir;
125
+ * `--user-dir <path>` resolves `<path>` relative to `cwd`.
126
+ */
127
+ function resolveUserDir(value, cwd = process.cwd()) {
128
+ if (value === undefined) return undefined;
129
+ if (value === true) return defaultCacheDir();
130
+ return path.resolve(cwd, value);
131
+ }
132
+
133
+ module.exports = {
134
+ parseNodeModulesParam,
135
+ defaultCacheDir,
136
+ resolveUserDir,
137
+ isDenied
138
+ };