diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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>
 
 [![CI](https://github.com/Novavero-AI/nova-cache/actions/workflows/ci.yml/badge.svg)](https://github.com/Novavero-AI/nova-cache/actions/workflows/ci.yml)
 [![Hackage](https://img.shields.io/hackage/v/nova-cache.svg)](https://hackage.haskell.org/package/nova-cache)
 ![GHC](https://img.shields.io/badge/GHC-9.8-purple)
-![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)
+![License](https://img.shields.io/badge/license-Apache--2.0-blue)
 
 </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>
diff --git a/exe/Main.hs b/exe/Main.hs
--- a/exe/Main.hs
+++ b/exe/Main.hs
@@ -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 =
diff --git a/nova-cache.cabal b/nova-cache.cabal
--- a/nova-cache.cabal
+++ b/nova-cache.cabal
@@ -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
diff --git a/src/NovaCache/NAR.hs b/src/NovaCache/NAR.hs
--- a/src/NovaCache/NAR.hs
+++ b/src/NovaCache/NAR.hs
@@ -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.
diff --git a/src/NovaCache/Signing.hs b/src/NovaCache/Signing.hs
--- a/src/NovaCache/Signing.hs
+++ b/src/NovaCache/Signing.hs
@@ -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) =
diff --git a/src/NovaCache/Store.hs b/src/NovaCache/Store.hs
--- a/src/NovaCache/Store.hs
+++ b/src/NovaCache/Store.hs
@@ -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 ()
diff --git a/src/NovaCache/Validate.hs b/src/NovaCache/Validate.hs
--- a/src/NovaCache/Validate.hs
+++ b/src/NovaCache/Validate.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -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),
