Changes touching this path
- installer: serve the R2 base first, and turn the curl | sh gate back on (#808)
The hero one-liner has been returning 404 in production. The site's /install.sh proxy
fetched the installer from GitHub anonymously, and the repo is private permanently
(ADR 0045), so there was nothing to fetch. #807 put the artifacts on R2; this points
the installer at them.
The proxy now reads upstream from dl.millerbyte.com and rewrites the generated
ARTIFACT_DOWNLOAD_URLS default to list the R2 base FIRST and GitHub second. dist's
installers already try each base in order, so the fallback costs nothing and keeps
working for anyone with credentials.
⭐ The rewrite derives the version from the line it is rewriting. The anchor already
contains the release tag, so capturing it means the site needs no version knowledge,
no 'latest' pointer to resolve and no credential — a test moves the tag and the R2
base follows.
⚠ The two installers genuinely differ and one regex would have silently matched
neither: sh builds a space-separated string, ps1 builds an array. The ps1 anchor is
pinned to the ASSIGNMENT rather than the URL, because the same URL also appears in
that script's help text — a loose match would rewrite the comment, leave the real
download list GitHub-only, and still pass every check that greps for the R2 base.
The rewrite THROWS if the anchor is absent. A silent no-op is the current bug wearing
a different hat: it would ship an installer pointing only at URLs that 404 for exactly
the audience the one-liner exists for, while the route returns 200 and the bytes look
like a real script. Nine tests cover both installers, both drift cases, and the
help-text trap; the anchors were also validated against the real 54 KB v0.4.2 scripts.
publish-r2 now also writes unversioned installer copies to /installers/. That is the
one deliberately mutable object in the bucket — every v{VERSION}/ prefix stays
immutable and year-cached, but 'latest' has to move or the one-liner pins itself to
whichever release shipped first. It carries max-age=300, matching the proxy.
smoke-install.yml loses its `if: false` and now defaults to the hero path rather than
a GitHub-direct URL. Testing a URL no user is told to run proved nothing; this
exercises the whole chain — proxy, rewrite, R2. The skip comment is kept as history,
because why it was disabled is why the gate is worth having.
6bf1df11 · dbf3dbe6… - the download hop lands as a counted redirect that cannot be steered, and the two things 1647 asks for that do not exist yet are named rather than faked: GET /dl/tag/asset records a start and 302s to R2. the target is CONSTRUCTED from two validated segments and no byte of the request appears in it, because /dl sits on the hostname that also hosts the identity flow and the install one-liner - a redirect an attacker can steer is a phishing primitive wearing our provenance, a link that genuinely starts at our domain and ends wherever they chose. validating and then echoing is one refactor away from that; building from a fixed base plus two matched shapes is not.
the segments are read from the RAW pathname rather than decoded route params, so this route decides what decoding means instead of inheriting it. every refusal is 404 rather than 400, because a 400 separates malformed from no-such-thing and telling a prober which guess was closer is free help. sha256.sum and the installer scripts are refused even though they are real files at that prefix: the hop counts download STARTS, and the land-time site gate fetches sha256.sum from R2 on every land - admitting it would make every land a download.
⭐ a mutation found one of my own guards doing nothing. deleting the explicit percent check turned NO test red, because the anchored patterns already reject percent - so the comment claiming it comes first because patterns are the fragile direction was describing a guard by what it looks like rather than by what it does. the code stays as a second fence and the comment now says it is one. unanchoring the tag pattern turns two tests red, which is the anchors earning their place, and the absolute-URL cases stay green under that mutation because the slash check is what catches them - a more precise picture than I had.
the write-behind queue is built here because 1647 is blocked on it and it does not exist; 1757 shares it. enqueue is synchronous and never awaits, because this path serves the install one-liner where the caller is a shell script already piped to sh - a database stall there is the most expensive on the site and the least recoverable. bounded, drops the NEWEST so a burst cannot evict what preceded it, counts every drop because a silently discarded row makes the dashboard wrong in the direction that looks like not much traffic, and a failed flush puts the rows BACK rather than losing a batch to a blip.
⚠ NO visitor_key, and that is a deviation stated rather than a gap missed. 1647 asks for one derived from X-Real-IP as the beacon does - but the beacon owns the daily salt and neither exists, and deriving one here would make the privacy notice statement that these records carry no address, no cookie, no key FALSE the day it deploys. downloads-per-day needs no identifier; uniques do, and they arrive with 1757.
⚠ only ONE of the two internal arms is reachable. a request with no X-Real-IP never passed nginx so it is ours by definition, which covers the deploy probes. the edge computes loot_internal for our own hosts but forwards only Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto, so install-detect - which runs the real one-liner through the public URL once per release - is counted as an ordinary download. a handful of rows per release rather than a distortion, and naming it beats a comment implying both arms work.
EventRow source widens from log to the set migration 0001 actually permits. it said log while the shipper was the only producer, which read as a fact about the schema and was a fact about the caller. /dl joins the prerender deny list, or crawlLinks follows the new links off /install and the BUILD counts a download per platform per land (#1647)
db35af90 · dbf3dbe6…diff - every compiled-in and current-tense loot host moves to loot.build (map #2412, ADR 0099): loot --help names security@loot.build and https://loot.build/trust, ALPHA_PROMISE_URL is https://loot.build/install and the telemetry DEFAULT_ENDPOINT https://loot.build/api/telemetry, so the next release carries them; the site install one-liners, R2_BASE and HOP_BASE (https://dl.loot.build, https://loot.build/dl), the metrics SITE_SCOPE and own-host referrer, the disclosure links on trust, terms, privacy and known-issues, and the install, quickstart and guides pages follow, with the privacy and terms markers moved to 25 September 2026 and re-pinned since readers see the new address; README, CONTEXT.md (the forge door now forge.loot.build), the release checklist, site CI and test fixtures follow, and the two specs carry a note instead of a partial rewrite. the destination census gains build in WEB_TLDS, without which security@loot.build would be invisible to its bare-host clause, and exempts the two shipped lines that tld makes read as hosts (state.build, r.build()) by their exact text; ADR 0074 section 9 says twelve-entry to match. 37 lines still name millerbyte.com outside ADR bodies, evidence, research and scratch, each kept on purpose: dated history, millerbyte.com the site and its @millerbyte/ui tokens, the two noted specs, and relay-era tooling with no successor host, filed as #2438. census red with build dropped from WEB_TLDS, restored; site gate 819 passed and within budget; workspace suite 4559 passed over 142 binaries, 13 ignored (#2418)
c5663115 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.