Commit graph

3 commits

Author SHA1 Message Date
Paulo Castellano
d5e28e3d02 fix(social): move Mastodon default instance to config + cleanup
Review follow-ups:

- verifyMastodon was the last hardcoded host left after the PR moved
  LinkedIn/YouTube/Bluesky to config. Adds trypost.platforms.mastodon
  .default_instance (env MASTODON_DEFAULT_INSTANCE) and reads from it.
- refreshToken() docblock now declares @throws PlatformUnavailableException
  (the whole point of the PR was missing from its contract).
- Strip the new explanatory comments inside catch blocks and tests —
  rationale lives in the commit / PR, not inline. The two comments
  inside empty `catch (TokenExpiredException) {}` blocks stay because
  there the comment is the only thing telling the reader why the
  exception is swallowed.
2026-05-19 08:27:46 -03:00
Paulo Castellano
6f96d67dbc fix(social): distinguish platform-down from token-expired
When a provider's API was down (5xx, timeout, DNS), the hourly
RefreshSocialToken job and daily VerifyWorkspaceConnections job were
treating it as "token revoked" and emailing the user to reconnect.
Bluesky going offline triggered false-positive disconnect notifications
because Bluesky access tokens are short-lived (2h) so every hourly
refresh failed during the outage.

- New PlatformUnavailableException: API unreachable / 5xx, transient.
  TokenExpiredException stays for 4xx (token is provably bad).
- New TokenRefreshClient: normalizes failure semantics for OAuth
  refresh HTTP calls across all providers. Takes a Platform enum so
  typos fail at compile time and the user-facing label comes from
  one source.
- ConnectionVerifier: all 8 refresh*Token methods route through the
  new client. Hardcoded OAuth URLs (LinkedIn, YouTube) and Bluesky's
  default PDS host moved into config/trypost.php alongside the
  existing per-platform entries.
- RefreshSocialToken job: PlatformUnavailableException → log warning
  and stop. Do NOT markAsTokenExpired, do NOT notify the user. Next
  scheduled tick retries.
- VerifyWorkspaceConnections job: PlatformUnavailableException from
  the inner refresh propagates and is treated as a transient skip.
2026-05-19 08:16:43 -03:00
Paulo Castellano
3ba47ad02a fix(social): proactive token refresh actually refreshes (not just verifies)
Three orthogonal fixes that together close the gap where social tokens
were silently aging out without ever being refreshed, then dying at the
provider when the refresh_token also got revoked.

The original failure mode: a user's X token expired because the hourly
proactive-refresh cron's smart `verify()` skip-logic kept saying 'token
still works, no need to refresh', and once the token actually expired,
the cron's WHERE clause excluded it from future runs. By the time anyone
noticed, the refresh_token at X was also gone.

(C) ConnectionVerifier: rename private `refreshTokenIfNeeded` →
    public `refreshToken`. Callers that want the smart 'try
    access_token first' behavior keep using `verify()`. Callers that
    want a proactive refresh (the cron) call `refreshToken` directly.

(B) RefreshExpiringTokens command: drop the
    `where('token_expires_at', '>', now())` filter. Already-expired
    tokens now get a last-chance refresh attempt before the
    refresh_token also dies at the provider. Status filter
    (`Connected`) still excludes accounts already marked TokenExpired.

(D) RefreshSocialToken job: switch from `verify()` to
    `refreshToken()`, and on `TokenExpiredException` call
    `markAsTokenExpired` so the user is notified immediately. The lock
    + transition detection in markAsTokenExpired prevents notification
    spam if subsequent cron passes also fail.

Tests:
- 3 new tests for RefreshSocialToken (calls refreshToken not verify,
  marks TokenExpired on TokenExpiredException, logs warning on other
  errors)
- Updated RefreshExpiringTokens test to assert already-expired tokens
  are now dispatched (was previously asserted as 'should NOT')
2026-05-12 19:36:35 -03:00