Hosted Registry Protocol

Immutable hosted package metadata, secure publish and yank flows, mirrorable artifact paths, and static registry fallback semantics.

docs/reference/guides/hosted-registry-protocol.html
docs/wiki/hosted-registry-protocol.html
ricochet-hosted-registry-v1
Ricochet stack and MVC flow Value::String Value::Map Value::Class Value::Block bytecode VM Controller View Postgres $name "home/index" swap view [ ... ] "methodName" Method

The hosted registry protocol is the Epic 8 implementation target. It extends Ricochet beyond local and static registries without replacing the current ricochet-static-registry-v1 workflow.

Hosted registry base URLs use HTTPS outside local fake-server tests. Static mirrors continue to use file:// or https:// index URLs.

Ricochet currently implements hosted client operations for discovery, search, metadata fetch, archive fetch, install, lockfile verification, publish, and yank, plus a local hosted registry reference server through rco registry serve and static mirror export through rco registry mirror.

Identity

The protocol name is Ricochet Hosted Registry Protocol and the version string is ricochet-hosted-registry-v1. Canonical JSON responses use vendor media types such as application/vnd.ricochet.registry.package.v1+json, and package archives use application/vnd.ricochet.package.archive.v1+gzip.

Package identities match the static registry rules. Unscoped names use ASCII letters, numbers, _, and -. Scoped names use @scope/name with the same segment rules, as in @ricochet/forms. Endpoint paths encode the full package identity as one percent-encoded segment.

Hosted clients fetch discovery before search, install, publish, or yank. A discovery response includes the protocol and registry base URL used for later endpoints and artifact resolution.

{
  "protocol": "ricochet-hosted-registry-v1",
  "base_url": "https://registry.example"
}

Metadata

Version records are immutable once published. A registry rejects any publish for an existing package/version pair, even if the uploaded bytes match the original. Yanking marks a version unavailable without deleting the historical metadata or artifacts.

{
  "protocol": "ricochet-hosted-registry-v1",
  "package": {
    "name": "@ricochet/forms",
    "latest": "0.1.0"
  },
  "versions": [
    {
      "version": "0.1.0",
      "yanked": false,
      "archive": {
        "path": "artifacts/@ricochet/forms/0.1.0/forms-0.1.0.tar.gz",
        "integrity": "sha256:<64 lowercase hex>"
      },
      "package_integrity": "sha256:<64 lowercase hex>",
      "provenance": {
        "attestation_path": "artifacts/@ricochet/forms/0.1.0/provenance.attestation",
        "attestation_integrity": "sha256:<64 lowercase hex>",
        "signature_path": "artifacts/@ricochet/forms/0.1.0/forms.sig",
        "signature_integrity": "sha256:<64 lowercase hex>",
        "signature_kind": "minisign"
      }
    }
  ]
}

Every version has archive.path, archive.integrity, package_integrity, and yanked. Provenance and signature fields are optional, but signature_kind is valid only with signature bytes and signature integrity. Clients preserve unknown signature kinds and report them as unverified unless policy requires a known verifier.

Endpoints

The required endpoint set is intentionally small: GET /v1 for discovery, GET /v1/search for search, GET /v1/packages/{package} for package metadata, GET /v1/packages/{package}/versions/{version} for one immutable record, PUT /v1/packages/{package}/versions/{version} for publish, and POST /v1/packages/{package}/versions/{version}/yank for yanking.

The read client accepts compact search responses with protocol and a packages array. Each result includes a package name and latest non-yanked semver latest; results is accepted as a beta compatibility alias. Hosted search prints the same simple name version style as static search.

{
  "protocol": "ricochet-hosted-registry-v1",
  "packages": [
    {
      "name": "@ricochet/forms",
      "latest": "0.1.0"
    }
  ]
}

Artifact paths are registry-relative. Clients resolve them against the registry base URL discovered from GET /v1, not against the package metadata URL, reject absolute paths, leading slashes, backslashes, . or .. segments, and any resolved URL outside the registry origin, then hash the archive bytes, unpack safely, confirm package name/version, and recompute the unpacked package tree sha256:.

Error responses use an envelope with error.code, error.message, optional details, and request_id. Duplicate publishes return 409 Conflict with a stable code such as version_exists. Rate limits use 429 with Retry-After.

Security

Publish uses rco publish PACKAGE --registry-url https://registry.example --token-env RICOCHET_REGISTRY_TOKEN. Yank uses rco registry yank PACKAGE VERSION --registry-url https://registry.example --token-env RICOCHET_REGISTRY_TOKEN.

Publish and yank requests use bearer tokens resolved from secret references. Future CLI configuration stores references such as token = { secret_env = "RICOCHET_REGISTRY_TOKEN" }, not literal tokens. Tokens must not be written to ricochet.toml, ricochet.lock, reports, or traces.

Mutating requests use client-generated Idempotency-Key headers bound to publisher, method, path, and body digest. Replaying the same key with identical bytes returns the original result; replaying it with different bytes returns 409 Conflict with idempotency_conflict. Registries may also require a fresh Ricochet-Date header.

Published version records stay immutable. Yanking appends availability state projected as yanked, yanked_at, yanked_by, and optionally yank_event_id; it does not rewrite archive paths, integrity fields, provenance, signature data, publisher, or published_at. Clients may cache immutable fields and artifacts for long periods, but package metadata and yank overlays must use validators and short or must-revalidate lifetimes.

Publisher authorization is package-scoped. Clients pin the registry source, package identity, exact version, archive integrity, package tree integrity, provenance integrity, signature integrity, and signature kind in ricochet.lock. Ordinary installs reject same-version hosted metadata or artifact changes that conflict with the lock.

The reference server runs locally with rco registry serve ./hosted-registry --publisher "@ricochet/*=RICOCHET_REGISTRY_TOKEN". It binds to 127.0.0.1:3001 by default, prints the discovered base URL on startup, stores metadata in registry.json, and stores package, provenance, and signature artifacts below artifacts/. --token-env NAME grants one all-package publisher token for local beta workflows; repeated --publisher PACKAGE=ENV policies grant exact packages or @scope/* scopes. The reference server verifies uploaded archive bytes, package tree integrity, manifest package identity/version, provenance/signature digests, idempotency keys, and duplicate-version rejection before persisting metadata.

Mirror Fallback

Hosted metadata exports to the current static format through rco registry mirror REGISTRY_URL PATH. The command writes ricochet-static-registry-v1 indexes, package metadata, registry-relative archive paths, yanked records, and provenance/signature fields so existing static registry clients can keep using --registry-url file://.../index.toml or --registry-url https://.../index.toml.

[registry]
format = "ricochet-static-registry-v1"

[packages]
"@ricochet/forms" = "packages/@ricochet/forms.toml"

Sequence

The hosted read client, publish/yank client, reference server, real-server duplicate publish rejection, and static mirror export slices are implemented.