feat(identities): surface real OAuth token health in Connected accounts - #110
Merged
Merged
Conversation
The five `/status` routes report whether a provider is configured server-side, never whether this account's stored token still works, so an expired Bluesky or Mastodon credential looked healthy until a cross-post silently failed. `POST /api/user/identities/verify` is the only route that loads the credential and exercises it upstream. Wire it to a manual "Check connection" action per row — manual because each call makes the backend hit the third-party provider, so an on-appear sweep would bill five remote round-trips to a screen opened to read. The route's answers are kept apart from its failures: 400 (credential rejected / no token stored) and 404 (identity row gone) return `needsReconnect` and light up the existing reconnect affordance, while 401, 5xx and transport errors throw and render as "couldn't run the check — your connection is unchanged". Without the new `postCamel(_:body:mappingStatuses:)`, `checkResponse` flattens a 400 and a 500 into the same `.server(…)`, making a dead token indistinguishable from a dead check. Also drops `providerId` from `verifyIdentity`: the route reads only `body.provider` and never looks at it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
One conflict, in APIClient.swift, resolved by taking this branch's version rather than keeping both: #90 (now on main) added a doc comment to the old two-argument `verifyIdentity(provider:providerId:)`, and this branch replaces that function outright with `verifyIdentity(provider:)` returning an `IdentityVerification` outcome. Main's copy is the version this supersedes, so keeping both would have left a stale duplicate and a signature that no longer compiles against the call site. Verified after resolving: only the single-argument form remains, and no `providerId` parameter survives anywhere in the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #92. Replaces #99, which GitHub auto-closed when its base branch
(
fix/unlink-identity-query-param, PR #93) was deleted on merge — a closed PR whose base branch nolonger exists cannot be reopened or retargeted. Same branch, same commits, now based on
main.LinkedIdentitiesViewshowed each provider's/status, which reports whether the provider isconfigured server-side — not whether this account's token still works. A user whose Bluesky or
Mastodon token had expired saw a healthy row and found out only when a cross-post silently failed.
POST /api/user/identities/verifyis the only route that actually checks.What's included
verifyIdentity(provider:)— the unusedproviderIdargument is gone; the route reads onlyprovider.Models/IdentityHealth.swift— the outcome type, distinguishing verified, needs reconnect, andcouldn't run the check.
LinkedIdentitiesView, with the reconnect affordanceon rejection. Manual and per-identity by design: each call hits the third-party provider, so an
on-appear sweep would be the wrong default.
postCamel(_:body:mappingStatuses:)inAPIClientTransport.swift— the write-side twin of theexisting
get(_:mappingStatuses:). Needed becausecheckResponsemaps both 400 and 500 toAPIError.server, which would have made "your token expired" and "the server fell over"indistinguishable — and telling those apart is one of W2: Surface OAuth token health in Connected accounts (verifyIdentity) #92's acceptance criteria.
Corrections to the issue, found by reading the route
200 {"success":true}, not{"ok":true}; the old struct decoded a field the servernever sends.
"No token to verify"when
providerData.access_tokenis missing. Both are reconnect-class, so they're folded togetherdeliberately.
Known backend hole — filed as #100
For any provider the route doesn't recognise, every branch is skipped, nothing is verified, and it
still stamps
lastVerifiedAtand returns200 {"success":true}. A bare"mastodon"string (no:instancesuffix) hits that path. The two 200s are byte-identical, so the client cannot compensateand this PR does not try.
Merge note
The doc comment #90 added to the old
verifyIdentitywas superseded, not merged alongside — thatconflict was resolved by taking this branch's version, since keeping both would have left a stale
duplicate and a signature the call site no longer compiles against.
Testing
xcodebuild … -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests test— 1248 tests, 0 failures after merging current
main.APIClientIdentitiesTestsandIdentityHealthTests; restored, green.Not verified
The live path against a genuinely revoked token — needs a real expired credential. Confirm by
revoking access at a provider, then tapping Check connection: it should flip to "Needs reconnect",
while airplane mode on a healthy row should instead read "Couldn't run the check just now."
🤖 Generated with Claude Code