serve: A login names its account by handle at once
MergedThis pull makes the OAuth callback track the logged-in account. It writes the handle from getSession into repo.handle only when that handle resolves back to the account's DID. set_handle now clears the handle from any other account in the same transaction. I found nothing that has to change.
- Verification: the handle is checked with
resolve_handlebefore it is kept, and a failed or mismatched resolution is logged and doesn't fail the login. That closes the takeover where a PDS claims someone else's handle. - Clearing in
set_handle:set_handleis also used by backfill and identity events. They get the same single-holder rule, which fits what the pull describes. - Test: the test covers the stale holder, the pending state and the lookup by handle. I did not see the runner's check results.
- Unchecked: I did not check that
trackcreates thereporow, soset_handlehas a row to update. The new test would fail if it didn't.
Carried forward: the change this pull makes at 3c1bbc38a88040715f7134e3356d6574b68b1dbe is the one reviewed at 98e1b2468f014387e866702c6719ff72d958d65f, so that review stands. What is new comes from its base.
This pull makes the login callback track the account. It keeps the getSession handle only when that handle resolves back to the account's DID. set_handle now clears the handle from any other holder in the same transaction.
I found nothing that must change. My earlier approval stands. The verification-before-keep check is still there. The one change I noticed since then is that a failed or mismatched resolution is now dropped silently. The body says so, and it doesn't fail the login. The new test covers the stale holder, the pending state and the lookup by handle. I did not see the runner's check results.
Minor, not blocking: a database error from track or set_handle propagates through named(...)? and fails the login, though the body says the handle step never fails it. That holds for resolution failures only.
- serve: A login names its account by handle at once
- main

What changes
A login tracks the account and keeps its handle. When the OAuth callback keeps the web session, grain-serve now tracks the account and writes the handle the PDS gave the session (
oauth::named, fromgetSession) intorepo.handle, provided that handle resolves back to the account's DID. So/profile/<handle>andsocial.grain.actor.getProfile?actor=<handle>find a newly logged-in account straight away. An identity event or a backfill pass still writes over it afterwards, as before.A handle names one account.
Index::set_handlenow clears the handle from any other account that held it, in the same transaction.Reader::resolvetakes the first DID that has a handle, so a stale row could otherwise win against the account that really has it.Why
Only backfill (
identity::handle) and Jetstream identity events wroterepo.handle, and both only for an account already tracked. The stream sends a handle only when it changes. So an account that had just logged in had no handle in the index, and its profile route said "Profile not found" until backfill reached it. The e2e flows ported from knotless found this.The handle is checked before it is kept: it must resolve to the DID, the same check knotless's handle cache runs (
verified_handle/rename). Without it, a PDS could claim someone else's handle ingetSessionand take over that handle's route here. A handle that doesn't resolve, or resolves to a different DID, is not kept and is left for backfill to look up. That never fails the login. A database error while tracking or keeping the handle does fail it, the same as a database error fromkeep_web_sessionjust before.Reader::resolvestill looks only in the index and does not resolve unknown handles over the network. That keeps every request for an unknown handle (crawlers, typos) from becoming a DNS or.well-knownlookup to whatever domain is asked for. A DID found that way would also give an empty page, because the index has nothing else for that account. Writing at login covers the case that matters, an account that is here.How it was checked
a_login_names_its_account_by_handle_at_onceincrates/grain-serve/tests/login.rs: a stale account holds the handle first. After login, before any backfill (the repo is stillpending), the account is tracked with its handle and the stale row has none. Once the profile is seeded,getProfileby handle returns the account's DID and handle. One localcargo check -p grain-serve --testspassed; the rest is left to the runner.Shortcomings
The login waits on one extra handle resolution, which is not cached. A handle that fails the check is dropped without a log line. No test covers that path: the test network's handles all resolve. Group accounts made through
login::provisionedare not changed by this pull.🤖 Generated with Claude Code