Skip to content

pnpr install accelerator: remove /v1/files and add forwarded-credential access for external private registries #12184

Description

@zkochan

Follow-up work from #12181, which landed the per-caller access gate on POST /v1/install. That PR authorizes every served package against pnpr's own packages: policy in-process (deny_unauthorized_packages in pnpr/crates/pnpr/src/install_accelerator.rs) — the correct and complete answer for the pnpr-as-authority regime (the service-token proxy deployment: one pnpr→upstream token, per-user pnpr tokens, access granted via pnpr's policy). Two pieces remain.

1. Remove the unauthenticated POST /v1/files

/v1/files serves any CAFS file by digest with no authentication and no package identity to gate on, so the access check in #12181 (which is per package) can't cover it — it has to be removed, not gated. It is already superseded by the inlineFiles single-response path (the pacquet client moved over in #12178); only the experimental TS path still uses the two-trip flow.

Work:

  • Server: make handle_install always inline; delete the /v1/files route, serve_files, handle_files, FilesRequest/FileDigest, is_valid_sha512_hex; rewrite the tests/install_accelerator.rs integration tests that exercise /v1/files directly.
  • TS: port @pnpm/pnpr.client (fetchFromPnpmRegistry) to parse the inline single-response and write CAFS directly; drop @pnpm/worker's fetchAndWriteCafsFiles / fetch-and-write-cafs message.
  • Forward Authorization on /v1/install from both clients (pacquet pnpr-client already needs it for the gate to authorize real users; TS client too).
  • Changeset for the touched TS packages.

2. Credential forwarding + per-user access grants (external private registries)

The #12181 gate authorizes against pnpr's own policy, which is the authority for everything the store can hold today — pnpr fetches anonymously, so cached content is pnpr-hosted or publicly fetchable. Once the client points the accelerator at an external registry and forwards the user's token, pnpr can fetch and cache content that carries no pnpr policy; its authority is that external registry, per user. The local check does not cover this (noted at deny_unauthorized_packages).

Design reached in discussion

Two regimes, dispatched by where a package resolved from:

Mechanism for the second regime — a per-(user, name@version) grant table (a small allow-list; CAFS bytes stay globally deduplicated, only the grant is per-user):

  • Serve a cache hit to user B iff (B, name@version) is granted → no upstream round trip.
  • Otherwise verify with B's forwarded token (the cold-path fetch/metadata check B would do on a first install anyway), and on success record the grant. So re-verification is confined to "first time this user wants this version."
  • Clear-on-discovery: whenever pnpr talks to the upstream as B and gets 401/403 for a package (a new version, a non-frozen resolve, any cold request), purge B's grants for it. Piggybacks on traffic pnpr already generates; clearing on a possibly-transient denial is fail-safe (worst case: one extra re-verify).
  • Optional TTL backstop for grants that traffic never revisits (a B who only ever re-installs the identical frozen lockfile of already-granted versions — the benign "B already holds these bytes" case). Permanent grant vs TTL'd grant is a policy knob: permanent matches local-retention; TTL lets revocation bite even already-seen versions within a window.

Why this can't precede credential forwarding

The grant table is inert without forwarding — there's no external-private content in the store to gate (everything is fetched anonymously today) and no user token to verify with, so it can't even be tested end to end. The real dependency chain:

  1. Forward the user's per-registry tokens on /v1/install (a map, not just the one Authorization header — namedRegistries), in both clients + worker.
  2. Use them server-side for resolution and fetch. Snag: config_for interns one &'static Config per registry setup; per-user auth_headers would leak a config per user, so auth must be threaded through the resolve/fetch path separately rather than via the interned config.
  3. The grant table (persistent, like VerdictCache).
  4. The clear-on-discovery hook in the fetch path.

Steps 3–4 are inert until 1–2 land. This is a feature, not a tweak, and is best done as its own PR.


Written by an agent (Claude Code, claude-opus-4-8).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions