prefetch-npm-deps: strip volatile fields from dist objects in packuments

The dist object in npm packuments contains fields that can change
after a package is published, causing hash mismatches in
fixed-output derivations:

  - signatures: changes when npm rotates registry signing keys
    (old key SHA256:jl3bwswu80Pjj... expired 2025-01-29)
  - npm-signature: legacy format being progressively removed
  - attestations: provenance metadata added post-publication

Only keep the three fields npm actually needs during install:
tarball, integrity, and shasum. Also strip the informational-only
fileCount and unpackedSize fields.

This is the same approach already used for top-level and
version-level field whitelisting.

Ref: https://github.com/numtide/llm-agents.nix/issues/2459
This commit is contained in:
Jörg Thalheim
2026-02-15 18:09:32 +01:00
parent 09a4946173
commit 203662a570
@@ -67,6 +67,20 @@ const ALLOWED_VERSION_FIELDS: &[&str] = &[
"scripts",
];
/// Allowed fields in `dist` sub-objects.
///
/// Only fields that npm reads during `npm install` are included.
/// Excluded volatile fields:
/// - `signatures`: changes when npm rotates registry signing keys
/// - `npm-signature`: legacy signature format, also mutable
/// - `attestations`: provenance attestations, can be added/updated post-publish
/// - `fileCount`, `unpackedSize`: informational only, not used during install
const ALLOWED_DIST_FIELDS: &[&str] = &[
"tarball", // URL to download the package tarball
"integrity", // SRI hash for verification (e.g., sha512-...)
"shasum", // SHA-1 fallback hash for older lockfiles
];
fn normalize_packument(
package_name: &str,
data: &[u8],
@@ -91,6 +105,12 @@ fn normalize_packument(
for version_val in versions.values_mut() {
if let Some(version_obj) = version_val.as_object_mut() {
version_obj.retain(|key, _| ALLOWED_VERSION_FIELDS.contains(&key.as_str()));
// Normalize the dist sub-object to strip volatile fields
// (signatures, npm-signature, attestations, fileCount, unpackedSize)
if let Some(Value::Object(dist_obj)) = version_obj.get_mut("dist") {
dist_obj.retain(|key, _| ALLOWED_DIST_FIELDS.contains(&key.as_str()));
}
}
}
}