~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1

A sign-in with no Bluesky profile makes a bare grain profile

Merged
Merged chadtmiller.com asked to merge into feature/rust-calls from grain.social/grain-ios:profile/seed-on-sign-in

What changes

Every account that signs in ends up with a grain profile. ProfileSeed, which AuthManager starts 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.rs on grain's login/bare-profile, pull at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzcciru6kc2b), read from the account's PDS with com.atproto.repo.getRecord:

  1. social.grain.actor.profile/self with a non-empty displayName: nothing is written (.named).
  2. Otherwise, with an app.bsky.actor.profile/self: the grain profile takes its non-empty displayName and description and its avatar blob as it is, and the existing createdAt or now. An existing one is putRecord with swapRecord = its CID; else one is createRecord at self (unchanged).
  3. With no Bluesky profile and no grain profile: a bare {"$type": "social.grain.actor.profile", "createdAt": now} is created at self (.made). This is new: before, nothing was written.
  4. With no Bluesky profile and an unnamed grain profile: it is left as it is (.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 on feature/rust-auth (pull Sign in at the account's own PDS with atproto OAuth), and the profile seed this extends exists only on feature/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 (stubbed URLSession, no network) covers the four cases: a named profile left alone; made at self from Bluesky and put over on its CID keeping createdAt; a bare one made at self with exactly $type and createdAt, and indexed, when there is neither profile; an unnamed grain profile left with no Bluesky profile. Run with xcodebuild test -only-testing:GrainTests/ProfileSeedTests on 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

  • Not tried on a device, or against a real PDS or grain-serve: no sign-in was run.
  • A PDS that answers a missing record with something other than a 400 RecordNotFound makes the seed fail (logged) rather than write.

🤖 Generated with Claude Code

Approved

This 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.

Merged as aa9d8fc