Changes touching this path
- Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257)
Thin end-to-end tracer for loot.millerbyte.com (spec docs/specs/loot-site.md
SS1-3): site/ = TanStack Start SSG consuming published @millerbyte/ui@^0.1.0
(theme.css at the root), five placeholder surface routes prerendered, deployed
on Vercel project #2 (root site/) at https://loot.millerbyte.com.
Install proxy: the spec's vercel.json external-rewrite mechanism was falsified
live (Vercel passes GitHub's releases/latest 302 through to the client), so
/install.sh and /install.ps1 are Start server routes that fetch upstream and
stream the bytes back 200 - spec SS2 amended in place.
site/** explicitly public in .lootattributes; .vercelignore allowlists site/
so .loot/ keys and docs/pitch/ can never reach the Vercel builder; site build
artifacts lootignored.
1931146c · dbf3dbe6… - primary catch-up: checkout at the #291 squash (site/ scaffold) folded for the drift reconcile (#297)
e6f7079a · dbf3dbe6…diff - Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257) (#291)
* Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257)
Thin end-to-end tracer for loot.millerbyte.com (spec docs/specs/loot-site.md
SS1-3): site/ = TanStack Start SSG consuming published @millerbyte/ui@^0.1.0
(theme.css at the root), five placeholder surface routes prerendered, deployed
on Vercel project #2 (root site/) at https://loot.millerbyte.com.
Install proxy: the spec's vercel.json external-rewrite mechanism was falsified
live (Vercel passes GitHub's releases/latest 302 through to the client), so
/install.sh and /install.ps1 are Start server routes that fetch upstream and
stream the bytes back 200 - spec SS2 amended in place.
site/** explicitly public in .lootattributes; .vercelignore allowlists site/
so .loot/ keys and docs/pitch/ can never reach the Vercel builder; site build
artifacts lootignored.
Landed via break-glass git branch: the loot-native lane land was blocked by
two live shared-store bugs (review-ferry folds a described WIP into a
reconcile finalize; describe/status disagree on the working change under
concurrent sessions) - tickets to follow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Apply #291 review findings
Cache only successful upstream responses in the installer proxy (a transient
GitHub failure must not be CDN-cached for 5 minutes); extract the install
one-liners to one shared module (three copies drifted apart otherwise); fix
the vite.config "fully static" comment and spec SS0 framing to name the two
server-route exceptions; tighten the .lootattributes rationale (the interim
manual deploy is gated by .vercelignore, not the GitHub projection); drop the
cargo-culted overrides block (its pins targeted server deps the site lacks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
231eb5f8diff - resolve conflict at site/src/routes/__root.tsx
2cb5212e · dbf3dbe6…diff - the forge's published surfaces render themselves anonymously, and each response's cache class rides out of the selector that produced it (#751)
#749 built the read tier and nothing called it. This is the six surfaces
#466's table named -- profile, repo home, tree, blob, history, diff -- as
SSR routes over that tier, plus robots and a per-owner sitemap.
Three properties are structural rather than configural, which was the point:
STRICTLY ANONYMOUS (#486 Q4). No handler in site/src/server/forge.ts reads
a request, a header, a cookie or a token; each takes only what the URL
already says. So 'never serve one identity's view to another' is a fact
about the module's signature rather than a cache rule someone must
remember. Adding an identity parameter is the one edit that would break it.
THE CACHE CLASS IS CARRIED, NEVER RE-DERIVED. resolveSelector already
answers how a resolution may be cached; routes read loaderData.cache and
map it in one place. That is what keeps the ADR 0032 trap shut: a
change_id LOOKS like an address but is the durable handle carried across
amends, so it resolves 'revalidate', and a pinned version_id is the only
cache-forever class. Asserted twice -- a unit test on the header text, and
a live render proving one URL family splits three ways.
HONEST RENDERING (#497). A locked path shows its visibility tag and no
bytes; grant_log is never queried, so a grantee set cannot leak from a
surface with no way to learn one. Burned reads as a tombstone, sealed as
'Not published', absent as absent -- three outcomes a blank page would
have made indistinguishable. The 404 is deliberately one 404: 'no such
repo' and 'metadata private' must not be told apart.
Also here: a published-only repo (metadata private, paths published) gets
its index and nothing else -- no tree, no history, no diff -- which is
0005's barrier showing through the UI unchanged. Per #748 the profile
lists metadata-public repos only, there is no global directory, and the
sitemap is therefore per-owner: readable at a known URL is not consent to
be advertised.
vite.config.ts grows the prerender deny-list its own comment called
unnecessary -- true until a route read a database.
Acceptance: 49 tests green (31 new pure ones; the 8 read-tier tests against
a throwaway 0001-0005 cluster), npm run build clean with exactly the five
authored surfaces prerendered, and a 34-assertion end-to-end smoke -- real
sealed bytes through R2 -> decrypt -> unzstd -> highlighted HTML, all three
cache classes, every honest-rendering outcome. The client bundle carries no
pg, no SQL and no credential.
Publication: README.md, LICENSE-MIT and LICENSE-APACHE reach @world here,
the set .lootattributes has declared since #750. Operator-authorised at the
landing sign; nothing else in the tree changes tier.
2aa21bbe · dbf3dbe6…diff - the browser mints the seed and the account binds the key: Clerk in the site's own routes, variant-B onboarding, and the account tier's one producer (#753)
Migration 0007 adds account_seed (the passphrase-wrapped envelope, opaque
ciphertext the server cannot open) and forge_identity, a NOLOGIN role scoped
to exactly the identity tier -- SELECT/INSERT on tenant, SELECT/INSERT plus a
column-scoped UPDATE of retired_at on account_key, the full row on
account_seed, and nothing else (pinned live by
forge_identity_is_scoped_to_the_identity_tier).
The producer lives in site/src/server/identity/ behind a verified Clerk
session (verifyToken in the site's own route handlers, reusing the millerbyte
Clerk instance and pattern, never the gateway service). Registration is one
transaction: upsert tenant on the verified clerk_user_id, add the key, store
the envelope, retire what rotation names -- and never zero live keys. The
Rust service still writes none of the tier; tests/account_tier.rs now guards
that boundary instead of a vacancy.
Onboarding is #487's variant B, the two-column contract: the app owns the 32
bytes (crypto.getRandomValues -> 24-word BIP39 over the ENTROPY, never the
64-byte derivation -> ed25519 pubkey via @noble/curves, held to the engine's
from_seed by the pinned RFC 8032 vector), a WebCrypto PBKDF2+AES-GCM envelope,
a .txt download that excludes the passphrase, and two consent checkboxes that
no download waives. Clerk mounts only under /account, which joins /k/ on the
prerender deny-list; every static surface stays static.
Follow-ons filed rather than folded: #926 (CLI raw-seed import, so BIP39
recovery reaches the CLI) and #927 (account-aware may_push, burn entitlement
via account, and the #506 historical-key 301s the anonymous read tier
deliberately cannot serve today).
278e1886 · dbf3dbe6…diff - the receipts are served, not linked: the Evidence page stops pointing at a repo its readers cannot open (#840)
Twelve github.com links, every one a 404 for the only audience this site has —
Connor-Miller/loot is private permanently (ADR 0045). Nine of them were the
Evidence page run-output and evidence-doc links, i.e. the receipt for every
thesis claim that page makes, so it asserted a set of proofs and handed the
reader nothing to open. The other three were the nav/footer repo links and a
LICENSE link, dead twice over since no file of that name exists either.
The nine receipts are now inlined at build time with `?raw` and disclosed in
place under each excerpt. A link — to GitHub, to a forge surface, to a route of
our own — is a promise checked at READ time by someone not here to fix it; an
import is checked by `vite build`, so moving one of these files breaks the build
in the commit that broke it. This page cannot go stale the way it just spent
months being stale.
Not served from the forge, which ADR 0045 says is where the world should read
loot: measured today, /k/connor and /k/connor/loot both answer 404 — it carries
no repo yet, so pointing there would have swapped nine dead links for two. When
it does, these become links again and the imports stay as the build-time check.
The license moved to a page of its own rather than being dropped: a reader
running curl | sh has a real reason to want the terms, and both texts are
imported rather than transcribed so they cannot drift from the repo.
A sweep test asks the reader question of the whole src/ tree, not the two files
that happened to be wrong. It bans href/to/url pointing at github.com and
deliberately allows the string in prose — installerProxy documents what dist
bakes into the installers, and install.tsx prints cargo install --git as text
(that command is #841 own decision). ⭐ The defect was never a wrong URL: it was
that nobody ever opened one anonymously, and the person editing these files is
always signed in.
Verified: no github.com href on any of the 6 prerendered pages, /license
prerenders, suite 77 passed | 23 skipped, build green.
⚠ Residue: the inlined evidence DOCUMENTS quote issue URLs in their own prose.
They render as inert text in a <pre>, not as links, and rewriting a historical
evidence document to hide its citations would falsify the thing it is evidence of.
ad95716f · dbf3dbe6…diff - the browser opens what the forge cannot: own-private read ships an owner read tier, a once-per-device unlock, and a grant mailbox that never acks (#1043)
c8dd4da1 · dbf3dbe6…diff - 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…diff - 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 site gets a typeface: Geist and Geist Mono self-hosted from a subset derived from the site own characters rather than a stock list that silently drops the arrow the landing page renders, and twenty-one ad-hoc sizes collapse into a six-step scale
43c52bad · dbf3dbe6…diff - the site chrome becomes an instrument: a slim mono Console bar carries the six links, the release tag it now imports rather than retypes and the account key chip on every page, the footer band collapses into a one-line end-mark that still carries the disclosure route, docs trades its in-page contents list for a prop-driven side navigation any later surface can mount, and the account page is re-presented as a dashboard of key rows and a four-step ceremony with every Clerk call, every byte of crypto and every load-bearing sentence untouched - the #1323 prototype verdict, raising every surface ceiling on the record for the shared stylesheet it costs
786e5a21 · dbf3dbe6…diff - the two nav entries that leave your machine carry an EXP marker, keyed on their own href so the bar still says nothing about who is reading, and a strict 20rem suppression keeps the chip from adding fourteen pixels of horizontal scroll to a Fold
112c1212 · dbf3dbe6…diff - the Console bar stops calling the forge a relay and stops asserting an ok no static document can know, keeping only the release tag because that is the one thing up there a test already fetches, and the eight budget reasons stop claiming no new dependency now that lucide costs 682 bytes on every surface
fc920df7 · dbf3dbe6…diff - the Console bar stops asking for more width than its box has at 280px, and the half of this ticket that had not landed turns out to be worse than it was filed: the recorded 22px shortfall reproduces to the pixel only once a scrollbar is suppressed, because the ticket took innerWidth as the box - with a classic scrollbar the same row spills 5px past the viewport on seven of the eight budgeted surfaces, and only the landing escapes because its gutter is wider than its own over-run, so the sentence saying the page does not scroll was true of the overlay shape alone. the fix is spacing and nothing else, three rules inside the media block that already cuts the EXP hole, tightening a row gap and two padding pairs while every item, its content and the gutter that aligns the bar to the text column stay exactly as they were - after it, all eight surfaces read zero horizontal scroll with nine pixels of slack in the box and twenty-four on the overlay shape, and 240, 300, 320 and 390 were swept for regressions. every reading carried a positive control injected in the same page evaluation and removed after, so no zero here comes from a probe that was not first shown able to see an overflow. the durable half is a budget rather than a render, since ADR 0071's gate weighs bytes without ever rendering at a width and nothing in this repo can: spacing is read live out of the stylesheet, brace-matched and shorthand-aware, against a box computed from the live gutter declaration, with one recorded font-metric constant labelled as recorded - the model reproduces four independently measured layouts to a hundredth of a pixel, and eleven mutations were each run red, including one that only moves a rule below the media block and changes no string at all. what it cannot catch is a font change or a new glyph, which is written at the top of the file rather than implied away; and the stale claim about fourteen pixels of scroll is narrowed in all three places that carried it, since correcting one of three is the trap #1629 records (#1360)
dd791126 · 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 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 - the private shell stops dying a minute after /account: it authenticated with Clerk 60-second __session cookie while no Clerk JS ran on /private to refresh it, so every private call 401d and a reload wiped the vault. per the operator grill (ADR 0096, reversing #930 dec. 4), Clerk JS now runs on the private routes, loaded on demand behind PrivateGate so public pages carry none, under the strict nonce CSP widened only for Clerk (its frontend API host read out of the publishable key, img.clerk.com, the Turnstile and protect hosts, and worker-src blob: for its session timers) with the request nonce passed to ClerkProvider; every private call goes through one privateFetch that sends a Bearer from getToken and retries once with a fresh token, and /api/private/* no longer accepts the cookie; the vault is wiped only when Clerk reports signed out or a different user, or on Lock, and an HTTP status never wipes it; signed-out /private renders SignIn in place and returns to the requested URL, and the private bar gains Sign out beside Lock. the vault, private shell and CSP comments and CONTEXT.md now say what is true: the wrap key can decrypt, so any script on this origin can recover the seed, and the wrap protects it at rest; the boundary is this origin script set, loot code plus Clerk. tests for the CSP, bearer lane, retry and wipe triggers and the built server, each red under a named mutation and restored; site gate 804 passed, every surface lighter; a CSP-enforcing signed-out load renders SignIn with no violation and /why loads no Clerk; workspace suite 4549 passed. the operator live check closes the ticket (#2396)
25ab3fa8 · 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.