Changes touching this path
- the trust matrix says what each reader can actually read, and /why stops claiming the relay cannot read your code (#1130)
Publishes /trust as the one canonical answer to who reads what: six readers
against four tiers, with a regardless-of-tier block for path names and DAG
metadata, and footnotes on the two cells whose plain answer misleads —
Restricted vs the git mirror rests on ADR 0028 projection omission rather than
keys, and an embargo reveal is enforced by the relay clock rather than by maths
(ADR 0027, written down nowhere until now).
#1089 falls out of the matrix as its ticket predicted: the /why headline was
false over ordinary code, since Internal is the default and a relay reads it by
design. Every other surface now cites /trust instead of answering independently,
which is what let /why and /docs drift into contradicting each other.
ADR 0069 records that Internal stays the default deliberately — Restricted by
default would make a clone readable by nobody and empty the git projection.
ADR 0068 records the at-rest design for #1138: .loot/keyring is raw oid-key
pairs beside the ciphertext it opens, so the unit of exposure is the whole
.loot/ directory.
Carries ADR 0067 from #1132, which shared this tree uncaptured.
361b2026 · dbf3dbe6… - the Known Issues page publishes what costs a tester their work, nine findings re-run against v0.4.16, and it is updated on report against a dated last-review rather than a cadence nobody keeps (#1143)
48f5788f · dbf3dbe6…diff - the known-issues pin becomes four tests that each say what broke, the phrase-issuer guard asserts the whole public surface of the mnemonic module instead of guessing a function name, and the trust versus known-issues split has one canonical copy the other sites point at (#1217)
f751adc3 · dbf3dbe6…diff - the landing page leads with the claim and the receipt arrives on scroll: the hero opens on Commit your .env with the mechanism as its subhead, the panes move below the fold as the first thing there, and the three edges and the invite-only paragraph move to the pages where those questions are actually asked
fbdc00bf · dbf3dbe6…diff - the run's own signature defect, found inside the file it built to fix it: source_walk said four consumers while the very next land added a fifth, and the sentence is now generated rather than corrected - it walks src and tests through the module's own code mask so a mention in a comment or a string is not a call, groups by enclosing function because the destination census reaches the walk through a use import and a call-site count would miss it entirely, and pins the shape as well as the number by requiring every consumer's file be named, the declaring units equal the consuming units, and every consumer call the discrimination floor first. that last requirement is where this ticket was wrong: it said one consumer skipped the floor and that two others called it, but the crate held exactly two call sites, so three of five skipped it - all five call it now, and the reason is recorded at the verbs site, since its three anchors check that this walk found Workspace's methods while the shared floor checks the walker on the two cases it was written for and this tier has none of. ADR 0074's section 9 stops describing a walk that no longer exists: only the mechanism sentence moves, with a dated amendment quoting the old text verbatim and saying why it was worse than stale, because it named a bug the walk did not have while carrying two it did, while the purpose paragraph and the six-entry allowlist claim are untouched. the trust page's twelve-item enumeration goes, and the durable half is a fourth pinned surface beside install, the ADR and CONTEXT: the positive arm pulls line three out of ADR 0066 itself and requires both pronoun-free halves verbatim rather than typing them, the negative arm forbids a code element inside the alpha section because that is the regrowth shape twice observed, and both were proved red before being trusted. two stale copies this ticket did not name are repointed at the derived header instead of restating a number, and ADR 0066's own amendment turns out to name four things while listing five, which git show against the pre-amendment line settles as five (#1676)
c1a195b9 · dbf3dbe6…diff - the site gets the privacy notice it has been collecting without, and every sentence is written against what is ACTUALLY retained rather than against the pipeline design: 1624 says do not ship the collector before the notice is live and the ordering is the point, and the collector shipped first - this closes that, late, and says so rather than pretending the sequence held.
writing it to the design would have published a claim that we collect MORE than we do. the beacon, the download hop and the telemetry channel are unbuilt, and visitor_key, country, device, browser and os have no producer at all, so a notice describing a daily visitor salt or country-level geo would be false in the direction nobody checks. what is actually kept is the access log for 30 days - time, method, address, status, bytes, referring URL, full User-Agent, cache status, response time, and NO client address, written as a literal dash - plus rows derived from it with the referrer reduced to a host and the User-Agent reduced to a coarse category.
⚠ one sentence of my own first draft was false and the scrubber refuted it: I wrote that the log records a route shape rather than the address you asked for, which is true only for repository and private-repository URLs. ordinary page addresses are recorded in full, so the claim understated collection on nearly every request. corrected to name the exception as an exception, checked against scrubPath rather than against memory.
the word anonymous is deliberately not used of the log, per 1617 - the rows carry no identifier, but a full User-Agent kept for thirty days could in principle distinguish an unusual browser, and claiming anonymity is the wording that turns a data-protection question into a consumer-protection one on a product sold on privacy. the page explains why it avoids the word instead of using it.
the objection is real machinery over a currently empty set, and the page says exactly that. one cookie, set only on request, holding a constant rather than an identifier - the opposite of a cookie that counts you. it suppresses collection that happens in the APP, it cannot reach the nginx access log which is written before the app sees the request, and today the log is the only source. so it presently suppresses nothing, which the notice states plainly rather than implying a working control. every future app-side collector must consult hasOptedOut, which is why it lives in lib rather than inside one caller.
the retention number gets the 1779 treatment, because it is the one number here that is a published statement about personal data: 30 lives in site/src/lib/retention.ts, the bound is enforced by RETAIN_DAYS in the scripts repo, and setup-site now refuses to deploy when they disagree - a copy with no check is how the notice quietly outlives the thing it describes, and the dangerous direction is raising retention on the box while the page still says thirty.
terms ships beside it with two deliberate absences: no governing law or jurisdiction, and no liability cap - inventing either would be a legal claim written by nobody qualified to make it, and a wrong one is worse than none. what is there instead is factual and checkable, including that backups are OPERATOR disaster recovery and not a per-account restore service, which is ADR 0046 and the opposite of what a reader would assume.
contact is security@millerbyte.com, the address that exists and is monitored, rather than a privacy@ alias that would have to be created at the mail host first - publishing an address that does not resolve would be worse than reusing one that does. /trust gets a pointer rather than a second copy, since two copies drift and then one of them is false. the footer carries both links on every surface, which costs about 650 bytes per surface and is recorded in budget.json with that reason, because a notice reachable only by typing its URL is not published in any sense that matters (#1624)
12951042 · dbf3dbe6…diff - the privacy notice stops containing four false statements, all of them found by review BEFORE the page was ever published, and the worst of them was the flattering kind: the page said No IP addresses, anywhere on a site sold on privacy, and nginx error log carries client addresses on every failed request for the same thirty days. scripts/lib/nginx-log.js says so in terms - error.log carries its own client ip exposure and is NOT addressed - and the strip step skips it deliberately because an error line leads with a date and the access-log expression would blank the wrong field. so the notice now discloses it in its own paragraph rather than burying it, says it is never read for analytics and never reaches the database, and says we would like to remove it and have not.
the second was the exact failure the retention constant was built to prevent, committed in the same change: the page promised the summary rows are kept 30 days and that both bounds are enforced by time, and NOTHING deletes from event. migration 0001 says it outright - until 1759 lands rows land here and nothing is dropped - and that same comment forbids the fix I first reached for, since expiry there is a partition DROP gated on rolled_up_through and NEVER time-based, so a silently failed rollup must accumulate partitions instead of deleting a day nobody aggregated. adding a time-based prune would have contradicted a recorded decision to make a sentence true. the sentence changes instead: the logs are bounded and the rows are not, said plainly, with what they contain so a reader can judge it.
third, no cookie set at all unless you ask for one below was false - Clerk is mounted on /account and sets session cookies - and Clerk is also a third party the notice never named. there is now a section on signing in, and a recipients section naming Clerk, the hosting provider and R2. fourth, the log never accumulates a list of the repositories or the paths of files inside them was over-broad: only forge verbs and /private are scrubbed, so browsing a PUBLIC repository logs owner, repo and file path whole, deliberately, because those addresses are already public and are how we tell whether anyone reads what is published. the page now says that instead of denying it.
three Article 13 items were simply missing and are added: the right to complain to a supervisory authority naming the ICO, the recipients above, and a rights section. two remain outstanding because they need facts I do not have rather than words - the controller identity and the hosting provider by name - and the page says the true general thing until those land.
the standards half found something sharper than a style point: the opt-out component re-implemented the cookie parser with a startsWith, which read loot_no_analytics=12 as an objection where the module correctly refuses it, and hardcoded the value the module names - which meant hasOptedOut had ZERO production callers and ten tests covered a function nothing ran while the page used the untested copy. it calls hasOptedOut now. /trust had retyped the retention number as a literal, escaping both the test and the deploy cross-check, in the same change that built that check; it uses the constant, and the test now scans both pages rather than one (#1624)
810dc185 · dbf3dbe6…diff - the beacon counts PEOPLE where the log can only count requests, and the notice that describes it was rewritten in the same change because deploying one without the other publishes a falsehood
loot#1757, over loot#1620 and loot#1617. POST /api/beacon takes one short
message per view and derives SHA-256(daily_salt || ip || ua || site) truncated
to 16 bytes, in process, discarding both inputs. It is canonical for uniques and
sessions and for nothing else: the log stays canonical for volume, because it
sees the ~53 prerendered pages, the cache hits and curl, none of which run any
JavaScript. Two sources counting volume would be two numbers to reconcile.
The key is length-prefixed per field rather than concatenated, and that is a
real defect closed rather than a flourish. One of the fields is the User-Agent,
which the CALLER chooses: with plain concatenation ip=1.2.3.4 ua=5 and
ip=1.2.3.45 ua= hash identically, so a visitor could be merged onto a neighbour.
The mutation that drops the framing reddens exactly that pin and nothing else.
Two decisions are recorded rather than defaulted. NO SALT, NO ROW: a beacon row
without a key adds to a count of beacon rows while contributing nothing to the
only two things this source is canonical for, so it would trade what we came for
against a number competing with the log. And the module is EAGER, which is the
answer to the ticket first hazard - ADR 0071 weighs the document plus eager
assets, so a deferred chunk is paid by every visitor and counted by nobody.
Measured at ~450 B on each of 62 surfaces and RECORDED, and loot#1640 is the
precedent that makes this more than taste: it measured a lazy route at 581 bytes
against 270 eager.
The edge half is new and had to be, because this is the site first
unauthenticated row-writing endpoint and the site vhost had no rate limiting at
all. limit_req at the /api/ PREFIX, not at /api/beacon, so ADR 0074 telemetry
arrives protected rather than silently exposed. Idempotence is remove-then-
reinsert between markers, and loot#1647 install-detect arm is closed in the same
patch since it needed the same only-if-absent vhost step. Its ordering is
load-bearing: the blanket header insert runs while only hand-written blocks
exist, which makes a duplicate unrepresentable instead of guarded against.
Found by RUNNING it, which is the whole argument for the fixture: the marker
ended in (generated), which awk read as a GROUP, so the strip silently never
fired - the second run appended a second /api/ block while grep -v tore the
limit_req line out of the first, leaving a vhost that would have failed nginx -t
on the box. Pinned now, with a control proving the predicate can say no.
The notice had to move with the code. Five published statements would have gone
false, the worst being that these records carry no key and nothing links two
requests to the same person. Rewritten to lead with the key, say the secret is
DESTROYED rather than merely rotated, and disclose that a visitor spanning
midnight is counted twice.
FOUR more contradictions were then found only by RENDERING the page, which is
the third time that has been the only thing that worked. Objecting still said
the log is the only thing we collect from, so this control has nothing to
suppress - three paragraphs after the new text says the browser sends nothing
when you object. Erasure and Your rights both still claimed nothing is keyed to
you. And JSX drops a newline beside a tag, so the page shipped visitorkey and
notstored as single words. The font gate then caught a star glyph absent from
the shipped subset, which would have rendered as tofu.
A test that asserted we do NOT collect a visitor key survived all of this green,
because its regex matched one verb phrasing the new prose never uses. It is
inverted now: the page MUST name the key, MUST say destroyed, MUST disclose the
midnight double-count. Leaving it would have let a later edit delete the
disclosure and stay green.
532 site tests, 322 scripts tests, budget green with the notice raise on the
record.
3c22dcae · 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.