@red-hat-developer-hub/cli-module-install-dynamic-plugins 0.2.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +126 -0
  3. package/bin/install-dynamic-plugins +32 -0
  4. package/dist/catalog-index.cjs.js +242 -0
  5. package/dist/catalog-index.cjs.js.map +1 -0
  6. package/dist/command.cjs.js +12 -0
  7. package/dist/command.cjs.js.map +1 -0
  8. package/dist/concurrency.cjs.js +86 -0
  9. package/dist/concurrency.cjs.js.map +1 -0
  10. package/dist/errors.cjs.js +11 -0
  11. package/dist/errors.cjs.js.map +1 -0
  12. package/dist/image-cache.cjs.js +116 -0
  13. package/dist/image-cache.cjs.js.map +1 -0
  14. package/dist/image-resolver.cjs.js +24 -0
  15. package/dist/image-resolver.cjs.js.map +1 -0
  16. package/dist/index.cjs.js +20 -0
  17. package/dist/index.cjs.js.map +1 -0
  18. package/dist/index.d.ts +12 -0
  19. package/dist/installer-npm.cjs.js +111 -0
  20. package/dist/installer-npm.cjs.js.map +1 -0
  21. package/dist/installer-oci.cjs.js +106 -0
  22. package/dist/installer-oci.cjs.js.map +1 -0
  23. package/dist/installer.cjs.js +426 -0
  24. package/dist/installer.cjs.js.map +1 -0
  25. package/dist/integrity.cjs.js +79 -0
  26. package/dist/integrity.cjs.js.map +1 -0
  27. package/dist/lock-file.cjs.js +100 -0
  28. package/dist/lock-file.cjs.js.map +1 -0
  29. package/dist/log.cjs.js +9 -0
  30. package/dist/log.cjs.js.map +1 -0
  31. package/dist/merger.cjs.js +333 -0
  32. package/dist/merger.cjs.js.map +1 -0
  33. package/dist/npm-key.cjs.js +44 -0
  34. package/dist/npm-key.cjs.js.map +1 -0
  35. package/dist/oci-key.cjs.js +102 -0
  36. package/dist/oci-key.cjs.js.map +1 -0
  37. package/dist/package.json.cjs.js +104 -0
  38. package/dist/package.json.cjs.js.map +1 -0
  39. package/dist/plugin-hash.cjs.js +85 -0
  40. package/dist/plugin-hash.cjs.js.map +1 -0
  41. package/dist/run.cjs.js +37 -0
  42. package/dist/run.cjs.js.map +1 -0
  43. package/dist/skopeo.cjs.js +87 -0
  44. package/dist/skopeo.cjs.js.map +1 -0
  45. package/dist/tar-extract.cjs.js +155 -0
  46. package/dist/tar-extract.cjs.js.map +1 -0
  47. package/dist/types.cjs.js +45 -0
  48. package/dist/types.cjs.js.map +1 -0
  49. package/dist/util.cjs.js +56 -0
  50. package/dist/util.cjs.js.map +1 -0
  51. package/dist/which.cjs.js +45 -0
  52. package/dist/which.cjs.js.map +1 -0
  53. package/package.json +68 -0
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ var errors = require('./errors.cjs.js');
4
+ var log = require('./log.cjs.js');
5
+ var types = require('./types.cjs.js');
6
+
7
+ const OCI_PATTERN = [
8
+ "^(",
9
+ escape(types.OCI_PROTO),
10
+ String.raw`[^\s/:@]+`,
11
+ // registry host
12
+ String.raw`(?::\d+)?`,
13
+ // optional port
14
+ String.raw`(?:/[^\s:@]+)+`,
15
+ // at least one path segment
16
+ ")",
17
+ String.raw`(?::([^\s!@:]+)`,
18
+ // tag
19
+ "|",
20
+ String.raw`@((?:sha256|sha512|blake3):[^\s!@:]+))`,
21
+ // or digest
22
+ String.raw`(?:!([^\s]+))?$`
23
+ // optional !<plugin-path>
24
+ ].join("");
25
+ const OCI_REGEX = new RegExp(OCI_PATTERN);
26
+ async function ociPluginKey(pkg, imageCache) {
27
+ const m = OCI_REGEX.exec(pkg);
28
+ if (!m) {
29
+ throw new errors.InstallException(
30
+ `oci package '${pkg}' is not in the expected format '${types.OCI_PROTO}<registry>:<tag>' or '${types.OCI_PROTO}<registry>@<algo>:<digest>' (optionally followed by '!<path>') where <registry> may include a port (e.g. host:5000/path) and <algo> is one of ${types.RECOGNIZED_ALGORITHMS.join(", ")}`
31
+ );
32
+ }
33
+ const registry = m[1];
34
+ const tag = m[2];
35
+ const digest = m[3];
36
+ let path = m[4] ?? null;
37
+ const version = tag ?? digest;
38
+ const inherit = tag === "{{inherit}}" && digest === void 0;
39
+ if (inherit && !path) {
40
+ return { pluginKey: registry, version, inherit, resolvedPath: null };
41
+ }
42
+ if (!path) {
43
+ path = await autoDetectPluginPath(
44
+ pkg,
45
+ registry,
46
+ version,
47
+ tag !== void 0,
48
+ imageCache
49
+ );
50
+ }
51
+ return {
52
+ pluginKey: `${registry}:!${path}`,
53
+ version,
54
+ inherit,
55
+ resolvedPath: path
56
+ };
57
+ }
58
+ async function autoDetectPluginPath(pkg, registry, version, isTag, imageCache) {
59
+ if (!imageCache) {
60
+ throw new errors.InstallException(
61
+ `Cannot auto-detect plugin path for ${pkg}: no image cache provided`
62
+ );
63
+ }
64
+ const fullImage = isTag ? `${registry}:${version}` : `${registry}@${version}`;
65
+ log.log(
66
+ `
67
+ ======= No plugin path specified for ${fullImage}, auto-detecting from OCI manifest`
68
+ );
69
+ const paths = await imageCache.getPluginPaths(fullImage);
70
+ if (paths.length === 0) {
71
+ throw new errors.InstallException(
72
+ `No plugins found in OCI image ${fullImage}. The image might not contain the 'io.backstage.dynamic-packages' annotation. Please ensure it was packaged using the @red-hat-developer-hub/cli plugin package command.`
73
+ );
74
+ }
75
+ if (paths.length > 1) {
76
+ const formatted = paths.map((p) => ` - ${p}`).join("\n");
77
+ throw new errors.InstallException(
78
+ `Multiple plugins found in OCI image ${fullImage}:
79
+ ${formatted}
80
+ Please specify which plugin to install using the syntax: ${fullImage}!<plugin-name>`
81
+ );
82
+ }
83
+ const resolved = paths[0];
84
+ log.log(
85
+ `
86
+ ======= Auto-resolving OCI package ${fullImage} to use plugin path: ${resolved}`
87
+ );
88
+ return resolved;
89
+ }
90
+ function tryParseOciRegistryAndPath(pkg) {
91
+ const m = OCI_REGEX.exec(pkg);
92
+ if (!m) return null;
93
+ return { registry: m[1], path: m[4] ?? null };
94
+ }
95
+ function escape(s) {
96
+ return s.replaceAll(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`);
97
+ }
98
+
99
+ exports.OCI_REGEX = OCI_REGEX;
100
+ exports.ociPluginKey = ociPluginKey;
101
+ exports.tryParseOciRegistryAndPath = tryParseOciRegistryAndPath;
102
+ //# sourceMappingURL=oci-key.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oci-key.cjs.js","sources":["../src/oci-key.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InstallException } from './errors';\nimport { log } from './log';\nimport { type OciImageCache } from './image-cache';\nimport { OCI_PROTO, RECOGNIZED_ALGORITHMS } from './types';\n\nconst OCI_PATTERN = [\n '^(',\n escape(OCI_PROTO),\n String.raw`[^\\s/:@]+`, // registry host\n String.raw`(?::\\d+)?`, // optional port\n String.raw`(?:/[^\\s:@]+)+`, // at least one path segment\n ')',\n String.raw`(?::([^\\s!@:]+)`, // tag\n '|',\n String.raw`@((?:sha256|sha512|blake3):[^\\s!@:]+))`, // or digest\n String.raw`(?:!([^\\s]+))?$`, // optional !<plugin-path>\n].join('');\n\nexport const OCI_REGEX = new RegExp(OCI_PATTERN);\n\nexport type ParsedOciKey = {\n /** `oci://registry/image:!plugin_path` — version-stripped identifier. */\n pluginKey: string;\n /** Tag (e.g. `1.2.3`) or digest (`sha256:...`). */\n version: string;\n /** True when tag was `{{inherit}}` (version to be resolved from an included config). */\n inherit: boolean;\n /**\n * Resolved plugin path — explicit `!<path>`, auto-detected from the image's\n * `io.backstage.dynamic-packages` annotation, or `null` when `{{inherit}}`\n * is used without a path (the merger resolves it later).\n */\n resolvedPath: string | null;\n};\n\n/**\n * Parse an `oci://...` package spec. Matches fast.py and the original\n * `OciPackageMerger.parse_plugin_key`. Calls into `imageCache.getPluginPaths`\n * to auto-detect single-plugin images when the `!path` suffix is omitted.\n */\nexport async function ociPluginKey(\n pkg: string,\n imageCache?: OciImageCache,\n): Promise<ParsedOciKey> {\n const m = OCI_REGEX.exec(pkg);\n if (!m) {\n throw new InstallException(\n `oci package '${pkg}' is not in the expected format '${OCI_PROTO}<registry>:<tag>' ` +\n `or '${OCI_PROTO}<registry>@<algo>:<digest>' (optionally followed by '!<path>') ` +\n `where <registry> may include a port (e.g. host:5000/path) ` +\n `and <algo> is one of ${RECOGNIZED_ALGORITHMS.join(', ')}`,\n );\n }\n\n const registry = m[1] as string;\n const tag = m[2];\n const digest = m[3];\n let path = m[4] ?? null;\n\n const version = (tag ?? digest) as string;\n const inherit = tag === '{{inherit}}' && digest === undefined;\n\n if (inherit && !path) {\n // The merger will match against an earlier included plugin from the same image.\n return { pluginKey: registry, version, inherit, resolvedPath: null };\n }\n\n if (!path) {\n path = await autoDetectPluginPath(\n pkg,\n registry,\n version,\n tag !== undefined,\n imageCache,\n );\n }\n\n return {\n pluginKey: `${registry}:!${path}`,\n version,\n inherit,\n resolvedPath: path,\n };\n}\n\nasync function autoDetectPluginPath(\n pkg: string,\n registry: string,\n version: string,\n isTag: boolean,\n imageCache: OciImageCache | undefined,\n): Promise<string> {\n if (!imageCache) {\n throw new InstallException(\n `Cannot auto-detect plugin path for ${pkg}: no image cache provided`,\n );\n }\n const fullImage = isTag ? `${registry}:${version}` : `${registry}@${version}`;\n log(\n `\\n======= No plugin path specified for ${fullImage}, auto-detecting from OCI manifest`,\n );\n const paths = await imageCache.getPluginPaths(fullImage);\n if (paths.length === 0) {\n throw new InstallException(\n `No plugins found in OCI image ${fullImage}. ` +\n `The image might not contain the 'io.backstage.dynamic-packages' annotation. ` +\n `Please ensure it was packaged using the @red-hat-developer-hub/cli plugin package command.`,\n );\n }\n if (paths.length > 1) {\n const formatted = paths.map(p => ` - ${p}`).join('\\n');\n throw new InstallException(\n `Multiple plugins found in OCI image ${fullImage}:\\n${formatted}\\n` +\n `Please specify which plugin to install using the syntax: ${fullImage}!<plugin-name>`,\n );\n }\n const resolved = paths[0] as string;\n log(\n `\\n======= Auto-resolving OCI package ${fullImage} to use plugin path: ${resolved}`,\n );\n return resolved;\n}\n\n/**\n * Synchronous parse for the disable-pre-merge pass. Returns `null` when the\n * package does not match the expected OCI grammar — callers decide how to\n * react (warn-and-skip when the entry is disabled, throw when it is enabled).\n * Mirrors the `match.group(1)` / `match.group(4)` access pattern used by\n * `pre_merge_oci_disabled_state` in the Python implementation.\n */\nexport function tryParseOciRegistryAndPath(\n pkg: string,\n): { registry: string; path: string | null } | null {\n const m = OCI_REGEX.exec(pkg);\n if (!m) return null;\n return { registry: m[1] as string, path: m[4] ?? null };\n}\n\nfunction escape(s: string): string {\n return s.replaceAll(/[.*+?^${}()|[\\]\\\\/]/g, String.raw`\\$&`);\n}\n"],"names":["OCI_PROTO","InstallException","RECOGNIZED_ALGORITHMS","log"],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var name = "@red-hat-developer-hub/cli-module-install-dynamic-plugins";
6
+ var version = "0.2.0";
7
+ var description = "Backstage CLI module that installs RHDH dynamic plugins from a dynamic-plugins.yaml config (OCI, NPM, local).";
8
+ var license = "Apache-2.0";
9
+ var backstage = {
10
+ role: "cli-module"
11
+ };
12
+ var homepage = "https://github.com/redhat-developer/rhdh-plugins/tree/main/workspaces/install-dynamic-plugins#readme";
13
+ var repository = {
14
+ type: "git",
15
+ url: "https://github.com/redhat-developer/rhdh-plugins",
16
+ directory: "workspaces/install-dynamic-plugins/packages/install-dynamic-plugins"
17
+ };
18
+ var bugs = "https://github.com/redhat-developer/rhdh-plugins/issues";
19
+ var keywords = [
20
+ "rhdh",
21
+ "backstage",
22
+ "dynamic-plugins",
23
+ "cli",
24
+ "cli-module",
25
+ "init-container"
26
+ ];
27
+ var engines = {
28
+ node: "22 || 24"
29
+ };
30
+ var main = "src/index.ts";
31
+ var types = "src/index.ts";
32
+ var bin = "bin/install-dynamic-plugins";
33
+ var files = [
34
+ "bin",
35
+ "dist"
36
+ ];
37
+ var publishConfig = {
38
+ access: "public",
39
+ main: "dist/index.cjs.js",
40
+ types: "dist/index.d.ts"
41
+ };
42
+ var scripts = {
43
+ build: "backstage-cli package build",
44
+ clean: "backstage-cli package clean",
45
+ lint: "backstage-cli package lint",
46
+ prepack: "backstage-cli package prepack",
47
+ postpack: "backstage-cli package postpack",
48
+ test: "backstage-cli package test",
49
+ tsc: "tsc"
50
+ };
51
+ var dependencies = {
52
+ "@backstage/cli-node": "^0.3.2",
53
+ cleye: "^2.6.0",
54
+ tar: "^7.5.13",
55
+ yaml: "^2.8.2"
56
+ };
57
+ var devDependencies = {
58
+ "@backstage/cli": "^0.36.0",
59
+ "@types/jest": "^30.0.0",
60
+ "@types/node": "^22.10.0",
61
+ "@types/tar": "^6.1.13",
62
+ typescript: "~5.8.0"
63
+ };
64
+ var packageJson = {
65
+ name: name,
66
+ version: version,
67
+ description: description,
68
+ license: license,
69
+ backstage: backstage,
70
+ homepage: homepage,
71
+ repository: repository,
72
+ bugs: bugs,
73
+ keywords: keywords,
74
+ engines: engines,
75
+ main: main,
76
+ types: types,
77
+ bin: bin,
78
+ files: files,
79
+ publishConfig: publishConfig,
80
+ scripts: scripts,
81
+ dependencies: dependencies,
82
+ devDependencies: devDependencies
83
+ };
84
+
85
+ exports.backstage = backstage;
86
+ exports.bin = bin;
87
+ exports.bugs = bugs;
88
+ exports.default = packageJson;
89
+ exports.dependencies = dependencies;
90
+ exports.description = description;
91
+ exports.devDependencies = devDependencies;
92
+ exports.engines = engines;
93
+ exports.files = files;
94
+ exports.homepage = homepage;
95
+ exports.keywords = keywords;
96
+ exports.license = license;
97
+ exports.main = main;
98
+ exports.name = name;
99
+ exports.publishConfig = publishConfig;
100
+ exports.repository = repository;
101
+ exports.scripts = scripts;
102
+ exports.types = types;
103
+ exports.version = version;
104
+ //# sourceMappingURL=package.json.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.json.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ var node_crypto = require('node:crypto');
4
+ var node_fs = require('node:fs');
5
+ var path = require('node:path');
6
+
7
+ function _interopNamespaceCompat(e) {
8
+ if (e && typeof e === 'object' && 'default' in e) return e;
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
26
+
27
+ function computePluginHash(plugin) {
28
+ const copy = {};
29
+ for (const [k, v] of Object.entries(plugin)) {
30
+ if (k === "pluginConfig" || k === "version" || k === "plugin_hash")
31
+ continue;
32
+ copy[k] = v;
33
+ }
34
+ if (plugin.package.startsWith("./")) {
35
+ copy._local_package_info = localPackageInfo(plugin.package);
36
+ }
37
+ const serialized = stableStringify(copy);
38
+ return node_crypto.createHash("sha256").update(serialized).digest("hex");
39
+ }
40
+ function localPackageInfo(pkgPath) {
41
+ const absPath = path__namespace.isAbsolute(pkgPath) ? pkgPath : path__namespace.join(process.cwd(), pkgPath.slice(2));
42
+ const pj = path__namespace.join(absPath, "package.json");
43
+ if (!node_fs.existsSync(pj)) {
44
+ try {
45
+ return { _directory_mtime: toSeconds(node_fs.statSync(absPath).mtimeMs) };
46
+ } catch {
47
+ return { _not_found: true };
48
+ }
49
+ }
50
+ try {
51
+ const info = {
52
+ _package_json: JSON.parse(node_fs.readFileSync(pj, "utf8")),
53
+ _package_json_mtime: toSeconds(node_fs.statSync(pj).mtimeMs)
54
+ };
55
+ for (const lockFile of ["package-lock.json", "yarn.lock"]) {
56
+ const lockPath = path__namespace.join(absPath, lockFile);
57
+ if (node_fs.existsSync(lockPath)) {
58
+ info[`_${lockFile}_mtime`] = toSeconds(node_fs.statSync(lockPath).mtimeMs);
59
+ }
60
+ }
61
+ return info;
62
+ } catch (err) {
63
+ return { _error: err.message };
64
+ }
65
+ }
66
+ function toSeconds(mtimeMs) {
67
+ return mtimeMs / 1e3;
68
+ }
69
+ function compareCodePoint(a, b) {
70
+ if (a < b) return -1;
71
+ if (a > b) return 1;
72
+ return 0;
73
+ }
74
+ function stableStringify(value) {
75
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
76
+ if (Array.isArray(value)) {
77
+ return `[${value.map(stableStringify).join(", ")}]`;
78
+ }
79
+ const obj = value;
80
+ const entries = Object.keys(obj).sort(compareCodePoint).map((k) => `${JSON.stringify(k)}: ${stableStringify(obj[k])}`);
81
+ return `{${entries.join(", ")}}`;
82
+ }
83
+
84
+ exports.computePluginHash = computePluginHash;
85
+ //# sourceMappingURL=plugin-hash.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-hash.cjs.js","sources":["../src/plugin-hash.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { createHash } from 'node:crypto';\nimport { statSync, existsSync, readFileSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { type Plugin } from './types';\n\n/**\n * Compute the config-hash for a plugin, used to detect \"already installed\".\n *\n * The hash is byte-compatible with the Python implementation\n * (`install-dynamic-plugins.py`): both versions\n * - strip `pluginConfig` and `version` before hashing,\n * - keep `last_modified_level` (the include-file precedence) inside the hash,\n * - serialize via stable, sort-keyed JSON,\n * - and use the same field names for local-package metadata\n * (`_local_package_info`, `_package_json`, `_package_json_mtime`,\n * `_directory_mtime`, `_not_found`, `_error`, `_<lockfile>_mtime`).\n *\n * Cross-compat matters because an in-place upgrade from the Python script\n * to this TS port should not trigger a full reinstall of every plugin on\n * the first run.\n */\nexport function computePluginHash(plugin: Plugin): string {\n const copy: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(plugin)) {\n if (k === 'pluginConfig' || k === 'version' || k === 'plugin_hash')\n continue;\n copy[k] = v;\n }\n if (plugin.package.startsWith('./')) {\n copy._local_package_info = localPackageInfo(plugin.package);\n }\n const serialized = stableStringify(copy);\n return createHash('sha256').update(serialized).digest('hex');\n}\n\ntype LocalPackageInfo = {\n _package_json?: unknown;\n _package_json_mtime?: number;\n _directory_mtime?: number;\n _not_found?: boolean;\n _error?: string;\n [key: string]: unknown;\n};\n\n/**\n * Inspect a local package path and return the metadata included in the\n * install hash. Field names and value formats match the Python helper\n * `get_local_package_info` so the resulting hash is identical.\n *\n * Mtime is stored in seconds-since-epoch as a float — Python's\n * `os.path.getmtime` returns float seconds, so we divide Node's\n * millisecond value by 1000.\n */\nfunction localPackageInfo(pkgPath: string): LocalPackageInfo {\n const absPath = path.isAbsolute(pkgPath)\n ? pkgPath\n : path.join(process.cwd(), pkgPath.slice(2));\n const pj = path.join(absPath, 'package.json');\n if (!existsSync(pj)) {\n try {\n return { _directory_mtime: toSeconds(statSync(absPath).mtimeMs) };\n } catch {\n return { _not_found: true };\n }\n }\n try {\n const info: LocalPackageInfo = {\n _package_json: JSON.parse(readFileSync(pj, 'utf8')),\n _package_json_mtime: toSeconds(statSync(pj).mtimeMs),\n };\n for (const lockFile of ['package-lock.json', 'yarn.lock']) {\n const lockPath = path.join(absPath, lockFile);\n if (existsSync(lockPath)) {\n info[`_${lockFile}_mtime`] = toSeconds(statSync(lockPath).mtimeMs);\n }\n }\n return info;\n } catch (err) {\n return { _error: (err as Error).message };\n }\n}\n\nfunction toSeconds(mtimeMs: number): number {\n return mtimeMs / 1000;\n}\n\n/**\n * Deterministic JSON stringification — sorts keys at every level and emits\n * Python-style separators (`, ` between elements, `: ` between key/value)\n * so the resulting string is byte-identical to Python's\n * `json.dumps(..., sort_keys=True)`. Required for hash compatibility with\n * the previous Python implementation.\n *\n * Uses an explicit code-point comparator (locale-independent, matches\n * Python's default `sorted()` ordering on str keys).\n */\nfunction compareCodePoint(a: string, b: string): number {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(', ')}]`;\n }\n const obj = value as Record<string, unknown>;\n const entries = Object.keys(obj)\n .sort(compareCodePoint)\n .map(k => `${JSON.stringify(k)}: ${stableStringify(obj[k])}`);\n return `{${entries.join(', ')}}`;\n}\n"],"names":["createHash","path","existsSync","statSync","readFileSync"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var errors = require('./errors.cjs.js');
5
+
6
+ async function run(cmd, errMsg, options = {}) {
7
+ if (cmd.length === 0) {
8
+ throw new errors.InstallException(`${errMsg}: empty command`);
9
+ }
10
+ const [bin, ...args] = cmd;
11
+ return new Promise((resolve, reject) => {
12
+ const child = node_child_process.spawn(bin, args, {
13
+ ...options,
14
+ stdio: ["ignore", "pipe", "pipe"]
15
+ });
16
+ let stdout = "";
17
+ let stderr = "";
18
+ child.stdout?.on("data", (chunk) => stdout += chunk.toString());
19
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
20
+ child.on(
21
+ "error",
22
+ (err) => reject(new errors.InstallException(`${errMsg}: ${err.message}`))
23
+ );
24
+ child.on("close", (code) => {
25
+ if (code === 0) {
26
+ resolve({ stdout, stderr });
27
+ } else {
28
+ const parts = [`${errMsg}: exit code ${code}`, `cmd: ${cmd.join(" ")}`];
29
+ if (stderr.trim()) parts.push(`stderr: ${stderr.trim()}`);
30
+ reject(new errors.InstallException(parts.join("\n")));
31
+ }
32
+ });
33
+ });
34
+ }
35
+
36
+ exports.run = run;
37
+ //# sourceMappingURL=run.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.cjs.js","sources":["../src/run.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { spawn, type SpawnOptions } from 'node:child_process';\nimport { InstallException } from './errors';\n\nexport type RunResult = {\n stdout: string;\n stderr: string;\n};\n\n/**\n * Execute a command, capturing stdout/stderr. Throws InstallException with full\n * context (exit code, stderr) on non-zero exit. Matches the Python `run()` contract.\n */\nexport async function run(\n cmd: string[],\n errMsg: string,\n options: SpawnOptions = {},\n): Promise<RunResult> {\n if (cmd.length === 0) {\n throw new InstallException(`${errMsg}: empty command`);\n }\n const [bin, ...args] = cmd as [string, ...string[]];\n return new Promise<RunResult>((resolve, reject) => {\n const child = spawn(bin, args, {\n ...options,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let stdout = '';\n let stderr = '';\n child.stdout?.on('data', (chunk: Buffer) => (stdout += chunk.toString()));\n child.stderr?.on('data', (chunk: Buffer) => (stderr += chunk.toString()));\n child.on('error', err =>\n reject(new InstallException(`${errMsg}: ${err.message}`)),\n );\n child.on('close', code => {\n if (code === 0) {\n resolve({ stdout, stderr });\n } else {\n const parts = [`${errMsg}: exit code ${code}`, `cmd: ${cmd.join(' ')}`];\n if (stderr.trim()) parts.push(`stderr: ${stderr.trim()}`);\n reject(new InstallException(parts.join('\\n')));\n }\n });\n });\n}\n"],"names":["InstallException","spawn"],"mappings":";;;;;AA2BA,eAAsB,GAAA,CACpB,GAAA,EACA,MAAA,EACA,OAAA,GAAwB,EAAC,EACL;AACpB,EAAA,IAAI,GAAA,CAAI,WAAW,CAAA,EAAG;AACpB,IAAA,MAAM,IAAIA,uBAAA,CAAiB,CAAA,EAAG,MAAM,CAAA,eAAA,CAAiB,CAAA;AAAA,EACvD;AACA,EAAA,MAAM,CAAC,GAAA,EAAK,GAAG,IAAI,CAAA,GAAI,GAAA;AACvB,EAAA,OAAO,IAAI,OAAA,CAAmB,CAAC,OAAA,EAAS,MAAA,KAAW;AACjD,IAAA,MAAM,KAAA,GAAQC,wBAAA,CAAM,GAAA,EAAK,IAAA,EAAM;AAAA,MAC7B,GAAG,OAAA;AAAA,MACH,KAAA,EAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM;AAAA,KACjC,CAAA;AACD,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,KAAA,CAAM,MAAA,EAAQ,GAAG,MAAA,EAAQ,CAAC,UAAmB,MAAA,IAAU,KAAA,CAAM,UAAW,CAAA;AACxE,IAAA,KAAA,CAAM,MAAA,EAAQ,GAAG,MAAA,EAAQ,CAAC,UAAmB,MAAA,IAAU,KAAA,CAAM,UAAW,CAAA;AACxE,IAAA,KAAA,CAAM,EAAA;AAAA,MAAG,OAAA;AAAA,MAAS,CAAA,GAAA,KAChB,MAAA,CAAO,IAAID,uBAAA,CAAiB,CAAA,EAAG,MAAM,CAAA,EAAA,EAAK,GAAA,CAAI,OAAO,CAAA,CAAE,CAAC;AAAA,KAC1D;AACA,IAAA,KAAA,CAAM,EAAA,CAAG,SAAS,CAAA,IAAA,KAAQ;AACxB,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,OAAA,CAAQ,EAAE,MAAA,EAAQ,MAAA,EAAQ,CAAA;AAAA,MAC5B,CAAA,MAAO;AACL,QAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,EAAG,MAAM,CAAA,YAAA,EAAe,IAAI,CAAA,CAAA,EAAI,CAAA,KAAA,EAAQ,GAAA,CAAI,IAAA,CAAK,GAAG,CAAC,CAAA,CAAE,CAAA;AACtE,QAAA,IAAI,MAAA,CAAO,MAAK,EAAG,KAAA,CAAM,KAAK,CAAA,QAAA,EAAW,MAAA,CAAO,IAAA,EAAM,CAAA,CAAE,CAAA;AACxD,QAAA,MAAA,CAAO,IAAIA,uBAAA,CAAiB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAC,CAAA;AAAA,MAC/C;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;;;;"}
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var errors = require('./errors.cjs.js');
5
+ var run = require('./run.cjs.js');
6
+ var which = require('./which.cjs.js');
7
+
8
+ class Skopeo {
9
+ path;
10
+ inspectRawCache = /* @__PURE__ */ new Map();
11
+ inspectCache = /* @__PURE__ */ new Map();
12
+ existsCache = /* @__PURE__ */ new Map();
13
+ constructor(skopeoPath) {
14
+ const resolved = skopeoPath ?? which.which("skopeo");
15
+ if (!resolved) throw new errors.InstallException("skopeo not found in PATH");
16
+ this.path = resolved;
17
+ }
18
+ async copy(src, dst) {
19
+ await run.run(
20
+ [
21
+ this.path,
22
+ "copy",
23
+ "--override-os=linux",
24
+ "--override-arch=amd64",
25
+ src,
26
+ dst
27
+ ],
28
+ `skopeo copy failed: ${src}`
29
+ );
30
+ }
31
+ async inspectRaw(url) {
32
+ const cached = this.inspectRawCache.get(url);
33
+ if (cached) return cached;
34
+ const pending = this.runInspect(url, true);
35
+ this.inspectRawCache.set(url, pending);
36
+ try {
37
+ return await pending;
38
+ } catch (err) {
39
+ this.inspectRawCache.delete(url);
40
+ throw err;
41
+ }
42
+ }
43
+ async inspect(url) {
44
+ const cached = this.inspectCache.get(url);
45
+ if (cached) return cached;
46
+ const pending = this.runInspect(url, false);
47
+ this.inspectCache.set(url, pending);
48
+ try {
49
+ return await pending;
50
+ } catch (err) {
51
+ this.inspectCache.delete(url);
52
+ throw err;
53
+ }
54
+ }
55
+ /**
56
+ * Returns true iff `skopeo inspect` succeeds; never throws. Result is
57
+ * memoized — subsequent calls for the same URL reuse the in-flight or
58
+ * resolved promise. This dedups the `resolveImage` registry probe across
59
+ * the many plugins that share the same OCI image (common for the RHDH
60
+ * plugin catalog).
61
+ */
62
+ async exists(url) {
63
+ const cached = this.existsCache.get(url);
64
+ if (cached) return cached;
65
+ const pending = new Promise((resolve) => {
66
+ const child = node_child_process.spawn(this.path, ["inspect", "--no-tags", url], {
67
+ stdio: "ignore"
68
+ });
69
+ child.on("error", () => resolve(false));
70
+ child.on("close", (code) => resolve(code === 0));
71
+ });
72
+ this.existsCache.set(url, pending);
73
+ return pending;
74
+ }
75
+ async runInspect(url, raw) {
76
+ const args = ["inspect", "--no-tags", url];
77
+ if (raw) args.splice(1, 0, "--raw");
78
+ const { stdout } = await run.run(
79
+ [this.path, ...args],
80
+ `skopeo inspect failed: ${url}`
81
+ );
82
+ return JSON.parse(stdout);
83
+ }
84
+ }
85
+
86
+ exports.Skopeo = Skopeo;
87
+ //# sourceMappingURL=skopeo.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skopeo.cjs.js","sources":["../src/skopeo.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { spawn } from 'node:child_process';\nimport { InstallException } from './errors';\nimport { run } from './run';\nimport { which } from './which';\n\n/**\n * Wrapper around the `skopeo` CLI with in-memory caching for `inspect` results.\n *\n * JS is single-threaded so the caches don't need locks, unlike the Python\n * version. Multiple concurrent callers inspecting the same image still share\n * the one in-flight request because we cache the promise, not just the value.\n */\nexport class Skopeo {\n private readonly path: string;\n private readonly inspectRawCache = new Map<string, Promise<unknown>>();\n private readonly inspectCache = new Map<string, Promise<SkopeoInspect>>();\n private readonly existsCache = new Map<string, Promise<boolean>>();\n\n constructor(skopeoPath?: string) {\n const resolved = skopeoPath ?? which('skopeo');\n if (!resolved) throw new InstallException('skopeo not found in PATH');\n this.path = resolved;\n }\n\n async copy(src: string, dst: string): Promise<void> {\n await run(\n [\n this.path,\n 'copy',\n '--override-os=linux',\n '--override-arch=amd64',\n src,\n dst,\n ],\n `skopeo copy failed: ${src}`,\n );\n }\n\n async inspectRaw(url: string): Promise<unknown> {\n const cached = this.inspectRawCache.get(url);\n if (cached) return cached;\n const pending = this.runInspect(url, true);\n this.inspectRawCache.set(url, pending);\n try {\n return await pending;\n } catch (err) {\n this.inspectRawCache.delete(url);\n throw err;\n }\n }\n\n async inspect(url: string): Promise<SkopeoInspect> {\n const cached = this.inspectCache.get(url);\n if (cached) return cached;\n const pending = this.runInspect(url, false) as Promise<SkopeoInspect>;\n this.inspectCache.set(url, pending);\n try {\n return await pending;\n } catch (err) {\n this.inspectCache.delete(url);\n throw err;\n }\n }\n\n /**\n * Returns true iff `skopeo inspect` succeeds; never throws. Result is\n * memoized — subsequent calls for the same URL reuse the in-flight or\n * resolved promise. This dedups the `resolveImage` registry probe across\n * the many plugins that share the same OCI image (common for the RHDH\n * plugin catalog).\n */\n async exists(url: string): Promise<boolean> {\n const cached = this.existsCache.get(url);\n if (cached) return cached;\n const pending = new Promise<boolean>(resolve => {\n const child = spawn(this.path, ['inspect', '--no-tags', url], {\n stdio: 'ignore',\n });\n child.on('error', () => resolve(false));\n child.on('close', code => resolve(code === 0));\n });\n this.existsCache.set(url, pending);\n return pending;\n }\n\n private async runInspect(url: string, raw: boolean): Promise<unknown> {\n const args = ['inspect', '--no-tags', url];\n if (raw) args.splice(1, 0, '--raw'); // inspect --raw --no-tags <url>\n const { stdout } = await run(\n [this.path, ...args],\n `skopeo inspect failed: ${url}`,\n );\n return JSON.parse(stdout);\n }\n}\n\nexport type SkopeoInspect = {\n Name?: string;\n Digest?: string;\n Labels?: Record<string, string>;\n [key: string]: unknown;\n};\n"],"names":["which","InstallException","run","spawn"],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs/promises');
4
+ var path = require('node:path');
5
+ var tar = require('tar');
6
+ var errors = require('./errors.cjs.js');
7
+ var log = require('./log.cjs.js');
8
+ var types = require('./types.cjs.js');
9
+ var util = require('./util.cjs.js');
10
+
11
+ function _interopNamespaceCompat(e) {
12
+ if (e && typeof e === 'object' && 'default' in e) return e;
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
30
+ var path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
31
+ var tar__namespace = /*#__PURE__*/_interopNamespaceCompat(tar);
32
+
33
+ const PACKAGE_PREFIX = "package/";
34
+ async function extractOciPlugin(tarball, pluginPath, destination) {
35
+ assertSafePluginPath(pluginPath);
36
+ const destAbs = path__namespace.resolve(destination);
37
+ const pluginDir = path__namespace.join(destAbs, pluginPath);
38
+ await fs__namespace.rm(pluginDir, { recursive: true, force: true });
39
+ await fs__namespace.mkdir(destAbs, { recursive: true });
40
+ const pluginPathBoundary = pluginPath.endsWith("/") ? pluginPath : `${pluginPath}/`;
41
+ let pending = null;
42
+ await tar__namespace.x({
43
+ file: tarball,
44
+ cwd: destAbs,
45
+ preservePaths: false,
46
+ filter: (filePath, entry) => {
47
+ if (pending) return false;
48
+ const stat = entry;
49
+ if (filePath !== pluginPath && !filePath.startsWith(pluginPathBoundary))
50
+ return false;
51
+ if (stat.size > types.MAX_ENTRY_SIZE) {
52
+ pending = new errors.InstallException(`Zip bomb detected in ${filePath}`);
53
+ return false;
54
+ }
55
+ if (stat.type === "SymbolicLink" || stat.type === "Link") {
56
+ const linkName = stat.linkpath ?? "";
57
+ const linkTarget = path__namespace.resolve(destAbs, linkName);
58
+ if (!util.isInside(linkTarget, destAbs)) {
59
+ log.log(
60
+ ` ==> WARNING: skipping file containing link outside of the archive: ${filePath} -> ${linkName}`
61
+ );
62
+ return false;
63
+ }
64
+ }
65
+ if (!util.isAllowedEntryType(stat.type)) {
66
+ pending = new errors.InstallException(
67
+ `Disallowed tar entry type ${stat.type} for ${filePath}`
68
+ );
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ });
74
+ if (pending) throw pending;
75
+ }
76
+ async function extractNpmPackage(archive) {
77
+ if (!archive.endsWith(".tgz")) {
78
+ throw new errors.InstallException(`Expected .tgz archive, got ${archive}`);
79
+ }
80
+ const pkgDir = archive.slice(0, -".tgz".length);
81
+ const pkgDirReal = path__namespace.resolve(pkgDir);
82
+ await fs__namespace.rm(pkgDir, { recursive: true, force: true });
83
+ await fs__namespace.mkdir(pkgDir, { recursive: true });
84
+ let pending = null;
85
+ await tar__namespace.x({
86
+ file: archive,
87
+ cwd: pkgDir,
88
+ preservePaths: false,
89
+ filter: (filePath, entry) => {
90
+ if (pending) return false;
91
+ const stat = entry;
92
+ if (stat.type === "Directory") return false;
93
+ if (stat.type === "File") {
94
+ if (!filePath.startsWith(PACKAGE_PREFIX)) {
95
+ pending = new errors.InstallException(
96
+ `NPM package archive does not start with 'package/' as it should: ${filePath}`
97
+ );
98
+ return false;
99
+ }
100
+ if (stat.size > types.MAX_ENTRY_SIZE) {
101
+ pending = new errors.InstallException(`Zip bomb detected in ${filePath}`);
102
+ return false;
103
+ }
104
+ stat.path = filePath.slice(PACKAGE_PREFIX.length);
105
+ return true;
106
+ }
107
+ if (stat.type === "SymbolicLink" || stat.type === "Link") {
108
+ const linkPath = stat.linkpath ?? "";
109
+ if (!linkPath.startsWith(PACKAGE_PREFIX)) {
110
+ pending = new errors.InstallException(
111
+ `NPM package archive contains a link outside of the archive: ${filePath} -> ${linkPath}`
112
+ );
113
+ return false;
114
+ }
115
+ stat.path = filePath.slice(PACKAGE_PREFIX.length);
116
+ stat.linkpath = linkPath.slice(PACKAGE_PREFIX.length);
117
+ const linkTarget = path__namespace.resolve(pkgDir, stat.linkpath);
118
+ if (!util.isInside(linkTarget, pkgDirReal)) {
119
+ pending = new errors.InstallException(
120
+ `NPM package archive contains a link outside of the archive: ${stat.path} -> ${stat.linkpath}`
121
+ );
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ pending = new errors.InstallException(
127
+ `NPM package archive contains a non-regular file: ${filePath}`
128
+ );
129
+ return false;
130
+ }
131
+ });
132
+ if (pending) throw pending;
133
+ await fs__namespace.rm(archive, { force: true });
134
+ return path__namespace.basename(pkgDirReal);
135
+ }
136
+ function assertSafePluginPath(pluginPath) {
137
+ if (path__namespace.isAbsolute(pluginPath)) {
138
+ throw new errors.InstallException(`Invalid plugin path (absolute): ${pluginPath}`);
139
+ }
140
+ if (pluginPath.length === 0) {
141
+ throw new errors.InstallException("Invalid plugin path (empty)");
142
+ }
143
+ const segments = pluginPath.split(/[/\\]/);
144
+ for (const segment of segments) {
145
+ if (segment === "" || segment === "." || segment === "..") {
146
+ throw new errors.InstallException(
147
+ `Invalid plugin path (path traversal detected): ${pluginPath}`
148
+ );
149
+ }
150
+ }
151
+ }
152
+
153
+ exports.extractNpmPackage = extractNpmPackage;
154
+ exports.extractOciPlugin = extractOciPlugin;
155
+ //# sourceMappingURL=tar-extract.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tar-extract.cjs.js","sources":["../src/tar-extract.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as tar from 'tar';\nimport { InstallException } from './errors';\nimport { log } from './log';\nimport { MAX_ENTRY_SIZE } from './types';\nimport { isAllowedEntryType, isInside } from './util';\n\nconst PACKAGE_PREFIX = 'package/';\n\n/**\n * Extract a slice of an OCI layer tarball into `destination/pluginPath`.\n *\n * Mirrors the Python `extract_oci_plugin` with the same security guards:\n * - reject absolute or `..`-containing plugin paths\n * - enforce per-entry size limit (MAX_ENTRY_SIZE) against zip bombs\n * - skip sym/hard links whose targets would escape `destination`\n * - reject device files / FIFOs (`tar` `filter` only emits regular types)\n *\n * Uses streaming via `node-tar` — no full-archive read into memory.\n */\nexport async function extractOciPlugin(\n tarball: string,\n pluginPath: string,\n destination: string,\n): Promise<void> {\n assertSafePluginPath(pluginPath);\n\n const destAbs = path.resolve(destination);\n const pluginDir = path.join(destAbs, pluginPath);\n await fs.rm(pluginDir, { recursive: true, force: true });\n await fs.mkdir(destAbs, { recursive: true });\n\n // Boundary-safe path prefix — prevents `plugin-one` from matching sibling\n // directories with the same prefix (e.g., `plugin-one-evil/`). Uses POSIX\n // semantics because `node-tar` always emits forward-slash entry paths\n // regardless of host OS.\n const pluginPathBoundary = pluginPath.endsWith('/')\n ? pluginPath\n : `${pluginPath}/`;\n\n // Errors thrown inside `tar` filter callbacks are sometimes swallowed by the\n // parser; capture them in a closure and re-throw after extraction completes.\n let pending: InstallException | null = null;\n\n await tar.x({\n file: tarball,\n cwd: destAbs,\n preservePaths: false,\n filter: (filePath, entry) => {\n if (pending) return false;\n const stat = entry as tar.ReadEntry;\n if (filePath !== pluginPath && !filePath.startsWith(pluginPathBoundary))\n return false;\n\n if (stat.size > MAX_ENTRY_SIZE) {\n pending = new InstallException(`Zip bomb detected in ${filePath}`);\n return false;\n }\n if (stat.type === 'SymbolicLink' || stat.type === 'Link') {\n const linkName = stat.linkpath ?? '';\n const linkTarget = path.resolve(destAbs, linkName);\n if (!isInside(linkTarget, destAbs)) {\n log(\n `\\t==> WARNING: skipping file containing link outside of the archive: ${filePath} -> ${linkName}`,\n );\n return false;\n }\n }\n if (!isAllowedEntryType(stat.type)) {\n pending = new InstallException(\n `Disallowed tar entry type ${stat.type} for ${filePath}`,\n );\n return false;\n }\n return true;\n },\n });\n\n if (pending) throw pending;\n}\n\n/**\n * Extract an NPM tarball (`npm pack` output). Entries all start with `package/`\n * which is stripped. Matches `extract_npm_package` in fast.py, including the\n * realpath-based escape check for symlinks inside the archive.\n *\n * Returns the directory name (basename) the package was extracted into.\n */\nexport async function extractNpmPackage(archive: string): Promise<string> {\n if (!archive.endsWith('.tgz')) {\n throw new InstallException(`Expected .tgz archive, got ${archive}`);\n }\n const pkgDir = archive.slice(0, -'.tgz'.length);\n const pkgDirReal = path.resolve(pkgDir);\n await fs.rm(pkgDir, { recursive: true, force: true });\n await fs.mkdir(pkgDir, { recursive: true });\n\n let pending: InstallException | null = null;\n\n await tar.x({\n file: archive,\n cwd: pkgDir,\n preservePaths: false,\n filter: (filePath, entry) => {\n if (pending) return false;\n const stat = entry as tar.ReadEntry;\n if (stat.type === 'Directory') return false;\n\n if (stat.type === 'File') {\n if (!filePath.startsWith(PACKAGE_PREFIX)) {\n pending = new InstallException(\n `NPM package archive does not start with 'package/' as it should: ${filePath}`,\n );\n return false;\n }\n if (stat.size > MAX_ENTRY_SIZE) {\n pending = new InstallException(`Zip bomb detected in ${filePath}`);\n return false;\n }\n stat.path = filePath.slice(PACKAGE_PREFIX.length);\n return true;\n }\n\n if (stat.type === 'SymbolicLink' || stat.type === 'Link') {\n const linkPath = stat.linkpath ?? '';\n if (!linkPath.startsWith(PACKAGE_PREFIX)) {\n pending = new InstallException(\n `NPM package archive contains a link outside of the archive: ${filePath} -> ${linkPath}`,\n );\n return false;\n }\n stat.path = filePath.slice(PACKAGE_PREFIX.length);\n stat.linkpath = linkPath.slice(PACKAGE_PREFIX.length);\n const linkTarget = path.resolve(pkgDir, stat.linkpath);\n if (!isInside(linkTarget, pkgDirReal)) {\n pending = new InstallException(\n `NPM package archive contains a link outside of the archive: ${stat.path} -> ${stat.linkpath}`,\n );\n return false;\n }\n return true;\n }\n\n pending = new InstallException(\n `NPM package archive contains a non-regular file: ${filePath}`,\n );\n return false;\n },\n });\n\n if (pending) throw pending;\n\n await fs.rm(archive, { force: true });\n return path.basename(pkgDirReal);\n}\n\n/**\n * Validate a plugin path against traversal attempts. Segment-based — a bare\n * `..` substring in a filename (`my..plugin`) is allowed; a `..` path segment\n * (`foo/../bar`) is not. Absolute paths, empty segments, and `.` segments are\n * also rejected.\n */\nfunction assertSafePluginPath(pluginPath: string): void {\n if (path.isAbsolute(pluginPath)) {\n throw new InstallException(`Invalid plugin path (absolute): ${pluginPath}`);\n }\n if (pluginPath.length === 0) {\n throw new InstallException('Invalid plugin path (empty)');\n }\n const segments = pluginPath.split(/[/\\\\]/);\n for (const segment of segments) {\n if (segment === '' || segment === '.' || segment === '..') {\n throw new InstallException(\n `Invalid plugin path (path traversal detected): ${pluginPath}`,\n );\n }\n }\n}\n"],"names":["path","fs","tar","MAX_ENTRY_SIZE","InstallException","isInside","log","isAllowedEntryType"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,cAAA,GAAiB,UAAA;AAavB,eAAsB,gBAAA,CACpB,OAAA,EACA,UAAA,EACA,WAAA,EACe;AACf,EAAA,oBAAA,CAAqB,UAAU,CAAA;AAE/B,EAAA,MAAM,OAAA,GAAUA,eAAA,CAAK,OAAA,CAAQ,WAAW,CAAA;AACxC,EAAA,MAAM,SAAA,GAAYA,eAAA,CAAK,IAAA,CAAK,OAAA,EAAS,UAAU,CAAA;AAC/C,EAAA,MAAMC,aAAA,CAAG,GAAG,SAAA,EAAW,EAAE,WAAW,IAAA,EAAM,KAAA,EAAO,MAAM,CAAA;AACvD,EAAA,MAAMA,cAAG,KAAA,CAAM,OAAA,EAAS,EAAE,SAAA,EAAW,MAAM,CAAA;AAM3C,EAAA,MAAM,qBAAqB,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,GAC9C,UAAA,GACA,GAAG,UAAU,CAAA,CAAA,CAAA;AAIjB,EAAA,IAAI,OAAA,GAAmC,IAAA;AAEvC,EAAA,MAAMC,eAAI,CAAA,CAAE;AAAA,IACV,IAAA,EAAM,OAAA;AAAA,IACN,GAAA,EAAK,OAAA;AAAA,IACL,aAAA,EAAe,KAAA;AAAA,IACf,MAAA,EAAQ,CAAC,QAAA,EAAU,KAAA,KAAU;AAC3B,MAAA,IAAI,SAAS,OAAO,KAAA;AACpB,MAAA,MAAM,IAAA,GAAO,KAAA;AACb,MAAA,IAAI,QAAA,KAAa,UAAA,IAAc,CAAC,QAAA,CAAS,WAAW,kBAAkB,CAAA;AACpE,QAAA,OAAO,KAAA;AAET,MAAA,IAAI,IAAA,CAAK,OAAOC,oBAAA,EAAgB;AAC9B,QAAA,OAAA,GAAU,IAAIC,uBAAA,CAAiB,CAAA,qBAAA,EAAwB,QAAQ,CAAA,CAAE,CAAA;AACjE,QAAA,OAAO,KAAA;AAAA,MACT;AACA,MAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,IAAkB,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxD,QAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,EAAA;AAClC,QAAA,MAAM,UAAA,GAAaJ,eAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,QAAQ,CAAA;AACjD,QAAA,IAAI,CAACK,aAAA,CAAS,UAAA,EAAY,OAAO,CAAA,EAAG;AAClC,UAAAC,OAAA;AAAA,YACE,CAAA,oEAAA,EAAwE,QAAQ,CAAA,IAAA,EAAO,QAAQ,CAAA;AAAA,WACjG;AACA,UAAA,OAAO,KAAA;AAAA,QACT;AAAA,MACF;AACA,MAAA,IAAI,CAACC,uBAAA,CAAmB,IAAA,CAAK,IAAI,CAAA,EAAG;AAClC,QAAA,OAAA,GAAU,IAAIH,uBAAA;AAAA,UACZ,CAAA,0BAAA,EAA6B,IAAA,CAAK,IAAI,CAAA,KAAA,EAAQ,QAAQ,CAAA;AAAA,SACxD;AACA,QAAA,OAAO,KAAA;AAAA,MACT;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,GACD,CAAA;AAED,EAAA,IAAI,SAAS,MAAM,OAAA;AACrB;AASA,eAAsB,kBAAkB,OAAA,EAAkC;AACxE,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAIA,uBAAA,CAAiB,CAAA,2BAAA,EAA8B,OAAO,CAAA,CAAE,CAAA;AAAA,EACpE;AACA,EAAA,MAAM,SAAS,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,OAAO,MAAM,CAAA;AAC9C,EAAA,MAAM,UAAA,GAAaJ,eAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AACtC,EAAA,MAAMC,aAAA,CAAG,GAAG,MAAA,EAAQ,EAAE,WAAW,IAAA,EAAM,KAAA,EAAO,MAAM,CAAA;AACpD,EAAA,MAAMA,cAAG,KAAA,CAAM,MAAA,EAAQ,EAAE,SAAA,EAAW,MAAM,CAAA;AAE1C,EAAA,IAAI,OAAA,GAAmC,IAAA;AAEvC,EAAA,MAAMC,eAAI,CAAA,CAAE;AAAA,IACV,IAAA,EAAM,OAAA;AAAA,IACN,GAAA,EAAK,MAAA;AAAA,IACL,aAAA,EAAe,KAAA;AAAA,IACf,MAAA,EAAQ,CAAC,QAAA,EAAU,KAAA,KAAU;AAC3B,MAAA,IAAI,SAAS,OAAO,KAAA;AACpB,MAAA,MAAM,IAAA,GAAO,KAAA;AACb,MAAA,IAAI,IAAA,CAAK,IAAA,KAAS,WAAA,EAAa,OAAO,KAAA;AAEtC,MAAA,IAAI,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxB,QAAA,IAAI,CAAC,QAAA,CAAS,UAAA,CAAW,cAAc,CAAA,EAAG;AACxC,UAAA,OAAA,GAAU,IAAIE,uBAAA;AAAA,YACZ,oEAAoE,QAAQ,CAAA;AAAA,WAC9E;AACA,UAAA,OAAO,KAAA;AAAA,QACT;AACA,QAAA,IAAI,IAAA,CAAK,OAAOD,oBAAA,EAAgB;AAC9B,UAAA,OAAA,GAAU,IAAIC,uBAAA,CAAiB,CAAA,qBAAA,EAAwB,QAAQ,CAAA,CAAE,CAAA;AACjE,UAAA,OAAO,KAAA;AAAA,QACT;AACA,QAAA,IAAA,CAAK,IAAA,GAAO,QAAA,CAAS,KAAA,CAAM,cAAA,CAAe,MAAM,CAAA;AAChD,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,IAAkB,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxD,QAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,EAAA;AAClC,QAAA,IAAI,CAAC,QAAA,CAAS,UAAA,CAAW,cAAc,CAAA,EAAG;AACxC,UAAA,OAAA,GAAU,IAAIA,uBAAA;AAAA,YACZ,CAAA,4DAAA,EAA+D,QAAQ,CAAA,IAAA,EAAO,QAAQ,CAAA;AAAA,WACxF;AACA,UAAA,OAAO,KAAA;AAAA,QACT;AACA,QAAA,IAAA,CAAK,IAAA,GAAO,QAAA,CAAS,KAAA,CAAM,cAAA,CAAe,MAAM,CAAA;AAChD,QAAA,IAAA,CAAK,QAAA,GAAW,QAAA,CAAS,KAAA,CAAM,cAAA,CAAe,MAAM,CAAA;AACpD,QAAA,MAAM,UAAA,GAAaJ,eAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,KAAK,QAAQ,CAAA;AACrD,QAAA,IAAI,CAACK,aAAA,CAAS,UAAA,EAAY,UAAU,CAAA,EAAG;AACrC,UAAA,OAAA,GAAU,IAAID,uBAAA;AAAA,YACZ,CAAA,4DAAA,EAA+D,IAAA,CAAK,IAAI,CAAA,IAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,WAC9F;AACA,UAAA,OAAO,KAAA;AAAA,QACT;AACA,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,OAAA,GAAU,IAAIA,uBAAA;AAAA,QACZ,oDAAoD,QAAQ,CAAA;AAAA,OAC9D;AACA,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,GACD,CAAA;AAED,EAAA,IAAI,SAAS,MAAM,OAAA;AAEnB,EAAA,MAAMH,cAAG,EAAA,CAAG,OAAA,EAAS,EAAE,KAAA,EAAO,MAAM,CAAA;AACpC,EAAA,OAAOD,eAAA,CAAK,SAAS,UAAU,CAAA;AACjC;AAQA,SAAS,qBAAqB,UAAA,EAA0B;AACtD,EAAA,IAAIA,eAAA,CAAK,UAAA,CAAW,UAAU,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAII,uBAAA,CAAiB,CAAA,gCAAA,EAAmC,UAAU,CAAA,CAAE,CAAA;AAAA,EAC5E;AACA,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAC3B,IAAA,MAAM,IAAIA,wBAAiB,6BAA6B,CAAA;AAAA,EAC1D;AACA,EAAA,MAAM,QAAA,GAAW,UAAA,CAAW,KAAA,CAAM,OAAO,CAAA;AACzC,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,OAAA,KAAY,EAAA,IAAM,OAAA,KAAY,GAAA,IAAO,YAAY,IAAA,EAAM;AACzD,MAAA,MAAM,IAAIA,uBAAA;AAAA,QACR,kDAAkD,UAAU,CAAA;AAAA,OAC9D;AAAA,IACF;AAAA,EACF;AACF;;;;;"}