Changes touching this path

  • 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…
  • the collaborator can read someone else's namespace: migration 0015 adds the fourth role forge_member and eleven forge_member views, one per relation 0009 serves, so the shared prefix-parameterised TreeReader reads either family unchanged - and the gate is strictly stronger than 0009's, because the app names only the account the Clerk session resolved to while the membership join sits INSIDE the barrier view, so the application cannot name a repo it is not a member of even by mistake. metadata_public is absent as both column and predicate, and repo_member is named exactly once, by the gate. Two premises did not survive, and the first is a defect in the decision itself: ADR 0077's and the ticket's literal current_setting of loot.reader_tenant cast to bigint DOES NOT fail closed. It fails closed for an UNSET parameter and not for a CLEARED one, because set_config with NULL leaves the empty string behind and casting that to bigint RAISES - which 0009 never met because decoding an empty string as hex does not. Proved by removing the fold from the shipped migration and reading invalid input syntax for type bigint, so the gate ships as nullif and then the cast, malformed values still raise deliberately, and withMember validates a positive decimal with the pin on both sides. The second is a defect in #1727, which landed earlier tonight: its exact-set spelling, that the views naming repo_member equal the forge_member family, is satisfiable ONLY by a family of one, because the ten chained views name the GATE view and never the base table - so a second member view gaining the roster, which is precisely the disclosure ADR 0077 forbids, read as CORRECTNESS under the old spelling. It is re-spelled to exactly forge_member_repo whenever the family exists, its vacuity guard extended to require the member family non-empty, and its positive control now plants four breaks where it planted two. The row-level-security interaction is proved rather than assumed, as three readings on one database in one moment: the seven enabled tables this family reads really carry relrowsecurity, read as a set; the role handed a direct SELECT on repo reads ZERO rows, which is 0014's own named accident and shows the policy system live for that role; and the same role on the same connection at the same moment reads its repo through the gate view and gets one. The mechanism is then named from the catalogue and red-proved by setting security_invoker, which takes the read to insufficient_privilege, and the failure text names the non-superuser deployment case so a future deploy diagnoses itself. The isolation break is ADR 0077's own named careless shape rather than an invented one - the gate keeps its repo_member join and loses only the correlation to repo, which passes every text guard in migrate.rs - and seven of eight tests fire on it. Rows are planted from tests and from the site, outside account_tier.rs's scan of src, the way rls.rs already recorded, so the tripwire is untouched and green. forge_member ships NOLOGIN and provisioning is a different repo, so the class answers 404 rather than 500 and is not an existence oracle (#1726) e76ca863 · dbf3dbe6…diff
  • two concurrent key rotations can no longer leave an account with no live key, which is a lockout because a key that is not live is refused the sync read, the push and the burn: the floor in site/src/server/identity/identity.ts is a count, the BEGIN it runs inside is the postgres default READ COMMITTED, and two registrations that each retire the key the other leaves live write different rows, wait for nothing, each count the other as live and both commit. registerKey now selects the key rows of that account in account_key under FOR UPDATE, ordered so two waiters take them in the same order, before it writes any of them, so the second registration waits and then counts a state that has stopped moving; and identity/db.ts stops rewrapping a refusal raised inside a transaction as a plain Error, which had been answering every one of them with the anonymous 500 rather than the 422 that carries the sentence the account page renders. the ticket preferred a database constraint and that was the right thing to prefer, but a floor of this kind says a row must EXIST, which a declarative postgres constraint does not say, and the trigger that would say it was measured failing on a throwaway cluster: with a DEFERRABLE INITIALLY DEFERRED constraint trigger counting live rows over a table shaped like account_key, two sessions each retiring the row the other left live both committed and zero live rows were left, because a deferred trigger fires at commit and still reads its own snapshot. what closes the race is a lock either way, so it is taken where it needs no migration, the forge release chain no-migration-since-v0.4.20 state stands, and this owes a site deploy and no forge deploy. red first against a throwaway postgres with the counts read: with the locking clause dropped the new concurrent test ends with the account on zero live keys and neither session refused (1 failed and 10 passed), with the refusal rewrapped the two class pins go red (2 failed and 9 passed), and with the pause keyed on a statement that never matches it refuses on zero arrivals rather than passing vacuously (1 failed and 10 passed). ADR 0038 records the decision beside the #2004 amendment whose argument rests on this invariant, and names what the lock does not cover. bash ci/local.sh is green against a postgres 18 it created (3977 passed over 122 binaries, 7 ignored, and 67 passed over the seven site live files with none skipped) and the site gate is green end to end (668 passed and 62 skipped over 61 files, 62 surfaces) with no ceiling moved (#2015) 18aca67b · dbf3dbe6…diff
  • the rename census gives each rename its own row and reads a function header as a qualifier run, so a second rename written into a function it already names arrives red: store_rename_census.rs keyed every hit as path plus enclosing fn into a set, so the twelve fs::rename sites under crates/*/src collapsed into nine rows with three of them carrying two renames each, and the property the file states, that a new rename arrives red until somebody says which end of it can be held, was already false at rows the file itself holds, while the section saying what it cannot catch did not name that limit. the key is now the file, the fn and which rename it is under that name in source order, and not the line number, because a line moves under any edit above it and the repair for that is retyping a digit in the table, which is the habit the file exists to refuse. a header is now a run of qualifier-shaped tokens and then fn, a bare word or a pub restriction or an extern abi string read as shapes rather than as the four visibility prefixes it held, so async fn, const fn, unsafe fn and extern fn key to themselves instead of to the function above them; no rename in the tree sits behind such a qualifier today, so a fixture over sample lines carries that half rather than the walk. proved as the ticket asked, with a thirteenth rename added inside a function the census already names: green before the change (2 passed and 0 failed) and red after it on the extra row (2 passed and 1 failed), and the fixture goes red under its own mutations, the ordinal dropped from the key (0 passed and 1 failed) and the visibility-prefix header reading restored (0 passed and 1 failed). sentences wider than their code are narrowed where a reader meets them: identity.ts and the ADR 0038 amendment said the property is that every write happens under the account lock, where a lock over an account with no key rows holds nothing, so two concurrent first registrations do not wait for each other and are safe for a different reason, neither retiring anything and a duplicate meeting the pubkey primary key; docs.$.tsx said a status added to DocStatus later renders no body, where it takes the written path and renders whatever is filed under that slug, blank only because the content gate refuses a body filed against a page the registry does not call written; and that gate now keys its filed set on what react renders rather than on nullishness, so a body of false or the empty string no longer counts as filed, pinned by a fixture that is red under the old reading (1 failed and 8 passed). type-program.test.ts seeds its import walk recursively, so a test moved into a subdirectory stays in the program it reads, measured at 62 files flat and 63 deep with one moved down. no migration, no wire or format byte moves, and the site half is comments and tests, so this owes no deploy. the workspace suite is green (4001 passed over 124 binaries, 7 ignored) and the site gate is green end to end (669 passed and 62 skipped over 61 files, 62 surfaces, no ceiling moved) (#2117) 0c90a730 · dbf3dbe6…diff
  • key management on the site: a key can be retired by itself, named, and chosen at unlock. per the operator grill on #2398 (ADR 0100): POST /api/identity/retire retires one key of the signed-in account, refusing the last live key under the account-row lock registration takes and never re-dating a retirement, with no route to un-retire and no deletion, since #506 keeps every key resolvable; /account lists live keys and folds retired ones under a collapsed section, and the retire confirmation names the repos the key owns. names live in a new owner-only account_key_label table (migration 0027) that only forge_identity reaches, trimmed, at most 40 characters, with control, line-separator and direction characters refused and a CHECK repeating the cap; they reach only /account, the unlock picker and the private bar, always beside the fingerprint. the unlock picker lists every live key, defaulting to the key that owns the URL namespace, then the key this device last unlocked, then the only live key, and a key with no stored passphrase asks for its 24 words and can save one. both routes are Bearer only and refuse a key of another account; /api/identity/me stops returning the internal tenant id. rebased by hand over migration 0026 and ADR 0099. pg tests pin the role reach, route and picker tests pin the rules, each red under a named mutation; bash ci/local.sh green against Postgres 18 (4568 passed), site gate 842 passed. owes the setup-forge tripwire update, a forge deploy, then a site deploy (#2398) a90c85fb · 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.