UNPKG

@salesforce/plugin-trust

Version:

validate a digital signature for a npm package

230 lines 8.9 kB
/* * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import os from 'node:os'; import path from 'node:path'; import { createRequire } from 'node:module'; import fs from 'node:fs'; import npmRunPath from 'npm-run-path'; import crossSpawn from 'cross-spawn'; import which from 'which'; import { SfError } from '@salesforce/core'; import { sleep, parseJson } from '@salesforce/kit'; import { Ux } from '@salesforce/sf-plugins-core'; import { setErrorName } from './errors.js'; export class NpmCommand { static runNpmCmd(cmd, options = {}) { const nodeExecutable = NpmCommand.findNode(options.cliRoot); const npmCli = NpmCommand.npmCli(); const args = [npmCli, ...cmd.split(/\s+/), ...(options.registry ? [`--registry=${options.registry}`] : [])]; const result = crossSpawn.sync(nodeExecutable, args, { cwd: options.cwd, env: npmRunPath.env({ env: process.env }), }); const stdout = result.stdout?.toString() ?? ''; const stderr = result.stderr?.toString() ?? ''; const code = result.status ?? 1; if (code !== 0) { throw new SfError(stderr, 'ShellExecError'); } return { code, stdout, stderr }; } static npxCli() { const require = createRequire(import.meta.url); const pkgPath = require.resolve('npm/package.json'); const fileData = fs.readFileSync(pkgPath, 'utf8'); const pkgJson = parseJson(fileData, pkgPath); const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep)); return path.join(prjPath, pkgJson.bin['npx']); } /** * Locate node executable and return its absolute path * First it tries to locate the node executable on the root path passed in * If not found then tries to use whatver 'node' resolves to on the user's PATH * If found return absolute path to the executable * If the node executable cannot be found, an error is thrown * * @private */ static findNode(root) { const isExecutable = (filepath) => { if (os.type() === 'Windows_NT') return filepath.endsWith('node.exe'); try { if (filepath.endsWith('node')) { fs.accessSync(filepath, fs.constants.X_OK); return true; } } catch { return false; } return false; }; if (root) { const sfdxBinDirs = NpmCommand.findSfdxBinDirs(root); if (sfdxBinDirs.length > 0) { const allFiles = sfdxBinDirs.flatMap((dir) => NpmCommand.findFilesRecursively(dir)); const node = allFiles.find((file) => isExecutable(file)); if (node) { return fs.realpathSync(node); } } } const nodePath = which.sync('node', { nothrow: true }); if (nodePath) return nodePath; throw setErrorName(new SfError('Cannot locate node executable.', 'CannotFindNodeExecutable'), 'CannotFindNodeExecutable'); } static findFilesRecursively(dir) { const results = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...NpmCommand.findFilesRecursively(fullPath)); } else { results.push(fullPath); } } return results; } /** * Returns the path to the npm-cli.js file in this package's node_modules * * @private */ static npmCli() { const require = createRequire(import.meta.url); const pkgPath = require.resolve('npm/package.json'); const fileData = fs.readFileSync(pkgPath, 'utf8'); const pkgJson = parseJson(fileData, pkgPath); const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep)); return path.join(prjPath, pkgJson.bin['npm']); } /** * Finds the bin directory in the sfdx installation root path * * @param sfdxPath * @private */ static findSfdxBinDirs(sfdxPath) { return sfdxPath ? [path.join(sfdxPath, 'bin'), path.join(sfdxPath, 'client', 'bin')].filter((p) => fs.existsSync(p)) : []; } } export class NpmModule { module; version; cliRoot; npmMeta; constructor(module, version = 'latest', cliRoot) { this.module = module; this.version = version; this.cliRoot = cliRoot; this.npmMeta = { moduleName: module, }; } ping(registry) { const result = NpmCommand.runNpmCmd(`ping ${registry ?? ''} --json`, { json: true, cliRoot: this.cliRoot }); return JSON.parse(result.stdout); } run(command) { return NpmCommand.runNpmCmd(command, { cliRoot: this.cliRoot, json: command.includes('--json') }); } show(registry) { const showCmd = NpmCommand.runNpmCmd(`show ${this.module}@${this.version} --json`, { registry, cliRoot: this.cliRoot, }); // `npm show` doesn't return exit code 1 when it fails to get a specific package version // If `stdout` is empty then no info was found in the registry. if (showCmd.stdout === '') { throw setErrorName(new SfError(`Failed to find ${this.module}@${this.version} in the registry`, 'NpmError'), 'NpmError'); } try { // `npm show` returns an array of results when the version is a range const raw = JSON.parse(showCmd.stdout); if (Array.isArray(raw)) { // Return the last result in the array since that will be the highest version // NOTE: .at() possibly returns undefined so instead directly index the array for the last element return raw[raw.length - 1]; } return raw; } catch (error) { if (error instanceof Error) { throw setErrorName(new SfError(error.message, 'ShellParseError'), 'ShellParseError'); } throw error; } } pack(registry, options) { try { NpmCommand.runNpmCmd(`pack ${this.module}@${this.version}`, { ...options, registry, cliRoot: this.cliRoot, }); } catch (err) { if (err instanceof Error) { const sfErr = SfError.wrap(err); const e = new SfError(`Failed to fetch tarball from the registry: \n${sfErr.message}`, 'NpmError'); throw setErrorName(e, 'NpmError'); } } return; } async fetchTarball(registry, options) { await this.pollForAvailability(() => { this.pack(registry, options); }); this.pack(registry, options); } // leave it because it's stubbed in the test // eslint-disable-next-line class-methods-use-this async pollForAvailability(checkFn) { const isNonTTY = process.env.CI !== undefined || process.env.CIRCLECI !== undefined; let found = false; let attempts = 0; const maxAttempts = 300; const ux = new Ux({ jsonEnabled: isNonTTY }); const start = isNonTTY ? (msg) => ux.log(msg) : (msg) => ux.spinner.start(msg); const update = isNonTTY ? (msg) => ux.log(msg) : (msg) => (ux.spinner.status = msg); const stop = isNonTTY ? (msg) => ux.log(msg) : (msg) => ux.spinner.stop(msg); start('Polling for new version(s) to become available on npm'); while (!found && attempts < maxAttempts) { attempts += 1; update(`attempt: ${attempts} of ${maxAttempts}`); try { checkFn(); found = true; } catch (error) { if (attempts === maxAttempts) { throw error; } found = false; } // eslint-disable-next-line no-await-in-loop await sleep(1000); } stop(attempts >= maxAttempts ? 'failed' : 'done'); } } //# sourceMappingURL=npmCommand.js.map