You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
W2: Surface OAuth token health in Connected accounts (verifyIdentity) #92
Split out of #64 (see PR #90). APIClient.verifyIdentity(provider:providerId:) has no caller; #64
kept it because it is the only primitive that closes a real gap.
The gap
LinkedIdentitiesView shows each provider's /status (linkedinStatus, twitterStatus, blueskyStatus, githubStatus, mastodonStatus), but those report whether the provider is
configured server-side — {"configured":true,…} — not whether this account's stored token still
works. So a user whose Bluesky or Mastodon token has expired sees a healthy-looking row and has no
way to find out until a cross-post silently fails.
POST /api/user/identities/verify is the route that actually checks: it loads the stored credential
and verifies it (restoring the DPoP-bound session for Bluesky, calling the provider otherwise).
Build this
A Check connection action per linked identity in LinkedIdentitiesView, calling verifyIdentity(provider:).
Show the outcome on the row: verified, or "needs reconnecting" with the existing reconnect
affordance.
Optionally verify all identities on appear — but only if it is cheap; each call hits the
provider, so a manual action may be the better default.
Remove the "no caller yet" comment on the function once wired.
Note
The route reads only provider; the providerId iOS passes is accepted and ignored. Drop the
parameter when wiring, unless #77 (which also touches this area) resolves it differently.
Acceptance criteria
An identity with a revoked/expired token reports as needing reconnection.
A healthy identity reports as verified.
A failure to reach the route is distinguishable from "token is bad".
Implemented in #99 (stacked on #93, which must merge first). Closing here — further work tracks in
the PR.
Three corrections to this issue's description, from reading the route
Success is 200 {"success":true}, not {"ok":true}. The pre-existing iOS Response struct
decoded ok. Harmless — the field was optional and discarded — but the old tests were stubbing a
shape the server never sends.
There are two different 400s, not one. The issue describes 400 as "token is bad". There is
also "No token to verify", returned when providerData.access_token is missing or not a string.
Different cause, same status, and both are reconnect-class — feat(identities): add per-identity connection check #99 folds them together deliberately
and documents why.
A 200 does not always mean "verified". See below — this is the important one.
Backend hole found while doing this — filed as #100
For any provider the route does not recognise (anything that is not bluesky, github, mastodon:*, linkedin, twitter), every branch is skipped, nothing is checked, and the route
still stamps lastVerifiedAt and answers 200 {"success":true}. A bare "mastodon" provider
string — no :instance suffix, which linkedIdentities can return — lands on exactly that path and
reports verified without verifying anything.
The client cannot tell that 200 apart from a real one, so #99 does not try to compensate. Tracked in #100.
One design call worth reviewing in the PR
checkResponse maps both 400 and 500 to APIError.server(message), so "your token expired" and "the
server fell over" were indistinguishable — which would have broken this issue's own acceptance
criterion that the two read differently. #99 adds postCamel(_:body:mappingStatuses:) to APIClientTransport.swift, the write-side twin of the existing get(_:mappingStatuses:) that APIClient+Lists.swift already uses. That is the one file outside this issue's stated scope.
Activity
Adron commentedon Sep 16, 2026
Implemented in #99 (stacked on #93, which must merge first). Closing here — further work tracks in
the PR.
Three corrections to this issue's description, from reading the route
Success is
200 {"success":true}, not{"ok":true}. The pre-existing iOSResponsestructdecoded
ok. Harmless — the field was optional and discarded — but the old tests were stubbing ashape the server never sends.
There are two different 400s, not one. The issue describes 400 as "token is bad". There is
also
"No token to verify", returned whenproviderData.access_tokenis missing or not a string.Different cause, same status, and both are reconnect-class — feat(identities): add per-identity connection check #99 folds them together deliberately
and documents why.
A 200 does not always mean "verified". See below — this is the important one.
Backend hole found while doing this — filed as #100
For any provider the route does not recognise (anything that is not
bluesky,github,mastodon:*,linkedin,twitter), every branch is skipped, nothing is checked, and the routestill stamps
lastVerifiedAtand answers200 {"success":true}. A bare"mastodon"providerstring — no
:instancesuffix, whichlinkedIdentitiescan return — lands on exactly that path andreports verified without verifying anything.
The client cannot tell that 200 apart from a real one, so #99 does not try to compensate. Tracked in
#100.
One design call worth reviewing in the PR
checkResponsemaps both 400 and 500 toAPIError.server(message), so "your token expired" and "theserver fell over" were indistinguishable — which would have broken this issue's own acceptance
criterion that the two read differently. #99 adds
postCamel(_:body:mappingStatuses:)toAPIClientTransport.swift, the write-side twin of the existingget(_:mappingStatuses:)thatAPIClient+Lists.swiftalready uses. That is the one file outside this issue's stated scope.