media.ts67 lines · main
1import { Hono } from 'hono';
2
3import { openPublicObject, openOwnedObject } from '../services/storage.js';
4import { resolveShareLink } from '../services/storage-share-links.js';
5import { originsForProject, brivenOwnOrigins } from '../services/auth-origin-allowlist.js';
6
7export const mediaRouter = new Hono();
8
9// Public: serve a file that's been marked public, from the clean media host.
10// No auth. Per-tenant CORS: echo the requesting Origin only if it's in the
11// project's allowed-domains (or a briven-own origin).
12//
13// Bodies are streamed through `new Response(ReadableStream, ...)` — the same
14// idiom the branding-logo + AI-stream routes use for serving object bytes —
15// so we build every header (incl. the tenant-scoped CORS ones) manually.
16mediaRouter.get('/media/:projectId/:fileId', async (c) => {
17 const projectId = c.req.param('projectId');
18 const fileId = c.req.param('fileId');
19 const obj = await openPublicObject(projectId, fileId);
20 if (!obj) return c.text('not found', 404);
21
22 const headers: Record<string, string> = {
23 'content-type': obj.contentType,
24 'cache-control': 'public, max-age=300',
25 };
26 if (obj.contentLength) headers['content-length'] = obj.contentLength;
27
28 const origin = c.req.header('origin');
29 if (origin) {
30 const allowed = await originsForProject(projectId).catch(() => [] as string[]);
31 if (allowed.includes(origin) || brivenOwnOrigins().includes(origin)) {
32 headers['access-control-allow-origin'] = origin;
33 headers['vary'] = 'Origin';
34 }
35 }
36
37 return new Response(obj.body, { status: 200, headers });
38});
39
40// Public: serve a file via a tokenized SHARE-LINK. No auth — the token IS the
41// authorization. `resolveShareLink` is strict-deny: it returns the file ONLY for
42// an active (unrevoked, unexpired) link whose token exact-matches. On any miss it
43// returns null and we answer a plain 404 — deliberately NOT leaking whether the
44// token, the link, or the file exists (all misses look identical).
45//
46// A valid link serves the file EVEN IF it is not marked public — that is the
47// point of a private, time-limited link — so we open the OWNED object (not the
48// public one). Same streaming idiom as /media. No CORS echo: a share-link is a
49// direct bearer URL (opened by navigation/download), not a cross-origin fetch
50// against a project's allow-listed app origins.
51mediaRouter.get('/link/:token', async (c) => {
52 const token = c.req.param('token');
53 const resolved = await resolveShareLink(token);
54 if (!resolved) return c.text('not found', 404);
55
56 const obj = await openOwnedObject(resolved.projectId, resolved.fileId);
57 if (!obj) return c.text('not found', 404);
58
59 const headers: Record<string, string> = {
60 'content-type': obj.contentType,
61 // Private link → don't let shared caches fan it out.
62 'cache-control': 'private, max-age=60',
63 };
64 if (obj.contentLength) headers['content-length'] = obj.contentLength;
65
66 return new Response(obj.body, { status: 200, headers });
67});