also looking at this
feat: crossposting — media (LinkedIn/IG/Threads), OAuth token refresh, and X/Twitter
#30
28 files
+1810
-109
| @@ -0,0 +1,45 @@ | ||
| 1 | import type { RequestInstance } from '@stacksjs/types' | |
| 2 | import { join } from 'node:path' | |
| 3 | import { Action } from '@stacksjs/actions' | |
| 4 | import { response } from '@stacksjs/router' | |
| 5 | import { 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 | */ | |
| 20 | export 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 | }) | |