Binary cache
proxy/ is a Vercel edge function. A WebAssembly module compiled from Rust,
proxy/src/lib.rs, makes every decision, and the JavaScript around it only
fetches. It serves four things from this project's GHCR namespace:
- A Nix binary cache.
tools/nix-cache-pushpushes each store path as one OCI artifact,nix-cache:<store hash>, holding its signed narinfo and its NAR. The proxy answers/<hash>.narinfo, with itsURL:pointed back at the proxy, and redirects/nar/<hash>/<file>to GHCR's blob storage, so a NAR never passes through it. A realnix copyonce pulled and verified a signed path through it against a fake registry. The automated tests inproxy/test/run the handler against a fake fetch that answers like the registry, with no Nix involved. The OCI artifacts do not require a hosted proxy to consume: a client withorascan pull an individual path directly from GHCR by its store hash, and serve the resulting signed narinfo/NAR locally. LosOS's installer uses this path, so cache substitution works without deployingproxy/to Vercel. - Build caches.
/build-cache/<name>/<arch>/<part>redirects to one part of a compiler cache that CI keeps in GHCR. The one cache,losos, is for the overlay's patched GTK and Qt and lives atlosos-ccache:<arch>. It is a zstd tarball split into 4 GB parts, starting atccache.tar.zst.part-00.tools/build-cache-fetchfetches and unpacks a cache through the proxy first, and from GHCR with oras when the proxy has nothing. - Updates.
/updates/<channel>/<arch>/<file>serves a file ofimages:<channel>-<arch>, which CI's publish job moves to each release that passed verification. WithLOSOS_PROXY_URLset, CI setslosos.update.baseUrlto<proxy>/updates/<channel>/<arch>/, so sysupdate fetches manifests and images through the proxy. The channels are the release cadences,nightly,weekly,monthlyandyearly. Without the variable, images update from each channel's newest GitHub release instead. They go through an index on the documentation site and a service on the machine that joins split files, as Releases explains. - The web flasher's files.
/flasher/<channel>/<arch>/<file>serves the same release's_gsi-files andSHA256SUMS. It streams them one byte range at a time instead of redirecting, because a page on another origin cannot read GHCR's storage. Web flasher has the details. Every read the proxy answers carriesAccess-Control-Allow-Origin: *.
CI does not read the cache through the public deployment. Each build job
runs the same handler with node on its own runner, proxy/serve.ts at
http://127.0.0.1:8484, and gives Nix that as its substituter. A run then
gets what earlier runs pushed whether or not the deployment is up. The
packages are public, so it needs no token. Any machine can do the same:
cd proxy && sh build.sh
GHCR_REPOSITORY=losos-project/desktop node --experimental-strip-types serve.ts 127.0.0.1:8484
To use it for a build:
substituters = https://cache.nixos.org/ https://<proxy>/ # or http://127.0.0.1:8484/
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= <the public half of NIX_CACHE_SIGNING_KEY>
Keep cache.nixos.org in the substituter list when adding the project cache. Without the optional project cache, Nix's default cache.nixos.org remains enabled.
Five things are set up once, outside the repository:
-
Vercel (optional for the binary cache). A deployment with root directory
proxy/andGHCR_REPOSITORY=dasmatus/losos-desktophosts the proxy's update, flasher, ping and shared-cache HTTP routes. Direct Nix cache consumers can pullnix-cache:<store hash>from GHCR withorasand do not need this deployment for cache substitution.vercel.jsonhas the rest. Its build installs a pinned Rust with rustup when Rust is missing, and adds the WebAssembly target when it is there. -
GHCR. Make the
losos-desktop/nix-cache,losos-desktop/imagesandlosos-desktop/losos-ccachepackages public once CI has created them. Or put a read-only token in Vercel asGHCR_TOKEN, with its owner's GitHub login asGHCR_USERNAME. -
A signing key. Run
nix key generate-secret --key-name losos-desktop-1and store the result as the secretNIX_CACHE_SIGNING_KEY. Store its public half, fromnix key convert-secret-to-public, as the variableNIX_CACHE_PUBLIC_KEY. -
LOSOS_PROXY_URL(optional). Set the deployment URL without a trailing slash when images should fetch updates from the hosted proxy. It is not required to publish or consume Nix cache artifacts: CI reads the GHCR cache throughproxy/serve.tson its own runner, and appliances can pull store-path artifacts directly withoras. Flake evaluation reads this variable for the optional update URL, so CI passes--impurewhen it builds, checks and collects the release. CI uploads signed cache paths to GHCR wheneverNIX_CACHE_SIGNING_KEYis set, whether or not this URL is configured. -
The update signing key, which every image trusts and every release is signed with. Make it on a machine you trust, with no passphrase, because CI signs unattended:
export GNUPGHOME=$(mktemp -d)mkdir -p nixos/keysgpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key 'LosOS Desktop updates' ed25519 sign nevergpg --armor --export > nixos/keys/update-signing.ascgpg --armor --export-secret-keys # paste into the secret UPDATE_SIGNING_KEYrm -rf "$GNUPGHOME" # once the secret and an offline copy are savedCommit
nixos/keys/update-signing.asc, whichlosos.update.pubringreads. sysupdate verifies every update against it. Put the key's fingerprint, fromgpg --show-keys --with-colons nixos/keys/update-signing.asc, inUPDATE_SIGNING_FPRinci.yml. Thepushjob fails without the secret, or with a secret for a different key, instead of publishing a release the images would refuse, and theformatjob fails a pull request whoseUPDATE_SIGNING_FPRis not the key innixos/keys/update-signing.asc. Keep an offline copy of the secret half. An image trusts only the keys it shipped with, so if the key is lost, every installed machine stops taking updates until it is reinstalled. A key that is still at hand can be replaced, as the next section says.
Before setting LOSOS_PROXY_URL, check the public deployment without a
Vercel login. /nix-cache-info must return the cache metadata, an absent
store hash must return 404 rather than 502 token: 403, and a published
release's /updates/<channel>/<arch>/SHA256SUMS must be readable. A ready
deployment alone does not prove GHCR access works. The variable also changes
the pm image's update source, so leave it unset while the registry is
inaccessible. Vercel deployment protection must allow anonymous clients.
Replacing the update key
A machine checks an update against the keys of the version it is running, not the one it is installing. So a new key reaches machines inside a release the old key signed, and only then may it sign one itself:
- Make the new key as in step 5 above, but write its public half to
nixos/keys/update-signing-next.asc, and keep its secret half offline for now. Merge that.losos.update.pubringreads both files, so every release from then on trusts both keys, and the old key still signs them. - Wait until the machines have installed one of those releases. A machine takes the newest release of its cadence, so each cadence has to publish one, the yearly one last.
- In one pull request, move
update-signing-next.ascoverupdate-signing.ascand put the new fingerprint inUPDATE_SIGNING_FPRinci.yml. Once it merges, put the new secret half inUPDATE_SIGNING_KEY. A run in between fails atpush, which compares the secret with the fingerprint, and publishes nothing.
A machine that installed none of the releases from step 1 refuses every release after step 3, and only a reinstall from a new installer brings it back. Leave enough time in step 2 for the machines that update least often.