Skip to content

Strip resolved URLs from published npm-shrinkwrap.json to honor consumers' configured registries (supply-chain / compliance) #1405

@skywolf-dl

Description

@skywolf-dl

Is your feature request related to a problem? Please describe.

The published @ui5/cli tarball contains an npm-shrinkwrap.json whose 519 entries have resolved URLs hardcoded to
https://registry.npmjs.org/.... Per npm's documented semantics, a published shrinkwrap takes precedence over the consumer's .npmrc,
so those 519 npmjs.org URLs propagate into every consumer's package-lock.json and npm fetches those tarballs from registry.npmjs.org
at install time — even when the consumer has pinned their registry to a private/enterprise mirror.

This is more than a routing inconvenience for a growing set of consumers. Several factors that didn't weigh as heavily when the
shrinkwrap was added in #294 (Jan 2019) or when #517 was closed (May 2021) now make the registry leakage a real security and
compliance problem:

  • Supply-chain attack surface. Compromises of widely-used npmjs-hosted packages (event-stream, ua-parser-js, node-ipc, repeated
    typosquatting waves, the more recent lottie-player / tj-actions / ledger-connect-kit / NX postinstall-token-exfiltration /
    s1ngularity-style incidents) have made "every public-registry fetch is an attack vector" the default assumption in modern enterprise
    security reviews. Organizations pin to internal mirrors specifically because those mirrors apply scanning, signature/provenance
    verification, allowlisting, and auditable proxy logging that public registry.npmjs.org cannot. A dependency that forces fetches from
    registry.npmjs.org regardless of consumer configuration defeats those controls for ~519 transitive packages on every install.
  • Compliance frameworks. SOC 2, ISO 27001, FedRAMP, and many regulated-industry regimes (financial services, healthcare, government,
    defense) require auditable provenance for build-time dependencies. "All dependencies were fetched from
    " is a routine audit attestation. @ui5/cli's shrinkwrap injects 519 lockfile entries that explicitly
    contradict that attestation, and consumers cannot fix it in their own repositories.
  • Air-gapped / network-restricted build environments. CI/CD environments that block egress to registry.npmjs.org (common in
    regulated industries, government cloud, and disconnected developer environments) fail outright when installing @ui5/cli, even when
    the consumer's mirror has all required packages cached.
  • Renovate and similar tooling. Projects on a Renovate-driven dependency cadence regenerate lockfiles continuously. Every Renovate
    run that touches a transitive of @ui5/cli writes npmjs.org URLs back into the consumer's lockfile. Consumer-side workarounds
    (post-install scripts, lockfile rewriters) must be maintained per-repo and re-applied on every Renovate PR, which is impractical at
    scale.

The 2019 motivation in #294 was concrete and reasonable: prevent a bad upstream patch (the dir-glob incident) from silently breaking
@ui5/cli. That goal is purely about version pinning — which versions get installed. None of #294's reasoning depends on which
mirror serves the tarball; the issue and its examples discuss versions throughout, never registries.

Describe the solution you'd like

Strip the resolved field from the npm-shrinkwrap.json before publishing, while keeping version and integrity. This preserves 100% of
the version-pinning benefit that motivated #294, while letting consumers' configured registries be honored.

A shrinkwrap pins three independent things:

Field Purpose Needed for #294?
version Which version to install Yes
integrity SHA-512 hash of the tarball content Yes
resolved Where to fetch the tarball from (fetch hint) No

resolved is only a hint to npm about where to fetch. npm fetches from resolved, then verifies against integrity. If the tarball
served by the consumer's mirror has the same content (i.e. same SHA-512), it passes integrity verification and is accepted as
identical to the one @ui5/cli was tested against. If a malicious or wrong tarball is served, integrity verification rejects it.
Tarball identity is established by integrity, not by URL.

So: dropping resolved from the published shrinkwrap loses zero reproducibility. The dir-glob-class scenario from #294 is still fully
prevented (npm refuses to install a different version). The Yarn dual-resolution scenario from #294 is still fully prevented (the
shrinkwrap still expresses the exact resolution tree). The only change is that consumers' configured registries get to serve the
bytes.

Implementation in the publish pipeline is a one-line transform, e.g. a prepublishOnly script:

  jq 'del(.packages[].resolved)' npm-shrinkwrap.json > npm-shrinkwrap.tmp \
    && mv npm-shrinkwrap.tmp npm-shrinkwrap.json

…or equivalent in whatever tool the release pipeline uses.

Describe alternatives you've considered

  1. Stop publishing the shrinkwrap entirely. Add npm-shrinkwrap.json to .npmignore. This is what the npm CLI itself does — npm pack
    npm produces a tarball with no shrinkwrap. It's the cleanest option but it gives up the version-pinning benefit @ui5/cli chose in
    Add npm shrinkwrap #294, so it's a bigger ask.
  2. Strip resolved and integrity. Same registry-routing benefit, but loses tamper detection. Not recommended — the integrity
    guarantee is independently valuable and there's no reason to drop it.
  3. Document the behavior prominently without changing it. This was effectively the outcome of [ui5/cli] npm-shrinkwrap.json causing registry in .npmrc to be ignored #517. It does not address the
    compliance and supply-chain concerns above; consumers in regulated environments can't satisfy "all dependencies came from the
    approved registry" with documentation alone.
  4. Consumer-side workarounds. Post-install hooks that delete node_modules/@ui5/cli/npm-shrinkwrap.json, or scripts that rewrite
    resolved URLs in the consumer's lockfile. These are brittle, must be maintained per-repo, must be re-applied after every Renovate
    PR, and don't satisfy security audits that examine the lockfile as the source of truth.

Of these, option 1 (drop the shrinkwrap altogether) and the proposed solution (strip resolved) are the only ones that actually solve
the problem. The proposed solution is the more conservative of the two — it preserves the original 2019 design intent in full while
removing the unintended registry-routing side effect.

Additional context

Reproduction

In a fresh project pinned to any non-npmjs.org registry:

  mkdir ui5-cli-shrinkwrap-repro && cd ui5-cli-shrinkwrap-repro
  echo 'registry=https://your-enterprise-registry.example.com/npm/' > .npmrc
  npm init -y
  npm install @ui5/cli@4.0.53

  grep -c 'registry.npmjs.org' package-lock.json
  # 519
  grep -c 'registry.npmjs.org' node_modules/@ui5/cli/npm-shrinkwrap.json
  # 519

All 519 hits in package-lock.json are nested under node_modules/@ui5/cli/node_modules/... and their resolved URLs match the shipped
shrinkwrap exactly.

When the configured enterprise registry is queried directly, it returns its own mirrored tarball URL — but npm uses the shrinkwrap's
URL instead:

  $ npm view @babel/parser@7.29.3 dist.tarball
  https://your-enterprise-registry.example.com/npm/@babel/parser/-/parser-7.29.3.tgz
  # ...yet the lockfile records registry.npmjs.org for the same package

Precedent

The npm CLI itself — the canonical example of a CLI tool published to the registry — does not ship a shrinkwrap. npm pack
npm@11.16.0 produces a tarball with no npm-shrinkwrap.json. Despite npm having every reason to want reproducibility of its own
runtime, the project does not push its pinned tree onto consumers. This is a useful data point against the reading that "CLIs should
ship shrinkwraps" is universal practice.

Related issues

This issue does not ask the maintainers to revisit #294's goal. It asks them to refine the implementation so that the goal is
preserved while a real, growing class of consumers (enterprise / regulated / air-gapped) is no longer harmed by an unintended side
effect.

Environment

  • @ui5/cli version: 4.0.53
  • Node.js: v24.14.1
  • npm: 11.11.0
  • OS: macOS / Darwin arm64
  • Consumer registry: enterprise Artifactory mirror (anonymized)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions