Files
Zoltan Kochan 40cf3cedba fix(pnpr): clarify publish, unpublish, and Worker errors (#17854)
Validate unpublish versions with the existing publish semver predicate before
reading or mutating storage. Reject build metadata in publish version keys at
the document validation boundary, preserving manifest normalization for
archives whose versions carry build metadata.

Catch startup, forwarding, and container-access failures in the documented
Cloudflare Worker and return a generic availability response with a retry
delay. Exercise the actual deployment snippet with mocked container failures.

Closes https://github.com/zkochan/pnpm-tasks/issues/19.
2026-11-07 19:55:33 +01:00
..
…

Cloudflare metadata cache

The deployment guide uses a pass-through Worker. For a custom caching Worker, copy cache-policy.mjs and revalidate-metadata.mjs alongside its entry point. They have no runtime dependencies.

Call fetchMetadata only for package metadata routes. base and name come from your registry route parser. Forward all other routes, including tarballs and mutations, directly to the container:

import { fetchMetadata } from './revalidate-metadata.mjs'

async function handleMetadata (request, ctx, route) {
  return fetchMetadata(request, {
    cache: caches.default,
    base: route.base,
    name: route.name,
    fetchFromRegistry,
    waitUntil: task => ctx.waitUntil(task),
  })
}

fetchFromRegistry must forward the supplied request, including its headers, to the container. Set the registry request headers before calling fetchMetadata. For example, set the deployment Worker's fixed Accept-Encoding: zstd, gzip before selecting the cache key. Forward the original URL, never the synthetic cache key.

The waitUntil callback keeps cache updates alive after the response returns. Cache API failures are logged and leave registry reads available. Unchanged 304 responses reuse the stored body without rewriting it.

Every metadata read reaches the registry. A stored body supplies its ETag in If-None-Match. A 304 reuses that body; a 200 replaces it. Access denials, missing packages, and server errors reach the client without a stale fallback. Requests retain their credentials during revalidation. Responses with cookies or caller-dependent cache headers cannot populate the cache.

This trades an origin round trip per read for freshness without global purges. A completed write does not need to enumerate metadata aliases or clear every data center. Reads that overlap a write may still see the earlier state, but a late cache fill must pass revalidation before a later read can use it. Freshness and access checks remain the registry's responsibility, including any replica synchronization delay.

The cache retains bodies for up to a day to reuse after a 304; that retention is never permission to serve them without revalidation. Responses preserve the registry's cache headers, so downstream clients still follow those lifetimes. Client conditional requests bypass cache storage and reach the registry with their validators unchanged. This preserves its 304 handling, including weak ETags and lists of validators.

Tarballs always pass through, including revision URLs such as widget@1.0.0+r1 and /-/tarballs/sha512/ URLs. Their bytes may be immutable, but access can change after an unpublish, a rule change, or a vulnerability check. Do not add an edge cache that serves them without checking access.

When replacing a Worker that cached tarballs or served fresh and stale metadata without revalidation, remove its cache lookup paths. Purge any separate CDN cache rules too. A local Cache API deletion clears only the data center handling that request. Changing this example's key namespace cannot invalidate another cache in front of it.

The key separates full JSON, install-v1, and supported content encodings. Install-v2, unsupported encoding preferences, queries, range requests, conditional requests, CORS requests, and explicit cache bypasses go directly to the registry. Cached compressed bytes retain their original content coding and ETag when restored. Use encodeBody: 'manual' when wrapping the returned response in a Worker so the bytes are not encoded twice.

Run the tests from the repository root:

node --test pnpr/examples/cloudflare/*.test.mjs