Registry client
rusty-js-pm is Cruft's package manager, the crate behind cruft install: it resolves a version specifier into a full dependency graph, fetches and verifies tarballs, and links them into node_modules. It refuses lifecycle scripts, pins tarball URLs to the registry origin, and rejects weak SHA-1-only hashes.
Between the specifier in a package.json and the files on disk, npm install runs a chain of trust decisions: a registry is asked for metadata, a tarball URL comes back, something downloads it, something checks (or does not check) that the bytes match a hash, something unpacks it into a directory. npm makes all of them silently.
rusty-js-pm is the crate that makes those same decisions for cruft install. This page walks the pipeline one trust boundary at a time and marks exactly where Cruft's implementation has a test behind it and where it does not.
Alpha, unaudited.rusty-js-pm(Cruft 0.0.10) is the crate standing in fornpmand a registry client. It has not been externally audited and has near-zero interop testing against a live registry. Everything below is exercised against offline fixtures, not the wild. Do not point it at a hostile registry in production.
Most of a package manager is the resolver
The download is the easy half. Before anything is fetched, a specifier like ^3.0.0 has to become a concrete version, and that version's dependencies have to become versions, recursively, until the graph is closed. That is the resolver, and it is where most of rusty-js-pm lives.
The crate splits cleanly: a resolver (specifier to ResolvedDep), a fetcher and extractor (URL plus integrity to on-disk tree), a linker (staging to flat node_modules), a lockfile codec, a semver engine, an integrity verifier, and a content-addressed store. The default registry is registry.npmjs.org, the same endpoint npm/bun/deno use.
What it deliberately does not do: peer-dependency reconciliation, workspace support, bun.lockb binary-lockfile parity, and a global hardlink cache (the first cut refetches). These are not bugs, they are unshipped scope. They are absent before you rely on parity.
Install does not run lifecycle scripts
When npm install esbuild runs, esbuild's postinstall script executes on the machine and downloads a platform binary. That script is arbitrary code from a stranger, and npm runs it by default.
rusty-js-pm does not run it. Lifecycle scripts (preinstall/install/postinstall) are rejected, not executed. This is the crate's strongest security posture. There is no arbitrary-code-execution surface on the install path because there is no code execution on the install path at all.
It also does not do this silently, which would be its own trap: a postinstall-reliant package (the esbuild class) that materialized incomplete would break later, far from the cause. So the installer extracts the lifecycle-script facts (InstallManifest.skipped_lifecycle_scripts) and the CLI reports each skipped script at install time. The trade is explicit: no supply-chain script execution, and you are told exactly which scripts a package wanted to run so nothing fails invisibly. Root devDependencies and optionalDependencies are also read into the seed set, not just dependencies.
Hashing the tarball does not vouch for its origin
The integrity hash protects against one thing: the bytes changing in transit or on a CDN. It does nothing about which server was asked, if the hash and the URL come from the same place.
The hash itself. Verification prefers SRI sha512 from dist.integrity. npm also ships a legacy dist.shasum, which is hex SHA-1, and SHA-1 is collision-broken. If a package advertised only shasum and no integrity, an attacker who could strip the integrity field would force the install onto the weak SHA-1 path. verify_bytes refuses SHA-1-only metadata by default, returning FetchError::WeakLegacyShasum unless CRUFT_PM_ALLOW_SHA1_SHASUM=1 is set explicitly. The strong hash cannot be downgraded to the weak one.
The mismatch aborts. A mismatched SHA-512 SRI on a real gzipped tarball produces an integrity error, and the staging directory is never created, so the gate cannot quietly become a no-op.
Where the tarball comes from. The expected hash and the tarball URL both come from the same packument dist object. An attacker who controls the packument controls both, and the integrity gate does not help here. So the resolver does not copy dist.tarball verbatim: exact and packument resolution reject dist.tarball unless it is HTTPS and the same authority as the configured registry, returning ResolverError::TarballOrigin. This closes the classic npm tarball-substitution / mirror attack, which integrity alone cannot mitigate because both values share a trust origin. Offline resolver tests cover same-origin accept, port-sensitive origin, cross-origin rejection, and non-HTTPS rejection.
A lockfile is an input, and inputs can be tampered with
On a --frozen install, cruft-lock.json (Cruft's package-lock.json analogue) is an untrusted file on disk that decides what gets fetched, not a trusted record you wrote.
The frozen path used to re-verify the downloaded tarball against the lockfile's own integrity field. A tampered lockfile pointing tarball_url at an attacker host with a matching integrity for the attacker's bytes is internally consistent, so it would pass: the re-verify defended against store/CDN corruption but not against lockfile tampering. Now every lockfile-emitted placement's tarball_url runs through the same HTTPS same-origin predicate before the frozen path trusts it. A tampered cruft-lock.json pointing at https://attacker.invalid, with a matching already-installed package staged, makes pm_install fail with ResolverError::TarballOrigin before the frozen skip path can accept the lockfile as authority.
Hostile metadata errors instead of crashing
A resolver parses attacker-controlled strings: version keys, semver ranges, dependency maps. A parser that unwraps on those is a denial-of-service reachable from any package. Several such hazards are closed:
- Semver now returns
SemverError::Parseinstead of panicking; numeric prereleases use fallible parse, and range-expansion increments use checked arithmetic (a version like1.0.0-99999999999999999999999999used to overflow and panic inparse_pre). - The fetcher enforces
MAX_TARBALL_BYTES(64 MiB) before verify or extract, so a tarball is no longer read whole into memory unbounded. - The resolver bounds cumulative graph edges behind
MAX_RESOLUTION_EDGES(10,000). - The content-addressed store key hashes the integrity string into an
addr-<sha256>component instead of lossy-collapsing base64+///=to_.
The tar extractor is hardened too: extraction blocks absolute paths, .. components, and symlink/hardlink entries. But that defense lives in the PM consumer, not in the underlying rusty-js-tar parser, which returns paths verbatim by design. Any other consumer of that parser inherits no protection. On the real install path, zip-slip / tar-slip is defended.
Limitations
- No live-registry interop. Almost everything above runs against offline fixtures and hermetic tarballs. There is no test corpus exercising a real
registry.npmjs.orground-trip, redirect chain, or malformed-but-real packument. The attacks are closed against their unit tests, not against the wild. - Redirect allow-listing is narrower than origin-pinning. Origin pinning covers the first tarball URL against the registry authority. Full redirect-target allow-listing and stronger registry attestation/signature policy are future hardening, not shipped. There is no SSRF egress control beyond the same-origin tarball check.
- The lockfile is authenticity-pinned by origin, not by signature. A tampered
tarball_urlis rejected, but there is no cryptographic lockfile authenticity; a lockfile whose entries all point at the real registry is trusted as written. - Resolution scope is partial. Peer deps, workspaces,
bun.lockb, and the hardlink cache are absent. npm resolves graphsrusty-js-pmwill not. - Maturity. npm has a decade of adversarial registry exposure, a signature/provenance story, workspaces, and lifecycle execution that real packages depend on.
rusty-js-pmtrades that breadth for a smaller, script-free, origin-pinned surface with unit tests. That is a defensible posture, not a parity claim.