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 forge live-database suites now run in CI: test-main.yml stands up a postgres:18 service, provisions it the way setup-forge.js does including its default privileges, migrates it with the commit own loot-forge --migrate, sets every LOOT_FORGE_TEST database url the Rust and site suites read, and runs cargo test and then the site pg tests against it. LOOT_FORGE_TEST_REQUIRE_DATABASE turns a database test skip into a failure through pg::live_opt_in and the site skippingLive helper, and with it set and no url 91 Rust tests and all six site pg files fail, while two half-suites that used to skip silently now say so. The default-privilege tripwires run in CI by decision, and fail on a cluster without provisioning. A new migration that neutralises the roster policy passed the bare suite and failed rls.rs and membership.pg.test.ts against the cluster. It is a canary on landed main and not a pre-land gate, and the prose that said no workflow sets these urls now says that instead (#1735) c0931f78 · 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
  • 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.