Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
With min-release-age (or its flattened equivalent before) set in .npmrc or via CLI, npm audit signatures fails with ETARGET for any package whose lockfile-pinned version was published after the cutoff — even though that exact version has already been installed by npm ci in the same pipeline run, moments earlier.
Observed in a CI pipeline with min-release-age=7:
> npm audit signatures
npm warn Fetching verification keys using TUF failed. Fetching directly from https://<private-registry>/.
npm error code ETARGET
npm error notarget No matching version found for caniuse-lite@1.0.30001790 with a date before 4/16/2026, 7:54:30 PM.
npm error notarget In most cases you or one of your dependencies are requesting a package version that doesn't exist.
##[error]Bash exited with code '1'.
Timeline of that same pipeline run:
npm ci — succeeds, installs caniuse-lite@1.0.30001790 (pinned in package-lock.json) into node_modules.
npm audit signatures — fails with ETARGET on the exact same version that was just installed.
The version exists, it's in the lockfile, it's on disk. It's rejected only because audit signatures re-resolves through the registry with the before filter applied, and the packument's time[version] is newer than the cutoff.
Expected Behavior
npm audit signatures should verify the signatures of the packages that are actually installed, using the exact versions recorded in node_modules / package-lock.json. min-release-age is an install-time policy; by the time we reach audit signatures, the policy has already been evaluated (or bypassed) during npm ci. Re-applying it during signature verification can only produce false negatives on already-admitted code — it cannot protect anything that isn't already protected, because the code is already on disk.
Concretely: the pacote.manifest() call inside audit signatures is asked for an exact name@version spec (not a range, not a tag), so the --before filter contributes nothing beyond turning valid lookups into ETARGETs.
Steps To Reproduce
- Create a project with a single dep:
// package.json
{ "name": "repro", "version": "1.0.0", "dependencies": { "kms-demo": "1.0.0" } }
npm install to produce node_modules and package-lock.json.
- Run:
npm audit signatures --before=2020-01-01
(or --min-release-age=99999, or set min-release-age=99999 in .npmrc)
- Observe:
npm error code ETARGET
npm error notarget No matching version found for kms-demo@1.0.0 with a date before 1/1/2020, 12:00:00 AM.
The package is installed. The version is pinned. audit signatures still refuses to verify it.
Environment
Root cause
workspaces/config/lib/definitions/definitions.js:1377-1383 flattens min-release-age into flatOptions.before = Date.now() - days * 86400000. Every consumer of flatOptions inherits that cutoff.
lib/utils/verify-signatures.js:294-306:
async verifySignatures (name, version, registry) {
const {
_integrity: integrity,
_signatures,
_attestations,
_attestationBundles,
_resolved: resolved,
} = await pacote.manifest(`${name}@${version}`, {
verifySignatures: true,
verifyAttestations: true,
...this.buildRegistryConfig(registry),
...this.npm.flatOptions, // ← leaks `before` into manifest lookup
})
...
}
name and version come from arb.loadActual() a few frames up — they are the exact versions present in node_modules. The pacote spec is exact. The before filter has no legitimate work to do here — its only possible effect is to reject a version that is already installed.
Proposed fix
One line, in lib/utils/verify-signatures.js:
} = await pacote.manifest(`${name}@${version}`, {
verifySignatures: true,
verifyAttestations: true,
...this.buildRegistryConfig(registry),
...this.npm.flatOptions,
+ before: null, // audit signatures verifies exact installed versions; install-time policy does not apply here
})
Why safe:
audit signatures only operates on loadActual() trees — the code is already installed. Stripping before cannot bypass the policy because the policy decision happened during install and can't be un-done at audit time.
- The spec is exact (
name@version), not a range/tag — before has no legitimate filtering work to do.
- No breaking change: the fix only removes false-negative ETARGETs; verification results themselves are unchanged.
Relation to prior issues / RFCs
- Distinguishes from #9212 (closed, not-planned): that issue was about
npm audit fix declining to install a fix younger than the cutoff. That is policy working as intended — audit fix must respect install-time policy because it installs new code. This issue is about npm audit signatures erroring on code that was already installed by npm ci seconds earlier in the same pipeline. That is not policy working — it is a pipeline-internal self-contradiction: npm ci admitted the version, audit signatures pretends it does not exist.
- Aligned with RRFC #856 (open): "Ability to override
--min-release-age on CLI or in the lockfile" proposes that lockfile-pinned versions be implicit exceptions to the policy. This fix is a narrower, immediately-actionable subset: only audit signatures (which cannot install anything) is exempted, and only for exact-version lookups. No new config surface.
- Related: #9005 (closed):
min-release-age leaking into git dep preparation — same family of "the flattened before filter is reaching internal invocations where it shouldn't".
Additional context — what the fix does not change
npm install / npm ci still apply min-release-age exactly as today.
npm audit fix still applies min-release-age exactly as today.
npm audit signatures on a package that is not installed (e.g., after a partial install) behaves as it does today — it returns what pacote returns; the exact-version lookup still fails if the tarball is genuinely gone.
- No change to TUF/keys fetch; no change to signature/attestation logic.
Happy to send a PR with the 1-line fix + regression test if this looks good.
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
With
min-release-age(or its flattened equivalentbefore) set in.npmrcor via CLI,npm audit signaturesfails withETARGETfor any package whose lockfile-pinned version was published after the cutoff — even though that exact version has already been installed bynpm ciin the same pipeline run, moments earlier.Observed in a CI pipeline with
min-release-age=7:Timeline of that same pipeline run:
npm ci— succeeds, installscaniuse-lite@1.0.30001790(pinned inpackage-lock.json) intonode_modules.npm audit signatures— fails withETARGETon the exact same version that was just installed.The version exists, it's in the lockfile, it's on disk. It's rejected only because
audit signaturesre-resolves through the registry with thebeforefilter applied, and the packument'stime[version]is newer than the cutoff.Expected Behavior
npm audit signaturesshould verify the signatures of the packages that are actually installed, using the exact versions recorded innode_modules/package-lock.json.min-release-ageis an install-time policy; by the time we reachaudit signatures, the policy has already been evaluated (or bypassed) duringnpm ci. Re-applying it during signature verification can only produce false negatives on already-admitted code — it cannot protect anything that isn't already protected, because the code is already on disk.Concretely: the
pacote.manifest()call insideaudit signaturesis asked for an exactname@versionspec (not a range, not a tag), so the--beforefilter contributes nothing beyond turning valid lookups into ETARGETs.Steps To Reproduce
npm installto producenode_modulesandpackage-lock.json.--min-release-age=99999, or setmin-release-age=99999in.npmrc)The package is installed. The version is pinned.
audit signaturesstill refuses to verify it.Environment
latest)lib/utils/verify-signatures.js— thepacote.manifest()call spreadsthis.npm.flatOptions, which carriesbeforederived frommin-release-ageviaworkspaces/config/lib/definitions/definitions.js#L1362-L1384.Root cause
workspaces/config/lib/definitions/definitions.js:1377-1383flattensmin-release-ageintoflatOptions.before = Date.now() - days * 86400000. Every consumer offlatOptionsinherits that cutoff.lib/utils/verify-signatures.js:294-306:nameandversioncome fromarb.loadActual()a few frames up — they are the exact versions present innode_modules. The pacote spec is exact. Thebeforefilter has no legitimate work to do here — its only possible effect is to reject a version that is already installed.Proposed fix
One line, in
lib/utils/verify-signatures.js:} = await pacote.manifest(`${name}@${version}`, { verifySignatures: true, verifyAttestations: true, ...this.buildRegistryConfig(registry), ...this.npm.flatOptions, + before: null, // audit signatures verifies exact installed versions; install-time policy does not apply here })Why safe:
audit signaturesonly operates onloadActual()trees — the code is already installed. Strippingbeforecannot bypass the policy because the policy decision happened during install and can't be un-done at audit time.name@version), not a range/tag —beforehas no legitimate filtering work to do.Relation to prior issues / RFCs
npm audit fixdeclining to install a fix younger than the cutoff. That is policy working as intended —audit fixmust respect install-time policy because it installs new code. This issue is aboutnpm audit signatureserroring on code that was already installed bynpm ciseconds earlier in the same pipeline. That is not policy working — it is a pipeline-internal self-contradiction:npm ciadmitted the version,audit signaturespretends it does not exist.--min-release-ageon CLI or in the lockfile" proposes that lockfile-pinned versions be implicit exceptions to the policy. This fix is a narrower, immediately-actionable subset: onlyaudit signatures(which cannot install anything) is exempted, and only for exact-version lookups. No new config surface.min-release-ageleaking intogit dep preparation— same family of "the flattenedbeforefilter is reaching internal invocations where it shouldn't".Additional context — what the fix does not change
npm install/npm cistill applymin-release-ageexactly as today.npm audit fixstill appliesmin-release-ageexactly as today.npm audit signatureson a package that is not installed (e.g., after a partial install) behaves as it does today — it returns what pacote returns; the exact-version lookup still fails if the tarball is genuinely gone.Happy to send a PR with the 1-line fix + regression test if this looks good.