image-transform.ts46 lines · main
| 1 | import { createHmac } from 'node:crypto'; |
| 2 | |
| 3 | import { env } from '../env.js'; |
| 4 | |
| 5 | const MEDIA_BASE = 'https://media.briven.tech'; |
| 6 | const MAX_DIM = 2000; // resize-bomb guard |
| 7 | |
| 8 | export function isImageTransformConfigured(): boolean { |
| 9 | return Boolean(env.BRIVEN_IMGPROXY_ENDPOINT && env.BRIVEN_IMGPROXY_KEY && env.BRIVEN_IMGPROXY_SALT); |
| 10 | } |
| 11 | |
| 12 | export 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 | */ |
| 24 | export 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 | } |