@everystack/server 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/image.ts +56 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"publishConfig": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"lint": "tsc --noEmit"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@aws-sdk/client-s3": "3.
|
|
63
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
62
|
+
"@aws-sdk/client-s3": "3.1053.0",
|
|
63
|
+
"@aws-sdk/s3-request-presigner": "3.1053.0",
|
|
64
64
|
"@everystack/auth": ">=0.1.0",
|
|
65
65
|
"@everystack/cli": ">=0.1.0",
|
|
66
66
|
"drizzle-orm": "0.41.0",
|
package/src/image.ts
CHANGED
|
@@ -201,10 +201,17 @@ export function createImageHandler(
|
|
|
201
201
|
const path = event.rawPath;
|
|
202
202
|
const key = path.replace(new RegExp(`^${pathPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), '');
|
|
203
203
|
|
|
204
|
+
// Error responses must never be cached by CloudFront — stale 500s
|
|
205
|
+
// are invisible and hard to debug.
|
|
206
|
+
const errorHeaders = {
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
'Cache-Control': 'no-store',
|
|
209
|
+
};
|
|
210
|
+
|
|
204
211
|
if (!key) {
|
|
205
212
|
return {
|
|
206
213
|
statusCode: 400,
|
|
207
|
-
headers:
|
|
214
|
+
headers: errorHeaders,
|
|
208
215
|
body: JSON.stringify({ error: 'Missing image key' }),
|
|
209
216
|
};
|
|
210
217
|
}
|
|
@@ -213,7 +220,7 @@ export function createImageHandler(
|
|
|
213
220
|
if (key.includes('..') || key.startsWith('/')) {
|
|
214
221
|
return {
|
|
215
222
|
statusCode: 403,
|
|
216
|
-
headers:
|
|
223
|
+
headers: errorHeaders,
|
|
217
224
|
body: JSON.stringify({ error: 'Forbidden' }),
|
|
218
225
|
};
|
|
219
226
|
}
|
|
@@ -224,7 +231,7 @@ export function createImageHandler(
|
|
|
224
231
|
if (!allowed) {
|
|
225
232
|
return {
|
|
226
233
|
statusCode: 403,
|
|
227
|
-
headers:
|
|
234
|
+
headers: errorHeaders,
|
|
228
235
|
body: JSON.stringify({ error: 'Forbidden' }),
|
|
229
236
|
};
|
|
230
237
|
}
|
|
@@ -280,7 +287,7 @@ export function createImageHandler(
|
|
|
280
287
|
if (!result.Body) {
|
|
281
288
|
return {
|
|
282
289
|
statusCode: 404,
|
|
283
|
-
headers:
|
|
290
|
+
headers: errorHeaders,
|
|
284
291
|
body: JSON.stringify({ error: 'Image not found' }),
|
|
285
292
|
};
|
|
286
293
|
}
|
|
@@ -291,17 +298,39 @@ export function createImageHandler(
|
|
|
291
298
|
if (isNotFound(error)) {
|
|
292
299
|
return {
|
|
293
300
|
statusCode: 404,
|
|
294
|
-
headers:
|
|
301
|
+
headers: errorHeaders,
|
|
295
302
|
body: JSON.stringify({ error: 'Image not found' }),
|
|
296
303
|
};
|
|
297
304
|
}
|
|
298
305
|
throw error;
|
|
299
306
|
}
|
|
300
307
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
// Detect actual image format from magic bytes — content types from
|
|
309
|
+
// user uploads are frequently wrong (e.g. application/x-www-form-urlencoded
|
|
310
|
+
// on a 19 MB JPEG). Trust the bytes, not the metadata.
|
|
311
|
+
const sharp = (await import('sharp')).default;
|
|
312
|
+
let detectedFormat: string | undefined;
|
|
313
|
+
try {
|
|
314
|
+
const meta = await sharp(originalData).metadata();
|
|
315
|
+
detectedFormat = meta.format; // jpeg, png, webp, gif, tiff, heif, etc.
|
|
316
|
+
} catch {
|
|
317
|
+
// Sharp can't parse it — not an image
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!detectedFormat) {
|
|
321
|
+
// Not an image — pass through non-image files (fonts, CSS, etc.)
|
|
322
|
+
// Lambda responses are capped at 6 MB. Base64 inflates ~33%.
|
|
323
|
+
const MAX_PASSTHROUGH = 4_500_000; // ~4.5 MB raw → ~6 MB base64
|
|
324
|
+
if (originalData.length > MAX_PASSTHROUGH) {
|
|
325
|
+
log('warn', 'File too large for passthrough', {
|
|
326
|
+
key, size: originalData.length, contentType: originalContentType,
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
statusCode: 413,
|
|
330
|
+
headers: errorHeaders,
|
|
331
|
+
body: JSON.stringify({ error: 'File too large' }),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
305
334
|
return {
|
|
306
335
|
statusCode: 200,
|
|
307
336
|
headers: {
|
|
@@ -316,10 +345,23 @@ export function createImageHandler(
|
|
|
316
345
|
// No transform params — serve original image as-is (EXIF-corrected webp)
|
|
317
346
|
const hasTransforms = params.w || params.h || params.fit || params.fm || params.q;
|
|
318
347
|
|
|
319
|
-
// Process with Sharp
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
348
|
+
// Process with Sharp — catch decode failures (e.g. HEIC without libheif)
|
|
349
|
+
let processed: { data: Buffer; contentType: string };
|
|
350
|
+
try {
|
|
351
|
+
processed = hasTransforms
|
|
352
|
+
? await processImage(originalData, params)
|
|
353
|
+
: await processImage(originalData, { fm: 'webp', q: 80 });
|
|
354
|
+
} catch (sharpError) {
|
|
355
|
+
log('error', 'Sharp processing failed', {
|
|
356
|
+
key, format: detectedFormat, contentType: originalContentType,
|
|
357
|
+
error: String(sharpError),
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
statusCode: 422,
|
|
361
|
+
headers: errorHeaders,
|
|
362
|
+
body: JSON.stringify({ error: 'Unsupported image format' }),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
323
365
|
|
|
324
366
|
// --- Write rendered image to S3 cache (fire-and-forget) ---
|
|
325
367
|
if (useCache && s3CacheKey) {
|
|
@@ -351,7 +393,7 @@ export function createImageHandler(
|
|
|
351
393
|
log('error', 'Image processing error', { error: String(error), path: event.rawPath });
|
|
352
394
|
return {
|
|
353
395
|
statusCode: 500,
|
|
354
|
-
headers: { 'Content-Type': 'application/json' },
|
|
396
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
|
|
355
397
|
body: JSON.stringify({ error: 'Image processing failed' }),
|
|
356
398
|
};
|
|
357
399
|
}
|