nova-cache 0.2.0.0 → 0.2.1.0
raw patch · 6 files changed
+67/−10 lines, 6 files
Files
- CHANGELOG.md +12/−0
- README.md +21/−1
- exe/Main.hs +29/−6
- nova-cache.cabal +1/−1
- src/NovaCache/Store.hs +3/−1
- test/Main.hs +1/−1
CHANGELOG.md view
@@ -1,5 +1,17 @@ # Changelog +## 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
README.md view
@@ -232,7 +232,27 @@ | `PUT` | `/<hash>.narinfo` | Upload narinfo | | `PUT` | `/nar/<file>` | Upload NAR file | -### Use as a Nix Substituter+### Public Binary Cache++A free, public binary cache is available at `cache.novavero.ai`. Add it to+your Nix configuration:++```nix+# nix.conf or /etc/nix/nix.conf+extra-substituters = https://cache.novavero.ai+extra-trusted-public-keys = cache.novavero.ai-1:2yJK0UZWlDDTpThzEdqfGWaj+j3ljOCGoA50Ims47dM=+```++Or use it directly:++```bash+nix build --substituters "https://cache.nixos.org https://cache.novavero.ai" \+ --trusted-public-keys "cache.nixos.org-1:DLD/YGKmo6OceLp6RsiGCbi5FwMExRzJcoJKanMPe/Q= cache.novavero.ai-1:2yJK0UZWlDDTpThzEdqfGWaj+j3ljOCGoA50Ims47dM="+```++### Self-Hosted Cache++Run your own instance: ```bash # Push to your cache
exe/Main.hs view
@@ -12,6 +12,7 @@ import NovaCache.NarInfo (NarInfo (..), parseNarInfo, renderNarInfo) import NovaCache.Signing (SecretKey, parseSecretKey, sign) import NovaCache.Store (FileStore, getCacheInfo, newFileStore, readNar, readNarInfo, writeNar, writeNarInfo)+import NovaCache.Validate (validateNarInfo) import System.Environment (getArgs, lookupEnv) import Text.Read (readMaybe) @@ -119,14 +120,21 @@ respond (responseLBS HTTP.status200 octetHeaders (BL.fromStrict content)) Nothing -> respond (responseLBS HTTP.status404 textHeaders "not found")- -- PUT /<hash>.narinfo (auth required)+ -- PUT /<hash>.narinfo (auth required, validated) ("PUT", [hashNarinfo]) | Just hashKey <- T.stripSuffix ".narinfo" hashNarinfo -> requireAuth cfg req respond $ do body <- BL.toStrict <$> strictRequestBody req- let signed = signNarInfo (cfgSigningKey cfg) body- writeNarInfo (cfgStore cfg) hashKey signed- respond (responseLBS HTTP.status200 textHeaders "ok")+ case parseNarInfo (TE.decodeUtf8 body) of+ Left err ->+ respond (badRequest (T.pack err))+ Right ni -> case validateNarInfo ni of+ Left errs ->+ respond (badRequest (T.unlines (map (T.pack . show) errs)))+ Right _ -> do+ let signed = signNarInfo (cfgSigningKey cfg) body+ writeNarInfo (cfgStore cfg) hashKey signed+ respond (responseLBS HTTP.status200 textHeaders "ok") -- PUT /nar/<file> (auth required) ("PUT", ["nar", fileName]) -> requireAuth cfg req respond $ do@@ -189,14 +197,29 @@ boolText True = "1" boolText False = "0" +-- | 400 Bad Request with a text error message.+badRequest :: Text -> Response+badRequest msg = responseLBS HTTP.status400 textHeaders (BL.fromStrict (TE.encodeUtf8 msg))+ -- | Content-Type: text/plain headers. textHeaders :: HTTP.ResponseHeaders textHeaders = [(HTTP.hContentType, "text/plain")] -- | Content-Type: application/x-nix-narinfo headers.+-- Narinfo files are content-addressed (keyed by store path hash) and+-- immutable once written, so they are safe to cache indefinitely at the+-- CDN edge. narInfoHeaders :: HTTP.ResponseHeaders-narInfoHeaders = [(HTTP.hContentType, "text/x-nix-narinfo")]+narInfoHeaders =+ [ (HTTP.hContentType, "text/x-nix-narinfo"),+ (HTTP.hCacheControl, "public, max-age=31536000, immutable")+ ] -- | Content-Type: application/octet-stream headers.+-- NAR files are content-addressed (keyed by content hash) and immutable+-- once written, so they are safe to cache indefinitely at the CDN edge. octetHeaders :: HTTP.ResponseHeaders-octetHeaders = [(HTTP.hContentType, "application/octet-stream")]+octetHeaders =+ [ (HTTP.hContentType, "application/octet-stream"),+ (HTTP.hCacheControl, "public, max-age=31536000, immutable")+ ]
nova-cache.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: nova-cache-version: 0.2.0.0+version: 0.2.1.0 synopsis: Pure Nix binary cache protocol library description: A focused, minimal, pure-first library implementing the full Nix binary
src/NovaCache/Store.hs view
@@ -53,8 +53,10 @@ narSubdir = "nar" -- | Default cache priority (lower = preferred by Nix).+-- Community caches should use a higher number than cache.nixos.org (40)+-- so the official cache is preferred and we serve as a fallback. defaultPriority :: Int-defaultPriority = 30+defaultPriority = 50 -- | Default Nix store directory. defaultStoreDir :: Text
test/Main.hs view
@@ -553,7 +553,7 @@ removeDirectoryRecursive tmpDir ok1 <- assertEqual "storeDir" "/nix/store" storeDir ok2 <- assertTrue "wantMassQuery" wantMass- ok3 <- assertEqual "priority" 30 priority+ ok3 <- assertEqual "priority" 50 priority pure (ok1 && ok2 && ok3), test "sanitizePath rejects traversal" $ assertEqual "dotdot" Nothing (Store.sanitizePath ".."),