* fix(git-fetcher): bundle transitive deps of bundled dependencies
The packlist `bundleDependencies` pass only spliced in each
directly-bundled package's own files; it never followed that package's
own dependencies, so a bundled dep's transitive (and hoisted)
dependencies were dropped from the published file set.
Replace the per-dependency recursion with npm-bundled's reachability
walk: seed from the root manifest's `bundleDependencies`, then
transitively pull in every reachable package's `dependencies` and
`optionalDependencies`, resolving each via the node module-resolution
walk-up (nested `node_modules/` first, then ancestor `node_modules/`).
The walk-up is what lets a hoisted transitive dep at the root
`node_modules/` be found and spliced in under its real path. A
visited-set keyed on the canonicalised resolved directory keeps a
diamond from being processed twice and stops dependency cycles.
Ports the upstream pnpm test "bundles transitive dependencies of
bundled dependencies (hoisted)" from
releasing/commands/test/publish/pack.ts, plus nested-wins-over-hoisted
and optionalDependencies/devDependencies coverage.
Resolves https://github.com/pnpm/pnpm/issues/12602
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHGos5SaEKKr7pUW4FXga
* docs(git-fetcher): fix stale bundling comment and pin test citation
Address review nits on the transitive-bundling change:
- The comment in `collect_own_files` referred to a `bundleDependencies`
pass "below", but bundling now lives in the separate
`collect_bundled_files`; describe the current structure instead.
- Pin the ported-test citation to a commit permalink, per
pacquet/AGENTS.md's rule that code citations link to a specific SHA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHGos5SaEKKr7pUW4FXga
* fix(git-fetcher): refuse bundled deps whose real path escapes the package
A `node_modules/<name>` entry can be a symlink pointing outside the
package (a sibling directory or an absolute host path). The name still
passes `is_safe_bundle_name` because it is a single safe segment, so the
walk-up resolves it and `collect_own_files` walks the symlink target —
splicing host files into the published set. The git/directory fetchers
import untrusted packages, making this an information-disclosure path.
Check each resolved dependency's canonical (symlink-resolved) path
against the canonical package root and refuse anything that escapes,
falling back to a lexical comparison when canonicalization is
unavailable. Reuses the canonicalize call already used for cycle dedup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHGos5SaEKKr7pUW4FXga
* fix(git-fetcher): fail closed when a bundled dep's real path is unverifiable
The symlink-escape guard fell back to a lexical path check whenever
`fs::canonicalize` returned `None` for the dependency. A symlink under
`node_modules/` is lexically inside the package even when its target
escapes, so that fallback was fail-open. When the root canonicalizes but
the dependency does not, refuse the dependency instead: a genuine
dependency always canonicalizes here, since `resolve_bundled_dependency`
already stat'd its `package.json` through the same path. The lexical
fallback now applies only when the root itself cannot be canonicalized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHGos5SaEKKr7pUW4FXga
* fix(git-fetcher): correct the upstream test citation to a real commit
The previous citation pinned SHA cab1c11c69, which does not exist — it
was taken from the issue body without verification. Point instead at the
in-repo copy of the test at pnpm11/releasing/commands/test/publish/pack.ts,
pinned to a commit that actually contains it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHGos5SaEKKr7pUW4FXga
---------
Co-authored-by: Claude <noreply@anthropic.com>
The benchmark report comment identifies which revision was benchmarked
only via the workflow run, leaving the PR comment detached from the
exact SHA when read later. Add a clickable short-SHA line directly
under the report header so the comment is self-describing.
SHA source is the PR head SHA on pull_request events (stable commit
that survives merge), falling back to github.sha for push and
workflow_dispatch. Display-only — the comment workflow continues to
resolve the PR number from the trusted workflow_run payload, so the
SHA in the fork-controlled artifact cannot redirect where the comment
lands.
* ci: run required checks on merge_group so the merge queue works
The merge queue dispatches a merge_group event against a temporary
gh-readonly-queue/main ref, but neither TS CI (ci.yml) nor Rust CI
(pacquet-ci.yml) listened for it. Their required status checks therefore
never ran in the queue, so every queued PR waited forever on the missing
contexts (e.g. Rust CI / Success never starting).
Add merge_group to both workflows' triggers and force the change
detection true for that event. Forcing matters because TS CI / Compile &
Lint is itself a required context: a skipped job never reports its
context, which would keep the queue waiting, so it has to actually run.
It also makes the queue test the fully merged result, which is the point
of a merge queue. The Rust deny job's nested path filter, which has no
push/PR base to diff against in the queue, runs unconditionally on
merge_group instead.
* ci: gate compile-and-lint and build-pnpr on merge_group explicitly
The merge queue tests the merged commit, so the gating jobs must run on
merge_group. build-pnpr (added by the Windows-sharding work) carries the
same event guard as compile-and-lint and feeds test-smoke/test/test-windows,
so it needs the merge_group clause too — otherwise the queue would skip the
whole test suite.
The e2e test harness gave each test its own store dir but left the
package-metadata cache at pnpm's shared default location. The mock
registry serves fixture packuments without cache-control headers, so once
one test cached a packument, a later test that changed that package's
dist-tag via addDistTag could resolve a stale version from the shared
cache. Set pnpm_config_cache_dir next to the per-test project dir so the
metadata cache is isolated too.
Address CodeRabbit review: instead of duplicating the default listen
address as a string literal in the CLI arg, reference the existing
Config::DEFAULT_LISTEN constant so the CLI default and the config default
cannot drift apart.
Change pnpr's default listen port from 4873 to 7677, which spells PNPR on
a standard telephone keypad (P=7, N=6, P=7, R=7). The old 4873 collides
with verdaccio's default, so a distinct, mnemonic default avoids clashes
when both run side by side.
Updated the DEFAULT_LISTEN constant, the --listen clap default, and the
README. Tests pin the listen address explicitly, so they are unaffected.
Run Windows TypeScript test jobs only on Node.js 22 and split them into three parallel chunks.
Windows TS tests are significantly slower than Linux, and running the full suite on three Node versions spends the extra capacity on duplicate runtime coverage instead of reducing wall clock time. The reusable test workflow now accepts chunk inputs, includes chunk identity in concurrency and artifact names, and keeps Bencher uploads with chunk-specific benchmark names.
Add a CI helper that discovers the selected workspace packages for the same full or affected test scope, shards Jest test files into balanced chunks, runs selected package files with the same Jest Node options and package pretest setup, and writes pnpm-style execution summaries so the pnpm CLI e2e duration extraction still works.
The Release workflow allowed workflow_dispatch from branch refs but only built `@pnpm/exe` artifacts on tag refs. A branch dispatch on main after the 11.9.0 release bump selected the platform artifact packages in the internal publish step, then failed in their prepublishOnly binary verification because the binaries had not been built.
Add a preflight job that accepts only refs/tags/v*.*.*. The real release job depends on that preflight, so non-tag dispatches fail before entering the protected release environment or configuring npm auth.
With non-tag refs rejected up front, the release job can run the full tag release path without per-step tag guards.
Lockfile verification fetches registry metadata to bind each entry's tarball URL
(and to apply the minimumReleaseAge / trust-downgrade policies). On a fetch
failure the abbreviated-metadata fetch collapsed every failure mode — 403, 401,
network error, 5xx — into the same "missing" value as a version genuinely absent
from the metadata, so a transport failure was reported as a tampering-style
ERR_PNPM_TARBALL_URL_MISMATCH.
Rather than mint a dedicated violation code/message for the transport case, the
verifier now propagates the registry's own fetch error and the install aborts
with it:
- runTarballUrlCheck / runAgeCheck / runTrustCheck rethrow the underlying fetch
error rather than folding it into a policy violation. The abbreviated-metadata
age shortcut still swallows the error and falls back to per-version lookups.
- The verification gate captures a thrown error per entry and rethrows the first
after the fan-out settles, so concurrent siblings hitting the same dead
registry do not raise unhandled rejections. A transport failure takes
precedence over collected policy violations: the run never finished, so the
batch is incomplete and the actionable failure is the transport error; a
re-run surfaces any remaining violations once the registry is reachable.
- Credential leak fixed at the source: `@pnpm/error`'s redactUrlCredentials
strips user:pass@ userinfo from a URL in FetchError messages, and the npm
resolver redacts the message, stack, and cause of META_FETCH_FAIL. It is a
single forward scan, not a regex (the input is uncontrolled, so a backtracking
pattern is a ReDoS vector), and it strips up to the last @ in the authority so
a raw @ inside the password cannot leak its tail.
pacquet parity: ResolutionVerification::FetchFailed is added (the runner aborts
with it rather than collecting it as a violation); the gate aborts via
VerifyError::RegistryMetaFetchFailed (ERR_PNPM_META_FETCH_FAIL),
collect_resolution_policy_violations returns a Result, and the pnpr server maps
the abort to a 502. The surfaced fetch error is credential-redacted. The
TARBALL_URL_FETCH_FAILED code, its VerifyError variant and hint, and the
pnpr-client interning are removed. Note: pacquet surfaces its existing fetch
error under ERR_PNPM_META_FETCH_FAIL rather than pnpm's literal ERR_PNPM_FETCH_403
— pacquet's fetch errors do not use the per-status code scheme today, so aligning
that globally is a separate effort. The behavior matches: abort with the
registry's error, never a tampering label, no credential leak.
Fixespnpm/pnpm#12489.
The single git-hosted version branch of getPatchedDependency spread the options
object instead of the parsed dependency, so the result lost `alias` (the package
name) and carried unrelated option fields. Spread `dep`, matching the sibling
branches.
Co-authored-by: Claude <noreply@anthropic.com>
The dlx cache path is <cacheDir>/dlx/<key>/<prepare>/node_modules/.pnpm/
<pkgId>/node_modules/<pkg>. The <key> (64-char sha256 hex) and <prepare>
(<time>-<pid> in hex) segments are dlx overhead on top of pnpm's already-
deep virtual-store layout. For a transitive dep with a long name this tips
the package directory over Windows' MAX_PATH (260) — measured at 259 chars
for @pnpm.e2e/pre-and-postinstall-scripts-example in the dlx e2e test. A
lifecycle script then runs with that directory as its cwd, and CreateProcess
fails to resolve an over-length cwd, which Node surfaces as the confusing
"spawn C:\Windows\system32\cmd.exe ENOENT". Flaky rather than constant
because the <prepare> and temp-dir segments vary in length, straddling 260.
Shorten both dlx-specific segments:
- cache key: createShortHash (32 hex, 128 bits) instead of createHexHash
(64). pacquet already truncated to 32, so this also restores parity.
- prepare dir: encode time and pid in base36 instead of hex.
The benchmark seeded an htpasswd user and sent it as Basic `_auth` on every
request to the pnpr server. pnpr verifies Basic credentials with bcrypt, which
is deliberately slow (~50-100ms), and there is no per-request cache, so every
request paid a fresh bcrypt — doubling the hot-store scenarios on the pnpr
testbed and making the benchmark unrepresentative of how clients actually
authenticate to a pnpr accelerator (npm uses `_authToken`; `_auth`/Basic is
legacy).
Log in as the seeded user once at server startup to mint a bearer token, and
write it as `_authToken` in the client's .npmrc. pnpr resolves Bearer tokens
with a SHA-256 hashmap lookup instead of bcrypt, so the per-request auth cost
disappears and the benchmark reflects a realistic, token-authenticated client.
The single login bcrypt runs against the direct port before the latency proxy,
outside the measured install loop.
Port pnpm's build-policy commands to the Rust CLI: `ignored-builds`,
`approve-builds`, and the `rebuild` flow they build on.
The rebuild flow reuses the frozen-install pipeline: a `RebuildOptions`
selection threads from `Install::run_rebuild` through `InstallFrozenLockfile`
and `run_build_phase` into `BuildModules`, where selected packages bypass the
`is_built` side-effects gate and the "up to date" short-circuit is skipped.
The allow-build policy is still honored, matching `pnpm rebuild`.
`approve-builds` re-loads config after writing settings so the rebuild's
policy reflects the new `allowBuilds`; the frozen path then rewrites
`.modules.yaml` to pnpm's exact end state (decided entries removed, undecided
kept). Adds a boolean-aware `set_allow_builds` writer to
`workspace-manifest-writer` and an `allow_build_key_from_ignored_build`
helper (port of `@pnpm/building.policy` key derivation).
Tested with unit tests for the helper and writer plus mocked-registry
integration tests covering listing, approve, deny, and rebuild.
Pacquet-side port of existing TypeScript commands; no changeset needed.
The proxied-tarball integrity fix (pnpm/pnpm#12570, GHSA-5f9g-98vq-2jxw)
started reconstructing each rewritten dist.tarball filename from its
version key (`<name>-<version>.tgz`). That assumes upstream tarball names
are always canonical, which breaks packages like esprima-fb whose real
npm tarball is `esprima-fb-3001.0001.0000-dev-harmony-fb.tgz` for version
`3001.1.0-dev-harmony-fb`: the rewritten URL no longer matched what npm
hosts, so the client recorded the wrong lockfile URL and the proxied
fetch 404'd.
Preserve the upstream basename verbatim in the rewrite again, and resolve
a tarball request's version and dist.integrity by matching the requested
filename against each version's dist.tarball basename instead of parsing
the version out of the filename. OSV screening re-runs against the
resolved version when it differs from the filename-derived one.
The GHSA protection is unchanged: served bytes are still verified against
the SRI declared by the version that owns that tarball name, so a
preserved name cannot smuggle in bytes of another provenance. Tests that
encoded the canonical-reconstruction behavior are updated to assert
basename preservation, with new coverage for non-canonical names.
Also fix a pre-existing compile break in the pnpr server test target:
UserStore::in_memory gained a MaxUsers argument (pnpm/pnpm#12581) but one
test call site was not updated.
pnpr's resolver endpoint could previously accept anonymous resolve jobs and start outbound dependency-resolution work before checking any caller identity. Mount auth middleware on /-/pnpr/v0/resolve so Basic/Bearer credentials from the HTTP Authorization header are verified before Axum buffers or parses the body. Reject duplicate or non-text Authorization headers rather than picking one.
The resolver now loads configured auth state at startup, including resolver-only deployments, so configured htpasswd/token/SQL backends are honored. New-user registration defaults to disabled unless auth.htpasswd.max_users is explicitly configured, preventing anonymous callers from creating credentials on a fresh server.
Update pnpm and pacquet resolver fixtures to provision and send pnpr credentials, and add regression coverage proving anonymous Git resolve requests do not trigger egress while authenticated resolver requests still work.
Enforce pnpr's unpublish package policy independently from publish authorization.
pnpr already parsed an `unpublish` field from Verdaccio-style package rules, but destructive registry writes still checked only the publish permission. That let a caller who was allowed to publish but excluded from unpublish delete packages, delete tarballs, or replace a packument.
The policy model now carries an effective unpublish access list. Missing, null, or empty unpublish values deny destructive writes. The bundled config and README examples set unpublish explicitly where authenticated users should keep destructive-write access.
Package deletion and tarball deletion now check Action::Unpublish. Full packument replacement checks both publish and unpublish because it can rewrite package contents while also serving the partial-unpublish flow.
Regression coverage exercises missing and explicit deny behavior, publish-authorized package deletion denial, publish-authorized tarball deletion denial, and packument replacement requiring both permissions.
pnpr allowed anonymous self-registration by default. An omitted
`auth.htpasswd.max_users` mapped to `Unlimited`, and combined with the
default `$authenticated` publish policy on `**`, an anonymous client
could self-register, obtain a token, and publish or overwrite packages.
Make registration opt-in:
- A missing `max_users` now disables registration (the enum default and
the YAML mapping are `Disabled`); there is no YAML spelling for
"unlimited". Operators set an explicit positive cap to allow sign-ups.
- The in-memory user store (no `auth.htpasswd.file`) honored no cap and
ignored config; it now takes the cap from `AuthState::load`.
- `router()`/`try_router()` honor `auth.htpasswd.max_users` via
`AuthState::in_memory_with_max_users` instead of hardcoding `Unlimited`,
so library embedders inherit the opt-in default.
- The bundled fallback `config.yaml` ships locked down (`max_users: -1`);
a `--max-users` CLI override lets tests/benchmarks opt in. pnpm's e2e
registry passes `--max-users 1000`.
Test fixtures opt in explicitly where they create accounts.
Addresses GHSA-fg2v-jp32-w784 (CAND-PNPM-029).
pnpr persisted and surfaced each bearer token's readonly and cidr_whitelist
restrictions but never applied them during authorization, so a token marked
read-only or pinned to a network could still publish and mutate packages from
anywhere (GHSA-rp44-v426-6m56 / CAND-PNPM-034).
TokenBackend::lookup_record (a default method reusing find_by_key) resolves
the full record, and an axum middleware authenticates every request once:
it resolves the caller, enforces the bearer token's read-only / CIDR
restrictions before a write handler buffers its body, and stashes the
resolved Identity in request extensions for handlers to read via an
AuthedCaller extractor.
- Read-only tokens are limited to non-mutating methods (PUT/DELETE -> 403).
- CIDR-pinned tokens are checked against the real socket peer (ConnectInfo),
never a forwarding header; CIDR matching handles IPv4/IPv6, /0../32 and /128,
bare hosts, and IPv4-mapped IPv6 peers. Malformed entries and an unavailable
peer fail closed.
Resolving the caller once removes the second token lookup authed requests
previously paid (a round-trip on the remote libsql backend) and the
policy/identity race two independent lookups allowed. Basic-auth, anonymous,
and unknown tokens pass through unchanged and stay subject to the per-package
access policy; a store failure surfaces as a 5xx. pnpr-only; no change to the
pnpm CLI or pacquet.
Normalize blank and whitespace-only runtime selectors consistently in pnpm and pacquet.
The runtime command can produce `runtime:` when no version is provided, and hand-edited manifests may contain whitespace-only selectors. Treat those cases as `latest` in the TypeScript runtime resolvers, the pacquet runtime resolvers, and the manifest conversion helpers.
Also make manifest writeback reject malformed dependency fields before pruning managed runtime entries. This prevents a non-object dependency field from being interpreted as a removed runtime dependency and causing silent data loss during save.
Windows can surface opening <storage>/foo/foo-1.0.0.tgz when <storage>/foo is a regular file as NotFound rather than NotADirectory. The tarball handler treated NotFound as a hosted-store miss, so it fetched from the configured upstream even though the hosted store was in a faulted state.
Re-check the package path before classifying NotFound as a miss. Missing package directories and real directories still behave as cache misses; a non-directory package path returns a storage error, so the tarball handler fails closed before proxying. Add a storage regression covering this classification and keep the existing integration test as the endpoint guard.
Reject unsafe pnpr usernames before auth storage lookup or persistence.
The htpasswd backend serializes users as line-oriented username:hash records, so usernames containing structural characters must never reach the persistence boundary. Move shared username validation ahead of existing-user lookup, apply the same Basic-auth guard, and reject invalid usernames while parsing persisted htpasswd files.
Apply the same contract to the libsql and SQL auth backends so configured auth storage cannot authenticate username shapes that the local htpasswd backend rejects.
Add unit and persistence regression coverage for delimiter, line break, comment-marker, control-character, and edge-whitespace usernames.
* fix(dlx): make failed-install cache cleanup best-effort
On Windows, `pnpm dlx` could fail with a spurious "EBUSY: resource busy
or locked, rmdir" error. When an install into the dlx cache failed, the
catch block removed the partially-populated prepare dir with
`fs.promises.rm(cachedDir, { recursive: true, force: true })`. That call
has no retries, so it died on the same lingering Windows handle (a
just-run install script's child process, or antivirus scanning freshly
written files) and threw EBUSY — which then replaced and masked the
original install error.
Make the cleanup best-effort: swallow its failure so the original error
always surfaces, and add `maxRetries`/`retryDelay` so the removal itself
succeeds once the transient lock clears. A leftover prepare dir is
harmless — it has a unique name and findCache only trusts the `pkg`
symlink.
The pacquet port already removes the prepare dir best-effort (`let _ =
fs::remove_dir_all(...)`) and returns the original error, so the
user-visible behavior already matches; only the (non-observable) retry
is absent there.
* fix(dlx): log dlx cache cleanup failures instead of swallowing them
Catch the best-effort cache cleanup with a narrow handler that logs the
failure via logger.warn (mirroring tryRemovePkg in modules-cleaner's
prune) instead of a blanket `.catch(() => {})`. The original install
error is still the one rethrown, so cleanup failures stay visible without
ever masking the real cause.
Port the runtime command to pacquet by adding a runtime/rt CLI entry and routing runtime set <name> <version> through the existing add pipeline as <name>@runtime:<version>.
The command matches pnpm's local save behavior: devEngines.runtime is the default, --save-prod targets engines.runtime, and --save-dev wins when both flags are present. The global path is parsed but rejected because pacquet does not yet support global package management, matching the existing update/outdated global boundary.
Add the manifest writer conversion that folds runtime:<version> dependencies back into devEngines.runtime and engines.runtime before saving, complementing the existing read-time conversion used by install and lockfile checks.
Implemented the `cache` command for pacquet with support for `list`, `list-registries`, `view`, and `delete`. This ensures the pacquet port has parity with the TypeScript CLI for cache management logic.
Related to pnpm/pnpm#11633.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Bind proxied tarball requests to the selected packument version.
Require supported dist.integrity before serving upstream or cached tarballs.
Verify cache hits before response construction.
Fail closed on a hosted-store fault instead of falling through to the
upstream proxy, so an I/O error in the authoritative store can never serve
bytes of a different provenance for the same package name.
Delete invalid cache entries and promote upstream bytes only after SRI verification.
For cache:false uplinks, verify into a temp file and stream the same open
handle (rewound to the start) instead of dropping and reopening it by path,
closing the TOCTOU window where an attacker-writable cache directory could
swap the verified bytes before they are served; remove the temp file after
streaming.
Harden publish attachment SRI parsing for missing or unsupported integrity.
Addresses GHSA-5f9g-98vq-2jxw.
Some registries generate tarballs on demand and cannot list an integrity in
their packument. pnpm then wrote integrity-less lockfile entries on the first
install and failed the next one with ERR_PNPM_MISSING_TARBALL_INTEGRITY, unable
to install from its own lockfile.
Compute the missing integrity from the downloaded bytes and write it into the
resolution before the lockfile is built:
- Add an optional `resolutionNeedsFetch` contract to the fetcher API (backward
compatible, since custom fetchers come from hooks). The remote-tarball fetcher
reports it when a resolution lacks integrity; the picked fetcher's signal flows
through PackageResponse -> ResolvedPackage so nothing re-derives it.
- The package requester downloads such tarballs (including under --lockfile-only /
skipFetch / not-installable) and fills the computed integrity onto the resolution
via the already-running `fetching` promise, so dependency resolution isn't
blocked. The deps-resolver awaits only the flagged entries before updateLockfile,
because the integrity feeds the global virtual-store paths.
- Move read-side enforcement into the npm resolver's lockfile verifier
(MISSING_TARBALL_INTEGRITY): reject a registry/http(s) tarball entry whose
integrity is missing/empty/non-string, fail-closed, before the URL-keyed and
semver short-circuits. Drop the earlier read-side auto-heal (a missing-field
bypass). Harden against tampered lockfiles (non-string tarball/integrity).
- Reuse the fetcher picked during resolution on the fetch path instead of running
pickFetcher (and a custom fetcher's async canFetch) twice per package.
Mirrored in pacquet: PrefetchingResolver computes the integrity for integrity-less
tarball resolutions during resolution (FetchTarballForResolution::run), deduped per
URL with a singleflight cache.
Closespnpm/pnpm#12145.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Implements the `cat-index` command for the Rust pacquet CLI. It mirrors the exact behavior of pnpm's `cat-index`.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Load the local OSV index for the pnpr registry surface when osv is enabled, not just for the resolver surface.
Registry responses now apply the index at serve time. Full and abbreviated packuments, version manifests, and dist-tag responses omit vulnerable versions without mutating cached upstream bytes. Direct version-manifest requests for filtered versions return 404, and dist-tags pointing at filtered versions are removed from served responses.
Tarball requests parse the version from the validated npm tarball filename and return 403 for vulnerable versions after the normal access policy check, before reading from cache or fetching upstream. This keeps private package metadata behind the existing auth boundary.
The tests cover proxied and cached packuments, direct version manifests, dist-tags, tarballs before upstream fetch, tarballs from cache, registry-only OSV loading, and private-package access ordering.
Closes https://github.com/pnpm/pnpm/issues/12561.
The pnpr resolver's two POST endpoints were the only proprietary routes
served outside npm's reserved /-/ namespace, where they overlapped the
package path space (a package literally named `v1`). Move them under the
/-/pnpr namespace alongside the existing capability handshake:
POST /v1/resolve -> POST /-/pnpr/v0/resolve
POST /v1/verify-lockfile -> POST /-/pnpr/v0/verify-lockfile
The GET /-/pnpr handshake now advertises protocol version 0 to match, and
the Rust client's PROTOCOL_VERSION drops to 0. Keeping every pnpr route in
the reserved namespace removes the package-collision concern and lets the
disabled-resolver branch stop special-casing the old npm-compatible GET
paths.
No backward compatibility is kept: the resolver protocol is not yet
released. Server and both clients (Rust pacquet-pnpr-client and the
TypeScript `@pnpm/pnpr.client`) change together.
pnpr now exposes its registry and resolver surfaces as independent,
config-toggleable features (per-surface `registry:` / `resolver:` blocks,
both enabled by default; CLI `--disable-registry` / `--disable-resolver`).
This supports running a stateless resolver-only tier in front of an
existing registry, or a registry with no server-side resolution. At least
one surface must stay enabled, validated at config load and after CLI
overrides.
The resolver paths overlap the registry's catch-all param routes, so when
the resolver is disabled they are registered to an explicit 404 stub —
otherwise a capability probe (GET /-/pnpr) would fall through to the
registry and be proxied upstream, surfacing a 502 where clients expect the
"no resolver" 404.
OSV stays a top-level (cross-cutting) setting rather than nesting under a
surface. Registry-side OSV screening is not yet implemented; tracked in
pnpm/pnpm#12561.
Updates the minimumReleaseAgeExcludes output to combine entries per module, matching rec in docs. (Closespnpm/pnpm#12534).
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Qodo re-runs its full review on every push (`push_commands`), which on a
long-running PR reposts low-severity perf/style nits as fresh inline
threads each commit. Keep the per-push review (it does catch issues
introduced in later commits) but raise `inline_comments_severity_threshold`
from 2 to 3 so only higher-severity findings get standalone inline
threads. `comments_location_policy = "both"` is unchanged, so every
finding still appears in the review summary — nothing is dropped, it's
just not re-threaded inline on each push.
Written by an agent (Claude Code, claude-opus-4-8).
`pacquet add <name>@owner/repo#sha` (and GitHub / GitLab / Bitbucket URL
forms) previously saved the request verbatim. Save the resolver's shortcut
form instead — `github:owner/repo#sha` — matching pnpm's saved
normalizedBareSpecifier. The normalization runs only in the non-registry
fallback (a git shorthand isn't claimed by the npm resolver, so no registry
fetch happens), leaving registry ranges, npm: aliases, file:, link:,
workspace:, and tarball URLs untouched.
Related to #12548.
Add feature-gated pnpr auth backends so deployments can build with libsql, PostgreSQL, or MySQL support instead of locking the registry to one database driver.
The auth state still resolves to the existing UserBackend and TokenBackend trait objects. Configuration now accepts backend.libsql, backend.postgres/backend.postgresql, or backend.mysql and rejects selecting multiple shared databases.
PostgreSQL and MySQL use a shared SQLx-backed auth implementation with driver-specific placeholder syntax. The shared auth schema uses common column types, and pnpr avoids SQLite-only upsert syntax in auth and verdict-cache writes.
Port pnpm's post-resolution manifest writer into the pacquet
package-manager crate as two modules mirroring pnpm's crate boundaries:
- update_project_manifest_object: port of @pnpm/pkg-manifest.utils'
updateProjectManifestObject (field upsert with cross-field delete,
peer-range derivation, guessDependencyType, empty-spec preservation).
- update_project_manifest: port of updateProjectManifest, matching
resolved direct deps to wanted deps by alias (with a github: shorthand
fallback) instead of by position, including the fix from #11373.
Both modules ship with the ported pnpm unit tests (14 total). They are
not yet wired into add/update; that integration (surfacing resolved
direct deps from Install and threading the catalog
userSpecifiedBareSpecifier) is tracked in #12548.
Related to #11267, #11373. Part of #12548.
Adds `pnpm cat-file` functionality to the `pacquet` CLI. This extracts the
base64-encoded file hash from an integrity string (e.g. `sha512-...`),
decodes it, and reads the corresponding file from the CAFS store directory
at `files/XX/YYYY...`.
Includes a new e2e test verifying correct lookup and read operations.
updateProjectManifest previously reconstructed the pairing between each
resolved direct dependency and the wanted dependency it came from — first by
alias, then by specifier shape, then by array position. Every one of those is
fragile: a failed optional dependency drops out of directDependencies and
shifts a positional pairing (#11267), and an aliasless selector that resolves
to an alias already in the manifest matches the stale aliased entry instead of
the request.
Carry the originating wanted dependency on each resolved direct dependency
(PkgAddressOrLinkBase.wantedDependency -> ResolvedDirectDependency.wantedDependency),
set where the resolver builds the result and already has it in scope, and have
updateProjectManifest read rdd.wantedDependency directly. This removes all the
alias/position/specifier heuristics (and the earlier normalizeGitHubBareSpecifier
band-aid) and fixes both correctness gaps structurally.
Adds unit tests for the failed-optional (#11267), alias-collision re-add, and
aliasless-optional-failure cases.
Closes#11267
---------
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@example.invalid>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Port the `restart` command from
<https://github.com/pnpm/pnpm/blob/d4a2b0364c/exec/commands/src/restart.ts>
to the Rust pacquet CLI. `restart` delegates to `RunArgs::run()` three
times (stop → restart → start), matching the TypeScript handler that
calls the `run` handler sequentially for each script name.
Add a standalone `stop` command alongside the existing `start` and
`test` shortcuts. `stop` silently succeeds when no script is defined
(like `start` with its `node server.js` fallback), matching npm
convention.
Both commands forward trailing args and the `--if-present` flag to
each script. Lifecycle hooks (`pre<name>`/`post<name>`) apply when
`enablePrePostScripts` is set, because each step runs through the
full `RunArgs` pipeline.
Tests cover:
- Sequential execution order (stop → restart → start)
- Failure propagation (stop fails → restart/start skipped)
- `--if-present` skipping missing scripts
- Args forwarding to each script
- Standalone `stop` with and without a script
- Regression guard for existing `start` command
Related: pnpm/pnpm#11633
Ports the create command from pnpm's TypeScript implementation
(@pnpm/exec.commands/create) to Rust. The handler converts the
user-provided name to a create-* package name via
convertToCreateName and delegates to the existing dlx
infrastructure — matching pnpm's exact design where create.ts
calls dlx.handler().
Implements the Stage 3 'Script & process' item from
pnpm/pnpm#11633 (pacquet roadmap).
Changes:
- Add pacquet/crates/cli/src/cli_args/create.rs: command handler,
name-conversion algorithm (13 unit tests covering all npm naming
conventions), and error for missing args.
- Wire Create(CreateArgs) into CliCommand dispatch.
- Add 5 integration tests (error handling, happy path, args
forwarding, --allow-build, -c/--shell-mode).
- Add create-touch-file-one-bin fixture to pnpr mock registry.
Add local OSV npm database support to pnpr. When enabled, pnpr loads an OSV npm dump (an `all.zip` or an extracted JSON directory) from disk at startup and fails before serving requests if the configured database is missing, not a regular file, empty of npm advisories, or otherwise invalid. Resolution then uses the in-memory index rather than querying OSV during package selection. Package names are matched case-insensitively, OSV range events are sorted before evaluation, and per-record reads are size-bounded.
Add a resolver-time package version guard to pacquet's npm resolver. The guard can reject a concrete package version, after which the picker filters that version out of the packument and tries the normal selection flow again. pnpr uses this hook to avoid vulnerable versions while preserving existing semver selection semantics. When every matching version is rejected, the resolver returns a distinct AllVersionsBlockedError rather than reporting the spec as unsupported.
Check frozen, cached, verified, and freshly produced lockfiles against the local OSV index, gating tarball entries on the tarball URL rather than the tamper-prone gitHosted flag. The pnpr lockfile-verdict cache records a content-based OSV database fingerprint in its policy snapshot, so a changed vulnerability database invalidates previous cached verification passes and forces rechecking under the new data.
Add fallible try_router / try_router_with_auth constructors so callers that build the router directly can handle an invalid OSV database recoverably instead of panicking.
Add tests for OSV range matching (including out-of-order events), exact-version and case-insensitive matching, withdrawn handling, zip and directory loading, empty/non-regular-file rejection, pnpr config parsing, guarded re-pick and all-versions-blocked behavior, and tarball OSV-checkability.
Reads `.modules.yaml` unconditionally before dispatching the install path. If the layout settings (nodeLinker, hoist patterns, etc.) differ from the currently active config, it purges `node_modules` completely. This mirrors upstream's `validateModules` prune side-effects and prevents leaving behind stale dependencies or symlinks when layout settings change.
claimChildrenResolution let a non-owner occurrence of a shared package reuse the
owner's missingPeersOfChildren when `existing.owner.depth >= currentDepth ||
existing.missingPeersOfChildren.resolved`. The second clause made reuse depend on
whether the owner's resolution had settled by claim time. Under concurrent
resolution that timing varies run to run, so a deeper consumer inherited the
owner's missing peers on some runs but not others — flipping an optional
transitive peer (e.g. `@babel/core`, reached via styled-jsx) in and out of a
package's resolved peer suffix and churning the lockfile, with intermittent
`pnpm dedupe --check` failures in CI.
Drop the `.resolved` clause: reuse only when the owner is at the same or a deeper
depth, a function of the dependency graph rather than completion order.
The `.resolved` flag was introduced in pnpm/pnpm#5467 (closing pnpm/pnpm#5454) to
avoid a deadlock where, with auto-install-peers in a workspace, a shared package
awaited its own not-yet-settled missingPeersOfChildren promise. Removing the
clause is strictly more conservative — a shallower owner's promise is never
reused, settled or not, so no unsettled promise is ever awaited and the deadlock
cannot return. The pnpm/pnpm#5454 regression test still passes, as do the peer,
dedupe, and cyclic suites. The deterministic owner selection from pnpm/pnpm#12362
made shared-package ownership order-independent but had moved this timing branch
in verbatim without neutralizing it.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The TypeScript stack was relocated under pnpm11/ in pnpm/pnpm#12537, which
moved copy-artifacts.ts to pnpm11/__utils__/scripts/src/. Its
`import.meta.dirname` + '../../..' now resolves to the pnpm11/ directory
instead of the repository root, so `pn copy-artifacts` wrote the release
tarballs to pnpm11/dist.
The Release workflow attests (`subject-path: 'dist/*'`) and uploads
(`files: dist/*`) from the repo-root dist, so a tagged release would have
produced a GitHub Release with no binary assets and a failing provenance
attestation, even though the npm publishes (filtered by package name)
still succeeded.
Resolve repoRoot to the actual repository root again and point the
artifact source directories at their new pnpm11/pnpm/ locations.
The TypeScript pnpm CLI was relocated from `pnpm/` to `pnpm11/pnpm/` in
pnpm/pnpm#12537, but the "Extract pnpm CLI e2e test duration" step still
passed `--package-dir pnpm`. That path no longer exists, so the
exec-summary lookup found no entry and the step exited 1, failing the
full TS CI test job on main.
Point `--package-dir` at the package's new location, `pnpm11/pnpm`.