ReviewOS

also looking at this

stacks/postline

feat: crossposting — media (LinkedIn/IG/Threads), OAuth token refresh, and X/Twitter

#30
Merged glennmichael123 wants to merge feat/crosspost-media-and-token-refresh into main
28 files +1810 -109
9 days ago

Rounds out Postline's crossposting: media everywhere, tokens that stay alive, and a brand-new X/Twitter provider. All app-owned (app/) so a framework upgrade can't clobber it — the vendored drivers are subclassed, never patched. Four commits:

1. LinkedIn image upload (#18)

The vendored LinkedIn driver could only post text or an article card. An app-owned subclass adds the versioned Images API flow (initializeUpload → PUT bytes → content.media URN), wired through LinkedInService.

2. Proactive OAuth token refresh (#19)

Connected OAuth providers went stale silently. Now Threads (short→long-lived + th_refresh_token), LinkedIn (captures the dropped refresh_token, refreshes near expiry), and Instagram (long-lived user-token exchange) stay alive. New social_identities.token_expires_at; refresh is best-effort, guarded, idempotent, and runs on the scheduled path.

3. Instagram/Threads uploaded-file media (#18)

IG/Threads publish an image by a public URL Meta fetches server-side. New unauthenticated GET /postline/media route serves a stored image by strictly-validated basename (no traversal; unguessable UUID). Queue path attaches a public url in hydrateContent; immediate-publish persists the upload then deletes it. URL-only providers use the url; byte-upload providers use bytes.

4. X/Twitter crossposting (new)

@stacksjs/socials only ships a sign-in provider for Twitter (no publish), so this is a full app-owned driver like Mastodon's:

  • OAuth 2.0 + PKCE connect (S256 challenge, refresh via offline.access), POST /2/tweets, image upload via POST /2/media/upload, reply-chained threads.
  • TwitterService + the same proactive token refresh; wired into CrosspostService, DriverRegistry, ProvidersStatusAction; /postline/twitter routes + Actions; the Accounts "Planned" stub is now a real OAuth connect card; the composer picks X up automatically. No migration — 'twitter' was already whitelisted in every provider CHECK.

Verification

  • bun test tests/unit — 74 pass / 0 fail (38 new: 10 LinkedIn media, 8 token primitives, 9 media hosting, 11 X/Twitter)
  • bun run typecheck — exit 0 · pickier — clean

Caveats / honest limits

  • Not live-verified — no real LinkedIn / Threads / Instagram / X credentials here. Driver primitives are unit-tested against the documented API shapes; live HTTP hasn't been exercised.
  • X/Twitter posting requires a paid API tier; its media endpoint (/2/media/upload) follows X's current v2 docs and may need live tuning per tier.
  • Run the migration (token_expires_at) before OAuth providers connect.
  • IG/Threads media needs a publicly reachable instance (STORAGE_PUBLIC_URL / APP_URL).
  • Does not touch #17 (real Bluesky publish proof — needs a live app password).

28 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.