Pantry registry

This reference is reproduced from Pantry v0.10.36 at immutable commit a6bdc42071cc659896d1b9ff9d7ab6862c72954d. The copied contract and its executable evidence are checksummed in evidence/pantry/evidence.lock.json. Stacks relies on this boundary; it does not redefine Pantry behavior.

ProvenanceValue
Pantry release0.10.36 / v0.10.36
Pantry commita6bdc42071cc659896d1b9ff9d7ab6862c72954d
Upstream contractdocs/registry.md
Contract digestsha256:c16a2ef8855d99b34398bddfd6f574e0e3001ce8a769dc6dd6792af418c4724b
Documentation checkbun run docs:contracts:check (48 source-linked markers)
Targeted HTTP/contract tests11 passed, 0 failed
Native test graphzig build test

The text below is the upstream implementation contract. Normative words apply to Pantry at the pinned revision, not to every historical Pantry version.

This is the authoritative technical and operational reference for the registry implemented in packages/registry. Route handlers remain the executable source of truth. bun run docs:contracts:check verifies that critical routes, methods, authentication boundaries, limits, storage semantics, and test links remain represented here.

Responsibilities and trust boundaries

The registry provides five distribution surfaces:

  1. Pantry/npm-compatible package metadata and tarballs.
  2. Commit-addressed preview packages used by pantry publish:commit.
  3. Content-addressed Zig packages.
  4. PHP/Composer packages.
  5. Native binary, desktop application, and font metadata/tarball proxying.

It also serves search, analytics, publisher accounts/tokens, build status, and the Pantry website. These surfaces share a process but not identical auth or integrity semantics; clients must use the contract for the selected surface.

Core package API

MethodPathAuthenticationBehavior
GET/healthPublicProcess health and current timestamp. It does not prove every storage dependency is writable.
GET/search?q={query}&limit={n}&format=jsonPublicSearch local metadata and optionally supplement from npm; query and result limits are bounded.
GET/packages/{name}PublicReturn the latest stored version, with npm fallback when enabled.
GET/packages/{name}/{version}PublicReturn an exact version; no version mutation occurs.
GET/packages/{name}/versionsPublicList stored versions, falling back to npm only when configured.
GET/packages/{name}/{version}/tarballPublicProxy exact bytes, update download analytics, and return 404 when unavailable.
POST/publishLegacy admin token or ptry_ API token with publish permissionAccept multipart metadata/tarball or JSON/base64, validate limits, reject duplicates, persist bytes then metadata.

Publishing validates package name, version, metadata size, content type, and a 50 MiB tarball limit. A duplicate {name, version} returns 409 before the tarball is buffered where possible. Successful publication returns 201; it does not overwrite an existing immutable version.

The registry computes SHA-256 over uploaded bytes and records it as sha256:{hex} alongside the canonical public proxy URL. This is integrity evidence for the bytes received. It does not authenticate the publisher; publisher identity comes from the accepted credential.

Commit package API

MethodPathAuthenticationBehavior
POST/publish/commitLegacy admin or ptry_ publish tokenPublish tarballs associated with a full commit SHA and optional repository/package directory.
GET/commits/{sha}PublicList packages stored for a commit.
GET/commits/{sha}/{name}PublicReturn commit-package metadata. Scoped names are supported.
GET/commits/{sha}/{name}/tarballPublicDownload exact commit-package bytes.
GET/{name}@{sha}PublicShort preview URL; resolves exact and supported alias forms.

Each commit tarball is limited to 50 MiB. Production object storage applies the documented expiry policy to the commits/ prefix. Commit packages are previews, not permanent semantic-version releases. Consumers should retain the commit SHA, source repository, checksum, and test evidence.

Zig API

MethodPathAuthenticationBehavior
GET/zig/search?q={query}&limit={n}PublicSearch Zig metadata; query length and limit are bounded.
GET/zig/packages/{name}PublicLatest Zig version and generated zig fetch and dependency snippets.
GET/zig/packages/{name}/{version}PublicExact Zig metadata.
GET/zig/packages/{name}/versionsPublicVersion list sorted by registry storage.
GET/zig/packages/{name}/{version}/tarballPublicDownload exact bytes and record analytics.
GET/zig/hash/{hash}PublicResolve a Zig SHA-256 multihash to metadata.
POST/zig/publishCurrent PANTRY_REGISTRY_TOKEN or PANTRY_TOKENPublish multipart tarball plus optional build.zig.zon; reject duplicate version.
DELETE/zig/packages/{name}Current registry tokenDelete all versions of the named Zig package.

Zig publication computes the 1220 SHA-256 multihash over received bytes. Manifest names using underscores are canonicalized to hyphens. The auth token is read per request so a rotated token takes effect without re-importing the route module; comparison is timing-safe and missing server configuration fails closed.

PHP/Composer API

MethodPathAuthenticationBehavior
GET/php/search?q={query}&limit={n}PublicSearch Composer package metadata.
GET/php/packages/{vendor}/{package}PublicLatest metadata plus a generated Composer require command.
GET/php/packages/{vendor}/{package}/{version}PublicExact version metadata.
GET/php/packages/{vendor}/{package}/versionsPublicList versions.
GET/php/packages/{vendor}/{package}/{version}/tarballPublicDownload exact bytes and record analytics.
POST/php/publishCurrent registry tokenPublish multipart tarball and composer.json; reject duplicate version.
DELETE/php/packages/{vendor}/{package}Current registry tokenDelete the complete package.

PHP token lookup is per request and fails closed when the server token is missing. The registry checksum is SHA-256 over the uploaded archive.

Bulk resolver and native binary surfaces

MethodPathPurpose
POST/npm/resolveResolve multiple npm dependency specs for Pantry's install pipeline.
GET/npm/resolve/{specs}GET compatibility form for bounded spec lists.
POST/registry/downloadStream or bundle resolved tarballs for efficient installs.
POST/npm/downloadCompatibility alias for registry download.
GET/binaries/{domain}/metadata.jsonReturn package version/platform metadata.
GET/binaries/{domain}/{version}/{platform}/{artifact}Proxy an exact binary or checksum from object storage.
GET/desktop-appsList the configured desktop catalog with live version/platform availability.
GET/fontsList the configured font catalog with live availability.

Binary paths are allowlisted and normalized before proxying. Public URLs may use the registry proxy while storage remains private. A binary checksum is evidence only when the client verifies it; availability metadata alone is not verification.

Authentication and authorization

The registry supports:

  • a legacy operator token from PANTRY_REGISTRY_TOKEN or PANTRY_TOKEN;
  • account API tokens prefixed ptry_, stored only as hashes and scoped with publish and/or read permissions;
  • HTTP-only web sessions for publisher/dashboard operations;
  • npm trusted-publisher/OIDC flows in the Pantry CLI for compatible targets.

Core and commit publication accept the legacy token or a valid user publish token. Build-event, build-log, and rebuild mutations accept an authorized bearer token or signed-in session. Zig and PHP mutation routes currently accept the operator token, not user API tokens. This is current behavior, not uniform auth.

MethodPathPurpose
POST/auth/signupCreate an account subject to signup policy.
POST/auth/loginVerify password and create a 30-day server-side session.
POST/auth/logoutRevoke the current session.
GET/auth/meReturn the authenticated account.
GET/auth/tokensList token metadata without hashes or secret values.
POST/auth/tokensCreate a token; the raw value is returned once.
DELETE/auth/tokens/{id}Revoke a token owned by the account.

Passwords are hashed. Login performs a dummy hash for missing users to reduce timing enumeration. Session/API secrets are stored as hashes, token expiry is enforced, and legacy token comparisons use constant-time primitives.

Publication validation and ownership

Package names and versions are normalized and validated before they influence a storage key. Metadata field sizes are bounded. Multipart and JSON publication share the same name, version, duplicate, and metadata validation. JSON base64 is decoded only after those cheap checks, and malformed data returns 400.

When a user API token publishes a core or commit package, the registry records the authenticated publisher. The legacy operator token represents _admin and does not fabricate a normal publisher identity. Publisher dashboard mutations verify ownership unless the account is an administrator.

Deletion is deliberately narrower than publication: Zig/PHP deletion currently uses the operator token, while publisher-account package settings use the account authorization path. Operators should not expose the legacy token to ordinary CI jobs when a scoped API token is supported.

Storage model

The registry separates tarball/blob storage from metadata and analytics:

ModeTarballs and binariesMetadataIntended use
LocalFiles under .registry or in-memory test storageLocal JSON/in-memory implementationsDevelopment and tests
Object storageS3-compatible provider selected by endpoint, bucket, region, and credentialsObject snapshots such as metadata/registry-index.jsonPortable production default, including non-AWS providers
AWS legacy/compatibleAWS S3DynamoDB tables selected by the metadata and analytics settingsExisting AWS deployments and staged migration

Production supports AWS-compatible and Hetzner object storage. The bucket may remain private because the server exposes canonical proxy routes. Writes upload bytes before version metadata. Operators must monitor both steps; an interrupted operation can require reconciliation even though the API returns success only after both complete.

METADATA_BACKEND explicitly selects object, dynamodb, or file. When it is unset, an object provider selects the portable object snapshot, a configured AWS table may retain DynamoDB compatibility, and local operation uses the file backend. Zig, PHP, authentication, and analytics factories follow the same deployment distinction: object snapshots are the portable path, while the DynamoDB implementations remain available when their table variables are set. Do not mix independently restored generations of these stores.

Important variables include BASE_URL, REGISTRY_INTERNAL_URL, S3_BUCKET, S3_ENDPOINT, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY, METADATA_BACKEND, DYNAMODB_TABLE, DYNAMODB_ANALYTICS_TABLE, PANTRY_REGISTRY_TOKEN, PANTRY_TOKEN, and NPM_FALLBACK. NPM_FALLBACK=false turns missing Pantry metadata into a local miss instead of querying npm. Prefer the S3_* names for portable object storage; legacy AWS_* credentials remain relevant to the AWS-specific backends.

The public BASE_URL is stored in metadata and returned to clients. The internal URL is used for server-side storage proxying when public routing would be inefficient or recursive. Operators must configure both with valid schemes and must not point the internal URL at an untrusted host.

npm fallback and outbound request safety

When enabled, npm fallback can supplement search, metadata, version lists, and tarball downloads. Local results win and duplicate npm search results are removed. External tarball URLs must use HTTPS and pass blocked-host checks; local/private-address targets are rejected to limit SSRF. Pantry-owned tarballs use a canonical internal storage key derived from validated name and version, not a user-supplied object key embedded in metadata.

Fallback is a read behavior. A miss in Pantry does not cause an npm package to be copied or published into Pantry. Disabling fallback is the correct mode for operators who require the registry to serve only explicitly stored artifacts.

Analytics and build operations

Public read routes include /analytics/{name}, /analytics/top, category period views, /api/packages, /api/build-status, /api/github-actions-status, /api/build-events-stream, /api/rebuild-queue, /api/unavailable-versions, and bounded build-log reads. State-changing routes /analytics/events, /api/build-events, /api/build-logs, and /api/rebuild validate bodies and apply the auth policy in their handler.

The SSE endpoint sends an initial snapshot, event updates, and heartbeat comments, then cleans up subscriptions and timers on disconnect. Builder ingestion bounds event batches and log lines to prevent one request from growing memory without limit. Rebuild package/domain inputs use a restricted character and length set.

Analytics failure must not corrupt package bytes. Download/publish analytics are best-effort in route families where the handler explicitly catches tracking errors. Operators should alert on persistent analytics failures without misreporting them as package-integrity failures.

HTTP and error contract

StatusMeaning
200Successful read, delete, or accepted operational mutation.
201Package or commit publication persisted.
400Invalid path parameter, metadata, manifest, JSON, or required field.
401Missing/invalid token or server has no required publish token.
404Package, version, hash, tarball, or route not found.
409Immutable package version already exists.
413Tarball exceeds its 50 MiB limit.
415Publish content type is unsupported.
500Unhandled server/storage failure; no success should be inferred.

CORS allows public browser reads and declared mutation methods/headers. Cache headers vary: volatile build state is no-store, bounded catalog views use short public caching, and package bytes are served with explicit content types.

Operational deployment and recovery

Production deployment must provide a writable metadata/blob backend, stable public URL, internal URL, operator token, and storage credentials. Keep the object bucket private unless a separate policy explicitly requires public access. Use the storage configuration script only with reviewed environment values.

Back up the active metadata backend and package prefixes together. For object mode this means the metadata snapshots; for DynamoDB mode this means a consistent table backup. Restoring metadata without corresponding tarballs creates visible versions that cannot be downloaded; restoring tarballs without metadata leaves unreachable objects. Reconciliation should compare metadata keys, object keys, checksums, and public proxy reads before traffic is promoted.

Token rotation is effective immediately for core, commit, Zig, PHP, and admin handlers that read the current environment; the service environment must itself be updated. Existing account sessions/API tokens have their own persisted expiry and revocation lifecycle.

Before a production deployment:

bun run docs:contracts:check
bun test packages/registry/src
bun run --cwd packages/registry typecheck
curl -fsS https://registry.pantry.dev/health

Then verify one real metadata lookup and tarball checksum through the public proxy. A green health route without a storage read is not end-to-end health.

Test and evidence map

ContractEvidence
Core metadata, publish, fallback, SSRF, binary proxy, and CORSpackages/registry/src/e2e.test.ts, pkgx-fallback.test.ts
Account, session, API-token, and publisher behaviorauth.test.ts, publisher.test.ts
Immutable commit packages and scoped namescommit-publish.test.ts
Zig auth rotation, publish, hash, versions, tarball, search, conflict, deletezig-routes.test.ts
PHP publish, read, search, conflict, and deletephp.test.ts
Object metadata, package, and analytics persistenceobject-metadata.test.ts, object-package-storage.test.ts, object-analytics.test.ts
Workspace range normalizationworkspace-protocol.test.ts
Documentation/source freshnessscripts/docs-contracts.test.ts, bun run docs:contracts:check