@everystack/server 0.2.4 → 0.2.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
@@ -65,6 +65,7 @@
65
65
  "@everystack/cli": ">=0.1.0",
66
66
  "drizzle-orm": "0.41.0",
67
67
  "postgres": "3.4.9",
68
+ "heic-decode": "2.1.0",
68
69
  "sharp": "0.34.5",
69
70
  "sst": "4.13.1"
70
71
  },
@@ -78,6 +79,9 @@
78
79
  "@everystack/cli": {
79
80
  "optional": true
80
81
  },
82
+ "heic-decode": {
83
+ "optional": true
84
+ },
81
85
  "sharp": {
82
86
  "optional": true
83
87
  },
@@ -0,0 +1,9 @@
1
+ declare module 'heic-decode' {
2
+ interface DecodedImage {
3
+ width: number;
4
+ height: number;
5
+ data: Uint8ClampedArray;
6
+ }
7
+ function decode(opts: { buffer: Buffer | ArrayBuffer }): Promise<DecodedImage>;
8
+ export default decode;
9
+ }
package/src/image.ts CHANGED
@@ -142,15 +142,17 @@ function isNotFound(error: any): boolean {
142
142
 
143
143
  export async function processImage(
144
144
  data: Buffer,
145
- params: TransformParams
145
+ params: TransformParams,
146
+ raw?: { width: number; height: number; channels: 1 | 2 | 3 | 4 }
146
147
  ): Promise<{ data: Buffer; contentType: string }> {
147
148
  // Sharp is provided via Lambda layer
148
149
  const sharp = (await import('sharp')).default;
149
150
 
150
- let pipeline = sharp(data);
151
+ let pipeline = raw ? sharp(data, { raw }) : sharp(data);
151
152
 
152
153
  // Auto-orient based on EXIF metadata (fixes rotated portrait thumbnails)
153
- pipeline = pipeline.rotate();
154
+ // Skip for raw pixel input — no EXIF to orient
155
+ if (!raw) pipeline = pipeline.rotate();
154
156
 
155
157
  // Resize
156
158
  if (params.w || params.h) {
@@ -346,21 +348,52 @@ export function createImageHandler(
346
348
  const hasTransforms = params.w || params.h || params.fit || params.fm || params.q;
347
349
 
348
350
  // Process with Sharp — catch decode failures (e.g. HEIC without libheif)
349
- let processed: { data: Buffer; contentType: string };
351
+ let processed: { data: Buffer; contentType: string } | undefined;
350
352
  try {
351
353
  processed = hasTransforms
352
354
  ? await processImage(originalData, params)
353
355
  : await processImage(originalData, { fm: 'webp', q: 80 });
354
356
  } 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
- };
357
+ // HEIC fallback: decode via WASM (heic-decode) then re-process with Sharp
358
+ if (detectedFormat === 'heif') {
359
+ try {
360
+ const decode = (await import('heic-decode')).default;
361
+ const { width, height, data } = await decode({ buffer: originalData });
362
+ const rawBuffer = Buffer.from(data.buffer);
363
+ const transformParams = hasTransforms ? params : { fm: 'webp' as const, q: 80 };
364
+ processed = await processImage(rawBuffer, transformParams, { width, height, channels: 4 });
365
+ } catch (heicError) {
366
+ log('warn', 'HEIC decode failed, passing through original', {
367
+ key, contentType: originalContentType,
368
+ error: String(heicError),
369
+ });
370
+ }
371
+ }
372
+
373
+ if (!processed) {
374
+ log('warn', 'Sharp decode failed, passing through original', {
375
+ key, format: detectedFormat, contentType: originalContentType,
376
+ error: String(sharpError),
377
+ });
378
+ // Can't decode — pass through original bytes (better than 422)
379
+ const MAX_PASSTHROUGH = 4_500_000;
380
+ if (originalData.length > MAX_PASSTHROUGH) {
381
+ return {
382
+ statusCode: 422,
383
+ headers: errorHeaders,
384
+ body: JSON.stringify({ error: 'Unsupported image format' }),
385
+ };
386
+ }
387
+ return {
388
+ statusCode: 200,
389
+ headers: {
390
+ 'Content-Type': originalContentType || 'application/octet-stream',
391
+ ...cacheHeaders,
392
+ },
393
+ body: originalData.toString('base64'),
394
+ isBase64Encoded: true,
395
+ };
396
+ }
364
397
  }
365
398
 
366
399
  // --- Write rendered image to S3 cache (fire-and-forget) ---