A sign-in with no Bluesky profile makes a bare grain profile
MergedThis folds the "no Bluesky profile" case into ProfileSeed.seed. An account with neither profile now gets a bare {$type, createdAt} grain profile via createRecord at self. An account with an unnamed grain profile and no Bluesky profile is left alone. I checked the logic against fromBluesky: with an empty Bluesky dictionary it yields only $type and createdAt, as the tests expect. The named, put and made-from-Bluesky paths are unchanged. The tests cover both new branches, including the exact key set and the indexing call. I didn't run them, and the checks don't build the app. The one gap, a PDS answering a missing record with something other than a 400 RecordNotFound, is stated in the PR and only gets logged. Nothing needs to change.
What changes
Every account that signs in ends up with a grain profile.
ProfileSeed, whichAuthManagerstarts in a detached task once an OAuth sign-in is stored, already made a grain profile from the Bluesky one. It now follows grain-serve's whole rule (crates/grain-serve/src/login/profile.rson grain'slogin/bare-profile, pull at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzcciru6kc2b), read from the account's PDS withcom.atproto.repo.getRecord:social.grain.actor.profile/selfwith a non-emptydisplayName: nothing is written (.named).app.bsky.actor.profile/self: the grain profile takes its non-emptydisplayNameanddescriptionand itsavatarblob as it is, and the existingcreatedAtor now. An existing one isputRecordwithswapRecord= its CID; else one iscreateRecordatself(unchanged).{"$type": "social.grain.actor.profile", "createdAt": now}is created atself(.made). This is new: before, nothing was written..noBluesky).The write goes to the PDS and is indexed through
social.grain.sync.indexRecords, as every write is. The sign-in never waits on it and a failure is only logged.Why
grain-serve indexes an account only once it has a
social.grain.actor.profile(or a group declaration). It writes one at web login, but the app signs in at the account's own PDS, so grain-serve never sees an app sign-in. An account with no Bluesky profile, a non-Bluesky PDS account say, signed in on the app and was never tracked.Why stacked on
feature/rust-calls, not on main. Main's sign-in is OAuth against grain's own server (/oauth/par,/oauth/authorize), which grain-serve does not serve; there, hatk's login hook writes the profile itself. Sign-in at the account's PDS exists only onfeature/rust-auth(pull Sign in at the account's own PDS with atproto OAuth), and the profile seed this extends exists only onfeature/rust-calls(pull Calls reach grain-serve by its own method names), hooked into that sign-in. A second seed on main would hook a sign-in that does not reach grain-serve and would duplicate this one, so the rule is folded into the existing one and lands with the sign-in that needs it.How it was checked
ProfileSeedTests(stubbedURLSession, no network) covers the four cases: a named profile left alone; made atselffrom Bluesky and put over on its CID keepingcreatedAt; a bare one made atselfwith exactly$typeandcreatedAt, and indexed, when there is neither profile; an unnamed grain profile left with no Bluesky profile. Run withxcodebuild test -only-testing:GrainTests/ProfileSeedTestson the iPhone 17 Pro Max simulator (iOS 26): 7 tests, 0 failures. swiftformat and swiftlint are clean on both files. The repo's knotless workflows do not build or test the app.Shortcomings
RecordNotFoundmakes the seed fail (logged) rather than write.🤖 Generated with Claude Code