@npmcli/arborist
Advanced tools
| // Determine whether a package name is exempt from the `min-release-age` / | ||
| // `before` release-age filter, based on the `min-release-age-exclude` config. | ||
| // | ||
| // Patterns are exact package names or `minimatch` globs (e.g. `@myorg/*`), and | ||
| // match against the package name only. This is a "named-only" exemption: a | ||
| // matched package's own dependencies still follow the filter unless they match | ||
| // a pattern too. | ||
| // | ||
| // Callers must match against the resolved registry identity of a package, not | ||
| // the self-reported alias or dependency-edge name. For `npm:` aliases the | ||
| // fetched package is the alias target, so run specs through `trustedSpecName` | ||
| // first; otherwise an alias key could match an exclude pattern and turn the | ||
| // filter off for the unrelated package it resolves to. | ||
| const { minimatch } = require('minimatch') | ||
| // This list only ever widens the exemption (turns the security filter off for a | ||
| // package), so disable pattern features that could silently turn it into a | ||
| // match-all: `nonegate` keeps a leading `!` literal (so a stray `!foo` exempts | ||
| // nothing instead of everything-but-foo), `nocomment` keeps a leading `#` | ||
| // literal, and `noext` disables extglobs. | ||
| const minimatchOptions = { nonegate: true, nocomment: true, noext: true } | ||
| const isReleaseAgeExcluded = (name, patterns) => { | ||
| if (!name || !Array.isArray(patterns) || patterns.length === 0) { | ||
| return false | ||
| } | ||
| return patterns.some(pattern => | ||
| name === pattern || minimatch(name, pattern, minimatchOptions)) | ||
| } | ||
| // Resolve the trusted registry name for an npa spec. For `npm:` aliases (e.g. | ||
| // `"x": "npm:other@1"`) the installed/fetched package is the alias target | ||
| // (`subSpec`), not the alias key, so the exemption must be keyed on the | ||
| // underlying package name. Mirrors `nameFromEdges` in script-allowed.js. | ||
| const trustedSpecName = (spec) => { | ||
| if (!spec) { | ||
| return undefined | ||
| } | ||
| if (spec.type === 'alias' && spec.subSpec && spec.subSpec.registry) { | ||
| return spec.subSpec.name | ||
| } | ||
| return spec.name | ||
| } | ||
| module.exports = { isReleaseAgeExcluded, trustedSpecName } |
| const isScriptAllowed = require('./script-allowed.js') | ||
| const getInstallScripts = require('./install-scripts.js') | ||
| // Shared allowScripts walk used by both the npm CLI | ||
| // (lib/utils/check-allow-scripts.js, lib/utils/strict-allow-scripts-preflight.js) | ||
| // and libnpmexec (npm exec / npx). It lives in arborist because that is the | ||
| // only package both callers can import. | ||
| // | ||
| // Walks a tree's inventory and returns the dep nodes that have | ||
| // install-relevant lifecycle scripts and are not yet covered (or explicitly | ||
| // denied) by the allowScripts policy. | ||
| // | ||
| // Returns an array of `{ node, scripts }` entries. `scripts` is an object | ||
| // describing the relevant lifecycle scripts that would run. | ||
| const collectUnreviewedScripts = async ({ | ||
| tree, | ||
| policy, | ||
| ignoreScripts = false, | ||
| dangerouslyAllowAllScripts = false, | ||
| includeWhenIgnored = false, | ||
| } = {}) => { | ||
| // With ignore-scripts set, no scripts run, so execution callers bail out | ||
| // here. approve/deny pass includeWhenIgnored so they keep listing | ||
| // unreviewed packages, which is what you need to move from a blanket | ||
| // ignore-scripts to an allowlist. Listing never runs anything. | ||
| if ((ignoreScripts && !includeWhenIgnored) || dangerouslyAllowAllScripts) { | ||
| return [] | ||
| } | ||
| if (!tree?.inventory) { | ||
| return [] | ||
| } | ||
| const resolvedPolicy = policy || null | ||
| const unreviewed = [] | ||
| for (const node of tree.inventory.values()) { | ||
| if (node.isProjectRoot || node.isWorkspace) { | ||
| continue | ||
| } | ||
| if (node.isLink) { | ||
| // Linked workspace dependencies are managed by the workspace owner. | ||
| continue | ||
| } | ||
| if (node.inBundle) { | ||
| // Bundled dependencies never run their install scripts and cannot be | ||
| // allowlisted, so they are never "pending". Skipping them keeps them | ||
| // out of the advisory warning and out of strict-allow-scripts. A | ||
| // package that needs a bundled dep's script must forward it as one of | ||
| // its own lifecycle scripts. | ||
| continue | ||
| } | ||
| const verdict = isScriptAllowed(node, resolvedPolicy) | ||
| if (verdict === true || verdict === false) { | ||
| continue | ||
| } | ||
| const scripts = await getInstallScripts(node) | ||
| if (Object.keys(scripts).length === 0) { | ||
| continue | ||
| } | ||
| unreviewed.push({ node, scripts }) | ||
| } | ||
| return unreviewed | ||
| } | ||
| // Builds the `ESTRICTALLOWSCRIPTS` error thrown by the strict-mode preflight | ||
| // from a list of `{ node, scripts }` entries. `remediation` is the | ||
| // caller-specific guidance appended after the package list (npm install vs | ||
| // npm exec have different remediation commands). | ||
| const strictAllowScriptsError = (unreviewed, { remediation } = {}) => { | ||
| const lines = unreviewed.map(({ node, scripts }) => { | ||
| const events = Object.entries(scripts) | ||
| .map(([event, body]) => `${event}: ${body}`) | ||
| .join('; ') | ||
| const name = node.package?.name || node.name | ||
| const version = node.package?.version || '' | ||
| const label = version ? `${name}@${version}` : name | ||
| return ` ${label} (${events})` | ||
| }).join('\n') | ||
| return Object.assign( | ||
| new Error( | ||
| `--strict-allow-scripts: ${unreviewed.length} package(s) have install ` + | ||
| `scripts not covered by allowScripts:\n${lines}\n${remediation}` | ||
| ), | ||
| { code: 'ESTRICTALLOWSCRIPTS' } | ||
| ) | ||
| } | ||
| module.exports = { collectUnreviewedScripts, strictAllowScriptsError } |
@@ -27,2 +27,3 @@ // mixin implementing the buildIdealTree method | ||
| const calcDepFlags = require('../calc-dep-flags.js') | ||
| const { isReleaseAgeExcluded, trustedSpecName } = require('../release-age-exclude.js') | ||
| const Shrinkwrap = require('../shrinkwrap.js') | ||
@@ -537,3 +538,7 @@ const { defaultLockfileVersion } = Shrinkwrap | ||
| const _isRoot = tree.isProjectRoot || tree.isWorkspace | ||
| const mani = await pacote.manifest(spec, { ...this.options, _isRoot }) | ||
| const mani = await pacote.manifest(spec, { | ||
| ...this.options, | ||
| _isRoot, | ||
| before: this.#releaseAgeBefore(spec), | ||
| }) | ||
| if (isTag) { | ||
@@ -782,2 +787,3 @@ // translate tag to a version | ||
| fullMetadata: false, | ||
| before: this.#releaseAgeBefore(spec), | ||
| }) | ||
@@ -881,3 +887,3 @@ node.package = { ...mani, _id: `${mani.name}@${mani.version}` } | ||
| await new Arborist({ ...this.options, path }) | ||
| .loadVirtual({ root: node }) | ||
| .loadVirtual({ root: node, subtreeOnly: true }) | ||
| } | ||
@@ -1286,2 +1292,15 @@ | ||
| // The effective `before` filter for a package, applying `min-release-age-exclude`. | ||
| // Returns null (no age filter) for an exempted package, otherwise the | ||
| // configured `before`. The exemption is keyed on the spec's trusted registry | ||
| // identity (alias targets are unwrapped) so an `npm:` alias key cannot disable | ||
| // the filter for the package it actually resolves to. | ||
| #releaseAgeBefore (spec) { | ||
| const { before, minReleaseAgeExclude } = this.options | ||
| if (!before) { | ||
| return before | ||
| } | ||
| return isReleaseAgeExcluded(trustedSpecName(spec), minReleaseAgeExclude) ? null : before | ||
| } | ||
| async #fetchManifest (spec, parent, edge) { | ||
@@ -1294,2 +1313,3 @@ // Enforce allow-* gates before consulting the manifest cache so a cached entry from a different edge cannot bypass the policy. | ||
| fullMetadata: true, | ||
| before: this.#releaseAgeBefore(spec), | ||
| _isRoot: !!(edge?.from?.isProjectRoot || edge?.from?.isWorkspace), | ||
@@ -1296,0 +1316,0 @@ } |
@@ -7,2 +7,3 @@ const { mkdirSync } = require('node:fs') | ||
| const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js') | ||
| const nameFromFolder = require('@npmcli/name-from-folder') | ||
@@ -43,5 +44,8 @@ // generate short hash key based on the dependency tree starting at this node | ||
| #generateChild (node, location, pkg, isInStore, root) { | ||
| #generateChild (node, location, pkg, isInStore, root, inBundle = false) { | ||
| const newChild = new IsolatedNode({ | ||
| isInStore, | ||
| inBundle, | ||
| isRegistryDependency: node.isRegistryDependency, | ||
| isRootDependency: node.isRootDependency, | ||
| location, | ||
@@ -156,2 +160,5 @@ name: node.packageName || node.name, | ||
| if (node.hasShrinkwrap) { | ||
| // strip any path traversal from package.json name fields before they hit path.join below | ||
| /* istanbul ignore next - packageName is always set for real packages */ | ||
| const safeName = nameFromFolder(node.packageName || node.path) | ||
| const dir = join( | ||
@@ -161,3 +168,3 @@ node.root.path, | ||
| '.store', | ||
| `${node.packageName}@${node.version}` | ||
| `${safeName}@${node.version}` | ||
| ) | ||
@@ -196,2 +203,9 @@ mkdirSync(dir, { recursive: true }) | ||
| result.version = node.version | ||
| // Carry the source node's registry-dependency flag so the store node retains it. | ||
| // IsolatedNode has no edges to recompute it from, and reify's registry-tarball allow-remote exemption depends on it. | ||
| result.isRegistryDependency = node.isRegistryDependency | ||
| // Same reasoning for allow-remote=root: the store node has no edgesIn, so capture from the source node whether it satisfies a valid edge from the project root or a workspace. | ||
| result.isRootDependency = [...node.edgesIn].some(e => | ||
| e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace) | ||
| ) | ||
| return result | ||
@@ -206,3 +220,4 @@ } | ||
| result.name = result.isWorkspace ? (node.packageName || node.name) : node.name | ||
| result.packageName = node.packageName || node.name | ||
| // strip any path traversal from package.json name fields before they hit path.join below | ||
| result.packageName = nameFromFolder(node.packageName || node.path) | ||
| result.package = { ...node.package } | ||
@@ -262,2 +277,18 @@ result.package.bundleDependencies = undefined | ||
| const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target) | ||
| // Optional peers declared only in peerDependenciesMeta (e.g. `@types/react`) have no edge, so the materialization above misses them. | ||
| // Resolve each from the tree and link it; if nobody provides it, node.resolve finds nothing and it stays omitted. | ||
| const peerMeta = node.package.peerDependenciesMeta | ||
| if (peerMeta) { | ||
| const resolvedNames = new Set([...nonOptionalDeps, ...optionalDeps].map(n => n.name)) | ||
| for (const peerName in peerMeta) { | ||
| if (!peerMeta[peerName]?.optional || resolvedNames.has(peerName)) { | ||
| continue | ||
| } | ||
| const resolved = node.resolve(peerName)?.target | ||
| if (resolved && resolved !== node && !resolved.inert && !isLocal(resolved)) { | ||
| optionalDeps.push(resolved) | ||
| } | ||
| } | ||
| } | ||
| result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.#workspaceProxy(n))) | ||
@@ -377,3 +408,3 @@ result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.#externalProxy(n))) | ||
| bundledTree.nodes.forEach(node => { | ||
| this.#generateChild(node, node.location, node.pkg, false, root) | ||
| this.#generateChild(node, node.location, node.pkg, false, root, true) | ||
| }) | ||
@@ -380,0 +411,0 @@ |
@@ -1,2 +0,2 @@ | ||
| const { resolve } = require('node:path') | ||
| const { isAbsolute, resolve } = require('node:path') | ||
| // mixin providing the loadVirtual method | ||
@@ -20,2 +20,4 @@ const mapWorkspaces = require('@npmcli/map-workspaces') | ||
| #rootOptionProvided | ||
| // when true, lockfile entries must stay inside `this.path` | ||
| #subtreeOnly = false | ||
@@ -32,2 +34,4 @@ // public method | ||
| this.#subtreeOnly = !!options.subtreeOnly | ||
| if (options.root && options.root.meta) { | ||
@@ -171,2 +175,28 @@ await this.#loadFromShrinkwrap(options.root.meta, options.root) | ||
| // throw if the resolved path is outside `base`, only in subtreeOnly mode | ||
| #assertContained (base, resolvedPath, location) { | ||
| if (!this.#subtreeOnly) { | ||
| return | ||
| } | ||
| if (isAbsolute(location)) { | ||
| throw Object.assign( | ||
| new Error(`invalid lockfile entry: "${location}" must be a relative location`), | ||
| { code: 'EINVALIDLOCATION', location, base } | ||
| ) | ||
| } | ||
| const rel = relpath(base, resolvedPath) | ||
| if ( | ||
| rel === '..' || | ||
| rel.startsWith('../') || | ||
| isAbsolute(rel) || | ||
| // non-root key that collapses back to base (e.g. 'node_modules/..') | ||
| (rel === '' && location !== '') | ||
| ) { | ||
| throw Object.assign( | ||
| new Error(`invalid lockfile entry: "${location}" resolves outside of "${base}"`), | ||
| { code: 'EINVALIDLOCATION', location, base, resolvedPath } | ||
| ) | ||
| } | ||
| } | ||
| // links is the set of metadata, and nodes is the map of non-Link nodes | ||
@@ -178,2 +208,4 @@ // Set the targets to nodes in the set, if we have them (we might not) | ||
| const targetPath = resolve(this.path, meta.resolved) | ||
| // check before nodes.get so we surface EINVALIDLOCATION instead of EMISSINGTARGET | ||
| this.#assertContained(this.path, targetPath, meta.resolved || location) | ||
| const targetLoc = relpath(this.path, targetPath) | ||
@@ -237,2 +269,3 @@ const target = nodes.get(targetLoc) | ||
| const path = resolve(p, location) | ||
| this.#assertContained(p, path, location) | ||
| // shrinkwrap doesn't include package name unless necessary | ||
@@ -267,2 +300,3 @@ if (!sw.name) { | ||
| const path = resolve(this.path, location) | ||
| this.#assertContained(this.path, path, location) | ||
| const link = new Link({ | ||
@@ -269,0 +303,0 @@ installLinks: this.installLinks, |
+29
-0
@@ -12,2 +12,23 @@ // Do not rely on package._fields, so that we don't throw | ||
| // A named ref (tag or branch) resolves to a commit hash, so look up the | ||
| // committish recorded for this edge in the lockfile to detect spec changes. | ||
| const lockedGitCommittish = (child, requestor) => { | ||
| const lock = requestor.root?.meta?.data?.packages?.[requestor.location] | ||
| const spec = lock && ( | ||
| lock.dependencies?.[child.name] || | ||
| lock.optionalDependencies?.[child.name] || | ||
| lock.devDependencies?.[child.name] || | ||
| lock.peerDependencies?.[child.name] | ||
| ) | ||
| if (!spec) { | ||
| return null | ||
| } | ||
| try { | ||
| const parsed = npa.resolve(child.name, spec, requestor.realpath) | ||
| return parsed.type === 'git' ? parsed.gitCommittish || '' : null | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
| const depValid = (child, requested, requestor) => { | ||
@@ -98,2 +119,10 @@ // NB: we don't do much to verify 'tag' type requests. | ||
| if (!requested.gitRange) { | ||
| // a named ref can't be verified against the resolved commit offline, | ||
| // so re-resolve if it differs from the committish in the lockfile | ||
| if (!reqCommit) { | ||
| const locked = lockedGitCommittish(child, requestor) | ||
| if (locked !== null && locked !== (requested.gitCommittish || '')) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
@@ -100,0 +129,0 @@ } |
| const { isNodeGypPackage } = require('@npmcli/node-gyp') | ||
| const PackageJson = require('@npmcli/package-json') | ||
@@ -73,11 +74,36 @@ // Returns the install-relevant lifecycle scripts that would run for a | ||
| // Lockfile-only nodes (e.g. `npm ci` before reify) carry | ||
| // `hasInstallScript: true` but no enumerated scripts: the lockfile | ||
| // records the presence flag but never the script bodies. Without this | ||
| // fallback the strict-allow-scripts preflight would miss them entirely | ||
| // and let postinstall run. We can't recover the real script body | ||
| // without fetching the manifest, so emit a sentinel describing that | ||
| // install scripts are present. | ||
| // Lockfile-only nodes carry `hasInstallScript: true` but no enumerated | ||
| // scripts: the lockfile records the presence flag, not the script bodies, | ||
| // so `node.package.scripts` is empty on a lockfile-driven install (`npm ci`, | ||
| // a repeat `npm install`). Before giving up, read the installed | ||
| // package.json from disk to recover the real script bodies. Builder#addToBuildSet | ||
| // does the same disk read to decide what to run, but unlike that path this | ||
| // one is read-only: we never mutate `node.package`. | ||
| if (Object.keys(collected).length === 0 && node.hasInstallScript === true) { | ||
| collected.install = '(install scripts present)' | ||
| const { content } = await PackageJson.normalize(node.path) | ||
| .catch(() => ({ content: {} })) | ||
| /* istanbul ignore next: normalize resolves to an object with a scripts | ||
| object, or our catch fallback returns {}; defensive guard only. */ | ||
| const diskScripts = content?.scripts || {} | ||
| if (diskScripts.preinstall) { | ||
| collected.preinstall = diskScripts.preinstall | ||
| } | ||
| if (diskScripts.install) { | ||
| collected.install = diskScripts.install | ||
| } | ||
| if (diskScripts.postinstall) { | ||
| collected.postinstall = diskScripts.postinstall | ||
| } | ||
| if (diskScripts.prepare && hasNonRegistryShape(node)) { | ||
| collected.prepare = diskScripts.prepare | ||
| } | ||
| // Still nothing. The package isn't on disk yet (e.g. `npm ci` before | ||
| // reify) or its package.json is unreadable. Emit a sentinel so the | ||
| // advisory and the strict-allow-scripts preflight still surface that | ||
| // install scripts are present. | ||
| if (Object.keys(collected).length === 0) { | ||
| collected.install = '(install scripts present)' | ||
| } | ||
| } | ||
@@ -84,0 +110,0 @@ |
@@ -23,2 +23,5 @@ // Alternate versions of different classes that we use for isolated mode | ||
| isInStore = false | ||
| inBundle = false | ||
| isRegistryDependency = false | ||
| isRootDependency = false | ||
| linksIn = new Set() | ||
@@ -51,2 +54,11 @@ meta = { loadedFromDisk: false } | ||
| } | ||
| if (options.inBundle) { | ||
| this.inBundle = true | ||
| } | ||
| if (options.isRegistryDependency) { | ||
| this.isRegistryDependency = true | ||
| } | ||
| if (options.isRootDependency) { | ||
| this.isRootDependency = true | ||
| } | ||
| if (options.optional) { | ||
@@ -109,2 +121,7 @@ this.optional = true | ||
| /* istanbul ignore next -- emulate lib/node.js */ | ||
| get packageName () { | ||
| return this.package.name || null | ||
| } | ||
| get version () { | ||
@@ -111,0 +128,0 @@ return this.package.version |
@@ -12,2 +12,3 @@ 'use strict' | ||
| const npmFetch = require('npm-registry-fetch') | ||
| const { isReleaseAgeExcluded } = require('./release-age-exclude.js') | ||
@@ -893,4 +894,5 @@ // handle results for parsed query asts, results are stored in a map that has a | ||
| // if the packument has a time property, and the user passed a before flag, then | ||
| // we filter this list down to only those versions that existed before the specified date | ||
| if (packument.time && opts.before) { | ||
| // we filter this list down to only those versions that existed before the specified date. | ||
| // packages matching `min-release-age-exclude` are exempt from this filter. | ||
| if (packument.time && opts.before && !isReleaseAgeExcluded(name, opts.minReleaseAgeExclude)) { | ||
| candidates = candidates.filter((version) => { | ||
@@ -897,0 +899,0 @@ // this version isn't found in the times at all, drop it |
+52
-20
@@ -27,9 +27,9 @@ const npa = require('npm-package-arg') | ||
| const isScriptAllowed = (node, policy) => { | ||
| // Bundled dependencies cannot be allowlisted in Phase 1. The RFC defers | ||
| // allowlisting them to a follow-up RFC because matching by name@version | ||
| // from the bundled tarball would reintroduce manifest confusion (a | ||
| // bundled tarball can claim any name and version). Returning null here | ||
| // marks bundled deps as unreviewed regardless of any policy entries, so | ||
| // their install scripts surface in the Phase 1 advisory warning and | ||
| // (eventually) get blocked at the install-time gate. | ||
| // Bundled dependencies never run their install scripts and cannot be | ||
| // allowlisted. Matching by name@version from the bundled tarball would | ||
| // reintroduce manifest confusion (a bundled tarball can claim any name | ||
| // and version). Returning null marks them as not-allowed regardless of | ||
| // any policy entry, so their install scripts are blocked by the | ||
| // install-time gate. A package that needs a bundled dep's script must | ||
| // forward it as one of its own lifecycle scripts. | ||
| if (node.inBundle) { | ||
@@ -101,2 +101,37 @@ return null | ||
| const resolvedSourceSpecs = (node) => { | ||
| const specs = [] | ||
| const seen = new Set() | ||
| const add = (spec) => { | ||
| if (typeof spec !== 'string' || spec === '' || seen.has(spec)) { | ||
| return | ||
| } | ||
| seen.add(spec) | ||
| specs.push(spec) | ||
| } | ||
| add(node?.resolved) | ||
| if (!node?.resolved && node?.linksIn && typeof node.linksIn[Symbol.iterator] === 'function') { | ||
| let hasIncomingLink = false | ||
| for (const link of node.linksIn) { | ||
| hasIncomingLink = true | ||
| add(link.resolved) | ||
| } | ||
| if (hasIncomingLink) { | ||
| // Link targets for local directory deps are separate inventory nodes | ||
| // whose own `resolved` is null. The incoming Link carries the saved spec | ||
| // (for example `file:../pkg`, relative to node_modules), while policy | ||
| // entries written by hand often use the dependency spec from package.json | ||
| // (for example `file:pkg`, resolved by npa to this target path). Include | ||
| // the real target paths so both forms can match the same local dep. | ||
| add(node.realpath) | ||
| add(node.path) | ||
| } | ||
| } | ||
| return specs | ||
| } | ||
| const matchRegistry = (node, parsed) => { | ||
@@ -287,13 +322,9 @@ // If this node is not a registry dep, refuse the match. A registry-style | ||
| const matchFileOrDir = (node, parsed) => { | ||
| if (!node.resolved) { | ||
| return false | ||
| } | ||
| return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec | ||
| return resolvedSourceSpecs(node) | ||
| .some(resolved => resolved === parsed.saveSpec || resolved === parsed.fetchSpec) | ||
| } | ||
| const matchRemote = (node, parsed) => { | ||
| if (!node.resolved) { | ||
| return false | ||
| } | ||
| return node.resolved === parsed.fetchSpec || node.resolved === parsed.saveSpec | ||
| return resolvedSourceSpecs(node) | ||
| .some(resolved => resolved === parsed.fetchSpec || resolved === parsed.saveSpec) | ||
| } | ||
@@ -325,7 +356,7 @@ | ||
| // Trusted display identity for human-facing output (`npm install` | ||
| // advisory, `npm approve-scripts --allow-scripts-pending`). Same idea as | ||
| // getTrustedRegistryIdentity, but for DISPLAY only — version falls back | ||
| // to node.version when the URL doesn't carry one. Must never be used | ||
| // for policy matching. | ||
| // Trusted display identity for human-facing output (the `npm install` | ||
| // blocked-scripts summary and `npm approve-scripts --allow-scripts-pending`). | ||
| // Same as getTrustedRegistryIdentity, but for display only: version | ||
| // falls back to node.version when the URL doesn't carry one. Do not | ||
| // use for policy matching. | ||
| const trustedDisplay = (node) => { | ||
@@ -344,2 +375,3 @@ const trusted = getTrustedRegistryIdentity(node) | ||
| module.exports.getTrustedRegistryIdentity = getTrustedRegistryIdentity | ||
| module.exports.resolvedSourceSpecs = resolvedSourceSpecs | ||
| module.exports.trustedDisplay = trustedDisplay |
@@ -932,4 +932,10 @@ // a module that manages a shrinkwrap file (npm-shrinkwrap.json or | ||
| const loc = relpath(this.path, node.path) | ||
| // Drop lockfile entries for extraneous nodes outside node_modules. These are stale workspace entries: the workspace was removed from package.json or its directory was deleted, so it should not be tracked in package-lock.json. | ||
| if (node.extraneous && !/(^|\/)node_modules\//.test(loc) && loc !== 'node_modules') { | ||
| // Drop lockfile entries for extraneous nodes outside node_modules that | ||
| // are direct fsChildren of the root (or detached link targets). These | ||
| // are stale top-level entries: a workspace or file: dep removed from | ||
| // the root manifest, or whose directory was deleted. Extraneous | ||
| // fsChildren nested under another package (e.g. a file: dep of another | ||
| // file: dep) are kept so `npm ci` can resolve the parent's dependency. | ||
| if (node.extraneous && !/(^|\/)node_modules\//.test(loc) && loc !== 'node_modules' && | ||
| (!node.fsParent || node.fsParent.isRoot)) { | ||
| continue | ||
@@ -936,0 +942,0 @@ } |
+1
-1
| { | ||
| "name": "@npmcli/arborist", | ||
| "version": "9.7.0", | ||
| "version": "9.8.0", | ||
| "description": "Manage node_modules trees", | ||
@@ -5,0 +5,0 @@ "dependencies": { |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
534668
2.94%67
3.08%13870
2.57%