@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,116 @@
1
+ 'use strict';
2
+
3
+ var node_crypto = require('node:crypto');
4
+ var fs = require('node:fs/promises');
5
+ var path = require('node:path');
6
+ var errors = require('./errors.cjs.js');
7
+ var log = require('./log.cjs.js');
8
+ var imageResolver = require('./image-resolver.cjs.js');
9
+ var types = require('./types.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
+
32
+ class OciImageCache {
33
+ constructor(skopeo, tmpDir) {
34
+ this.skopeo = skopeo;
35
+ this.tmpDir = tmpDir;
36
+ }
37
+ skopeo;
38
+ tmpDir;
39
+ tarballs = /* @__PURE__ */ new Map();
40
+ async getTarball(image) {
41
+ const resolved = await imageResolver.resolveImage(this.skopeo, image);
42
+ let pending = this.tarballs.get(resolved);
43
+ if (!pending) {
44
+ pending = this.downloadAndLocateTarball(resolved);
45
+ this.tarballs.set(resolved, pending);
46
+ pending.catch(() => this.tarballs.delete(resolved));
47
+ }
48
+ return pending;
49
+ }
50
+ async getDigest(image) {
51
+ const resolved = await imageResolver.resolveImage(this.skopeo, image);
52
+ const dockerUrl = resolved.replace(types.OCI_PROTO, types.DOCKER_PROTO);
53
+ const data = await this.skopeo.inspect(dockerUrl);
54
+ const digest = data.Digest;
55
+ if (!digest) throw new errors.InstallException(`No digest returned for ${image}`);
56
+ const [, hash] = digest.split(":");
57
+ if (!hash)
58
+ throw new errors.InstallException(`Malformed digest ${digest} for ${image}`);
59
+ return hash;
60
+ }
61
+ /**
62
+ * Plugin paths are published via the `io.backstage.dynamic-packages` OCI
63
+ * annotation (base64-encoded JSON array of `{path: {...}}` objects). An
64
+ * image with no annotation returns an empty list.
65
+ */
66
+ async getPluginPaths(image) {
67
+ const resolved = await imageResolver.resolveImage(this.skopeo, image);
68
+ const dockerUrl = resolved.replace(types.OCI_PROTO, types.DOCKER_PROTO);
69
+ const manifest = await this.skopeo.inspectRaw(dockerUrl);
70
+ const annotation = manifest.annotations?.["io.backstage.dynamic-packages"];
71
+ if (!annotation) return [];
72
+ let entries;
73
+ try {
74
+ const decoded = Buffer.from(annotation, "base64").toString("utf8");
75
+ entries = JSON.parse(decoded);
76
+ } catch (err) {
77
+ throw new errors.InstallException(
78
+ `Could not decode 'io.backstage.dynamic-packages' annotation on ${image}: ${err.message}`
79
+ );
80
+ }
81
+ if (!Array.isArray(entries)) return [];
82
+ const paths = [];
83
+ for (const entry of entries) {
84
+ if (entry && typeof entry === "object") {
85
+ paths.push(...Object.keys(entry));
86
+ }
87
+ }
88
+ return paths;
89
+ }
90
+ async downloadAndLocateTarball(resolved) {
91
+ const digest = node_crypto.createHash("sha256").update(resolved).digest("hex");
92
+ const localDir = path__namespace.join(this.tmpDir, digest);
93
+ await fs__namespace.mkdir(localDir, { recursive: true });
94
+ const dockerUrl = resolved.replace(types.OCI_PROTO, types.DOCKER_PROTO);
95
+ log.log(` ==> Downloading ${resolved}`);
96
+ await this.skopeo.copy(dockerUrl, `dir:${localDir}`);
97
+ const manifestPath = path__namespace.join(localDir, "manifest.json");
98
+ const manifest = JSON.parse(
99
+ await fs__namespace.readFile(manifestPath, "utf8")
100
+ );
101
+ const firstLayer = manifest.layers?.[0]?.digest;
102
+ if (!firstLayer) {
103
+ throw new errors.InstallException(`OCI manifest for ${resolved} has no layers`);
104
+ }
105
+ const [, filename] = firstLayer.split(":");
106
+ if (!filename) {
107
+ throw new errors.InstallException(
108
+ `Malformed layer digest ${firstLayer} in ${resolved}`
109
+ );
110
+ }
111
+ return path__namespace.join(localDir, filename);
112
+ }
113
+ }
114
+
115
+ exports.OciImageCache = OciImageCache;
116
+ //# sourceMappingURL=image-cache.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-cache.cjs.js","sources":["../src/image-cache.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 * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { InstallException } from './errors';\nimport { log } from './log';\nimport { resolveImage } from './image-resolver';\nimport { type Skopeo } from './skopeo';\nimport { DOCKER_PROTO, OCI_PROTO } from './types';\n\ntype OciManifest = {\n layers?: Array<{ digest: string }>;\n annotations?: Record<string, string>;\n};\n\n/**\n * Shared cache that keeps each OCI image's single-layer tarball on disk and\n * returns the path. If several plugins point at the same image (very common\n * for multi-plugin overlays) we download once and extract slices from that\n * same tarball.\n *\n * The cache stores *promises*, so concurrent `getTarball` calls for the same\n * image share the in-flight `skopeo copy` rather than racing. This is the\n * JS equivalent of fast.py's `threading.Lock` guard around the cache.\n */\nexport class OciImageCache {\n private readonly tarballs = new Map<string, Promise<string>>();\n\n constructor(\n private readonly skopeo: Skopeo,\n private readonly tmpDir: string,\n ) {}\n\n async getTarball(image: string): Promise<string> {\n const resolved = await resolveImage(this.skopeo, image);\n let pending = this.tarballs.get(resolved);\n if (!pending) {\n pending = this.downloadAndLocateTarball(resolved);\n this.tarballs.set(resolved, pending);\n pending.catch(() => this.tarballs.delete(resolved));\n }\n return pending;\n }\n\n async getDigest(image: string): Promise<string> {\n const resolved = await resolveImage(this.skopeo, image);\n const dockerUrl = resolved.replace(OCI_PROTO, DOCKER_PROTO);\n const data = await this.skopeo.inspect(dockerUrl);\n const digest = data.Digest;\n if (!digest) throw new InstallException(`No digest returned for ${image}`);\n const [, hash] = digest.split(':');\n if (!hash)\n throw new InstallException(`Malformed digest ${digest} for ${image}`);\n return hash;\n }\n\n /**\n * Plugin paths are published via the `io.backstage.dynamic-packages` OCI\n * annotation (base64-encoded JSON array of `{path: {...}}` objects). An\n * image with no annotation returns an empty list.\n */\n async getPluginPaths(image: string): Promise<string[]> {\n const resolved = await resolveImage(this.skopeo, image);\n const dockerUrl = resolved.replace(OCI_PROTO, DOCKER_PROTO);\n const manifest = (await this.skopeo.inspectRaw(dockerUrl)) as OciManifest;\n const annotation = manifest.annotations?.['io.backstage.dynamic-packages'];\n if (!annotation) return [];\n let entries: unknown;\n try {\n const decoded = Buffer.from(annotation, 'base64').toString('utf8');\n entries = JSON.parse(decoded);\n } catch (err) {\n throw new InstallException(\n `Could not decode 'io.backstage.dynamic-packages' annotation on ${image}: ${(err as Error).message}`,\n );\n }\n if (!Array.isArray(entries)) return [];\n const paths: string[] = [];\n for (const entry of entries) {\n if (entry && typeof entry === 'object') {\n paths.push(...Object.keys(entry as Record<string, unknown>));\n }\n }\n return paths;\n }\n\n private async downloadAndLocateTarball(resolved: string): Promise<string> {\n const digest = createHash('sha256').update(resolved).digest('hex');\n const localDir = path.join(this.tmpDir, digest);\n await fs.mkdir(localDir, { recursive: true });\n const dockerUrl = resolved.replace(OCI_PROTO, DOCKER_PROTO);\n log(`\\t==> Downloading ${resolved}`);\n await this.skopeo.copy(dockerUrl, `dir:${localDir}`);\n\n const manifestPath = path.join(localDir, 'manifest.json');\n const manifest = JSON.parse(\n await fs.readFile(manifestPath, 'utf8'),\n ) as OciManifest;\n const firstLayer = manifest.layers?.[0]?.digest;\n if (!firstLayer) {\n throw new InstallException(`OCI manifest for ${resolved} has no layers`);\n }\n const [, filename] = firstLayer.split(':');\n if (!filename) {\n throw new InstallException(\n `Malformed layer digest ${firstLayer} in ${resolved}`,\n );\n }\n return path.join(localDir, filename);\n }\n}\n"],"names":["resolveImage","OCI_PROTO","DOCKER_PROTO","InstallException","createHash","path","fs","log"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ var log = require('./log.cjs.js');
4
+ var types = require('./types.cjs.js');
5
+
6
+ async function resolveImage(skopeo, image) {
7
+ const { proto, raw } = stripProto(image);
8
+ if (!raw.startsWith(types.RHDH_REGISTRY)) return image;
9
+ const dockerUrl = `${types.DOCKER_PROTO}${raw}`;
10
+ if (await skopeo.exists(dockerUrl)) return image;
11
+ const fallback = raw.replace(types.RHDH_REGISTRY, types.RHDH_FALLBACK);
12
+ log.log(` ==> Falling back to ${types.RHDH_FALLBACK} for ${raw}`);
13
+ return `${proto}${fallback}`;
14
+ }
15
+ function stripProto(image) {
16
+ if (image.startsWith(types.OCI_PROTO))
17
+ return { proto: types.OCI_PROTO, raw: image.slice(types.OCI_PROTO.length) };
18
+ if (image.startsWith(types.DOCKER_PROTO))
19
+ return { proto: types.DOCKER_PROTO, raw: image.slice(types.DOCKER_PROTO.length) };
20
+ return { proto: "", raw: image };
21
+ }
22
+
23
+ exports.resolveImage = resolveImage;
24
+ //# sourceMappingURL=image-resolver.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-resolver.cjs.js","sources":["../src/image-resolver.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 { log } from './log';\nimport { type Skopeo } from './skopeo';\nimport { DOCKER_PROTO, OCI_PROTO, RHDH_FALLBACK, RHDH_REGISTRY } from './types';\n\n/**\n * Resolve a (possibly oci:// / docker://) image reference. If it points at\n * `registry.access.redhat.com/rhdh/...` and that registry rejects the image,\n * fall back to `quay.io/rhdh/...` (same protocol). Mirrors fast.py `resolve_image`.\n */\nexport async function resolveImage(\n skopeo: Skopeo,\n image: string,\n): Promise<string> {\n const { proto, raw } = stripProto(image);\n if (!raw.startsWith(RHDH_REGISTRY)) return image;\n\n const dockerUrl = `${DOCKER_PROTO}${raw}`;\n if (await skopeo.exists(dockerUrl)) return image;\n\n const fallback = raw.replace(RHDH_REGISTRY, RHDH_FALLBACK);\n log(`\\t==> Falling back to ${RHDH_FALLBACK} for ${raw}`);\n return `${proto}${fallback}`;\n}\n\nfunction stripProto(image: string): { proto: string; raw: string } {\n if (image.startsWith(OCI_PROTO))\n return { proto: OCI_PROTO, raw: image.slice(OCI_PROTO.length) };\n if (image.startsWith(DOCKER_PROTO))\n return { proto: DOCKER_PROTO, raw: image.slice(DOCKER_PROTO.length) };\n return { proto: '', raw: image };\n}\n"],"names":["RHDH_REGISTRY","DOCKER_PROTO","RHDH_FALLBACK","log","OCI_PROTO"],"mappings":";;;;;AAwBA,eAAsB,YAAA,CACpB,QACA,KAAA,EACiB;AACjB,EAAA,MAAM,EAAE,KAAA,EAAO,GAAA,EAAI,GAAI,WAAW,KAAK,CAAA;AACvC,EAAA,IAAI,CAAC,GAAA,CAAI,UAAA,CAAWA,mBAAa,GAAG,OAAO,KAAA;AAE3C,EAAA,MAAM,SAAA,GAAY,CAAA,EAAGC,kBAAY,CAAA,EAAG,GAAG,CAAA,CAAA;AACvC,EAAA,IAAI,MAAM,MAAA,CAAO,MAAA,CAAO,SAAS,GAAG,OAAO,KAAA;AAE3C,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,OAAA,CAAQD,mBAAA,EAAeE,mBAAa,CAAA;AACzD,EAAAC,OAAA,CAAI,CAAA,qBAAA,EAAyBD,mBAAa,CAAA,KAAA,EAAQ,GAAG,CAAA,CAAE,CAAA;AACvD,EAAA,OAAO,CAAA,EAAG,KAAK,CAAA,EAAG,QAAQ,CAAA,CAAA;AAC5B;AAEA,SAAS,WAAW,KAAA,EAA+C;AACjE,EAAA,IAAI,KAAA,CAAM,WAAWE,eAAS,CAAA;AAC5B,IAAA,OAAO,EAAE,OAAOA,eAAA,EAAW,GAAA,EAAK,MAAM,KAAA,CAAMA,eAAA,CAAU,MAAM,CAAA,EAAE;AAChE,EAAA,IAAI,KAAA,CAAM,WAAWH,kBAAY,CAAA;AAC/B,IAAA,OAAO,EAAE,OAAOA,kBAAA,EAAc,GAAA,EAAK,MAAM,KAAA,CAAMA,kBAAA,CAAa,MAAM,CAAA,EAAE;AACtE,EAAA,OAAO,EAAE,KAAA,EAAO,EAAA,EAAI,GAAA,EAAK,KAAA,EAAM;AACjC;;;;"}
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var cliNode = require('@backstage/cli-node');
6
+ var _package = require('./package.json.cjs.js');
7
+
8
+ var index = cliNode.createCliModule({
9
+ packageJson: _package.default,
10
+ init: async (reg) => {
11
+ reg.addCommand({
12
+ path: ["install"],
13
+ description: "Install RHDH dynamic plugins listed in dynamic-plugins.yaml into the given directory.",
14
+ execute: { loader: () => import('./command.cjs.js') }
15
+ });
16
+ }
17
+ });
18
+
19
+ exports.default = index;
20
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/index.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 { createCliModule } from '@backstage/cli-node';\nimport packageJson from '../package.json';\n\n/**\n * Entry exposed to `backstage-cli` discovery. The package's `bin`\n * (`install-dynamic-plugins`) takes a fast path that calls `installer.main`\n * directly, so this default export only matters when a host project loads\n * the package via `backstage-cli` and reaches the `install` command through\n * the cli-module dispatch.\n */\nexport default createCliModule({\n packageJson,\n init: async reg => {\n reg.addCommand({\n path: ['install'],\n description:\n 'Install RHDH dynamic plugins listed in dynamic-plugins.yaml into the given directory.',\n execute: { loader: () => import('./command') },\n });\n },\n});\n"],"names":["createCliModule","packageJson"],"mappings":";;;;;;;AAyBA,YAAeA,uBAAA,CAAgB;AAAA,eAC7BC,gBAAA;AAAA,EACA,IAAA,EAAM,OAAM,GAAA,KAAO;AACjB,IAAA,GAAA,CAAI,UAAA,CAAW;AAAA,MACb,IAAA,EAAM,CAAC,SAAS,CAAA;AAAA,MAChB,WAAA,EACE,uFAAA;AAAA,MACF,SAAS,EAAE,MAAA,EAAQ,MAAM,OAAO,kBAAW,CAAA;AAAE,KAC9C,CAAA;AAAA,EACH;AACF,CAAC,CAAA;;;;"}
@@ -0,0 +1,12 @@
1
+ import * as _backstage_cli_node from '@backstage/cli-node';
2
+
3
+ /**
4
+ * Entry exposed to `backstage-cli` discovery. The package's `bin`
5
+ * (`install-dynamic-plugins`) takes a fast path that calls `installer.main`
6
+ * directly, so this default export only matters when a host project loads
7
+ * the package via `backstage-cli` and reaches the `install` command through
8
+ * the cli-module dispatch.
9
+ */
10
+ declare const _default: _backstage_cli_node.CliModule;
11
+
12
+ export { _default as default };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs/promises');
4
+ var path = require('node:path');
5
+ var errors = require('./errors.cjs.js');
6
+ var integrity = require('./integrity.cjs.js');
7
+ var log = require('./log.cjs.js');
8
+ var run = require('./run.cjs.js');
9
+ var tarExtract = require('./tar-extract.cjs.js');
10
+ var types = require('./types.cjs.js');
11
+ var util = require('./util.cjs.js');
12
+
13
+ function _interopNamespaceCompat(e) {
14
+ if (e && typeof e === 'object' && 'default' in e) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
32
+ var path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
33
+
34
+ async function installNpmPlugin(plugin, destination, skipIntegrity, installed) {
35
+ if (plugin.disabled) {
36
+ return { pluginPath: null, pluginConfig: {} };
37
+ }
38
+ const hash = plugin.plugin_hash;
39
+ if (!hash) {
40
+ throw new errors.InstallException(
41
+ `Internal error: plugin ${plugin.package} missing plugin_hash`
42
+ );
43
+ }
44
+ const pkg = plugin.package;
45
+ const config = plugin.pluginConfig ?? {};
46
+ const isLocal = pkg.startsWith("./");
47
+ const actualPkg = isLocal ? path__namespace.join(process.cwd(), pkg.slice(2)) : pkg;
48
+ const verifyRemoteIntegrity = !isLocal && !skipIntegrity;
49
+ if (verifyRemoteIntegrity && !plugin.integrity) {
50
+ throw new errors.InstallException(
51
+ `No integrity hash provided for Package ${pkg}. This is an insecure installation. To ignore this error, set the SKIP_INTEGRITY_CHECK environment variable to 'true'.`
52
+ );
53
+ }
54
+ log.log(" ==> Running npm pack");
55
+ const archiveName = await npmPack(actualPkg, destination);
56
+ if (!isSafeArchiveName(archiveName)) {
57
+ throw new errors.InstallException(
58
+ `npm pack returned an unsafe filename for ${pkg}: '${archiveName}'`
59
+ );
60
+ }
61
+ const archive = path__namespace.join(destination, archiveName);
62
+ if (verifyRemoteIntegrity) {
63
+ log.log(" ==> Verifying package integrity");
64
+ await integrity.verifyIntegrity(pkg, archive, plugin.integrity);
65
+ }
66
+ const pluginPath = await tarExtract.extractNpmPackage(archive);
67
+ await fs__namespace.writeFile(
68
+ path__namespace.join(destination, pluginPath, types.CONFIG_HASH_FILE),
69
+ hash
70
+ );
71
+ util.markAsFresh(installed, pluginPath);
72
+ return { pluginPath, pluginConfig: config };
73
+ }
74
+ async function npmPack(actualPkg, destination) {
75
+ const { stdout } = await run.run(
76
+ ["npm", "pack", "--json", "--ignore-scripts", actualPkg],
77
+ `npm pack failed for ${actualPkg}`,
78
+ { cwd: destination }
79
+ );
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(stdout);
83
+ } catch (err) {
84
+ throw new errors.InstallException(
85
+ `npm pack produced invalid JSON for ${actualPkg}: ${err.message}`
86
+ );
87
+ }
88
+ if (!Array.isArray(parsed) || parsed.length === 0) {
89
+ throw new errors.InstallException(
90
+ `npm pack produced no archives for ${actualPkg}`
91
+ );
92
+ }
93
+ const first = parsed[0];
94
+ if (!isNpmPackJsonEntry(first)) {
95
+ throw new errors.InstallException(
96
+ `npm pack output missing 'filename' for ${actualPkg}`
97
+ );
98
+ }
99
+ return first.filename;
100
+ }
101
+ function isNpmPackJsonEntry(value) {
102
+ return !!value && typeof value === "object" && typeof value.filename === "string";
103
+ }
104
+ function isSafeArchiveName(name) {
105
+ if (!name || name === "." || name === "..") return false;
106
+ if (name.startsWith("..")) return false;
107
+ return !/[/\\]/.test(name);
108
+ }
109
+
110
+ exports.installNpmPlugin = installNpmPlugin;
111
+ //# sourceMappingURL=installer-npm.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer-npm.cjs.js","sources":["../src/installer-npm.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 { InstallException } from './errors';\nimport { verifyIntegrity } from './integrity';\nimport { log } from './log';\nimport { run } from './run';\nimport { extractNpmPackage } from './tar-extract';\nimport { CONFIG_HASH_FILE, type Plugin } from './types';\nimport { markAsFresh } from './util';\n\nexport type NpmInstallResult = {\n pluginPath: string | null;\n pluginConfig: Record<string, unknown>;\n};\n\n/**\n * Install a single NPM-packaged (or local) plugin into `destination`.\n * Runs `npm pack` to produce the tarball, verifies integrity for remote\n * packages (unless skipped), then extracts.\n *\n * Concurrency is the caller's responsibility — `installNpm` in `index.ts`\n * runs a bounded `mapConcurrent` (default 3 workers via `getNpmWorkers()`)\n * over a list of plugins that have already passed the `definitelyNoOp`\n * pre-pass, so by the time this function is called the plugin definitely\n * needs work.\n */\nexport async function installNpmPlugin(\n plugin: Plugin,\n destination: string,\n skipIntegrity: boolean,\n installed: Map<string, string>,\n): Promise<NpmInstallResult> {\n if (plugin.disabled) {\n return { pluginPath: null, pluginConfig: {} };\n }\n const hash = plugin.plugin_hash;\n if (!hash) {\n throw new InstallException(\n `Internal error: plugin ${plugin.package} missing plugin_hash`,\n );\n }\n const pkg = plugin.package;\n const config: Record<string, unknown> = plugin.pluginConfig ?? {};\n\n const isLocal = pkg.startsWith('./');\n const actualPkg = isLocal ? path.join(process.cwd(), pkg.slice(2)) : pkg;\n\n const verifyRemoteIntegrity = !isLocal && !skipIntegrity;\n if (verifyRemoteIntegrity && !plugin.integrity) {\n throw new InstallException(\n `No integrity hash provided for Package ${pkg}. This is an insecure installation. ` +\n `To ignore this error, set the SKIP_INTEGRITY_CHECK environment variable to 'true'.`,\n );\n }\n\n log('\\t==> Running npm pack');\n const archiveName = await npmPack(actualPkg, destination);\n if (!isSafeArchiveName(archiveName)) {\n throw new InstallException(\n `npm pack returned an unsafe filename for ${pkg}: '${archiveName}'`,\n );\n }\n const archive = path.join(destination, archiveName);\n\n if (verifyRemoteIntegrity) {\n log('\\t==> Verifying package integrity');\n // `plugin.integrity` is guaranteed present — the check above throws otherwise.\n await verifyIntegrity(pkg, archive, plugin.integrity as string);\n }\n\n const pluginPath = await extractNpmPackage(archive);\n await fs.writeFile(\n path.join(destination, pluginPath, CONFIG_HASH_FILE),\n hash,\n );\n\n markAsFresh(installed, pluginPath);\n return { pluginPath, pluginConfig: config };\n}\n\n/**\n * Run `npm pack --json` and extract the archive filename from the structured\n * output. The text form of `npm pack` intermixes warnings with the filename\n * (last-line parsing is fragile); `--json` gives `[{ filename, ... }]`.\n */\nasync function npmPack(\n actualPkg: string,\n destination: string,\n): Promise<string> {\n // `--ignore-scripts` blocks `preinstall` / `prepack` / `prepare` lifecycle\n // hooks that NPM packages can declare. Dynamic plugins are not expected\n // to ship build steps that need to run at install time, and skipping the\n // hooks both removes a code-execution-on-install attack surface and\n // shaves a fork+exec per package off the wall clock.\n const { stdout } = await run(\n ['npm', 'pack', '--json', '--ignore-scripts', actualPkg],\n `npm pack failed for ${actualPkg}`,\n { cwd: destination },\n );\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout);\n } catch (err) {\n throw new InstallException(\n `npm pack produced invalid JSON for ${actualPkg}: ${(err as Error).message}`,\n );\n }\n if (!Array.isArray(parsed) || parsed.length === 0) {\n throw new InstallException(\n `npm pack produced no archives for ${actualPkg}`,\n );\n }\n const first = parsed[0];\n if (!isNpmPackJsonEntry(first)) {\n throw new InstallException(\n `npm pack output missing 'filename' for ${actualPkg}`,\n );\n }\n return first.filename;\n}\n\nfunction isNpmPackJsonEntry(value: unknown): value is { filename: string } {\n return (\n !!value &&\n typeof value === 'object' &&\n typeof (value as { filename?: unknown }).filename === 'string'\n );\n}\n\n/**\n * Reject any filename that would let `npm pack` escape `destination` once\n * passed to `path.join` — directory separators, leading `..`, or empty.\n * `npm pack` is expected to emit a flat `<name>-<version>.tgz`, so any\n * non-flat name is treated as adversarial.\n */\nfunction isSafeArchiveName(name: string): boolean {\n if (!name || name === '.' || name === '..') return false;\n if (name.startsWith('..')) return false;\n return !/[/\\\\]/.test(name);\n}\n"],"names":["InstallException","path","log","verifyIntegrity","extractNpmPackage","fs","CONFIG_HASH_FILE","markAsFresh","run"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs/promises');
4
+ var path = require('node:path');
5
+ var errors = require('./errors.cjs.js');
6
+ var log = require('./log.cjs.js');
7
+ var tarExtract = require('./tar-extract.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
+
32
+ function splitOciPackage(pkg) {
33
+ const bang = pkg.indexOf("!");
34
+ if (bang === -1) return null;
35
+ const imagePart = pkg.slice(0, bang);
36
+ const pluginPath = pkg.slice(bang + 1);
37
+ if (!imagePart || !pluginPath) return null;
38
+ return { imagePart, pluginPath };
39
+ }
40
+ async function installOciPlugin(plugin, destination, imageCache, installed) {
41
+ if (plugin.disabled) {
42
+ return { pluginPath: null, pluginConfig: {} };
43
+ }
44
+ const hash = plugin.plugin_hash;
45
+ if (!hash) {
46
+ throw new errors.InstallException(
47
+ `Internal error: plugin ${plugin.package} missing plugin_hash`
48
+ );
49
+ }
50
+ const pkg = plugin.package;
51
+ const config = plugin.pluginConfig ?? {};
52
+ const pullPolicy = types.effectivePullPolicy(plugin);
53
+ if (await isAlreadyInstalled(
54
+ pkg,
55
+ hash,
56
+ pullPolicy,
57
+ destination,
58
+ imageCache,
59
+ installed
60
+ )) {
61
+ installed.delete(hash);
62
+ return { pluginPath: null, pluginConfig: config };
63
+ }
64
+ if (!plugin.version) {
65
+ throw new errors.InstallException(`No version for ${pkg}`);
66
+ }
67
+ const parts = splitOciPackage(pkg);
68
+ if (!parts) {
69
+ throw new errors.InstallException(
70
+ `OCI package ${pkg} missing !plugin-path suffix`
71
+ );
72
+ }
73
+ const { imagePart, pluginPath } = parts;
74
+ const tarball = await imageCache.getTarball(imagePart);
75
+ await tarExtract.extractOciPlugin(tarball, pluginPath, destination);
76
+ const pluginDir = path__namespace.join(destination, pluginPath);
77
+ await fs__namespace.mkdir(pluginDir, { recursive: true });
78
+ await fs__namespace.writeFile(
79
+ path__namespace.join(pluginDir, types.IMAGE_HASH_FILE),
80
+ await imageCache.getDigest(imagePart)
81
+ );
82
+ await fs__namespace.writeFile(path__namespace.join(pluginDir, types.CONFIG_HASH_FILE), hash);
83
+ util.markAsFresh(installed, pluginPath);
84
+ return { pluginPath, pluginConfig: config };
85
+ }
86
+ async function isAlreadyInstalled(pkg, hash, pullPolicy, destination, imageCache, installed) {
87
+ const pathInstalled = installed.get(hash);
88
+ if (pathInstalled === void 0) return false;
89
+ if (pullPolicy === types.PullPolicy.IF_NOT_PRESENT) {
90
+ log.log(` ==> ${pkg}: already installed, skipping`);
91
+ return true;
92
+ }
93
+ if (pullPolicy !== types.PullPolicy.ALWAYS) return false;
94
+ const digestFile = path__namespace.join(destination, pathInstalled, types.IMAGE_HASH_FILE);
95
+ if (!await util.fileExists(digestFile)) return false;
96
+ const localDigest = (await fs__namespace.readFile(digestFile, "utf8")).trim();
97
+ const parts = splitOciPackage(pkg);
98
+ if (!parts) return false;
99
+ const remoteDigest = await imageCache.getDigest(parts.imagePart);
100
+ if (localDigest !== remoteDigest) return false;
101
+ log.log(` ==> ${pkg}: digest unchanged, skipping`);
102
+ return true;
103
+ }
104
+
105
+ exports.installOciPlugin = installOciPlugin;
106
+ //# sourceMappingURL=installer-oci.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer-oci.cjs.js","sources":["../src/installer-oci.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 { InstallException } from './errors';\nimport { type OciImageCache } from './image-cache';\nimport { log } from './log';\nimport { extractOciPlugin } from './tar-extract';\nimport {\n CONFIG_HASH_FILE,\n effectivePullPolicy,\n IMAGE_HASH_FILE,\n type Plugin,\n PullPolicy,\n} from './types';\nimport { fileExists, markAsFresh } from './util';\n\n/**\n * Split an OCI package spec into `<image-part>!<plugin-path>`. Uses\n * `indexOf` so plugin paths containing `!` (legal per the OCI grammar) are\n * preserved on the right side instead of being silently truncated by\n * `String#split`.\n */\nfunction splitOciPackage(\n pkg: string,\n): { imagePart: string; pluginPath: string } | null {\n const bang = pkg.indexOf('!');\n if (bang === -1) return null;\n const imagePart = pkg.slice(0, bang);\n const pluginPath = pkg.slice(bang + 1);\n if (!imagePart || !pluginPath) return null;\n return { imagePart, pluginPath };\n}\n\nexport type OciInstallResult = {\n /** The installed plugin's directory name (relative to destination), or null when skipped. */\n pluginPath: string | null;\n pluginConfig: Record<string, unknown>;\n};\n\n/**\n * Install a single OCI-packaged plugin into `destination`. Returns the\n * on-disk directory name and the plugin's own config (for merging into the\n * global app-config).\n */\nexport async function installOciPlugin(\n plugin: Plugin,\n destination: string,\n imageCache: OciImageCache,\n installed: Map<string, string>,\n): Promise<OciInstallResult> {\n if (plugin.disabled) {\n return { pluginPath: null, pluginConfig: {} };\n }\n const hash = plugin.plugin_hash;\n if (!hash) {\n throw new InstallException(\n `Internal error: plugin ${plugin.package} missing plugin_hash`,\n );\n }\n const pkg = plugin.package;\n const config: Record<string, unknown> = plugin.pluginConfig ?? {};\n const pullPolicy = effectivePullPolicy(plugin);\n\n if (\n await isAlreadyInstalled(\n pkg,\n hash,\n pullPolicy,\n destination,\n imageCache,\n installed,\n )\n ) {\n installed.delete(hash);\n return { pluginPath: null, pluginConfig: config };\n }\n\n if (!plugin.version) {\n throw new InstallException(`No version for ${pkg}`);\n }\n const parts = splitOciPackage(pkg);\n if (!parts) {\n throw new InstallException(\n `OCI package ${pkg} missing !plugin-path suffix`,\n );\n }\n const { imagePart, pluginPath } = parts;\n\n const tarball = await imageCache.getTarball(imagePart);\n await extractOciPlugin(tarball, pluginPath, destination);\n\n const pluginDir = path.join(destination, pluginPath);\n await fs.mkdir(pluginDir, { recursive: true });\n await fs.writeFile(\n path.join(pluginDir, IMAGE_HASH_FILE),\n await imageCache.getDigest(imagePart),\n );\n await fs.writeFile(path.join(pluginDir, CONFIG_HASH_FILE), hash);\n\n markAsFresh(installed, pluginPath);\n return { pluginPath, pluginConfig: config };\n}\n\n/**\n * Returns true when the plugin is already installed and can be skipped:\n * - IfNotPresent policy → skip unconditionally\n * - Always policy → skip only when the remote digest matches what's on disk\n */\nasync function isAlreadyInstalled(\n pkg: string,\n hash: string,\n pullPolicy: PullPolicy,\n destination: string,\n imageCache: OciImageCache,\n installed: Map<string, string>,\n): Promise<boolean> {\n const pathInstalled = installed.get(hash);\n if (pathInstalled === undefined) return false;\n\n if (pullPolicy === PullPolicy.IF_NOT_PRESENT) {\n log(`\\t==> ${pkg}: already installed, skipping`);\n return true;\n }\n\n if (pullPolicy !== PullPolicy.ALWAYS) return false;\n\n const digestFile = path.join(destination, pathInstalled, IMAGE_HASH_FILE);\n if (!(await fileExists(digestFile))) return false;\n\n const localDigest = (await fs.readFile(digestFile, 'utf8')).trim();\n const parts = splitOciPackage(pkg);\n if (!parts) return false;\n const remoteDigest = await imageCache.getDigest(parts.imagePart);\n if (localDigest !== remoteDigest) return false;\n\n log(`\\t==> ${pkg}: digest unchanged, skipping`);\n return true;\n}\n"],"names":["InstallException","effectivePullPolicy","extractOciPlugin","path","fs","IMAGE_HASH_FILE","CONFIG_HASH_FILE","markAsFresh","PullPolicy","log","fileExists"],"mappings":"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"}