nova-cache-0.5.0.0: CHANGELOG.md
# Changelog
## 0.5.0.0 - 2026-07-12
- **Signing: fingerprints sort and deduplicate references.** C++ Nix computes and verifies narinfo fingerprints over a sorted, deduplicated store-path set; signing in the narinfo's file order produced signatures real Nix clients reject while nova-cache's own `verify` (recomputing from the same order) passed and masked the divergence. References are now sorted by basename and deduplicated before signing.
- **Removed `NovaCache.Compression`, the `compression` flag, and the `lzma` dependency.** The module had no consumer, and the default-on manual flag made every Hackage install require system liblzma dev files, which plain Windows and minimal Linux machines lack - `cabal install` of downstream packages failed while CI (which pins the flag off inside the repo) stayed green. xz support returns with its first real consumer as a size-bounded decoder suitable for untrusted cache data.
- **Wire-format strictness now matches upstream Nix.** `validateNarInfo` requires the StorePath field to be absolute and references to be bare basenames (the other spellings produce narinfos real clients reject at parse time, and a bare StorePath also derived an empty store dir inside the signed fingerprint); store-path names are ASCII-only and capped at 211 characters; NAR parsing rejects backslashes in entry names (the Windows traversal vector) and nonzero string padding; and key parsing rejects an empty name or empty key material at load time instead of producing signatures no trust anchor can match. New `parseStorePathBaseName` and `parseAbsoluteStorePath` expose the per-field parsers.
- **Upstream-optional narinfo fields are now optional.** Only StorePath, URL, NarHash, and NarSize are required; `Compression` defaults to bzip2 as upstream, and `FileHash`/`FileSize` are `Maybe` (breaking record change, covered by the major bump). Valid narinfos from foreign caches no longer fail to parse over absent optional fields.
- **New `NovaCache.Server` module: the cache's HTTP protocol as a WAI `Application`.** Routing, write authentication, request-body limits, and the narinfo validation/signing pipeline move from the server executable into tested library API. Deployment branding stays out of the library: embedders supply their own root-page response, and the bundled executable carries its landing page itself. Adds `wai` and `http-types` to the library dependencies.
- **`GET /narinfo-hashes` requires the write key and is `Cache-Control: no-store`.** The listing enumerates the whole store - something the public cache protocol deliberately never offers - and lists a directory per hit; it exists only for the push tool, which already holds the write key.
- **NAR bodies no longer transit memory.** Uploads stream to a temp file under a running size cap and rename into place atomically (`NovaCache.Store.writeNarStreaming`); downloads are served from disk via WAI's `responseFile` (`NovaCache.Store.narFilePath`). A multi-GB NAR previously occupied that much RAM per request in both directions.
- **`HEAD` is answered wherever `GET` is.** Clients probing narinfo existence with `HEAD` previously got 404.
- **The server refuses to start when `CACHE_API_KEY` normalizes to empty** (BOM or whitespace only - the copy-paste artifact). An empty armed key would authenticate an empty bearer token.
- **The server refuses to start when a configured signing key fails to load.** It previously logged a warning and ran unsigned, persisting narinfos no trust anchor can verify - the same misconfiguration class the key parser now rejects, closed at the process boundary too.
- **Configurable bind host: `--host` / `HOST`.** The default stays all interfaces, so existing deployments do not silently rebind.
- The deploy workflow pins cloudflared by version and checksum instead of pulling `latest`, and builds the server on GitHub's hosted arm64 image (same Ubuntu as the production box), shipping the binary over the Access tunnel - the production host deliberately carries no compiler toolchain.
- **The sdist ships `NOTICE`.** Apache-2.0 section 4(d) asks redistributions to carry it; the file existed in the repo but not in the released tarball.
- **CI builds from the sdist in isolation**, so tree-vs-tarball divergences (files missing from the tarball, dev-only project settings) fail the pipeline instead of surfacing at install time. CI also compiles the server executable and test suites under `-Werror` on every platform.
- Dropped the server executable's unused `crypton` dependency.
- Workflows run with a read-only `GITHUB_TOKEN`.
## 0.4.2.1 - 2026-06-12
- **Relicensed from BSD-3-Clause to Apache-2.0.** Apache adds an explicit patent grant and trademark terms, and a `NOTICE` file now carries the copyright (Novavero AI Inc.). Earlier releases on Hackage remain under their original licenses.
- **Source is now ASCII-only**, enforced by a CI check. Removes some mis-encoded characters from comments and keeps the tree clean and portable.
- Widened the `containers` upper bound to allow `containers` 0.8.
- Server: the landing page wraps the config snippet and reflows the stats grid on small screens.
## 0.4.2.0 - 2026-06-10
- **Signing: public-key derivation and rendering** - new `toPublicKey`
(derive the `PublicKey` for a `SecretKey`) and `renderPublicKey` (render
the `name:base64` trust-anchor format clients put in
`trusted-public-keys`).
- **Signing: `normalizeKeyText`** - strips byte-order marks and surrounding
whitespace from key text. The server applies it to `CACHE_API_KEY` and
the signing key file, so keys that picked up a BOM or stray CRLF in
transit load byte-clean instead of silently failing auth or signing.
- **Server: the landing page shows the cache public key**, derived at
startup from the live signing key, as a copyable `trusted-public-keys`
line. The published trust anchor can no longer drift from the key in
use.
- README: the public cache key now matches the key the server signs with.
- Removed the CI seed action and its README section; native pushing from
nova-nix replaces it.
## 0.4.1.1 - 2026-06-10
- Documentation-only release: the 0.4.1.0 entry below now records everything
that release actually contained. No code changes.
## 0.4.1.0 - 2026-06-10
- **Server: landing page at `GET /`** - a human-facing page with live stats
(store-path count, signing status, priority), the substituter snippet, and
protocol endpoint documentation. The cache protocol routes are unchanged;
the page is briefly cacheable (`max-age=300, must-revalidate`) since its
stats change as paths are added.
- **Correctness and hardening sweep** (a post-0.4.0.0 audit; note: shipped in
this release without individual entries at tag time - recorded here for the
honest history):
- `Base32.decode` rejects non-canonical encodings (overflow bits must be zero).
- NAR deserialisation is strict on untrusted input: trailing bytes rejected,
regular nodes require a `contents` token, directory entry names must be
sorted, unique, and safe.
- narinfo integer fields parse as strict base-10 (no hex/octal/whitespace),
so a non-canonical `NarSize` can no longer be re-signed under the cache key.
- Hash validation compares decoded digest bytes, accepting any valid
encoding of the same hash.
- Ed25519 verification is name-keyed, matching Nix's lookup-by-name.
- `decompressXz` re-raises asynchronous exceptions, so timeouts can cancel it.
- Server: per-route body caps (narinfo small, NAR large), uploaded narinfos
must name the store path of the request URL, signing failures refuse the
write instead of storing unsigned, and narinfo responses are revalidatable
rather than immutable.
- **Breaking (should have been a major bump; noted for transparency):**
`getCacheInfo` returns a `CacheInfo` record instead of a tuple.
## 0.4.0.0 - 2026-06-08
### Breaking changes
- **`NarInfo` drops the `niSystem` field** - `System` is not part of the Nix
narinfo wire format (Nix ignores unknown keys), so it is removed from the
exported record and `renderNarInfo` no longer emits a `System:` line.
### Security
- **API key required for writes** - the server refuses to start when
`CACHE_API_KEY` is unset, unless `--allow-open-writes` is passed explicitly,
so a missing environment variable can no longer leave the cache
world-writable.
- **Streaming request-body cap** - request bodies are read in bounded chunks
with a running size check, so an unsized (chunked) upload can no longer
buffer past the 100 MB limit into memory before it is rejected.
- **Stricter path validation** - `sanitizePath` now accepts only
`[A-Za-z0-9._+-]` names that are neither dotfiles nor Windows reserved device
names (`nul`, `con`, `com1`...), rejecting device access, alternate-data-stream
syntax, and the temporary-write prefix in addition to traversal.
- **No internal detail in error responses** - store reads tolerate I/O errors
(treated as absence) and any uncaught handler exception maps to a plain 500,
so filesystem paths and error text are never returned to clients.
### Bug fixes
- **CRLF-tolerant narinfo parsing** - lines are split on the first colon with a
trailing carriage return stripped, so narinfo produced by other tools (or
with `\r\n` line endings) parses correctly instead of corrupting text fields
or rejecting valid integer fields.
## 0.3.2.1 - 2026-03-18
- Replace partial `decodeUtf8` with total `decodeLatin1` in `Base64.encode`
and `Signing.sign` - provably safe on base64 ASCII output, eliminates last
partial function usage
- Fix redundant `T.breakOn` call in `NarInfo.parseLine`
- No API changes
## 0.3.2.0 - 2026-03-18
### Server logging
- **Request logging** - Apache Combined format via `wai-extra` middleware,
configurable with `LOG_REQUESTS=0` to disable.
- **Error logging** - auth rejections, validation failures, oversized uploads,
and bad paths now logged to stderr with request method and path context.
- **`withLimitedBody` combinator** - extracted common body-limiting pattern from
PUT handlers, reducing duplication.
- **`notFound` named constant** - replaces three inline 404 responses.
- Fixed `loadSigningKey` warnings going to stdout instead of stderr.
### Seed action fixes
- **Resolve runtime outputs, not derivations** - `nix-instantiate` replaced
with `nix-build --no-out-link` so the cache stores actual binaries instead of
`.drv` build recipes.
- **Filter uploads to diff-only** - `nix copy` exports transitive deps; uploads
now filtered against the missing-hashes diff to avoid re-uploading cached paths.
- **Fix broken pipe** - `find | xargs` with `pipefail` caused spurious failures;
now writes to intermediate files.
- **Fix xargs line-too-long** - large path lists passed via file instead of inline.
- **Diagnostic output** - upload failure HTTP codes now printed; `nix copy` errors
no longer silenced.
- **Parallelism** - per-upload `--max-time` added (120s for NARs, 30s for narinfos).
### Server validation
- **Reject derivation narinfos** - `validateNarInfo` now rejects any narinfo
where StorePath ends in `.drv` with a new `DerivationStorePath` error. Binary
caches serve build outputs, not build recipes.
- 1 new test (75 total)
## 0.3.1.0 - 2026-03-07
### Drop `memory` dependency, use `ram`
- **`memory` -> `ram`** - Replaced `memory` package with `ram` (modern, minimal replacement). Same `Data.ByteArray` API, drops the heavy `basement` transitive dependency. Fixes build failure with `crypton >= 1.1` which switched from `memory` to `ram` internally - `Data.ByteArray.ByteArrayAccess` instances were no longer compatible across packages.
- **`crypton >= 1.1` required** - Lower bound bumped from `1.0` to `1.1` so that `crypton` and `nova-cache` both link against `ram` for `ByteArrayAccess` instances. Older `crypton` versions used `memory`, causing instance mismatches at link time.
- All imports remain `Data.ByteArray` - no source-level changes needed for downstream consumers.
## 0.3.0.0 - 2026-02-28
### Breaking changes
- **`decompressXz`** signature changed from `ByteString -> ByteString` to
`ByteString -> IO (Either String ByteString)` - catches lzma exceptions
instead of crashing on malformed input
- **`writeNarInfo`** / **`writeNar`** now return `IO Bool` instead of `IO ()` -
`False` indicates a rejected path (traversal, empty)
### Security
- **Request body size limit** - PUT endpoints reject bodies over 100 MB with
413 Payload Too Large (prevents memory exhaustion)
- **Constant-time auth comparison** - API key check uses `constEq` from
`Data.ByteArray` instead of `==` (prevents timing side-channel)
### Bug fixes
- **Safe UTF-8 decoding in NAR deserializer** - `decodeUtf8` replaced with
`decodeUtf8'`; malformed UTF-8 in symlink targets or directory entry names
now returns a parse error instead of throwing
### New features
- **`GET /narinfo-hashes`** endpoint - returns all cached narinfo hashes as
newline-delimited text, enabling efficient cache diffing
- **`listNarInfoHashes`** function added to `NovaCache.Store`
### Improvements
- Server logs warnings to stderr when narinfo signing fails (parse error or
sign error) instead of silently returning unsigned body
- Server returns 400 Bad Request when PUT paths fail sanitization instead of
silently discarding the upload
- README: license badge and footer corrected from MIT to BSD-3-Clause
- README: `parallel` input documented in seed action table
- README: `CACHE_API_KEY` and `SIGNING_KEY_FILE` env vars documented
- Seed action: replaced per-path HEAD checks with single `GET /narinfo-hashes`
call and local diff for dramatically faster cache seeding
## 0.2.4.1 - 2026-02-26
- License changed from MIT to BSD-3-Clause
## 0.2.4.0 - 2026-02-25
- New module: `NovaCache.Base64` - base64 encode/decode re-exported so
downstream consumers don't need a direct `base64-bytestring` dependency
- No changes to existing modules
## 0.2.3.0 - 2026-02-23
- Drop `unix` dependency - `checkExecutable` now uses cross-platform
`System.Directory.getPermissions` instead of `System.Posix.Files`
- Fixes Windows build (the `unix` package is not available on Windows)
- No API changes
## 0.2.2.0 - 2026-02-23
- Gate `NovaCache.Compression` and `lzma` dependency behind a `compression`
cabal flag (default on, backwards compatible)
- Consumers that only need hashing/NAR/narinfo can build with `-compression`
to avoid the system `liblzma` C dependency
- Compression tests moved to separate test suite (`nova-cache-compression-test`)
- No changes to any library source modules
## 0.2.1.0 - 2026-02-22
- Server: `validateNarInfo` wired into PUT handler - rejects malformed uploads
with 400 Bad Request and collected validation errors
- Server: `Cache-Control: public, max-age=31536000, immutable` on narinfo and
NAR GET responses for CDN edge caching
- Store: default priority changed from 30 to 50 (community cache fallback
behind cache.nixos.org at 40)
- Seed action: fix round-trip validation to account for server-side signing;
now verifies StorePath field, signature presence, and NAR fetchability
- Public binary cache documented with key and nix.conf instructions
## 0.2.0.0 - 2026-02-22
- New module: `NovaCache.Validate` - pure protocol validation layer
- `ValidationError` sum type with 10 constructors covering sizes, store paths,
hash formats, content hashes, and Ed25519 signatures
- `validateNarInfo` - field semantic validation (non-negative sizes, parseable
store paths/hashes/references), collects all errors instead of short-circuiting
- `validateNarHash` / `validateFileHash` - SHA-256 content hash verification
against declared narinfo values
- `validateSignature` - Ed25519 signature verification against a trusted public
key (at-least-one semantics, matching Nix behaviour)
- `validateFull` - composes all four stages, collecting errors across all
- 17 new tests (74 total across 9 groups)
- No new dependencies
## 0.1.0.0 - 2026-02-21
- Initial release (renamed from gb-nix-cache)
- Nix-base32 encoding/decoding
- SHA-256 hashing with Nix hash formatting
- Store path parsing and rendering
- NAR binary format serialization/deserialization
- NarInfo text format parsing/rendering
- Ed25519 signing and verification
- xz compression/decompression
- Filesystem storage backend
- Optional WAI cache server (behind `server` flag)
- Path traversal protection on all store operations
- Total port parsing (no partial `read`)
- 54 tests across 8 modules