Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `<storage.path>/.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).

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
]

[workspace.package]
version = "1.2.2"
version = "1.3.0"
edition = "2021"
rust-version = "1.88"
license = "MIT"
Expand Down
6 changes: 3 additions & 3 deletions deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion nora-registry/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading