Commit Graph
47 Commits
Author SHA1 Message Date
Zoltan Kochan 3bca7ffd2b fix(pnpr): record a token's last use off the request path, at most once a minute (#17266)
Every token-authenticated request awaited an UPDATE of last_used_at before
the handler ran. With the libsql backend that is a round trip to the primary
even when an embedded replica answers the lookup locally, so a replica
removed almost none of the latency for authenticated traffic.

LastUsedWriter in pnpr-auth now owns the write. It skips the write while the
stored time, which the authenticating lookup already read, is under a minute
old; claims the interval per token in memory so a burst of concurrent
requests from one token writes once even before the first write lands or the
replica syncs it back; and runs the write in a spawned task the request never
waits for. Both middleware sites, bearer tokens and OCI tokens' parents, go
through it. GET /-/npm/v1/tokens therefore reports a last-used time accurate
to within a minute.

Related to https://github.com/pnpm/tasks/issues/194
2026-10-23 09:45:07 +02:00
Zoltan Kochan 7fb2bfab4a docs(pnpr): correct configuration keys, defaults, and commands (#17245)
Part of https://github.com/pnpm/tasks/issues/191.

Wrong claims:
- cli: drop the nonexistent `add-user --password` flag and document the
  terminal/stdin password read; `oci-gc --min-age` and `cache-gc --max-age`
  are the flags (the `-secs` forms are aliases); `--from` defaults to the
  `storage` key, not an internal field; `info` and `http` log the same
  access records; only global and server options have PNPR_* variables.
- configuration: `packumentTtl` defaults to 5m; the minimal config now
  sets `auth.htpasswd.file`, and the auth section says accounts live in
  memory without it or a backend; grouped hosted registries default their
  org to `<ecosystem>~<name>`; the handshake is served for any surface;
  the pipeline surface also satisfies "something must be served"; add
  `package.update_maintainers` to the audit list.
- tls-and-reverse-proxy: remove the invalid `publicUrl:` YAML key and the
  nonexistent `rate_limiting` setting.
- backup-and-restore: `pnpr verify` hashes by default (`--check-hashes`
  does not exist, `--skip-hashes` skips only the hash
  comparison); `migrate-storage
  --verify` compares bytes.
- webhooks: audit events do not cover publishes or token creation; the
  example uses a real action and the `actor` field.
- cli: the `RUST_LOG` example keeps `pnpr::audit=info`.
- install-acceleration: the `--public-url` host is exempt from the
  private-address refusal.
- shared-side-effects-cache: `pnpm pipeline` publishes workspace-task
  artifacts.

Missing facts: `routes.allowedPrivateNetworks`, the upstream `search` key
and canonical `maxAge`, built-in `npm.jsr.io` route, relative path
resolution, `log` defaults and `type`, `${VAR:-default}` substitution and
the full secret-field list, `oci` defaults, `artifacts.quota` bounds,
`migrate-storage` destination rules, `--osv false`, `--packument-ttl`
default, S3 `forcePathStyle`/`allowHttp` defaults, grouped org layout,
OIDC key table, container image section, `backend:` ignoring the htpasswd
and token files.

Duplicates and links: health probe bodies, reload procedure, and the
resolver allowlist now link to their one home; broken `#secret` anchor
fixed; links into the pnpm docs use absolute pnpm.io URLs.

Stale history: drop "pnpr v1" and the per-paragraph "Since alpha.N" /
"Since pnpm 12.x" prefixes, and the contributor-only extensibility text.

Also corrects the `as_filter_directive` doc comment, which claimed `http`
quiets the rest of the crate.
2026-10-23 00:11:00 +02:00
Zoltan Kochan 26047ac598 fix(pnpr): run libsql auth operations on a pool of connections (#17237)
The libsql backend ran every operation on one shared connection. On a
remote database that connection is a Hrana stream, and each response
carries the baton for the stream's next request. An operation dropped
while its request was in flight (a client that disconnected, the auth
timeout) never received the new baton, so the connection kept the spent
one and the server refused every later request with "Received a baton
that has already been used". From then on every login and every
authenticated request failed with a 500 until pnpr restarted. One shared
stream also put concurrent operations into each other's transactions.

Operations now check a connection out of a pool and run alone on it. A
connection returns to the pool only after its operation succeeded outside
a transaction; any other is closed and replaced on a later checkout. This
is what sqlx does for the PostgreSQL and MySQL backends. Pooled
connections keep their HTTP client, so reuse costs no new TCP or TLS
handshake, which a connection per operation would have paid on every
token lookup. backend.libsql.maxConnections sizes the pool, defaulting to
10 like sqlx.
2026-10-22 22:42:11 +02:00
Zoltan KochanandClaude Opus 5.5 6f95957063 docs: lint prose with Vale (#17238)
* docs: lint prose with Vale

Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.

Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
  words, "please", "note that", hedges, puffery, optional plurals,
  exclamation marks, ellipses, curly quotes, generic link text, product
  spellings, sentence-case headings, repeated words, double spaces,
  implementation history in comments; in changesets, bold or italic
  emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
  "so that" tails in changesets.
- suggestion: passive voice.

.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).

The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.

Spelling stays with cspell, which checks identifiers that Vale does not
see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: keep prose lint from flattening meaning

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:48:09 +02:00
Zoltan KochanandClaude Opus 5.5 cb496f5427 docs(pnpr): get zstd through Cloudflare's edge (#17230)
Cloudflare removes zstd from Accept-Encoding before a Worker runs, and
from cf.clientAcceptEncoding too, so pnpr behind the recipe's Worker never
sent zstd. The Worker now asks pnpr for "zstd, gzip", the value pnpm sends
on archive requests, and returns the coded body with encodeBody "manual".
Cloudflare passes a zstd body to a client that accepts it and decodes it
for one that does not. Verified on a trial deployment: a tar for pnpm
arrives zstd-coded, metadata for a gzip client is still gzip-coded, and a
gzip-only client gets the tar uncompressed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:40:28 +02:00
Zoltan Kochan 79b4dd2d78 fix(pnpr): follow a Turso Cloud primary from the embedded replica (#17225)
Enable libsql's `sync` feature so the embedded replica
(`backend.libsql.replicaPath`) can follow a Turso Cloud primary.

The replica was built with libsql's `replication` feature only, whose
protocol Turso Cloud stopped serving: pnpr failed to start with
"Write delegation: Unimplemented ... deprecated version of sync". With
the `sync` feature, `Builder::new_remote_replica` defaults to
`SyncProtocol::Auto`: it probes the primary's `/info` route and uses the
newer sync protocol (bootstrap from `/export`, frames from `/sync`,
writes over Hrana) when the primary answers, and keeps the replication
protocol otherwise. A self-hosted `sqld` answers 404 on `/info`, so it
is unaffected.

The new test runs a mock primary that serves those routes in front of a
local libsql database and checks that the replica bootstraps from the
export, reads locally, and writes to the primary. Without the feature
it fails the way the issue describes.

`connect` also creates the replica's parent directory, as the local
stores do for their files, and with `syncInterval: 0` it runs the
initial sync under the startup deadline, since libsql would otherwise
bootstrap inside `Database::connect` with no bound.

Closes pnpm/tasks#183.
2026-10-22 19:19:52 +02:00
Zoltan Kochan 81e6e9ec1a docs: say where serverType pnpr applies and what a tar main base costs (#17227)
The registries page presented the pnpr layout as the layout of every pnpr
registry, but a pnpr .tgz base advertises the npm layout, which the
undeclared default already reconstructs; serverType: pnpr belongs on a tar
base. The pnpr configuration page now says tarballs: tar may go on the main
router and what that costs other clients. The Cloudflare recipe's Worker
waits through a cold start instead of failing it, and the page notes that
the edge rewrites Accept-Encoding.
2026-10-22 18:53:48 +02:00
Zoltan Kochan f880d91124 fix(pnpr): recognise a unique violation reported by Turso (#17223)
The libsql auth backend classified a duplicate row only through SQLite
result codes. Turso's remote protocol reports it as a Hrana stream error
whose message carries the code name, so the counter seed on a second
start against the same database failed instead of being tolerated, and
pnpr exited at startup. A taken username reported the same way answered
500 instead of the login or name-taken path.

Parse the Hrana message the way the lock-conflict check already does.
Related to https://github.com/pnpm/tasks/issues/183.
2026-10-22 17:36:24 +02:00
Zoltan Kochan e2a11f5611 perf(install): precalculate package bins and shebang runtimes (#17186)
Precalculate package binary entrypoints and shebang runtimes during tar
streaming in pnpr (dist.pnpr.bins) and during cold tarball extraction in
pnpm so .bin command shims can be linked without reading package.json,
walking directories.bin, or opening bin scripts on disk.

- Add PackageBin, ScriptRuntime, parse_shebang_from_bytes,
  resolve_package_bins, and verify_package_bins in pnpm-package-manifest
  with strict path normalization and shell-safe runtime validation.
- Extend inspect_tar in pnpm-tarball to compute bins and shebang runtimes
  in the same streaming pass as requiresBuild when requiresBuild is false.
- Emit dist.pnpr.bins in pnpr packument responses and cache it in
  pnpr-storage TarMetadataCache.
- Propagate cold-extracted manifests and precalculated bins through
  CreateVirtualStoreOutput into LinkVirtualStoreBins and direct-dep bin
  linking, and verify warm-cache dist.pnpr.bins against the store index
  manifest and CAS file list before skipping shebang reads.

Closes pnpm/tasks#178
2026-10-21 11:42:03 +02:00
Zoltan Kochan 1e2a9a0424 feat: surface tarball-verified requiresBuild from pnpr and use it during install (#17164)
Compute and persist per-tarball requiresBuild in pnpr when publishing or
decoding upstream tarballs, project it onto served packuments and
manifests under versions[v].dist.pnpr.requiresBuild and
POST /-/pnpr/v0/resolve package frames, and consume it in pnpm v12 to:

- overlap remote side-effects artifact resolution with cold tarball
  downloads when cold snapshots already have a verified requiresBuild,
- skip unnecessary node --version probes and BuildModules passes when no
  package requires a build, and
- skip redundant background extraction scheduling and CAS package.json
  reads during early materialization.

Closes https://github.com/pnpm/tasks/issues/177.

Written by an agent (Antigravity, Gemini 4 Argon).
2026-10-20 21:42:54 +02:00
Zoltan Kochan e01508f253 feat(pnpr): harden secrets, derive subkeys, and validate ecosystems (#17126)
Complete Section B deliverables (B4, B5, B6, B7) for pnpr production readiness.

- B5: Fail fast at startup when secret-bearing configuration fields (secret,
  secrets.cache, secrets.oci, s3.secretAccessKey, auth.scim.token,
  clientSecret, authToken, or upstream tokens) contain unresolved \${VAR}
  references rather than falling back to empty strings or per-process keys.
  Support independent configuration of secrets.cache and secrets.oci, and
  derive domain-separated HMAC-SHA256 subkeys from root secret.
- B4: Enforce per-ecosystem validation rules rejecting \`teamsManagedBy: api\`
  on non-npm registries and \`unpublish\` rules on Cargo and PyPI registries.
- B6: Set \`publish = false\` on \`pnpr/crates/pnpr/Cargo.toml\` to guard against
  accidental crates.io publication.
- B7: Document the v1 extensibility model: compile-time trait implementations
  (\`UserBackend\`, \`TokenBackend\`), no dynamic plugin runtime.

Relates to pnpm/tasks#131.
2026-10-19 15:19:33 +02:00
Zoltan Kochan 85cd31b9e5 feat(pnpr): settle Section A production readiness requirements (#17112)
Complete production-readiness requirements for pnpr Section A.

- Persisted-document format versions: introduce formatVersion to packuments,
  OCI images, crate documents, and state journals, rejecting documents written
  by newer format versions.
- CLI surface grammar: support `pnpr serve` subcommand, and add parse_cli_interval
  for duration units (s/m/h/d) on --packument-ttl, --min-age, and --max-age.
- Strict config validation: reject unknown fields, disallow removed configuration
  blocks, and normalize field naming across config and CLI flags.
- Client protocol versioning: add capability handshake at /-/pnpr, v0 resolve
  and publish endpoints, policy echo in done frames, and structured error codes.
- Distribution improvements: add Node.js fallback wrapper, fix engines range,
  and package tini in pnpr Docker container.

Relates to pnpm/tasks#131.
2026-10-19 09:20:12 +02:00
Zoltan Kochan 85f7fa1e98 feat(pnpr)!: give the hosted store a layout with one place per thing (#17100)
The hosted store shared one key space between packages, orgs and blobs:
a registry without an org stored packages at the store root, an org's
registry stored them in `<org>/`, a package's blobs sat beside its
document, and a nested image repository sat inside its parent's
directory. So an org collided with an unscoped package of the same name
(an unpublish of that package removed the org's whole namespace), the
implicit `<ecosystem>~<name>` org of a grouped registry was a valid npm
name, and OCI GC and verify needed ad-hoc exclusion lists. On S3,
object_store percent-encodes `~` in keys while the upload sweep compared
raw prefixes, so grouped OCI registries were never swept. This is part
of section A4 of pnpm/tasks#131.

The new layout (pnpr_storage::layout), the same on disk and in a bucket:

  layout.json                          {"layout": 1}
  default/packages/<name>/document.json
  default/packages/<name>/blobs/<file>
  orgs/<org>/packages/...

Every name, org and file name is one segment: `[a-z0-9._-]` as is,
every other byte as `!` and two hex digits. That keeps nested names
siblings, separates names that differ only in case on case-insensitive
filesystems, and leaves nothing for object_store to escape, so a key is
byte for byte the local path. Per-namespace records stay inside their
namespace; records every namespace shares stay at the root. Listing is
one directory read, so the `.package-index` workaround is gone, and the
blob inventory reports package and blob names instead of paths.

Startup refuses a store marked with another layout or an unmarked store
holding data, and marks an empty one. The proxy cache uses the same
package layout and is emptied once when its marker is missing.
`pnpr upgrade-storage` converts an earlier store on disk or in a bucket,
telling org directories from packages by the configured orgs; it moves
blobs before documents so an interrupted run can resume, and refuses a
directory that is both an org and a package. export, verify and
migrate-storage check the layout, and migrate-storage marks its target.

The registry fixtures the pnpm test suites serve are built in the new
layout, and the cached build is keyed by the layout version.
2026-10-18 21:48:58 +02:00
Zoltan Kochan 7b0d6873a6 feat: pin tar hashes with lockfile.integrityTarget and address revisions by tar digest (#17092)
lockfile.integrityTarget: tar makes the integrity of every package tarball
entry the hash of its uncompressed tar, recorded as settings.integrityTarget.
The lockfile then survives a registry recompressing its tarballs, switching
to tar resources, or being replaced by a mirror that does either. A lockfile
recorded under another target is stale: a frozen install fails with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and any other install re-resolves with
update-checksums semantics so no hash of the old kind carries forward. The
setting is opt-in and the lockfile version is unchanged.

The tarball crate now reports both hashes of every extraction: the resource
after content decoding, and the tar a .tgz decodes to. A pin names one of
them (IntegrityKind::Resource / Tar), or either at resolution, where a locked
or published integrity may be of either kind (ResourceOrTar, sha512 only).
Under a tar pin every gzip member is decoded and nothing may follow the tar.
The resource pin keeps today's behaviour, extracting the first member and
tolerating trailing data, but then records no tar.

Store rows extracted from a .tgz record tarIntegrity and get an alias under
the tar key, in a table pnpm v11 never reads. Store lookups follow aliases,
so a tar-pinned frozen install is served offline by a store that .tgz
installs populated, and resolution takes a version's tar hash from its row
before downloading anything.

Registry revisions now always pin their tar, revision 0 included, and are
fetched with a tar pin, so the digest route can use any content coding.
Revision metadata must list revision 0 and the selected revision, and a
digest-route URL without a revision is rejected. pnpr keys its digest
references by tar hash on every registry format, serves the route as a
negotiated tar resource, and projects revision 0 explicitly. pnpm v11 cannot
extract tar resources, so it now rejects revisions with
ERR_PNPM_REGISTRY_REVISIONS_UNSUPPORTED.

Implements pnpm/rfcs text/0000-tar-integrity-for-every-source.md and
pnpm/rfcs#42.
2026-10-18 17:36:30 +02:00
Zoltan Kochan 430d892741 feat(pnpr)!: serve every ecosystem under its own prefix (#17087)
pnpr served npm at the server root when npm was the only configured
ecosystem and moved it under /npm/ as soon as a second ecosystem was
added. Adding an ecosystem therefore broke every .npmrc registry URL,
every tarball URL already written into a lockfile, named-registry URLs,
and OIDC workload publish paths, and /npm meant the package `npm` on one
server and the ecosystem prefix on another. This settles section A1 of
pnpm/tasks#131: every ecosystem is always prefixed.

- Ecosystem::base_path is now independent of the configuration. The
  per-configuration Registries::base_path and is_only_ecosystem are gone,
  and the routes, the resolver's tarball URLs, OIDC workload paths and
  pnpr's own-origin route classification all use it.
- The default image registry still answers at /v2/ on the host root; an
  image client cannot be pointed at a prefix. The registry directory keeps
  its `prefixed` and `namedPrefixed` flags for the web UI.
- The read-only token allowlist matched the npm audit endpoints at the
  root only, so read-only tokens were already refused for audits on
  multi-ecosystem servers. It now matches them under the npm prefix.
- /metrics no longer switches between Prometheus metrics and the npm
  package `metrics` on the Accept header; the package is /npm/metrics.
- The pnpm v11 and v12 test harnesses, the registry-mock crate and the
  integrated benchmark address the fixture registry at /npm/ and keep the
  server origin for the pnpr protocol endpoints. The benchmark asks each
  revision mock's registry directory whether npm is prefixed, so it can
  still compare against a pnpr revision from before this change.
2026-10-18 16:55:55 +02:00
Zoltan Kochan e3c80c6ed0 fix(pnpr): reserve SCIM-provisioned usernames (#17080)
A user a SCIM client provisioned has no password account until they sign
in, so anyone could take the name first through npm adduser and keep it.
Registration now treats every username in the SCIM directory as reserved,
like admin and OIDC-bound names. Provisioned users sign in through OIDC
or get their account from an admin. A password account that exists when
its user is provisioned keeps working.

A login that creates an account checks the name again before issuing a
token, so a provisioning that lands mid-login takes the account back, as
does a directory that can no longer be read.

Raised in review of pnpm/pnpm#17046.
2026-10-18 13:09:12 +02:00
Zoltan Kochan dfb72de39d perf(pnpr): serve name@version tarballs without the packument (#17074)
A `.tgz` registry now serves a hosted version's blob, or an upstream
version's cached `.tgz`, under the canonical `<basename>-<version>.tgz`
the way the filename route does, so a warm hit loads no packument. A cold
upstream request still reads the requested version's dist, because the
filename route binds a filename to whichever version's `dist.tarball`
names it.

Without a fresh cached packument, the route reads that dist from the
upstream's `<name>/<version>` manifest, a few KB instead of the whole
packument. A manifest of another version, as an upstream returns for a
dist-tag, or one without a valid `dist.integrity` falls back to the
packument, as does an upstream that serves no manifest. An unreachable
upstream gets the stale cached packument at once.

A version that is not a safe path segment is not found, since the
canonical filename is built from it.

A tar registry's response needs the version's `.tgz` integrity. A dist
read from an upstream is kept in a per-replica cache. One read from a
cached packument is tagged with that packument's stamp (mtime and size)
and answers only while the packument is fresh and unchanged, so a
refetched packument retires its dists at once. One read from a version
manifest answers for the packument TTL. Authorization runs before every
lookup. Hosted dists and `cache: false` upstreams are not cached.

A large packument took 3-8 ms to read and parse per request (react,
`@types/node`, typescript). Warm requests for those versions now take
0.06-0.8 ms.

Closes pnpm/tasks#151
2026-10-18 11:47:14 +02:00
Zoltan Kochan dcfdbc547d fix(pnpr): close gaps left in reserved-username registration (#17073)
Close gaps left in reserved-username registration, from the review of
pnpm/pnpm#17046:

- A login that saw a reserved account, and then created it because an
  admin removed the account in between, now takes back the account and
  its token and answers 403. Previously the caller's password stayed
  stored under the admin's name. The check runs before a token is issued.
  The account is deleted only while it still accepts the login's
  password, so an admin's concurrent rewrite stays, and the name's tokens
  are revoked afterwards.
- `pnpr add-user` revokes tokens left under the username before it
  creates the account, as the admin API already did. The create-or-update
  step and token revocation move to `AuthState` so both share them.
- Document `pnpr add-user`, including that it must run while pnpr is
  stopped when users live in an htpasswd file, and stop telling admins to
  self-register the first admin account.
2026-10-18 09:43:53 +02:00
Zoltan Kochan 8b69106ef5 feat: install from tar registries with zstd content coding (#17054)
Implements pnpm/rfcs#39 in pnpm v12 and pnpr.

pnpm:
- `serverType: pnpr` rebuilds tarball URLs as `<registry>/<name>@<version>`.
- Tarball requests go through HTTP clients that leave Content-Encoding in
  place. pnpm decodes zstd (8 MiB window, RFC 9659) and gzip itself, because
  reqwest's built-in decoder allows a 128 MiB window and cannot be configured.
- The decoded body is sniffed: the gzip magic means a .tgz, anything else a
  tar. The hash covers the decoded resource on both paths.
- A decoded-output budget bounds what a response makes pnpm decode before its
  hash is checked: file payload up to `maxTarballUnpackedSize` (4 GiB),
  tightened by `dist.unpackedSize` for tars only; 256 MiB of non-payload;
  at most 10 KiB of zeros after the end-of-archive marker; empty bodies fail.
- A missing `dist.tarball` is rebuilt from the declared layout, and fails
  with ERR_PNPM_MISSING_TARBALL_URL without one. A missing `dist.integrity`
  is learned by the existing resolve-time archive read.
- `--update-checksums` re-resolves every locked package.

pnpr:
- `tarballs: tar` makes a registry's metadata describe tars and its
  `<name>@<version>` route serve them. Every registry serves that route.
- Tar hashes are derived at publish or on the first decode and stored in a
  reserved `.tar-integrity` namespace of the shared hosted backend.
- A cached upstream .tgz is re-checked as its tar streams; a mismatch fails
  the response and evicts the copy.
- gzip clients get the stored .tgz. zstd clients get a level-19 variant from
  the local cache once a bounded worker made one, and a level-3 stream until
  then. Decoding stops at 4.25 GiB, and copies kept for the worker are
  coalesced per version and drawn from a 1 GiB quota.
2026-10-18 01:25:41 +02:00
Zoltan Kochan 198cc7f398 feat(pnpr): provide minimal production default configuration and separate test fixtures (#17027)
* feat(pnpr): provide minimal production default configuration and separate test fixtures

Replace the bundled test configuration in `pnpr/crates/config/config.yaml`
with a minimal production-safe default configuration (Section A3 of
https://github.com/pnpm/tasks/issues/131):

- Configure public npmjs upstream proxy and hosted local registry at
  `/~local/` with default registry set to npmjs.
- Generate a secure 32-byte CSPRNG random secret by default when secret is
  omitted, and disable registration (`max_users: -1`).
- Clean up unused Verdaccio legacy keys (`plugins`, `middlewares`, `web`)
  and remove test fixtures from production default configuration.
- Move test mock configuration and fixture scopes to `pnpr/.fixtures/config.yaml`.
- Move test presets (`REGISTRY_MOCK_LOCAL_PATTERNS`, `registry_mock_rules`,
  `registry_mock_graph`, and `MockConfigPresets` trait with `Config::proxy`
  and `Config::static_serve`) from `pnpr-config` into `pnpr-fixtures`.
- Update Jest global setup and `pnpm-registry-mock` to load test configuration
  from `pnpr/.fixtures/config.yaml`.
- Update documentation in `pnpr/docs/configuration.md` and `pnpr/docs/quick-start.md`.

Related to https://github.com/pnpm/tasks/issues/131

* fix: drop obsolete quick-xml advisories from deny.toml

quick-xml was upgraded to 0.41.0, resolving RUSTSEC-2026-0194 and
RUSTSEC-2026-0195, so cargo-deny no longer encounters them.

* fix: ignore unreachable zip advisories in deny.toml

* fix(pnpr-config): require authentication to access private packages on local registry
2026-10-17 13:23:18 +02:00
Zoltan Kochan 157994e9f4 docs(pnpr): document reverse-proxy and TLS setup, webhooks, lifecycle, and pnprServer (#17013)
* docs(pnpr): document reverse-proxy and TLS setup, webhooks, lifecycle, and pnprServer

Document production deployment and operational architecture for pnpr and
pnpm (Section C13 and Section F of https://github.com/pnpm/tasks/issues/131):

- Add Reverse proxy and TLS deployment guide (pnpr/docs/tls-and-reverse-proxy.md)
  with architecture rationale and configuration recipes for Nginx, Caddy,
  Traefik, and AWS ALB.
- Add Webhooks and event notifications guide (pnpr/docs/webhooks.md)
  documenting the decoupled event architecture, structured audit logs
  (pnpr::audit), and object storage bucket notifications.
- Document signals, readiness probe state transitions, process lifecycle, and
  standard exit codes in pnpr/docs/cli.md.
- Document S3 commit journal lifecycle, multi-replica considerations, and
  disaster recovery caveats in pnpr/docs/storage.md.
- Document the pnprServer workspace setting reference in
  pnpm/docs/settings/dependency-resolution.md and pnpm11/docs/settings/dependency-resolution.md.
- Update sidebars and quick start navigation.

Related to https://github.com/pnpm/tasks/issues/131

* docs(pnpr): clarify pnprServer offloads resolution and streams package metadata

Clarify that pnprServer offloads dependency resolution rather than file
fetching: the server streams back resolved package metadata frames as it walks
the graph so the client can prefetch tarballs from registries in parallel.

* docs(pnpr): address review feedback on Vector remap and pnprServer tarball fetch boundary

- Add remap transform in Vector example in pnpr/docs/webhooks.md to parse
  raw JSON from Docker container logs before filtering audit events.
- Clarify direct public tarball fetching and server-side private upstream
  credential boundaries in pnpm/docs/settings/dependency-resolution.md and
  pnpm11/docs/settings/dependency-resolution.md.

* docs(pnpr): correct probe endpoints, shutdown lifecycle, and log shippers

* docs(pnpr): correct S3 commit journal staging paths and write-ahead sequence

* docs(pnpr): clarify proxy hop coverage and orphan pruning scope

* docs(pnpr): distinguish X-Real-IP from hop traversal and document ALB target port restriction
2026-10-17 11:08:04 +02:00
Zoltan Kochan bc35481b80 feat(pnpr): add maintenance tooling and verdaccio htpasswd support (#17002)
Add maintenance subcommands and migration tooling for pnpr (Section C12 of
https://github.com/pnpm/tasks/issues/131):

- Implement `pnpr config check` (`pnpr config-check`) to validate configuration
  and report diagnostic details on routes, upstreams, auth, storage, and surfaces.
- Implement `pnpr verify` to audit hosted package documents against storage
  blobs, checking for missing files, corrupted checksums, and unreferenced orphans.
- Implement `pnpr export --out <dir>` to dump packages, documents, and blobs,
  with optional `--include-auth` support.
- Implement `pnpr migrate-storage` to migrate hosted packages and blobs from local
  filesystem storage to an S3-compatible object store.
- Support Apache/Verdaccio `{SHA}` format in `pnpr-auth`, automatically upgrading
  authenticated users to bcrypt upon first login, and provide `pnpr import-htpasswd`.
- Add backup and disaster recovery documentation in `pnpr/docs/backup-and-restore.md`
  and update CLI documentation.

Related to https://github.com/pnpm/tasks/issues/131
2026-10-17 01:24:35 +02:00
Zoltan Kochan 7d4b626898 feat(pnpr): support login web flow for SSO (#16969)
Support npm login web flow (`/-/v1/login`) so `npm login` and `pnpm login`
authenticate via browser SSO when an OIDC login provider is configured.

- Implement `POST /-/v1/login` and scoped `POST /~{registry}/-/v1/login`:
  return 404 when no OIDC login providers are configured to allow fallback
  to CouchDB auth; return 202 Accepted with `loginUrl` and `doneUrl` when enabled.
- Implement `GET /-/v1/login/start`: validate session and redirect to single
  provider login or render an HTML picker for multiple providers.
- Implement `GET /-/v1/login/poll/{session}`: return 202 with `Retry-After: 1`
  while pending, and 200 with `{ "token": "<token>" }` once authenticated, consuming session.
- Update OIDC login flow to support `return=cli&session={session}` and fulfill
  CLI web login session upon completion.
- Export `bundled_root_certs` in `pnpm-network` and configure OIDC reqwest client
  with root certificates.

Related to https://github.com/pnpm/tasks/issues/131
2026-10-16 21:17:18 +02:00
Zoltan Kochan 27004ec0d2 feat(pnpr): add packument caching headers and conditional request support (#16962)
Add ETag generation, Vary: Accept, and Cache-Control headers to packument
responses, and return 304 Not Modified when an incoming If-None-Match or
If-Modified-Since header matches the current packument state.

- Compute strong ETag over rendered packument response body
- Return 304 Not Modified with empty body preserving ETag, Vary, and Cache-Control
- Support If-None-Match (with weak comparison per RFC 9110) taking precedence over If-Modified-Since
- Emit Vary: Accept on packument responses, preserving Accept and appending Authorization when private_no_cache applies
- Set Cache-Control: public, max-age={ttl} on public packuments from upstream maxage or config packument_ttl
- Maintain Cache-Control: private, no-store on private or caller-scoped responses
- Extract OSV filtering helpers into osv_filtering.rs and server lifecycle into lifecycle.rs to keep source files within line limits

Related to https://github.com/pnpm/tasks/issues/131
2026-10-16 11:38:36 +02:00
Zoltan Kochan 1d909993c4 feat(pnpr): admin audit log for user, rule, team, scim, and unpublish operations (#16960)
Emit structured audit log events under target `pnpr::audit` at info level
for administrative and destructive actions:
- User management (create, password update, delete, token revoke, register)
- Rule override updates and deletions
- Team creation, destruction, member add/remove, and package access grants
- SCIM user provisioning, replacement, patching, and deprovisioning
- Package and tarball unpublishing, dist-tag set/remove, and OCI deletions

All audit events inherit active request span context (request_id,
client_ip, method, uri) through tracing spans.

Ref: https://github.com/pnpm/tasks/issues/131
2026-10-16 00:03:58 +02:00
Zoltan Kochan 8e057376d6 feat(pnpr): cache eviction, lru size cap, and orphan namespace pruning (#16946)
* feat(pnpr): implement cache eviction, lru size cap, and orphan namespace pruning

Add configurable cache budget and TTL retention policies for the
upstream proxy cache, automated orphan namespace pruning on startup,
periodic runtime cache eviction, and a new `pnpr cache-gc` CLI command.

Closes pnpm/tasks#131 (Section C5).

* test(registry-mock): poll for the live registry child during pending startup

Poll for the child process until it appears with its command line or the
timeout elapses, avoiding a race when sysinfo samples the process table
before the spawned child has completed execve.

* test(registry-mock): record spawned child PID via oneshot channel

* test(pnpr): set accessed time alongside modified time in cache eviction tests

* fix(pnpr): address PR review findings on cache eviction

- Reject unknown fields on cache configuration table
- Count only successfully removed files in eviction reports and size accounting
- Log and continue orphan namespace pruning if individual entry removal fails
- Prevent symlink traversal during orphan namespace pruning
- Skip pruning ~upstreams/ when secret is not explicitly configured to protect sibling caches

* fix(pnpr): preserve in-flight tmp files and check mtime before LRU eviction

- Ignore active temporary files (.tmp*) unless older than 1 hour threshold
- Prevent in-flight tmp files from inclusion in LRU candidates or totals
- Check file effective time before LRU deletion to avoid removing replaced files
- Add test verifying in-flight tmp files are preserved during eviction
2026-10-15 12:13:18 +02:00
Zoltan Kochan 698893b8f1 feat(pnpr): metrics, request IDs, and enriched access logging (#16944)
* feat(pnpr): metrics, request IDs, and enriched access logging

Expose Prometheus metrics endpoint at `GET /-/metrics` and `GET /metrics`
emitting standard text exposition with in-flight gauges, request counters,
duration histograms, and cache statistics.

Forward and validate incoming `x-request-id` headers or generate fresh
UUID v4s, recording them on response headers, tracing spans, and enriched
access logs under `pnpr::access` alongside user identity, client IP,
user agent, latency, and bytes.

Resolves pnpm/tasks#131 (Section C4).

* fix(pnpr): normalize metrics methods and clarify scraper docs

Normalize request methods to standard HTTP methods or "OTHER" and key
by static strings to eliminate allocations and bound cardinality.
Clarify scraper access policy and byte accounting in documentation.

* fix(pnpr): clarify metrics endpoint doc for npm accept headers

Clarify that requests to GET /metrics carrying npm packument Accept
headers receive the metrics package packument, as noted by CodeRabbit.
2026-10-15 02:00:55 +00:00
Zoltan Kochan abc609944c feat(pnpr): rate limits, request timeouts, and coalescing (#16943)
Add pre-buffer authorization for package writes, upstream request coalescing,
login rate limiting, and request timeouts to pnpr.

- Pre-buffer authorization: package mutations (publish, update, dist-tags) check
  caller authentication and write permissions before reading the request body.
- Upstream coalescing (singleflight): deduplicate concurrent in-flight fetches for
  the same package/namespace so cache misses do not hammer upstream registries.
- Rate limiting and concurrency limits: throttle failed login attempts by IP and
  username with 429 Too Many Requests and Retry-After headers, and limit concurrent
  login verifications and package publish buffering.
- Request timeouts: add configurable requestTimeout layer to pnpr HTTP service.

Ref: https://github.com/pnpm/tasks/issues/131
2026-10-15 02:29:21 +02:00
Zoltan Kochan 0ce4551bd5 feat(pnpr): support trusted proxies for client IP resolution (#16939)
Adds trusted-proxy configuration and client IP resolution to pnpr.

When behind a reverse proxy, load balancer, or CDN, requests arrive from the
proxy's IP. To accurately record client IPs in access logs and enforce IP/CIDR
restrictions on authentication tokens, pnpr needs to inspect forwarding headers
(`Forwarded`, `X-Forwarded-For`, `X-Real-IP`).

However, forwarding headers can be trivially spoofed if accepted blindly. This
change adds `trusted_proxies` configuration (`HttpConfig`) supporting CIDR
blocks, exact IPs, and `loopback`. If the direct TCP peer is not trusted, all
forwarding headers are ignored. If trusted, the configured `trusted_proxy_header`
(defaulting to `x-forwarded-for`) is inspected without falling back to other
headers. Hops are traversed right-to-left, skipping trusted intermediate
proxies until the first untrusted client IP is found.

Ref: https://github.com/pnpm/tasks/issues/131
2026-10-15 00:46:10 +02:00
Zoltan Kochan 7b38a86167 feat(pnpr): support SIGTERM container shutdown and readiness probes (#16934)
Listen for Unix SIGTERM signals alongside SIGINT (Ctrl-C) to perform
graceful server shutdown in container environments such as Docker and
Kubernetes.

Implement readiness checking on storage backends (local filesystem and
S3), cache directories, and auth backends (htpasswd, SQLite, Libsql,
PostgreSQL, MySQL). Expose dedicated `/-/ready` and `/-/live` probes
mounted globally and under registry prefixes, and make `/-/ping` return
503 Service Unavailable when storage or authentication is unhealthy.

Refs: https://github.com/pnpm/tasks/issues/131
2026-10-14 19:35:46 +02:00
Zoltan Kochan cb31e127a0 docs(pnpr): remove stale duplicated docs and fix drifted claims (#16926)
* docs(pnpr): remove stale duplicated docs and fix drifted claims

The crate README and OIDC.md under pnpr/crates/pnpr duplicated the website
docs and had drifted: a compilerCaches key that no longer exists, hardcoded
quota numbers, the old registry/ path. The pnpr.client README used the
pre-rename remoteSideEffectsCache spellings and claimed the artifact owner
must equal the username. Point both at pnpr/docs instead.

Also fix claims the code contradicts: the handshake is served when any
surface is enabled and advertises ecosystems, publish, and pipeline; the
resolve request field is registries, not namedRegistries; pnpm does not
call the handshake before an npm resolve; there is no config reload; the
packument TTL has no config key; and the --config help still described a
verdaccio-shaped file.

Install instructions now name the next dist-tag, because latest still
points at a July 2026 pre-alpha snapshot.

* docs(pnpr): address review on handshake, verify order, and root-mounted endpoints

* docs(pnpr): document the /npm/ registry bases on multi-ecosystem servers
2026-10-14 12:56:39 +02:00
Zoltan Kochan 7a349e398f fix(pnpr): connect the libsql backend over TLS (#16835)
The workspace builds libsql without its `tls` feature, so a
`backend.libsql` config panicked at startup with "The `tls` feature is
disabled, you must provide your own http connector", for every remote
URL including plain `http://`.

pnpr now gives libsql its own connector. It is built on the workspace's
rustls 0.23 with the aws-lc-rs provider, and it verifies servers against
the platform trust store through rustls-platform-verifier. Enabling
libsql's `tls` feature instead would add hyper-rustls 0.25 and a second
rustls release. The lockfile gains no new crates.

- The trust store is loaded only for URLs libsql dials over HTTPS, so an
  `http://` database works on a host without CA certificates. The
  `http://` check is case-insensitive.
- A connector built for HTTPS refuses plain-HTTP URIs, so a `base_url`
  in a Hrana response cannot move the auth token out of TLS.
- An `authToken` with a non-loopback `http://` url fails at startup,
  the HTTPS-or-loopback rule pnpr-client applies to its credentials. With a
  token, the connector also refuses each later plain-HTTP request to a
  non-loopback host.
2026-10-10 22:02:25 +02:00
Zoltan Kochan e5988d13b1 feat: share build output between machines through pnpr or a Turborepo remote cache (#16812)
Dependency side effects and `pnpm pipeline` task results now share one
artifact format, one trust model, and two transports.

`pnpm pipeline` stores cacheable task results as `workspace-task`
artifacts of the shared artifact protocol: P-256 signed envelopes whose
manifest mirrors a local cache entry (`meta.json` and `outputs/`). A
remote artifact is verified against `trustedKeys`, written into the
local cache, and restored through the same checks as a local entry.
Results of tasks that ran are published in the background when
`publish` is on, and the run waits for the uploads before it ends.

`ArtifactStore` in pnpm-pnpr-client is either a pnpr server or a server
that speaks the Turborepo Remote Cache API. Both answers go through the
same client-side verification. On the Turborepo API an artifact is the
signed envelope stored under a hash of the owner, the input key, and
the compatibility scope from `compatibility_scopes`, and each blob once
per owner under a hash of its integrity, mirroring pnpr's resolve and
blob routes. The API cannot create conditionally, so a later
publication replaces an earlier one. A failed per-key lookup is a miss. Both transports
refuse credentials for a server that is neither HTTPS nor loopback.
The remote side-effects cache now goes through `ArtifactStore`, so it
can use a Turborepo server too. pnpr-client's candidate filter now
permits `workspace-task` candidates, which the pnpr server already
accepted.

`ArtifactSigner` replaces the duplicated signing setup in deps-restorer
and the pipeline, and `decode_trusted_keys` moves into the protocol
crate.

The new `remoteCache` setting holds `url`, `team`, `token`, `org`, and
the signing fields, with `PNPM_REMOTE_CACHE_*` overrides. Every field
except `org` is machine-only and refused in pnpm-workspace.yaml with
ERR_PNPM_WORKSPACE_REMOTE_CACHE_TRUST: `token` is not URL-scoped, so a
repository that could name `url` could have the token sent anywhere.
`sideEffectsCache.remote` keeps `packages`; its signing fields remain a
fallback through `Config::remote_cache_settings()`. Only
`remoteCache.publish` publishes pipeline tasks, so a machine that
publishes dependency builds does not start uploading task outputs and
logs. The section is
boxed on `Config` to keep CLI futures under clippy's large_futures
limit.

Each artifact kind has its own size limits: a dependency build keeps
64 MiB per file and per artifact, and a task result may hold files of
up to 4 GiB and 16 GiB in all. Blobs travel between files and servers
as streams, so no result is held in memory. On pnpr,
`PUT /-/pnpr/v0/artifacts/blob` stores one blob ahead of its envelope
and commits it only once its bytes match the integrity, and `HEAD`
tells a publisher which blobs to skip. `artifacts.quota` makes pnpr's
storage bounds configurable.

pnpm-testing-utils gains an in-memory Turborepo Remote Cache server for
tests.

On pnpr, `artifacts.orgs` declares who may read and who may publish what
an organization owns: its signed artifacts, its compiler cache, and its
pipeline run records. It replaces the username-equals-organization rule
for artifacts, `artifacts.compilerCaches`, and `pipeline.workspaces`.
The shared-artifact store asks an `OrgAccess` policy the server builds
from it, and `CallerOrgs` replaces the duplicated authorize functions.
Pipeline run records are keyed `<org>/<workspace>/<run id>` under a new
`.pipeline-runs/v1` namespace; `pnpm pipeline --report` sends
`remoteCache.org`.
2026-10-10 13:51:38 +02:00
Zoltan Kochan 1b5569adf0 feat(pnpr): let the web UI sign in with OIDC (#16774)
- `GET /-/pnpr/v0/sign-in` lists the OIDC providers that offer browser
  sign-in, so the UI's sign-in screen knows which buttons to show.
- `/-/oidc/{provider}/login?return=ui` records in the sealed login
  cookie that the sign-in ends in the web UI. The callback then puts a
  one-time code in an HttpOnly `__Host-` cookie and redirects to
  `/-/ui/sign-in/oidc` instead of showing the token. The redirect is
  relative, so it works behind a path prefix. No URL carries the code
  or the token, so a forwarded link cannot sign another browser in.
  `return=ui` is refused when pnpr serves no web UI.
- `POST /-/oidc/handoff` trades the code in that cookie for the session
  token and clears the cookie. A code works once, within a minute.
  Codes live in memory next to the sessions they carry, so the existing
  affinity requirement for multi-replica OIDC sign-in covers them.
- Browser sessions and handoff codes are grouped in `BrowserSessions`.

Password sign-in needs no server change: the UI uses npm's login
endpoint.

The bundled web UI moves to pnpr-web 0.2.5, which adds the sign-in
screen.
2026-10-09 11:48:10 +02:00
Zoltan Kochan 7e4db25f84 feat(pnpr): serve the web UI at /-/ui/ (#16769)
pnpr now serves its web UI at /-/ui/ from the same origin, so the UI
needs no CORS setup and no server URL. The UI ships as the separate
`@pnpm/pnpr-ui` package, an optional peer dependency of `@pnpm/pnpr`.
pnpr finds it next to its own package (npm and pnpm both link a resolved
peer beside the package that declares it), or reads it from `ui.dir`,
and `ui.enabled: false` turns it off.

- Serving: files from the UI directory, `index.html` for every other
  path outside `assets/`, a 404 for a missing hashed asset. The page
  gets a relative `<base>` so deep routes and path prefixes work without
  an absolute Vite base. Responses carry a CSP that keeps scripts and
  requests on this origin.
- The UI source stays on bit.cloud. `pnpr/ui` is a workspace of its own
  that pins `@pnpm/pnpr.apps.pnpr-web` from bit.cloud's registry and
  builds it into `pnpr/npm/pnpr-ui/dist`.
- Release: a `build-pnpr-ui` job builds the bundle. The verify and
  publish jobs download it. The smoke test installs the UI beside the
  wrapper and checks that pnpr serves it. `@pnpm/pnpr-ui` releases in a
  fixed group with `@pnpm/pnpr`.
2026-10-09 00:56:04 +02:00
Zoltan Kochan 3b5af3f793 feat(pnpr): deprovision accounts through SCIM (#16742)
With `auth.scim.token`, pnpr serves the SCIM 2.0 `Users` endpoints and
discovery documents under `/-/pnpr/v0/scim/v2`, authenticated only by that
token.

An OIDC sign-in creates no account row and its session lives in one
replica's memory, so deleting an account is not enough to deprovision
anyone. The SCIM directory is therefore one record in the hosted store,
reloaded on the managed-state TTL like rosters and rules. Authentication
refuses any user identity the directory marks inactive, and so does the
npm login endpoint. Deactivating or deleting a user also revokes its
stored tokens, removes its password account, and drops this replica's
browser sessions for it.

A deleted user stays in the record as inactive and unlisted, so the
username keeps being refused until it is provisioned again. Provisioning
creates no password: the identity provider is the sign-in path.

Follow-up of pnpm/tasks#111.
2026-10-08 19:08:16 +02:00
Zoltan KochanandClaude Opus 5.5 0925ab2e4e feat(pnpr): map npm access grant and revoke onto rules (#16740)
`npm access grant`, `npm access revoke`, and `npm access list packages
<scope:team>` now work on a registry with `rulesManagedBy: api`. A grant
edits the access and publish lists of the package's own `packages:` key
through the same read-modify-write path as the rules admin API, and stores
only the lists it changes, so untouched lists keep following the YAML.

A package that only a scope pattern covers is refused, because editing the
pattern would grant the team every package in the scope.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:52:48 +02:00
Zoltan Kochan 0158dba19a fix(pnpr): honor a token only while its account exists (#16738)
A token kept authenticating after its account was gone: after a removal
whose token revocation failed, after a login that raced a removal, or
after an operator deleted the account from the htpasswd file by hand.
Authentication lookups now require a stored owner.

- The libsql, PostgreSQL, and MySQL backends join `users` in the
  authentication lookups (`lookup`, `lookup_record`), so the check adds
  no round trip on the request path.
- The local backend wraps its token store in `OwnedTokens`, which checks
  the in-memory htpasswd map.
- Listing and revocation still see a removed account's tokens, so they
  can be cleaned up.

Only a login issues tokens, so every legitimate token has a stored
owner. Tests that issued tokens for accounts they never stored now
store the account first.
2026-10-08 17:37:35 +02:00
Zoltan Kochan 23edc7d004 feat(pnpr): make rule changes conditional on the version they read (#16739)
The package rules admin API reports the version of a registry's stored
changes as `version` and `ETag`, and takes `If-Match` on `PUT` and
`DELETE`. A change based on a version another admin has since replaced
answers 412 and changes nothing; without `If-Match` the last change
still wins.

- Resetting the rules now stores the empty set of changes rather than
  removing the record, so a reset goes through the same compare-and-set
  as every other write and can be conditional too.
- The version is the SHA-256 of the stored record, `config` before
  any change is stored. GET reads the rules view and the version from
  one read of the stored record, so a replica that lags another's
  write cannot pair a newer version with older rules.
- `If-Match` uses strong comparison and accepts a list of tags. An
  unreadable header answers 400 rather than counting as absent.
- CORS allows `If-Match` and exposes `ETag`.

Follow-up of pnpm/tasks#111.
2026-10-08 16:45:22 +02:00
Zoltan Kochan 3b4c1f7cd6 feat(pnpr): grant registry teams from OIDC groups (#16735)
An OIDC login can map the groups its ID token lists to registry teams
under `auth.oidc[].login.groups`: a claim (default `groups`) and a list
of `{group, registry, team}` grants. A session whose claim lists a group
is a member of the granted team for as long as the session lasts.

- The memberships ride on the request identity, not the roster:
  `Identity::User` carries `memberships`, and a `team:` token admits a
  member of its roster or a credential that carries a membership of that
  same roster's team. Removing someone from a group takes effect at their
  next sign-in, and the grant never reaches the user's password logins
  or tokens.
- The auth crate maps the claim to grants at sign-in and keeps them on
  the session; the server resolves each grant to the registry's live
  team directory per request.
- Startup rejects a grant that names no hosted registry, or a team a
  config-managed roster does not declare.

Phase 4 of pnpm/tasks#111 (group mapping; SCIM is left out).
2026-10-08 15:13:05 +02:00
Zoltan Kochan 1066dce1b0 feat(pnpr): let admins change package rules through an admin API (#16732)
A hosted registry that sets `rulesManagedBy: api` lets the admins in
`auth.admins` replace its registry-level `access` and the `access`,
`publish`, and `unpublish` lists of the patterns its `packages:` map
declares, through `/-/pnpr/v0/admin/rules/{ecosystem}/{name}`, the pair
the registry directory lists. The patterns stay in config, because they
feed routing.

- `PackageRules` becomes a shared handle on a swappable `RuleTable`, so
  a replaced table reaches every holder at once, including the resolver's
  route context, which is built once per process.
- Changes are stored as a hosted-store record next to the team rosters.
  `ManagedState` replaces `TeamRosters` and reloads both on one schedule
  and lock; rosters load before rules, because a rule can name a team.
- Changes are validated strictly when written. When read back, a `team:`
  reference to a team the roster lacks admits nobody, and changes that
  cannot be read or applied close the registry until a read succeeds.
- `TeamsManagement` becomes `Management`, and `TeamConflict` becomes
  `AdminConflict`, since both now cover more than teams.
- The registry directory reports `rulesManagedBy`.

Phase 3 of pnpm/tasks#111.
2026-10-08 14:20:58 +02:00
Zoltan Kochan 14640273b5 feat(pnpr): let admins manage accounts through an admin API (#16729)
Admins listed in `auth.admins` can now list, create, re-key, and remove
accounts and revoke their tokens under `/-/pnpr/v0/admin/users`.

- `UserBackend` gains `list_users`, `create_user`, `set_password`, and
  `delete_user`, implemented for the htpasswd store, libsql, PostgreSQL,
  and MySQL. The htpasswd `UserStore` moves to its own module.
- Admin-created accounts ignore `max_users`, so an operator can keep
  self-registration disabled and still add people. In the SQL backends
  they still count against the `users` counter the cap reads, and a
  removal takes one off it, so the cap stays exact.
- Removing an account revokes its tokens, deletes it, and revokes again,
  so a failed revocation leaves the account in place.
- A login keeps its token only while the bcrypt hash it verified is
  still stored; a registration only while the stored account accepts
  the password that created it. A removal, a re-creation, or a password
  change during the login revokes the token.
- An htpasswd edit runs whole on a blocking thread under a mutex and
  becomes current only after its write succeeds, so writes stay ordered
  and a failed write changes nothing.
- `require_admin` is shared by the team and account endpoints.

Phase 2 of pnpm/tasks#111.
2026-10-08 13:30:41 +02:00
Zoltan Kochan 398b450580 feat(pnpr): let admins manage teams through the npm team API (#16725)
A hosted registry that sets `teamsManagedBy: api` accepts `pnpm team`
create, destroy, add, and rm from the usernames in `auth.admins`. Other
registries keep answering `teams_config_managed`.

- `team:` tokens read a shared, swappable `TeamDirectory` per registry
  instead of a member set copied at config load, so a roster change
  reaches every rule that names the team at once.
- The roster lives in the hosted store under `.team-rosters/v0`, written
  with compare-and-set and retried on conflict. The YAML `teams:` map
  seeds it until the first change is stored.
- Each replica reloads the rosters once its copy is older than 10
  seconds. Only the first load after startup holds up a request; later
  reloads run in the background. A roster that cannot be read is served
  empty, so its `team:` grants admit nobody, and the load is retried
  after one second.
- Destroying a team a `packages:` rule names is refused with 409.
- Roster edits hold the reload lock, so a reload that read the store
  before an edit cannot publish its older copy after it.
- Team write bodies are capped at 64 KiB.
- The registry directory reports `admin` and each hosted registry's
  `teamsManagedBy`, so the web UI can offer edit controls.
- CORS allows PUT, POST, and DELETE. Credentials travel only in the
  Authorization header, so allowing writes does not expose them to CSRF.

Phase 1 of pnpm/tasks#111.
2026-10-08 11:59:11 +02:00
Zoltan KochanandClaude Opus 5.5 c13953b3bc docs: label shell commands and title file snippets (#16699)
Code blocks that hold only shell commands now use the `sh` language,
and blocks showing a file's contents carry a `title` naming the file,
across the pnpm v12, pnpm v11, and pnpr docs. Bold file-name labels
above code blocks are folded into the titles.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:14:37 +02:00
078c0aa4f3 fix(pnpr): allow Quay's CDN hosts for OCI layer redirects (#16550)
Quay redirects blob downloads to cdn01.quay.io and its sibling CDN hosts.
The OCI download allowlist only covered the Docker Hub and GHCR CDNs, so
the redirect was refused and every Quay layer pull through pnpr failed
with a 502. Allow cdn.quay.io and cdn01 to cdn06.quay.io for the
https://quay.io origin.

Closes pnpm/pnpm#16548

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:21:42 +02:00
a44e9079e3 feat(pnpr): compute missing upstream tarball integrity (#16358)
Upstream packuments may omit dist.integrity, leaving pnpm clients unable to
install those versions through pnpr.

When pnpr fetches a packument from a caching upstream, it downloads each
version that lacks a usable integrity (capped at 64 per fetch), verifies a
declared legacy shasum, caches the tarball, and persists the computed SHA-512
into the cached packument. Resolution and later tarball requests then use the
same integrity.

Pinning is per version: a version that cannot be downloaded, fails its
shasum, or has a malformed dist keeps its upstream metadata and is refused by
the tarball route, while the rest of the package stays installable. Pinning
runs only on an upstream fetch, not on cached reads, so unpinnable versions
cost one request per refresh. The downloads run without the striped package
lock, which hosted publishes share. Each tarball is synced and closed but
not promoted. Under the lock, pnpr re-reads the cached packument, writes a
pin only where that version still declares the same tarball and shasum
without a usable integrity, persists the packument, and only then renames
those tarballs into the cache; the rest are abandoned. A refresh that lands
meanwhile is kept, a published pin always describes the cached bytes, and
a cached tarball never lacks its published pin. If the packument write
fails, the staged tarballs are abandoned and the unpinned packument is
served. Packument refreshes and authoritative-404 purges take the same lock
for their cache writes, and pinning publishes nothing when the cached
packument is gone. Cache-disabled
upstreams are left unchanged because the computed pin cannot be stored.

Related to pnpm/tasks#24

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:08:54 +02:00
Zoltan Kochan 07839df5c3 docs: move product documentation beside its implementation (#16385)
* docs: move website documentation into the pnpm repository

* docs: keep v11 and v12 documentation beside their implementations

* docs(pnpr): keep registry documentation beside its implementation

* ci: classify product documentation as documentation changes

* test: cover product documentation in Rust CI exclusions

* ci: isolate documentation publishing and support manual sync

* test: make dependency status fixture timestamps deterministic
2026-09-29 15:56:43 +02:00