nova-cache 0.4.2.0 → 0.4.2.1
raw patch · 10 files changed
+290/−109 lines, 10 filesdep ~containers
Dependency ranges changed: containers
Files
- CHANGELOG.md +62/−55
- LICENSE +196/−23
- README.md +3/−3
- exe/Main.hs +13/−13
- nova-cache.cabal +6/−5
- src/NovaCache/NAR.hs +2/−2
- src/NovaCache/Signing.hs +2/−2
- src/NovaCache/Store.hs +2/−2
- src/NovaCache/Validate.hs +3/−3
- test/Main.hs +1/−1
CHANGELOG.md view
@@ -1,12 +1,19 @@ # Changelog -## 0.4.2.0 — 2026-06-10+## 0.4.2.1 - 2026-06-12 -- **Signing: public-key derivation and rendering** — new `toPublicKey`+- **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+- **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.@@ -18,20 +25,20 @@ - Removed the CI seed action and its README section; native pushing from nova-nix replaces it. -## 0.4.1.1 — 2026-06-10+## 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+## 0.4.1.0 - 2026-06-10 -- **Server: landing page at `GET /`** — a human-facing page with live stats+- **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+ 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,@@ -50,114 +57,114 @@ - **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+## 0.4.0.0 - 2026-06-08 ### Breaking changes -- **`NarInfo` drops the `niSystem` field** — `System` is not part of the Nix+- **`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+- **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+- **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+- **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+ 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+- **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+- **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+## 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+ 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+## 0.3.2.0 - 2026-03-18 ### Server logging -- **Request logging** — Apache Combined format via `wai-extra` middleware,+- **Request logging** - Apache Combined format via `wai-extra` middleware, configurable with `LOG_REQUESTS=0` to disable.-- **Error logging** — auth rejections, validation failures, oversized uploads,+- **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+- **`withLimitedBody` combinator** - extracted common body-limiting pattern from PUT handlers, reducing duplication.-- **`notFound` named constant** — replaces three inline 404 responses.+- **`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+- **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+- **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;+- **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+- **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).+- **Parallelism** - per-upload `--max-time` added (120s for NARs, 30s for narinfos). ### Server validation -- **Reject derivation narinfos** — `validateNarInfo` now rejects any narinfo+- **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+## 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.+- **`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+## 0.3.0.0 - 2026-02-28 ### Breaking changes - **`decompressXz`** signature changed from `ByteString -> ByteString` to- `ByteString -> IO (Either String ByteString)` — catches lzma exceptions+ `ByteString -> IO (Either String ByteString)` - catches lzma exceptions instead of crashing on malformed input-- **`writeNarInfo`** / **`writeNar`** now return `IO Bool` instead of `IO ()` —+- **`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+- **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+- **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+- **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+- **`GET /narinfo-hashes`** endpoint - returns all cached narinfo hashes as newline-delimited text, enabling efficient cache diffing - **`listNarInfoHashes`** function added to `NovaCache.Store` @@ -173,24 +180,24 @@ - 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+## 0.2.4.1 - 2026-02-26 - License changed from MIT to BSD-3-Clause -## 0.2.4.0 — 2026-02-25+## 0.2.4.0 - 2026-02-25 -- New module: `NovaCache.Base64` — base64 encode/decode re-exported so+- 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+## 0.2.3.0 - 2026-02-23 -- Drop `unix` dependency — `checkExecutable` now uses cross-platform+- 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+## 0.2.2.0 - 2026-02-23 - Gate `NovaCache.Compression` and `lzma` dependency behind a `compression` cabal flag (default on, backwards compatible)@@ -199,9 +206,9 @@ - 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+## 0.2.1.0 - 2026-02-22 -- Server: `validateNarInfo` wired into PUT handler — rejects malformed uploads+- 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@@ -211,22 +218,22 @@ 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+## 0.2.0.0 - 2026-02-22 -- New module: `NovaCache.Validate` — pure protocol validation layer+- 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+- `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+- `validateNarHash` / `validateFileHash` - SHA-256 content hash verification against declared narinfo values-- `validateSignature` — Ed25519 signature verification against a trusted public+- `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+- `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+## 0.1.0.0 - 2026-02-21 - Initial release (renamed from gb-nix-cache) - Nix-base32 encoding/decoding
LICENSE view
@@ -1,29 +1,202 @@-Copyright (c) 2026 Novavero AI -All rights reserved.+ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/ -Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Redistributions of source code must retain the above copyright notice,- this list of conditions and the following disclaimer.+ 1. Definitions. -2. Redistributions in binary form must reproduce the above copyright notice,- this list of conditions and the following disclaimer in the documentation- and/or other materials provided with the distribution.+ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document. -3. Neither the name of the copyright holder nor the names of its- contributors may be used to endorse or promote products derived from- this software without specific prior written permission.+ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF-THE POSSIBILITY OF SUCH DAMAGE.+ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
README.md view
@@ -1,12 +1,12 @@ <div align="center"> <h1>nova-cache</h1> <p><strong>The Nix binary cache protocol, in Haskell.</strong></p>-<p>nix-base32, NAR archives, narinfo, store paths, and Ed25519 signing — with an optional WAI cache server. A pure core; IO is confined to the compression, storage, and server boundaries.</p>+<p>nix-base32, NAR archives, narinfo, store paths, and Ed25519 signing - with an optional WAI cache server. A pure core; IO is confined to the compression, storage, and server boundaries.</p> [](https://github.com/Novavero-AI/nova-cache/actions/workflows/ci.yml) [](https://hackage.haskell.org/package/nova-cache) -+ </div> @@ -99,4 +99,4 @@ --- -<p align="center"><sub>BSD-3-Clause · <a href="https://github.com/Novavero-AI">Novavero AI</a></sub></p>+<p align="center"><sub>Apache-2.0 - <a href="https://github.com/Novavero-AI">Novavero AI Inc.</a></sub></p>
exe/Main.hs view
@@ -60,7 +60,7 @@ requestLogEnvVar :: String requestLogEnvVar = "LOG_REQUESTS" --- | Maximum narinfo request body — narinfo is small text, so a tight cap.+-- | Maximum narinfo request body - narinfo is small text, so a tight cap. maxNarInfoBodySize :: Int maxNarInfoBodySize = 4 * 1024 * 1024 -- 4 MB @@ -136,7 +136,7 @@ hPutStrLn stderr $ "FATAL: " ++ apiKeyEnvVar- ++ " is not set — refusing to start with unauthenticated writes. "+ ++ " is not set - refusing to start with unauthenticated writes. " ++ "Set " ++ apiKeyEnvVar ++ ", or pass --allow-open-writes to override (not recommended on a public host)."@@ -177,7 +177,7 @@ -- | WAI application implementing the Nix binary cache HTTP protocol. app :: Config -> Application app cfg req respond = case (requestMethod req, pathInfo req) of- -- GET / — human-facing landing page (the protocol lives at the other routes)+ -- GET / - human-facing landing page (the protocol lives at the other routes) ("GET", []) -> do pathCount <- length <$> listNarInfoHashes (cfgStore cfg) let info = getCacheInfo (cfgStore cfg)@@ -264,7 +264,7 @@ first (T.unlines . map (T.pack . show)) (validateNarInfo ni) -- | Whether the narinfo's declared StorePath actually carries the requested--- hash — so an authenticated writer cannot store a narinfo describing path X+-- hash - so an authenticated writer cannot store a narinfo describing path X -- under path Y's key (a cache-poisoning / confused-deputy shape). narInfoHashMatches :: Text -> NarInfo -> Bool narInfoHashMatches hashKey ni =@@ -335,7 +335,7 @@ -- | Sign a validated 'NarInfo' if a signing key is configured. ----- With no key, returns the unsigned rendering (intentional — the operator+-- With no key, returns the unsigned rendering (intentional - the operator -- configured none). With a key, FAILS CLOSED: a signing error returns 'Left' -- so the handler refuses the write rather than persisting an unsigned narinfo -- on a cache that is supposed to sign.@@ -389,8 +389,8 @@ -- | The landing page served at @GET /@. Static apart from four live -- values (store-path count, store dir, signing status, public key); styled--- to match novavero.ai. Nothing user-supplied is interpolated — the key--- line is operator configuration — so no escaping is needed.+-- to match novavero.ai. Nothing user-supplied is interpolated - the key+-- line is operator configuration - so no escaping is needed. landingHtml :: CacheInfo -> Bool -> Maybe Text -> Int -> Text landingHtml info signingEnabled pubKey pathCount = T.unlines@@ -400,7 +400,7 @@ "<meta charset=\"UTF-8\" />", "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />", "<title>cache.novavero.ai - Nix binary cache</title>",- "<meta name=\"description\" content=\"The Novavero Nix binary cache, serving store paths for nova-nix — the Windows-native Nix.\" />",+ "<meta name=\"description\" content=\"The Novavero Nix binary cache, serving store paths for nova-nix - the Windows-native Nix.\" />", "<link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml," <> novaveroLogoSvgEscaped <> "\" />", "<style>", "* { margin: 0; padding: 0; box-sizing: border-box; }",@@ -413,13 +413,13 @@ ".brand svg { width: 44px; height: 44px; border-radius: 10px; }", "h1 { color: #fff; font-size: 1.6rem; font-family: ui-monospace, Consolas, monospace; }", ".tagline { color: #9ca3af; margin-bottom: 2.5rem; }",- ".stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 2.5rem; }",+ ".stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 2.5rem; }", ".stat { padding: 16px; border: 1px solid #232733; border-radius: 12px; text-align: center; }", ".stat .value { color: #fff; font-size: 1.4rem; font-weight: 600; }", ".stat .label { color: #6b7280; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }", "h2 { color: #fff; font-size: 1rem; margin: 2rem 0 0.5rem; }", "p { font-size: 0.9rem; margin-bottom: 0.75rem; }",- "pre { background: #0d0f14; border: 1px solid #232733; border-radius: 8px; padding: 14px; overflow-x: auto; margin: 0.75rem 0; }",+ "pre { background: #0d0f14; border: 1px solid #232733; border-radius: 8px; padding: 14px; white-space: pre-wrap; word-break: break-all; margin: 0.75rem 0; }", "code { font-family: ui-monospace, Consolas, monospace; font-size: 0.85rem; color: #e5e7eb; }", ".footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid #232733; font-size: 0.85rem; color: #6b7280; }", "</style>",@@ -427,7 +427,7 @@ "<body>", "<div class=\"container\">", "<div class=\"brand\">" <> novaveroLogoSvg <> "<h1>cache.novavero.ai</h1></div>",- "<p class=\"tagline\">Nix binary cache — serving store paths for <a href=\"https://github.com/Novavero-AI/nova-nix\">nova-nix</a>, the Windows-native Nix.</p>",+ "<p class=\"tagline\">Nix binary cache - serving store paths for <a href=\"https://github.com/Novavero-AI/nova-nix\">nova-nix</a>, the Windows-native Nix.</p>", "<div class=\"stats\">", "<div class=\"stat\"><div class=\"value\">" <> T.pack (show pathCount) <> "</div><div class=\"label\">store paths</div></div>", "<div class=\"stat\"><div class=\"value\">" <> (if signingEnabled then "ed25519" else "off") <> "</div><div class=\"label\">signing</div></div>",@@ -493,8 +493,8 @@ textHeaders = [(HTTP.hContentType, "text/plain")] -- | Content-Type and caching headers for a narinfo response.--- A narinfo body is NOT immutable for a fixed key — re-uploading the same store--- path to add or rotate a signature changes it — so it is cacheable but must+-- A narinfo body is NOT immutable for a fixed key - re-uploading the same store+-- path to add or rotate a signature changes it - so it is cacheable but must -- stay revalidatable (no @immutable@). narInfoHeaders :: HTTP.ResponseHeaders narInfoHeaders =
nova-cache.cabal view
@@ -1,14 +1,15 @@ cabal-version: 3.0 name: nova-cache-version: 0.4.2.0+version: 0.4.2.1 synopsis: Pure-first Nix binary cache protocol library description:- A pure-first library implementing the Nix binary cache protocol —+ A pure-first library implementing the Nix binary cache protocol - nix-base32, NAR serialization, narinfo parsing, Ed25519 signing, store- path handling, and content validation — with an optional WAI server.+ path handling, and content validation - with an optional WAI server. -license: BSD-3-Clause+license: Apache-2.0 license-file: LICENSE+copyright: 2026 Novavero AI Inc. author: Devon Tomlin maintainer: devon.tomlin@novavero.ai homepage: https://github.com/Novavero-AI/nova-cache@@ -51,7 +52,7 @@ base >= 4.16 && < 5 , base64-bytestring >= 1.2 && < 1.3 , bytestring >= 0.11 && < 0.13- , containers >= 0.6 && < 0.8+ , containers >= 0.6 && < 0.9 , crypton >= 1.1 && < 2 , directory >= 1.3 && < 1.4 , filepath >= 1.4 && < 1.6
src/NovaCache/NAR.hs view
@@ -211,7 +211,7 @@ pure (NarRegular False contents, final) | otherwise = -- 'contents' is mandatory (even an empty file serialises with it), so- -- a regular node without it is malformed — reject, matching Nix.+ -- a regular node without it is malformed - reject, matching Nix. Left ("expected 'executable' or 'contents' in regular, got: " ++ show tok) -- | Parse a symlink node.@@ -376,7 +376,7 @@ -- Not a symlink, directory, or regular file: a special file (FIFO, -- socket, device) or a path that vanished mid-walk. Fail loudly rather -- than fabricating an empty regular (which would silently change the NAR- -- and its hash) — matching Nix, which aborts on unsupported types.+ -- and its hash) - matching Nix, which aborts on unsupported types. fail ("serialiseFromPath: not a regular file (special or vanished): " ++ path) -- | Check whether a file has the executable permission set.
src/NovaCache/Signing.hs view
@@ -91,7 +91,7 @@ -- | Normalize key text picked up from an environment variable or a file: -- drop byte-order marks and surrounding whitespace. 'T.strip' alone is not--- enough — a BOM is not whitespace, so it survives stripping and silently+-- enough - a BOM is not whitespace, so it survives stripping and silently -- corrupts the key. None of the dropped characters can be legitimate key -- material, so normalizing is always safe. normalizeKeyText :: Text -> Text@@ -121,7 +121,7 @@ sk <- cryptoSecretKey (BS.take ed25519SeedSize secretBytes) pure PublicKey {pkName = keyName, pkBytes = convert (Ed25519.toPublic sk)} --- | Render a public key in Nix's @name:base64@ trust-anchor format — the+-- | Render a public key in Nix's @name:base64@ trust-anchor format - the -- exact string a client puts in @trusted-public-keys@. renderPublicKey :: PublicKey -> Text renderPublicKey (PublicKey keyName bytes) =
src/NovaCache/Store.hs view
@@ -184,7 +184,7 @@ -- dot and are not a Windows reserved device name. This rejects directory -- separators, @.@\/@..@ traversal, dotfiles (including the temp-write prefix), -- NUL bytes, alternate-data-stream (@name:stream@) syntax, and device names--- like @nul@ — so a client-supplied hash or NAR filename can never escape the+-- like @nul@ - so a client-supplied hash or NAR filename can never escape the -- store directory or resolve to a device, on any platform. sanitizePath :: Text -> Maybe FilePath sanitizePath txt@@ -217,7 +217,7 @@ -- | Write a file atomically via write-to-temp-then-rename. -- -- Writes to a temporary file in the same directory, then renames to--- the target path. On POSIX, @rename@ is atomic — readers see either+-- the target path. On POSIX, @rename@ is atomic - readers see either -- the old content or the new content, never a partial write. -- Cleans up the temporary file on failure. atomicWriteFile :: FilePath -> ByteString -> IO ()
src/NovaCache/Validate.hs view
@@ -2,7 +2,7 @@ -- -- Validates narinfo field semantics (sizes, store paths, hash formats), -- NAR/file content hashes against declared values, and Ed25519 signatures.--- All functions are pure — no IO, no side effects.+-- All functions are pure - no IO, no side effects. module NovaCache.Validate ( ValidationError (..), validateNarInfo,@@ -47,7 +47,7 @@ SignatureInvalid !Text | -- | The narinfo has zero signatures. NoSignatures- | -- | StorePath is a derivation (.drv) — binary caches serve build outputs only.+ | -- | StorePath is a derivation (.drv) - binary caches serve build outputs only. DerivationStorePath !Text deriving (Eq, Show) @@ -120,7 +120,7 @@ -- --------------------------------------------------------------------------- -- | Validate signatures against a trusted public key. At least one signature--- must verify (matches Nix behaviour — any trusted key suffices).+-- must verify (matches Nix behaviour - any trusted key suffices). -- Returns 'Left [NoSignatures]' when there are no signatures at all. validateSignature :: PublicKey -> NarInfo -> Either [ValidationError] () validateSignature pk ni
test/Main.hs view
@@ -157,7 +157,7 @@ test "decode invalid character" $ assertLeft "invalid char" (Base32.decode "hello!"), test "encode length for 32 bytes" $- -- 32 bytes -> ceil(32*8/5) = ceil(51.2) = 52 chars+ -- 32 bytes gives ceil(32*8/5) = ceil(51.2) = 52 chars let bs = BS.replicate 32 0x42 encoded = Base32.encode bs in assertEqual "encoded length" 52 (T.length encoded),