Skip to content

[BUG] npm ls --json --long outputs nothing and exits 1 when a deprecation message contains a URL (redaction corrupts compact JSON) #9873

Description

@jansinger

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

Current Behavior

npm ls --json --long prints nothing to stdout and exits 1 whenever the dependency tree contains a package whose deprecated message contains a URL and whose compact-serialized JSON later contains an @ (e.g. a scoped package _id). No error is shown; the debug log only records verbose exit 1.

Minimal reproduction:

$ mkdir repro && cd repro
$ npm init -y >/dev/null
$ npm install @esbuild-kit/esm-loader@2.6.5   # deprecated: "Merged into tsx: https://tsx.hirok.io"
$ npm ls --json --long; echo "exit=$?"
exit=1

(stdout is 0 bytes, stderr only prints the debug-log path.)

This silently breaks downstream tooling: @cyclonedx/cyclonedx-npm runs npm ls --json --long --all internally and now fails with Error: failed to parse npm-ls response on any project that transitively pulls in such a deprecated package (e.g. anything depending on tsx's predecessor packages) — i.e. SBOM generation is broken.

Expected Behavior

npm ls --json --long prints the JSON tree (as npm ≤ 11.14.1 does), exit 0.

Steps To Reproduce

  1. npm install @esbuild-kit/esm-loader@2.6.5
  2. npm ls --json --long
  3. Empty stdout, exit code 1

Bisect: works on 11.14.1, broken since 11.15.0, still broken on 11.19.0 and 12.0.2.

Root cause analysis

The swallowed exception (surfaced by calling npm.exec('ls', …) directly) is:

SyntaxError: Unexpected token '*', ..."io","dev":***@esbuil"... is not valid JSON
    at JSON.parse (<anonymous>)
    at redactValue (npm/lib/utils/display.js:102:35)
    at getJsonBuffer (npm/lib/utils/display.js:111:27)

display.js redacts JSON output via

const redactValue = (obj) => JSON.parse(redactLog(JSON.stringify(obj)))

JSON.stringify produces a compact string, and @npmcli/redact's URL-credential matcher then matches from an https:// inside the deprecated message across the (whitespace-free) JSON up to the next @ (here inside the scoped _id), treating everything in between as user:password@:

before: ..."deprecated":"Merged into tsx: https://tsx.hirok.io","dev":true,"license":"MIT","_id":"@esbuild-kit/esm-loader@2.6.5",...
after:  ..."deprecated":"Merged into tsx: https://tsx.hirok.io","dev":***@esbuild-kit/esm-loader@2.6.5",...

The redacted string is no longer valid JSON, JSON.parse throws inside the output flush, and the error is swallowed → empty stdout, exit 1.

The same corruption cannot happen when redacting the pretty-printed CLI output (newlines stop the matcher), which is why this only bit once redactValue started round-tripping through compact JSON.stringify (first released in 11.15.0).

Possible fixes: redact leaf string values individually (walk the object) instead of redacting the serialized document, or make the credential matcher stop at "/\ characters.

Environment

  • npm: 11.15.0 … 12.0.2 (bisected; 11.14.1 OK)
  • Node.js: v24.18.0
  • OS: macOS (Darwin 27.0.0); also reproduced on Linux CI runners via cyclonedx-npm failures
  • npm config: defaults

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions