Skip to content

feat(identities): surface real OAuth token health in Connected accounts - #110

Merged
Adron merged 2 commits into
mainfrom
feat/identity-token-health
Sep 17, 2026
Merged

Adron merged 2 commits into
mainfrom
feat/identity-token-health

Conversation

@Adron

@Adron Adron commented Sep 17, 2026

Copy link
Copy Markdown
Member

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 no
longer exists cannot be reopened or retargeted. Same branch, same commits, now based on main.

LinkedIdentitiesView showed each provider's /status, which reports whether the provider is
configured 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/verify is the only route that actually checks.

What's included

  • verifyIdentity(provider:) — the unused providerId argument is gone; the route reads only
    provider.
  • Models/IdentityHealth.swift — the outcome type, distinguishing verified, needs reconnect, and
    couldn't run the check.
  • A per-identity Check connection action in LinkedIdentitiesView, with the reconnect affordance
    on 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:) in APIClientTransport.swift — the write-side twin of the
    existing get(_:mappingStatuses:). Needed because checkResponse maps both 400 and 500 to
    APIError.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

  • Success is 200 {"success":true}, not {"ok":true}; the old struct decoded a field the server
    never sends.
  • There are two distinct 400s, not one — the issue's "token is bad" plus "No token to verify"
    when providerData.access_token is missing. Both are reconnect-class, so they're folded together
    deliberately.

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 lastVerifiedAt and returns 200 {"success":true}. A bare "mastodon" string (no
:instance suffix) hits that path. The two 200s are byte-identical, so the client cannot compensate
and this PR does not try.

Merge note

The doc comment #90 added to the old verifyIdentity was superseded, not merged alongside — that
conflict 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.
  • Tests were mutation-checked: five simultaneous mutations produced 8 failures across
    APIClientIdentitiesTests and IdentityHealthTests; 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

Adron and others added 2 commits September 16, 2026 12:53
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
@Adron
Adron merged commit 3ecffb0 into main Sep 17, 2026
1 check passed
@Adron
Adron deleted the feat/identity-token-health branch September 17, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W2: Surface OAuth token health in Connected accounts (verifyIdentity)

1 participant