diff --git a/CHANGELOG.md b/CHANGELOG.md index a37b292f..7ebf01c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ # Changelog ## [Unreleased] +## [1.3.0] - 2026-09-06 + ### Added +- **PyPI Simple JSON carries PEP 700 fields (#896)** — the PEP 691 JSON response (`application/vnd.pypi.simple.v1+json`) now also emits `meta.api-version: "1.1"`, a project-level `versions[]` list, and per-file `upload-time` (RFC 3339) and `size` (bytes), so tools like Renovate can compute a minimum release age without fetching every file. `size` is emitted for locally stored artifacts and `upload-time` from the cached upstream dates on the proxy path — each is included where known. - **Hash pins on S3/GCS via object metadata** — the SHA-256 integrity pin is no longer a local-filesystem-only feature. On object-store backends it is written as the user-defined `sha256` object metadata, atomically with the object, and read back on GET/HEAD, so buffered reads verify at rest and raw files get `ETag`, `If-None-Match` (304) and `If-Match` conditional overwrite on every backend. Pins are now a backend concern: the local backend keeps its NDJSON sidecar (same path and format, no migration), the object-store backend keeps object metadata, and the storage wrapper only validates keys and runs the fail-closed verify gate. Objects written before the upgrade carry no metadata and stay open-world until they are rewritten; `nora re-pin` rewrites the object on an object store, since object metadata cannot be changed in place. - **Raw upload integrity via `Repr-Digest` (RFC 9530)** — a raw `PUT` may declare `Repr-Digest: sha-256=:BASE64:`; NORA verifies the received body against it before committing, so a corrupted or truncated upload is rejected with `400` instead of being pinned. The pin itself is always the server-computed hash; the header only gates the commit. A `Repr-Digest` without a sha-256 entry is rejected rather than silently skipped. - **npm serves the abbreviated packument to installers** — `npm install` asks for `application/vnd.npm.install-v1+json`, and NORA ignored it and returned the full document to every client. The packument path now projects to npm's abbreviated shape, keeping the per-version fields an installer actually resolves on (`dependencies`, `os`, `cpu`, `engines`, `peerDependenciesMeta`, `dist`, `deprecated`) and dropping readme, maintainers, repository, per-version `description`, `scripts` and `gitHead`. Measured against a live upstream with every version preserved: lodash 247 652 → 71 989 B (−70.9%), express 804 975 → 344 703 B (−57.2%). The short form is derived locally rather than requested upstream, so exactly one canonical object stays cached per package and a short document can never displace the full one; `Vary: Accept` goes with it, because metadata is `Cache-Control: public` and the body now varies by a request header. An unparsable body is served unchanged rather than turned into an error (#957). +### Changed +- **Compile-time integrity witness on the streaming serve path (#849)** — the streaming artifact serve now routes through a sealed sole-sink whose only constructor takes an EOF-verifying stream, so handing a raw reader to the response body on an integrity path is a compile error — the type-level match of the buffered `verified_body` sink. A blob tampered on disk aborts the body mid-stream (the client gets a broken transfer, never the tampered bytes under a clean `200`) instead of streaming out unverified; explicit partial-content range serves take a separate open-world sink. +- **Registry dispatch is keyed on the `RegistryType` enum (#369)** — dispatch across config, retention, metrics and the UI is now an exhaustive `match RegistryType` generated from one list, instead of scattered string comparisons. Adding a format is a single line and can no longer silently miss a call site (it becomes a compile error). No behavioral change. + +### Security +- **The repository signing key is never enumerated by storage `list()` (#891)** — the OpenPGP signing key at `/.signing/nora.key` (persisted owner-only, `0600`) was swept into every enumeration-based operation because `list()`/`list_with_meta()` excluded only the pin sidecar: `backup` wrote it into the tar at `0644`, `migrate --to s3` copied it into the bucket as a plaintext object, and GC/retention and the browse UI treated it as an artifact. Both backends now exclude the `.signing/` prefix from enumeration, so the key can neither be exfiltrated (tar / object) nor deleted; it is loaded via direct filesystem I/O and never through `list()`, so there is no runtime impact. Provision the key out-of-band. + ### Fixed - **npm rebuilds a missing packument instead of answering 404** — a hosted package whose derived `metadata.json` was absent returned `404` while every published version was still sitting in storage. The reassembly already existed (`regenerate_packument`, which lists `versions/`, `dist-tags/` and `pkg.json`) but only the publish path reached it, so a read fell through to the upstream proxy and 404'd for a package that exists only in this registry. The read path now rebuilds when `versions/` is non-empty, serves the result and re-materializes the packument so the cost is paid once — under the same `publish_lock` as publish, so a fleet stampeding one package rebuilds it once rather than once per request, and before the namespace guard, because serving locally-owned bytes is always allowed while that guard exists to stop the upstream fetch. A name with nothing behind it still returns 404. New `nora_packument_rebuilt_total{registry}`: a non-zero rate means storage was written or restored outside NORA (#956). diff --git a/Cargo.lock b/Cargo.lock index 66c5511f..2bfc0e0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2269,7 +2269,7 @@ dependencies = [ [[package]] name = "nora-registry" -version = "1.2.2" +version = "1.3.0" dependencies = [ "ar", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index d69fda40..1fbc25bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "1.2.2" +version = "1.3.0" edition = "2021" rust-version = "1.88" license = "MIT" diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 01f75cee..39f790aa 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -1,14 +1,14 @@ # Production Docker Compose for NORA Registry -# Usage: NORA_VERSION=1.0.0 docker compose -f deploy/docker-compose.prod.yml up -d +# Usage: NORA_VERSION=1.3.0 docker compose -f deploy/docker-compose.prod.yml up -d # # Required env vars (set in .env or shell): -# NORA_VERSION — image tag (default: 1.0.0) +# NORA_VERSION — image tag (default: 1.3.0) # NORA_PUBLIC_URL — external URL for download links # NORA_AUTH_ENABLED — enable authentication (default: true) services: nora: - image: ghcr.io/getnora-io/nora:${NORA_VERSION:-1.0.0} + image: ghcr.io/getnora-io/nora:${NORA_VERSION:-1.3.0} restart: unless-stopped read_only: true tmpfs: diff --git a/nora-registry/src/openapi.rs b/nora-registry/src/openapi.rs index f5c19778..3de3492f 100644 --- a/nora-registry/src/openapi.rs +++ b/nora-registry/src/openapi.rs @@ -21,7 +21,7 @@ use crate::AppState; #[openapi( info( title = "Nora", - version = "1.2.2", + version = "1.3.0", description = "Multi-protocol package registry supporting Docker, Maven, npm, Cargo, PyPI, Go, Raw, RubyGems, Terraform, Ansible, NuGet, pub.dev, Conan, RPM, and Debian", license(name = "MIT"), contact(name = "The NORA Authors", url = "https://getnora.dev")