@everystack/server 0.3.1 → 0.3.2

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.
Files changed (2) hide show
  1. package/package.json +10 -10
  2. package/src/image.ts +66 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -74,6 +74,11 @@
74
74
  "default": "./src/media.ts"
75
75
  }
76
76
  },
77
+ "scripts": {
78
+ "test": "jest",
79
+ "build": "tsc --build",
80
+ "lint": "tsc --noEmit"
81
+ },
77
82
  "peerDependencies": {
78
83
  "esbuild": ">=0.20.0",
79
84
  "@aws-sdk/client-cloudfront-keyvaluestore": ">=3.1053.0",
@@ -129,6 +134,8 @@
129
134
  "@aws-sdk/client-s3": "3.1053.0",
130
135
  "@aws-sdk/lib-storage": "3.1053.0",
131
136
  "@aws-sdk/signature-v4a": "3.1063.0",
137
+ "@everystack/auth": "workspace:*",
138
+ "@everystack/cli": "workspace:*",
132
139
  "@types/aws-lambda": "8.10.161",
133
140
  "@types/jest": "29.5.14",
134
141
  "@types/node": "22.19.18",
@@ -138,13 +145,6 @@
138
145
  "postgres": "3.4.9",
139
146
  "sst": "4.13.1",
140
147
  "ts-jest": "29.4.9",
141
- "typescript": "5.9.3",
142
- "@everystack/auth": "0.3.0",
143
- "@everystack/cli": "0.3.3"
144
- },
145
- "scripts": {
146
- "test": "jest",
147
- "build": "tsc --build",
148
- "lint": "tsc --noEmit"
148
+ "typescript": "5.9.3"
149
149
  }
150
- }
150
+ }
package/src/image.ts CHANGED
@@ -155,6 +155,63 @@ export function parseParams(query: Record<string, string | undefined>): Transfor
155
155
  return params;
156
156
  }
157
157
 
158
+ /**
159
+ * ISOBMFF `ftyp` brands that mean "HEVC-coded HEIF" — i.e. a HEIC still image
160
+ * (iPhones shoot these by default). Sharp reports all of these as format
161
+ * `'heif'`, and our HEIC rescue path (heic-decode) can decode them.
162
+ */
163
+ const HEIC_BRANDS = new Set([
164
+ 'heic', 'heix', 'heim', 'heis', 'hevc', 'hevx', 'hevm', 'hevs', 'mif1', 'msf1',
165
+ ]);
166
+ const AVIF_BRANDS = new Set(['avif', 'avis']);
167
+
168
+ /**
169
+ * Identify an image format from its magic bytes, independently of Sharp.
170
+ *
171
+ * Sharp builds without libheif cannot even *identify* a HEIF container —
172
+ * `sharp(buf).metadata()` throws, format detection yields `undefined`, and a
173
+ * real HEIC photo is misrouted down the non-image passthrough path (413 when
174
+ * over the passthrough cap, or raw `image/heic` bytes that only Safari renders).
175
+ * Trusting the bytes instead of Sharp makes HEIC detection build-independent and
176
+ * lets the existing `heif` rescue path fire.
177
+ *
178
+ * Returns a Sharp-compatible format string (`jpeg`, `png`, `gif`, `webp`,
179
+ * `heif`, `avif`) or `undefined` when the bytes aren't a recognized image.
180
+ */
181
+ export function sniffImageFormat(data: Buffer): string | undefined {
182
+ if (data.length < 3) return undefined;
183
+
184
+ // JPEG: FF D8 FF
185
+ if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) return 'jpeg';
186
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
187
+ if (
188
+ data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 &&
189
+ data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a
190
+ ) return 'png';
191
+ // GIF: "GIF87a" / "GIF89a"
192
+ if (data.toString('ascii', 0, 3) === 'GIF') return 'gif';
193
+ // WebP: "RIFF"...."WEBP"
194
+ if (data.toString('ascii', 0, 4) === 'RIFF' && data.toString('ascii', 8, 12) === 'WEBP') return 'webp';
195
+ // TIFF: "II*\0" / "MM\0*"
196
+ if (
197
+ (data[0] === 0x49 && data[1] === 0x49 && data[2] === 0x2a && data[3] === 0x00) ||
198
+ (data[0] === 0x4d && data[1] === 0x4d && data[2] === 0x00 && data[3] === 0x2a)
199
+ ) return 'tiff';
200
+
201
+ // ISOBMFF (HEIC/HEIF/AVIF): [4-byte size]["ftyp"][major brand][minor][compatible…]
202
+ if (data.toString('ascii', 4, 8) === 'ftyp') {
203
+ const boxSize = Math.min(data.readUInt32BE(0) || data.length, data.length);
204
+ const brands: string[] = [data.toString('ascii', 8, 12)];
205
+ for (let off = 16; off + 4 <= boxSize; off += 4) {
206
+ brands.push(data.toString('ascii', off, off + 4));
207
+ }
208
+ if (brands.some((b) => HEIC_BRANDS.has(b))) return 'heif';
209
+ if (brands.some((b) => AVIF_BRANDS.has(b))) return 'avif';
210
+ }
211
+
212
+ return undefined;
213
+ }
214
+
158
215
  function isNotFound(error: any): boolean {
159
216
  return (
160
217
  error.name === 'NotFound' ||
@@ -367,7 +424,15 @@ export function createImageHandler(
367
424
  const meta = await sharp(originalData).metadata();
368
425
  detectedFormat = meta.format; // jpeg, png, webp, gif, tiff, heif, etc.
369
426
  } catch {
370
- // Sharp can't parse it — not an image
427
+ // Sharp can't parse it — not an image, OR a HEIF container this Sharp
428
+ // build has no libheif to identify. Fall through to the magic-byte sniff.
429
+ }
430
+
431
+ // A Sharp build without libheif throws on HEIF and leaves detectedFormat
432
+ // undefined, misrouting real HEIC photos to the non-image passthrough
433
+ // below. Sniff the bytes so `heif` is detected and the rescue path fires.
434
+ if (!detectedFormat) {
435
+ detectedFormat = sniffImageFormat(originalData);
371
436
  }
372
437
 
373
438
  if (!detectedFormat) {