* vpm: validate registry TLS, check returned identity, skip client setup for serve Three review findings on the registry transport, each with a test. Registry requests over HTTPS now validate certificates, whether or not they carry a token; tokenless metadata was previously fetched with validation off, so a MITM could replace it. Plain HTTP and non-registry traffic are unchanged. decode_registry_module now checks the body against the requested route: a body naming another module, or another version when one was requested, is refused instead of decoded. A cross-wired registry previously flowed into resolution output. v registry serve no longer requires a writable VModules home. Dispatch runs before ensure_vmodules_dir_exist, and init_settings skips the log-cache setup for the registry command, so a service account can serve a registry from its working directory. * vpm: refuse world-readable signing keys, never assert SBOM license conclusions Two Codex review findings on the registry, each with a test. A signing key file readable by group or others lets any local user forge signatures, so it is no longer loaded. The check is Unix-only by design: Windows has no equivalent mode check and accepts the file there, while a missing file is refused everywhere. A refused key serves unsigned, exactly like a missing one, which verification already fails closed on. `licenseConcluded` is a derived result and nothing here derives one, so it is now always NOASSERTION. The publisher's declaration stays in `licenseDeclared`. Consumers previously read an unexamined claim as a compliance result.
8.9 KiB
VPM Registry Protocol Design
Overview
The VPM registry protocol defines how registries communicate with VPM. It enables third-party
registries and private registries. The reference implementation is not connected to v install;
the client integration remains separate work.
Running the Reference Server
Run the server from the repository root:
./v registry serve --port 9090
The standalone tool also accepts vpm registry serve [--port <port>], with -p as a port alias.
Ports use decimal digits and must be from 1 to 65535; the default is 9090. The server loads
.vpm-registry/index.json and artifacts from the working directory and listens on all interfaces.
Publishing and yanking remain in-process Registry methods.
Unknown routes return HTTP 404, including conditional requests for missing metadata. Successful GET metadata supports ETag revalidation with case-insensitive HTTP header names. Archive transports verify the exact bytes sent against recorded metadata. Paths retain percent-encoded spelling; the server removes trailing slashes before routing.
Design Goals
- CDN-compatible: No query parameters, just GET on predictable paths (like Go's proxy protocol)
- Content-addressed artifacts: Immutable once published, like Zig's hash-as-version or Go's immutable zips
- Scalable: Per-file fetch, not full clone (like Cargo's sparse protocol)
- Signed registry: Mirrors don't need to be trusted (like Hex's signed protobuf registry)
- Checksum transparency log: Append-only Merkle tree for all published hashes, like Go's sum.golang.org
- Private registries: Token-based authentication (like Cargo or Packagist)
- SBOM/SPDX output: Supply chain security
API Endpoints
Core Endpoints (Go-style, CDN-compatible)
| Endpoint | Purpose |
|---|---|
GET /<module>/@v/list |
List all known versions of a module |
GET /<module>/@v/<version>.info |
JSON metadata for a specific version |
GET /<module>/@v/<version>.mod |
The v.mod file for that version |
GET /<module>/@v/<version>.zip |
The full module source as a zip file |
GET /<module>/@latest |
Latest version info |
Registry Configuration (Cargo sparse-style)
| Endpoint | Purpose |
|---|---|
GET /config.json |
Registry URLs, the auth_required flag, and signing public key |
Authentication Endpoints
There are none. No route in this repository accepts or issues credentials, so there is nothing to log in to and no token to obtain from one. What the client sends, and what the server does with it, is described under Authentication below.
Publishing
| Endpoint | Purpose |
|---|---|
PUT /api/publish |
Publish a new module version |
PUT /api/yank/<module>/<version> |
Yank a version |
PUT /api/unyank/<module>/<version> |
Unyank a version |
Search and Discovery
| Endpoint | Purpose |
|---|---|
GET /api/search?q=<query> |
Search modules |
GET /api/modules/<module> |
Module metadata |
GET /api/modules/<module>/versions |
List versions |
Metadata Format
Per-version entry (JSON):
{
"name": "mymodule",
"version": "1.2.3",
"description": "A sample module",
"license": "MIT",
"dependencies": {
"othermodule": "^1.0.0"
},
"checksum": "sha256:abc123...",
"yanked": false,
"published_at": "2024-01-01T00:00:00Z",
"features": {
"extra": ["othermodule/extra"]
}
}
Content Addressing
- Each version's source is stored as a zip file
- Published module names and versions must each be one nonempty path component, without separators,
drive prefixes,
.or..components, trailing dots/spaces, or NUL/line-break characters - The zip file's SHA-256 checksum is recorded in the metadata
- Accepted raw,
sha256:,SHA256:, and omitted checksums are stored assha256:<verified digest> - The checksum is used for integrity verification on download
- Artifact downloads require a recorded version and a matching, nonempty checksum; orphan files are never served
- A transparency log records all published checksums
Registry Signing
- The registry signs its metadata with a private key
config.jsonincludes the registry's signing public key- Mirrors serve the signed metadata without needing to be trusted
- Clients verify the signature before accepting metadata
- Signing sorts module, dependency, and feature map keys; map insertion order does not affect the signed bytes. Array order remains significant, and the change log is excluded.
Yank/Retire Semantics
- Yanked versions are not selected by new resolutions
- Existing lockfiles that reference a yanked version continue to work
- Yanking is reversible (unyank)
- Retired versions are marked as deprecated with a reason
Caching Strategy
- ETag / Last-Modified headers for conditional requests
- CDN-friendly: all endpoints are GET with predictable paths
- Immutable artifacts cached indefinitely
- Metadata cached with ETag validation
SBOM/SPDX Output
v sbomcommand outputs SPDX JSON format- Includes all transitive dependencies
- Includes license information
- Includes checksums for all packages
- JSON uses the SPDX 2.3 field names, with
filesAnalyzed: falsefor registry metadata. Package IDs encode both module name and version, so each package version has a distinct identifier. - Field names and package identifiers follow the official SPDX JSON schema.
Change Feed
GET /api/changes?since=<timestamp>returns changes since timestamp- Enables incremental syncing for mirrors
- Each change is a JSON object with module, version, and change type
Authentication
Authentication here is entirely a client-side concern. The VPM client attaches a bearer token to its own registry requests. The registry server in this repository reads no credential and authenticates nothing.
Authorization: Bearer <token>is set by the VPM client only.- Registry requests over HTTPS validate TLS certificates, whether or not they carry a token.
- The token comes from
VPM_TOKEN_<HOST>, where<HOST>is the registry's hostname with.and-folded to_and uppercased, sohttps://vpm.example.com/readsVPM_TOKEN_VPM_EXAMPLE_COM.VPM_TOKENis the fallback when no scoped variable is set, and covers a machine with a single private registry.registry_tokenincmd/tools/vpm/common.v:149builds the name;vpm_http_requestatcommon.v:98attaches the header when it is non-empty, so a registry that needs none receives no such header at all. - There is no login endpoint, and nothing obtains or issues a token. No route in this repository accepts credentials. A token is provisioned in the environment by whoever configures it, which is why it is a VPM client variable and not part of the protocol.
- The registry server authenticates nothing.
auth_requiredis a field ofRegistryConfig(registry.v:26) that is hardcoded tofalse(registry.v:478) and that no code reads, so no endpoint can require a credential. It is present in/config.jsonso that a client can parse the document, not because anything checks it. - A registry answering
401is reported byrequire_registry_token(common.v:166) as an error naming the variable to set, rather than as an opaque transport failure. This repository's registry answers only200,304and404, so that path is reached against a third-party registry. - Authenticated HTTP redirects keep the registry's scheme, host and effective port
(
vpm_registry_redirect,common.v:133), sohttp://andhttps://are different origins, as are a default port and an explicit one. A request that would cross an origin fails rather than leaking the token to the new target. Unauthenticated requests are unaffected: the check is skipped when the request carries noAuthorizationheader.
Migration Path
- Existing VPM registry continues to work
- New protocol is opt-in via registry configuration
- VPM falls back to direct VCS for modules not in any registry
- Gradual migration: registry serves metadata, VCS serves source
Research Summary
| Pattern | Go | Cargo | Zig | npm | Hex | Packagist |
|---|---|---|---|---|---|---|
| SemVer | Yes | Yes | No (hash) | Yes | Yes | Yes |
| Content-addressed | zip | .crate | hash | tarball | tarball | No (VCS) |
| Lockfile | go.sum | Cargo.lock | build.zig.zon | package-lock.json | mix.lock | composer.lock |
| Checksum DB | sum.golang.org | Index cksum | N/A | dist.integrity | Signed registry | N/A |
| Yank/Retire | No | Yes | N/A | Yes (revoke) | Yes (retire) | No |
| CDN-compatible | Yes | Yes (sparse) | N/A | Yes | Yes | Yes |
| Signed registry | No | No | No | No | Yes | No |
| Auth for reads | No | Optional | No | No | Optional | No |
| Auth for writes | N/A | Token | N/A | Token | OAuth2/Key | Token |
| Search | No | Yes | N/A | Yes | Yes | Yes |
| Change feed | No | No | No | No | No | Yes |