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
app/Actions/Postline/MediaServeAction.tsadded+45-0
Changes to app/Actions/Postline/MediaServeAction.ts
@@ -0,0 +1,45 @@
1import type { RequestInstance } from '@stacksjs/types'
2import { join } from 'node:path'
3import { Action } from '@stacksjs/actions'
4import { response } from '@stacksjs/router'
5import { isSafeMediaFilename, MEDIA_DIR, mediaContentType } from '../../Support/Social/uploads'
6
7/**
8 * Serve a stored post image so a remote platform can fetch it server-side.
9 *
10 * Instagram and Threads publish an image by handing the Meta Graph API a public
11 * `image_url` it downloads itself — an uploaded file sitting on local disk is
12 * unreachable to them. This route exposes those files at a stable URL.
13 *
14 * It is intentionally UNAUTHENTICATED (Meta fetches it with no session), so it
15 * is deliberately narrow: only a strictly-validated basename is accepted
16 * (`isSafeMediaFilename` rejects any path separator or traversal), the filename
17 * is an unguessable per-upload UUID, and nothing outside the media directory
18 * can be reached.
19 */
20export default new Action({
21 name: 'Postline Media Serve',
22 description: 'Serve a stored post image for remote platforms (Instagram/Threads) to fetch.',
23 method: 'GET',
24
25 async handle(request: RequestInstance) {
26 const file = String(request.get('file') || '').trim()
27 if (!isSafeMediaFilename(file))
28 return response.json({ ok: false, error: 'Invalid media reference.' }, { status: 400 })
29
30 const stored = Bun.file(join(MEDIA_DIR, file))
31 if (!(await stored.exists()))
32 return response.json({ ok: false, error: 'Media not found.' }, { status: 404 })
33
34 const bytes = new Uint8Array(await stored.arrayBuffer())
35 return new Response(bytes as unknown as BodyInit, {
36 status: 200,
37 headers: {
38 'content-type': mediaContentType(file),
39 // Filenames are unique per upload, so the bytes never change — let a
40 // platform re-fetching the same URL hit its cache instead of the app.
41 'cache-control': 'public, max-age=31536000, immutable',
42 },
43 })
44 },
45})