Commit graph

4 commits

Author SHA1 Message Date
Paulo Castellano
94d0156ec0 Give Instagram/Threads a wider proactive-refresh window
Extension-model tokens (Instagram/Threads) can't be refreshed once they
expire, so the shared 30-minute cron window left only a ~15-minute buffer
against queue backlog on the default queue — and a lapse forces a full
reconnect. Rotating platforms go through verify() and won't rotate a
still-valid token, so they keep the tight 30-minute window; extension
platforms now get a 24-hour lead via a per-model query.
2026-07-03 10:33:26 -03:00
Paulo Castellano
2f4b974130 Fix X token chain breaking from over-rotation
X OAuth2 refresh tokens are single-use: each refresh rotates the pair and
invalidates the previous refresh_token, and reusing a rotated one kills the
whole family. Three things made this fragile and disconnected accounts far
more often than necessary:

- The proactive refresh job called refreshToken() directly, bypassing the
  access-token-first guard in verify() and rotating on every run.
- RefreshExpiringTokens used a 2h window on an hourly schedule — equal to the
  2h access-token lifetime — so every X account was rotated every hour even
  while its token was still valid.
- A single 4xx refresh failure disconnected the account without checking
  whether a concurrent refresh had already persisted a working token.

Changes:
- RefreshSocialToken now routes through verify() (access-token-first), so it
  only rotates when the access_token is actually invalid.
- Shrink the proactive window to 30m and run the command every 15m, so the
  window still covers the run interval but rotation happens near real expiry.
- verify() tolerates the lost-rotation race: on a 4xx refresh, reload and
  verify with a concurrently-refreshed token before marking TokenExpired.

Refs #126
2026-07-02 20:32:55 -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
Paulo Castellano
c48c774e23 feat: publishing engine improvements — rate limit retry, inline token refresh, per-platform queues, proactive refresh
- Add HasSocialHttpClient trait with 429 rate limit retry (3 attempts, 5s delay)
- Integrate trait into all 10 publishers (YouTube uses Google SDK)
- Add inline token refresh retry in PublishToSocialPlatform job
- Add per-platform Horizon queues via Platform::queue() and Platform::allQueues()
- Add RefreshExpiringTokens hourly command for proactive token refresh
- Fix token leaks: redact response bodies in all Log::error calls
- Fix token leaks: remove $response->body() from exception messages
- Fix ConnectionVerifier: redact all refresh error logs
- Fix null checks on API response IDs (Instagram, Threads, Pinterest, Facebook)
- Fix PublishPost::failed() to mark post as failed
- Fix StoreChunkedMediaRequest: validate max 1GB total size
- Fix scheduled_at validation: string → date
- Fix StoreMediaRequest: images max 10MB, videos max 1GB, only MP4 video
2026-04-01 10:51:53 -03:00