image-transform.ts46 lines · main
1import { createHmac } from 'node:crypto';
2
3import { env } from '../env.js';
4
5const MEDIA_BASE = 'https://media.briven.tech';
6const MAX_DIM = 2000; // resize-bomb guard
7
8export function isImageTransformConfigured(): boolean {
9 return Boolean(env.BRIVEN_IMGPROXY_ENDPOINT && env.BRIVEN_IMGPROXY_KEY && env.BRIVEN_IMGPROXY_SALT);
10}
11
12export interface TransformOpts {
13 width?: number;
14 height?: number;
15 resize?: 'fit' | 'fill' | 'auto';
16}
17
18/**
19 * Build a signed imgproxy URL for a PUBLIC file. imgproxy fetches the source
20 * from the public media URL, resizes, and returns it. The HMAC signature (key
21 * + salt, both hex) stops attackers from requesting arbitrary transforms
22 * (resize-bomb abuse). Dimensions are clamped to MAX_DIM.
23 */
24export function signedTransformUrl(projectId: string, fileId: string, opts: TransformOpts = {}): string {
25 const endpoint = env.BRIVEN_IMGPROXY_ENDPOINT;
26 const keyHex = env.BRIVEN_IMGPROXY_KEY;
27 const saltHex = env.BRIVEN_IMGPROXY_SALT;
28 if (!endpoint || !keyHex || !saltHex) {
29 throw new Error('image transforms not configured');
30 }
31 const w = Math.min(Math.max(0, Math.floor(opts.width ?? 0)), MAX_DIM);
32 const h = Math.min(Math.max(0, Math.floor(opts.height ?? 0)), MAX_DIM);
33 const resize = opts.resize ?? 'fit';
34 const source = `${MEDIA_BASE}/media/${projectId}/${fileId}`;
35 const encodedSource = Buffer.from(source).toString('base64url');
36 // imgproxy processing options: resize + gravity smart; extension omitted (keeps source format)
37 const processing = `rs:${resize}:${w}:${h}:0/g:sm`;
38 const path = `/${processing}/${encodedSource}`;
39 const key = Buffer.from(keyHex, 'hex');
40 const salt = Buffer.from(saltHex, 'hex');
41 const hmac = createHmac('sha256', key);
42 hmac.update(salt);
43 hmac.update(path);
44 const signature = hmac.digest('base64url');
45 return `${endpoint.replace(/\/$/, '')}/${signature}${path}`;
46}