img-upload-kit 0.0.0-stage → 2.0.0

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 (112) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +192 -0
  4. package/README.md +242 -2
  5. package/dist/bin.js +1186 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/blur.cjs +41 -0
  8. package/dist/blur.cjs.map +1 -0
  9. package/dist/blur.d.cts +17 -0
  10. package/dist/blur.d.ts +17 -0
  11. package/dist/blur.js +41 -0
  12. package/dist/blur.js.map +1 -0
  13. package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
  14. package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
  15. package/dist/blurService-D5TpMNji.d.ts +83 -0
  16. package/dist/blurService-DacwylJx.d.cts +83 -0
  17. package/dist/chunk-2OVNXHTN.cjs +514 -0
  18. package/dist/chunk-2OVNXHTN.cjs.map +1 -0
  19. package/dist/chunk-3ICG2SLL.cjs +135 -0
  20. package/dist/chunk-3ICG2SLL.cjs.map +1 -0
  21. package/dist/chunk-4ASFG3KB.js +514 -0
  22. package/dist/chunk-4ASFG3KB.js.map +1 -0
  23. package/dist/chunk-AI3JJLSA.js +2370 -0
  24. package/dist/chunk-AI3JJLSA.js.map +1 -0
  25. package/dist/chunk-CV23O4YU.js +145 -0
  26. package/dist/chunk-CV23O4YU.js.map +1 -0
  27. package/dist/chunk-G7LLSKBV.js +158 -0
  28. package/dist/chunk-G7LLSKBV.js.map +1 -0
  29. package/dist/chunk-IB3TEDLR.js +294 -0
  30. package/dist/chunk-IB3TEDLR.js.map +1 -0
  31. package/dist/chunk-KJVYWKIG.cjs +158 -0
  32. package/dist/chunk-KJVYWKIG.cjs.map +1 -0
  33. package/dist/chunk-NEDBIIE3.cjs +226 -0
  34. package/dist/chunk-NEDBIIE3.cjs.map +1 -0
  35. package/dist/chunk-QHU7FUO4.cjs +37 -0
  36. package/dist/chunk-QHU7FUO4.cjs.map +1 -0
  37. package/dist/chunk-QQHZJKNA.js +135 -0
  38. package/dist/chunk-QQHZJKNA.js.map +1 -0
  39. package/dist/chunk-S2IFAWJK.js +37 -0
  40. package/dist/chunk-S2IFAWJK.js.map +1 -0
  41. package/dist/chunk-TT5C7K3Q.js +226 -0
  42. package/dist/chunk-TT5C7K3Q.js.map +1 -0
  43. package/dist/chunk-U2TWTEBY.cjs +2370 -0
  44. package/dist/chunk-U2TWTEBY.cjs.map +1 -0
  45. package/dist/chunk-WISG74VL.cjs +294 -0
  46. package/dist/chunk-WISG74VL.cjs.map +1 -0
  47. package/dist/chunk-YRXXQUDE.cjs +145 -0
  48. package/dist/chunk-YRXXQUDE.cjs.map +1 -0
  49. package/dist/cli.cjs +1241 -0
  50. package/dist/cli.cjs.map +1 -0
  51. package/dist/cli.d.cts +99 -0
  52. package/dist/cli.d.ts +99 -0
  53. package/dist/cli.js +1194 -0
  54. package/dist/cli.js.map +1 -0
  55. package/dist/headless.cjs +27 -0
  56. package/dist/headless.cjs.map +1 -0
  57. package/dist/headless.d.cts +4 -0
  58. package/dist/headless.d.ts +4 -0
  59. package/dist/headless.js +27 -0
  60. package/dist/headless.js.map +1 -0
  61. package/dist/index-BIdnqtW8.d.ts +234 -0
  62. package/dist/index-Brk_uHs6.d.cts +234 -0
  63. package/dist/index-CPMlaQsC.d.ts +635 -0
  64. package/dist/index-DHINe-rA.d.cts +635 -0
  65. package/dist/index.cjs +65 -0
  66. package/dist/index.cjs.map +1 -0
  67. package/dist/index.d.cts +43 -0
  68. package/dist/index.d.ts +43 -0
  69. package/dist/index.js +65 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/memoryAdapter-Ck0ccZbF.d.cts +25 -0
  72. package/dist/memoryAdapter-DxEKaG0b.d.ts +25 -0
  73. package/dist/placeholders-C4ftwaII.d.ts +5 -0
  74. package/dist/placeholders-x_3rt65M.d.cts +5 -0
  75. package/dist/react.cjs +3779 -0
  76. package/dist/react.cjs.map +1 -0
  77. package/dist/react.d.cts +476 -0
  78. package/dist/react.d.ts +476 -0
  79. package/dist/react.js +3779 -0
  80. package/dist/react.js.map +1 -0
  81. package/dist/server-handler.cjs +14 -0
  82. package/dist/server-handler.cjs.map +1 -0
  83. package/dist/server-handler.d.cts +3 -0
  84. package/dist/server-handler.d.ts +3 -0
  85. package/dist/server-handler.js +14 -0
  86. package/dist/server-handler.js.map +1 -0
  87. package/dist/server.cjs +1801 -0
  88. package/dist/server.cjs.map +1 -0
  89. package/dist/server.d.cts +142 -0
  90. package/dist/server.d.ts +142 -0
  91. package/dist/server.js +1801 -0
  92. package/dist/server.js.map +1 -0
  93. package/dist/storage.cjs +447 -0
  94. package/dist/storage.cjs.map +1 -0
  95. package/dist/storage.d.cts +163 -0
  96. package/dist/storage.d.ts +163 -0
  97. package/dist/storage.js +447 -0
  98. package/dist/storage.js.map +1 -0
  99. package/dist/styles/animated-image.css +112 -0
  100. package/dist/styles/dropzone-and-tiles.css +247 -0
  101. package/dist/styles/modals-and-dock.css +248 -0
  102. package/dist/styles/primitives.css +197 -0
  103. package/dist/styles/quality-and-inputs.css +184 -0
  104. package/dist/styles/tokens.css +123 -0
  105. package/dist/styles.css +1122 -0
  106. package/dist/types-BrP-o9nd.d.cts +85 -0
  107. package/dist/types-sZ3KoFQY.d.ts +85 -0
  108. package/dist/types-yShGNQnW.d.cts +61 -0
  109. package/dist/types-yShGNQnW.d.ts +61 -0
  110. package/dist/upload-C5Trd6Dh.d.cts +154 -0
  111. package/dist/upload-C5Trd6Dh.d.ts +154 -0
  112. package/package.json +198 -4
package/dist/server.js ADDED
@@ -0,0 +1,1801 @@
1
+ import {
2
+ UploadValidationError,
3
+ createUploadHandler,
4
+ createUploadHttpHandler,
5
+ isClientValidationError
6
+ } from "./chunk-4ASFG3KB.js";
7
+ import "./chunk-S2IFAWJK.js";
8
+ import {
9
+ DEFAULT_ALLOWED_FORMATS,
10
+ DEFAULT_IMAGE_DIMENSIONS,
11
+ STANDARD_PRESETS,
12
+ SVG_MIME_TYPE,
13
+ hexToRgb,
14
+ rgbToHex,
15
+ validateImageFileStrict
16
+ } from "./chunk-TT5C7K3Q.js";
17
+
18
+ // src/server/optimization/types.ts
19
+ var DEFAULT_LIMIT_INPUT_PIXELS = 5e7;
20
+ var PRIMARY_AVIF_EFFORT = 6;
21
+ var RETRY_AVIF_EFFORT = 5;
22
+ var BLUR_PLACEHOLDER_WIDTH = 128;
23
+ var BLUR_PLACEHOLDER_FALLBACK_WIDTH = 96;
24
+ var BLUR_PLACEHOLDER_QUALITY = 80;
25
+ var BLUR_PLACEHOLDER_MAX_CHARS = 8e3;
26
+ var EDGE_COLOR_FALLBACK = "#6B7280";
27
+ var EDGE_WHITE_FALLBACK = "#F9FAFB";
28
+ var WHITE_DOMINANCE_RATIO = 1.35;
29
+
30
+ // src/server/security/sanitizeSvg.ts
31
+ var DEFAULT_MAX_SVG_BYTES = 1024 * 1024;
32
+ var FORBIDDEN_ELEMENTS = [
33
+ "script",
34
+ "foreignObject",
35
+ "iframe",
36
+ "embed",
37
+ "object",
38
+ "applet",
39
+ "meta",
40
+ "link",
41
+ "base",
42
+ "audio",
43
+ "video",
44
+ "canvas",
45
+ "animate",
46
+ "animateMotion",
47
+ "animateTransform",
48
+ "set",
49
+ "handler",
50
+ "listener",
51
+ "tbreak"
52
+ ];
53
+ var SAFE_NAMED_ENTITIES = /* @__PURE__ */ new Set(["amp", "lt", "gt", "quot", "apos"]);
54
+ var SAFE_HREF_FRAGMENT_RE = /^#[a-zA-Z0-9_.:-]+$/;
55
+ var SAFE_DATA_IMAGE_RE = /^data:image\/(?:png|jpeg|webp);base64,[a-zA-Z0-9+/=\s]+$/i;
56
+ function stripControlAndSpace(str) {
57
+ let out = "";
58
+ for (let i = 0; i < str.length; i++) {
59
+ if (str.charCodeAt(i) > 32) out += str[i];
60
+ }
61
+ return out;
62
+ }
63
+ function decodeHtmlEntitiesForCheck(value) {
64
+ const decoded = value.replace(
65
+ /&#x([0-9a-fA-F]+);?/g,
66
+ (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16) || 0)
67
+ ).replace(
68
+ /&#(\d+);?/g,
69
+ (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10) || 0)
70
+ );
71
+ return stripControlAndSpace(decoded);
72
+ }
73
+ function isSafeSvgHref(rawValue) {
74
+ const trimmed = rawValue.trim();
75
+ if (SAFE_HREF_FRAGMENT_RE.test(trimmed)) return true;
76
+ const decoded = decodeHtmlEntitiesForCheck(trimmed);
77
+ if (SAFE_HREF_FRAGMENT_RE.test(decoded)) return true;
78
+ if (SAFE_DATA_IMAGE_RE.test(trimmed) && SAFE_DATA_IMAGE_RE.test(decoded)) {
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ function sanitizeCssBlock(css) {
84
+ return css.replace(/@import\b[^;{}]*(?:;|$)/gi, "").replace(/expression\s*\([^)]*\)/gi, "").replace(/(?:javascript|vbscript|data)\s*:[^;)"']*/gi, "").replace(/-moz-binding\s*:[^;}"']*/gi, "").replace(/behavior\s*:[^;}"']*/gi, "").replace(/url\s*\(\s*(['"]?)(.*?)\1\s*\)/gi, (full, _q, target) => {
85
+ const cleaned = decodeHtmlEntitiesForCheck(target.trim());
86
+ return SAFE_HREF_FRAGMENT_RE.test(cleaned) ? full : "none";
87
+ });
88
+ }
89
+ function sanitizeTagAttributes(attrs) {
90
+ const attrRe = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
91
+ const kept = [];
92
+ for (const match of attrs.matchAll(attrRe)) {
93
+ const rawName = match[1];
94
+ const lowerName = rawName.toLowerCase();
95
+ const localName = lowerName.includes(":") ? lowerName.split(":")[1] : lowerName;
96
+ const rawValue = match[2] ?? match[3] ?? match[4] ?? "";
97
+ if (lowerName.startsWith("on") || localName.startsWith("on")) {
98
+ continue;
99
+ }
100
+ if (localName === "href" || lowerName === "xlink:href" || localName === "src" || localName === "action" || localName === "formaction" || localName === "data") {
101
+ if (!isSafeSvgHref(rawValue)) {
102
+ continue;
103
+ }
104
+ const escaped = rawValue.replace(/"/g, "&quot;");
105
+ kept.push(`${rawName}="${escaped}"`);
106
+ continue;
107
+ }
108
+ if (localName === "style") {
109
+ const cleanStyle = sanitizeCssBlock(rawValue).replace(/"/g, "'");
110
+ if (cleanStyle.trim()) {
111
+ kept.push(`style="${cleanStyle}"`);
112
+ }
113
+ continue;
114
+ }
115
+ const decodedVal = decodeHtmlEntitiesForCheck(rawValue).toLowerCase();
116
+ if (decodedVal.includes("javascript:") || decodedVal.includes("vbscript:") || decodedVal.includes("data:text/") || decodedVal.includes("data:image/svg")) {
117
+ continue;
118
+ }
119
+ if (/url\s*\(/i.test(rawValue)) {
120
+ const sanitizedUrlAttr = sanitizeCssBlock(rawValue).replace(/"/g, "&quot;");
121
+ kept.push(`${rawName}="${sanitizedUrlAttr}"`);
122
+ continue;
123
+ }
124
+ if (match[2] !== void 0 || match[3] !== void 0 || match[4] !== void 0) {
125
+ kept.push(`${rawName}="${rawValue.replace(/"/g, "&quot;")}"`);
126
+ } else {
127
+ kept.push(rawName);
128
+ }
129
+ }
130
+ return kept.length > 0 ? ` ${kept.join(" ")}` : "";
131
+ }
132
+ function sanitizeSvg(input, options = {}) {
133
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_SVG_BYTES;
134
+ const byteLength = typeof input === "string" ? Buffer.byteLength(input, "utf8") : input.length;
135
+ if (byteLength > maxBytes) {
136
+ throw new UploadValidationError(
137
+ `SVG file size (${byteLength} bytes) exceeds maximum allowed SVG size of ${maxBytes} bytes`
138
+ );
139
+ }
140
+ let svg = typeof input === "string" ? input : input.toString("utf8");
141
+ svg = svg.replace(/^\uFEFF/, "");
142
+ svg = svg.replace(/<!DOCTYPE[^[>]*(?:\[[\s\S]*?\]\s*)?>/gi, "");
143
+ svg = svg.replace(/<!ENTITY[\s\S]*?>/gi, "");
144
+ svg = svg.replace(/<!--[\s\S]*?-->/g, "");
145
+ svg = svg.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
146
+ svg = svg.replace(/<\?(?!xml\b)[\s\S]*?\?>/gi, "");
147
+ svg = svg.replace(
148
+ /&([a-zA-Z_][\w.-]*);/g,
149
+ (full, name) => SAFE_NAMED_ENTITIES.has(name) ? full : ""
150
+ );
151
+ const tagPattern = FORBIDDEN_ELEMENTS.join("|");
152
+ const pairedRe = new RegExp(
153
+ `<\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\s*>`,
154
+ "gi"
155
+ );
156
+ const singleRe = new RegExp(
157
+ `<\\s*\\/?\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*\\/?>`,
158
+ "gi"
159
+ );
160
+ const htmlNamespaceRe = /<\s*\/?\s*(?:xhtml|html):[a-zA-Z0-9_-]+\b[^>]*\/?>/gi;
161
+ for (let pass = 0; pass < 4; pass++) {
162
+ const prev = svg;
163
+ svg = svg.replace(pairedRe, "").replace(singleRe, "").replace(htmlNamespaceRe, "");
164
+ if (svg === prev) break;
165
+ }
166
+ svg = svg.replace(
167
+ /(<\s*style\b[^>]*>)([\s\S]*?)(<\s*\/\s*style\s*>)/gi,
168
+ (_m, open, body, close) => `${open}${sanitizeCssBlock(body)}${close}`
169
+ );
170
+ svg = svg.replace(
171
+ /<([a-zA-Z_:][\w:.-]*)(\s[^<>]*?)?(\s*\/?)>/g,
172
+ (_full, tagName, attrs, selfClose) => {
173
+ const cleanAttrs = attrs ? sanitizeTagAttributes(attrs) : "";
174
+ return `<${tagName}${cleanAttrs}${selfClose}>`;
175
+ }
176
+ );
177
+ if (!/<svg[\s>/]/i.test(svg)) {
178
+ throw new UploadValidationError(
179
+ "Invalid SVG: missing root <svg> element after sanitization"
180
+ );
181
+ }
182
+ if (pairedRe.test(svg) || singleRe.test(svg) || /<!(?:DOCTYPE|ENTITY)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg) || /(?:javascript|vbscript)\s*:/i.test(svg)) {
183
+ throw new UploadValidationError(
184
+ "Invalid SVG: unsafe content remained after sanitization"
185
+ );
186
+ }
187
+ return Buffer.from(svg.trim(), "utf8");
188
+ }
189
+
190
+ // src/server/optimization/loadSharp.ts
191
+ var SHARP_MISSING_MESSAGE = "img-upload-kit/server requires `sharp` \u2014 run `npm i sharp`";
192
+ var sharpPromise = null;
193
+ function unwrapDefault(mod) {
194
+ const record = mod;
195
+ return record && record.default !== void 0 ? record.default : mod;
196
+ }
197
+ function loadSharp() {
198
+ if (!sharpPromise) {
199
+ sharpPromise = import("sharp").then(
200
+ (mod) => unwrapDefault(mod),
201
+ (error) => {
202
+ sharpPromise = null;
203
+ throw new Error(SHARP_MISSING_MESSAGE, { cause: error });
204
+ }
205
+ );
206
+ }
207
+ return sharpPromise;
208
+ }
209
+
210
+ // src/server/optimization/imageOptimizer.ts
211
+ var PASSTHROUGH_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif"]);
212
+ function extractSvgDimensions(svgText, metaWidth, metaHeight) {
213
+ let width = metaWidth;
214
+ let height = metaHeight;
215
+ if (!width || !height) {
216
+ const svgTagMatch = svgText.match(/<svg\b[^>]*>/i);
217
+ if (svgTagMatch) {
218
+ const tag = svgTagMatch[0];
219
+ const wMatch = tag.match(/\bwidth\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
220
+ const hMatch = tag.match(/\bheight\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
221
+ if (!width && wMatch) width = Math.round(parseFloat(wMatch[1]));
222
+ if (!height && hMatch) height = Math.round(parseFloat(hMatch[1]));
223
+ }
224
+ }
225
+ if (!width || !height) {
226
+ const viewBoxMatch = svgText.match(
227
+ /viewBox\s*=\s*["']\s*([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)\s*["']/i
228
+ );
229
+ if (viewBoxMatch) {
230
+ if (!width) width = Math.round(parseFloat(viewBoxMatch[3]));
231
+ if (!height) height = Math.round(parseFloat(viewBoxMatch[4]));
232
+ }
233
+ }
234
+ return {
235
+ width: width && width > 0 ? width : 600,
236
+ height: height && height > 0 ? height : 600
237
+ };
238
+ }
239
+ async function optimizeImage(buffer, options = {}) {
240
+ const sharp = await loadSharp();
241
+ const maxDimensions = options.maxDimensions || DEFAULT_IMAGE_DIMENSIONS;
242
+ const { outputFormat, quality } = options;
243
+ const limitInputPixels = options.limitInputPixels ?? DEFAULT_LIMIT_INPUT_PIXELS;
244
+ const primaryEffort = options.effort ?? PRIMARY_AVIF_EFFORT;
245
+ const retryEffort = options.effort !== void 0 ? Math.max(1, options.effort - 1) : RETRY_AVIF_EFFORT;
246
+ let pipeline = sharp(buffer, {
247
+ failOn: "error",
248
+ limitInputPixels
249
+ }).rotate();
250
+ let metadata;
251
+ try {
252
+ metadata = await pipeline.metadata();
253
+ } catch (err) {
254
+ const msg = err instanceof Error ? err.message : String(err);
255
+ if (msg.toLowerCase().includes("pixel limit") || msg.toLowerCase().includes("exceeds") || msg.toLowerCase().includes("unsupported image format")) {
256
+ throw new UploadValidationError(msg);
257
+ }
258
+ throw err;
259
+ }
260
+ const originalFormat = metadata.format;
261
+ if (originalFormat === "svg") {
262
+ const sanitizedBuffer = sanitizeSvg(buffer);
263
+ const svgText = sanitizedBuffer.toString("utf8");
264
+ const dims = extractSvgDimensions(svgText, metadata.width, metadata.height);
265
+ return {
266
+ buffer: sanitizedBuffer,
267
+ width: dims.width,
268
+ height: dims.height,
269
+ format: "svg",
270
+ size: sanitizedBuffer.length,
271
+ ...quality !== void 0 ? { quality } : {}
272
+ };
273
+ }
274
+ const isPhoto = originalFormat === "jpeg" || Boolean(metadata.density && metadata.density > 150);
275
+ let baseQuality = quality ?? (isPhoto ? 85 : 88);
276
+ baseQuality = Math.max(1, Math.min(100, baseQuality));
277
+ pipeline = pipeline.resize(maxDimensions.width, maxDimensions.height, {
278
+ fit: "inside",
279
+ withoutEnlargement: true,
280
+ kernel: "lanczos3"
281
+ });
282
+ if (outputFormat === "jpeg" || outputFormat === "png" || outputFormat === "webp") {
283
+ let cloned = pipeline.clone();
284
+ if (outputFormat === "jpeg") {
285
+ cloned = cloned.jpeg({ quality: baseQuality, mozjpeg: true });
286
+ } else if (outputFormat === "png") {
287
+ cloned = cloned.png();
288
+ } else if (outputFormat === "webp") {
289
+ cloned = cloned.webp({ quality: baseQuality, effort: 4 });
290
+ }
291
+ const outputBuffer = await cloned.toBuffer({ resolveWithObject: true });
292
+ return {
293
+ buffer: outputBuffer.data,
294
+ width: outputBuffer.info.width || maxDimensions.width,
295
+ height: outputBuffer.info.height || maxDimensions.height,
296
+ format: outputFormat,
297
+ size: outputBuffer.info.size,
298
+ quality: baseQuality
299
+ };
300
+ }
301
+ const avifBuffer = await pipeline.clone().avif({
302
+ quality: baseQuality,
303
+ effort: primaryEffort,
304
+ chromaSubsampling: "4:2:0",
305
+ bitdepth: 8
306
+ }).toBuffer({ resolveWithObject: true });
307
+ const avifSize = avifBuffer.info.size;
308
+ const originalSize = buffer.length;
309
+ if (avifSize <= originalSize || outputFormat === "avif") {
310
+ return {
311
+ buffer: avifBuffer.data,
312
+ width: avifBuffer.info.width || maxDimensions.width,
313
+ height: avifBuffer.info.height || maxDimensions.height,
314
+ format: "avif",
315
+ size: avifSize,
316
+ quality: baseQuality
317
+ };
318
+ }
319
+ const scaledQuality = Math.max(
320
+ 40,
321
+ Math.round(baseQuality * Math.sqrt(originalSize / avifSize))
322
+ );
323
+ const avifRetry = await pipeline.clone().avif({
324
+ quality: scaledQuality,
325
+ effort: retryEffort,
326
+ chromaSubsampling: "4:2:0",
327
+ bitdepth: 8
328
+ }).toBuffer({ resolveWithObject: true });
329
+ if (avifRetry.info.size <= originalSize) {
330
+ return {
331
+ buffer: avifRetry.data,
332
+ width: avifRetry.info.width || maxDimensions.width,
333
+ height: avifRetry.info.height || maxDimensions.height,
334
+ format: "avif",
335
+ size: avifRetry.info.size,
336
+ quality: scaledQuality
337
+ };
338
+ }
339
+ const isRotated90 = typeof metadata.orientation === "number" && metadata.orientation >= 5 && metadata.orientation <= 8;
340
+ const orientedWidth = isRotated90 ? metadata.height : metadata.width;
341
+ const orientedHeight = isRotated90 ? metadata.width : metadata.height;
342
+ const wasDimensionCapped = (orientedWidth ?? 0) > maxDimensions.width || (orientedHeight ?? 0) > maxDimensions.height;
343
+ if (!wasDimensionCapped && PASSTHROUGH_FORMATS.has(originalFormat ?? "")) {
344
+ const srcFmt = originalFormat;
345
+ let passthrough = pipeline.clone();
346
+ if (srcFmt === "jpeg") {
347
+ passthrough = passthrough.jpeg({ quality: baseQuality, mozjpeg: true });
348
+ } else if (srcFmt === "png") {
349
+ passthrough = passthrough.png({ compressionLevel: 9 });
350
+ } else if (srcFmt === "webp") {
351
+ passthrough = passthrough.webp({ quality: baseQuality, effort: 4 });
352
+ } else {
353
+ passthrough = passthrough.avif({
354
+ quality: baseQuality,
355
+ effort: retryEffort
356
+ });
357
+ }
358
+ let passthroughOut = await passthrough.toBuffer({ resolveWithObject: true });
359
+ if (srcFmt === "jpeg" && passthroughOut.info.size > originalSize) {
360
+ passthroughOut = await pipeline.clone().jpeg({ quality: 10 }).toBuffer({ resolveWithObject: true });
361
+ }
362
+ return {
363
+ buffer: passthroughOut.data,
364
+ width: passthroughOut.info.width || orientedWidth || maxDimensions.width,
365
+ height: passthroughOut.info.height || orientedHeight || maxDimensions.height,
366
+ format: srcFmt,
367
+ size: passthroughOut.info.size,
368
+ quality: baseQuality
369
+ };
370
+ }
371
+ const webpBuffer = await pipeline.clone().webp({ quality: baseQuality, effort: 6, smartSubsample: true }).toBuffer({ resolveWithObject: true });
372
+ const useWebP = webpBuffer.info.size <= avifRetry.info.size;
373
+ const winner = useWebP ? {
374
+ buf: webpBuffer.data,
375
+ fmt: "webp",
376
+ sz: webpBuffer.info.size,
377
+ w: webpBuffer.info.width,
378
+ h: webpBuffer.info.height,
379
+ q: baseQuality
380
+ } : {
381
+ buf: avifRetry.data,
382
+ fmt: "avif",
383
+ sz: avifRetry.info.size,
384
+ w: avifRetry.info.width,
385
+ h: avifRetry.info.height,
386
+ q: scaledQuality
387
+ };
388
+ return {
389
+ buffer: winner.buf,
390
+ width: winner.w || maxDimensions.width,
391
+ height: winner.h || maxDimensions.height,
392
+ format: winner.fmt,
393
+ size: winner.sz,
394
+ quality: winner.q
395
+ };
396
+ }
397
+
398
+ // src/server/optimization/blurGenerator.ts
399
+ import { encode as encodeBlurHash } from "blurhash";
400
+
401
+ // src/server/optimization/colorHistogram.ts
402
+ async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
403
+ const sharp = await loadSharp();
404
+ try {
405
+ const metadata = await sharp(buffer).metadata();
406
+ const maxSize = 400;
407
+ const needsResize = metadata.width && metadata.height && (metadata.width > maxSize || metadata.height > maxSize);
408
+ if (needsResize) {
409
+ const resized = await sharp(buffer).resize(maxSize, maxSize, {
410
+ fit: "inside",
411
+ kernel: "lanczos3",
412
+ withoutEnlargement: true
413
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
414
+ return extractDominantColorHistogramAdvanced(
415
+ resized.data,
416
+ resized.info.width,
417
+ resized.info.height,
418
+ mode
419
+ );
420
+ }
421
+ const raw = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
422
+ return extractDominantColorHistogramAdvanced(
423
+ raw.data,
424
+ raw.info.width,
425
+ raw.info.height,
426
+ mode
427
+ );
428
+ } catch {
429
+ const stats = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
430
+ return extractDominantColorHistogram(stats.data);
431
+ }
432
+ }
433
+ function extractDominantColorHistogramAdvanced(data, width, height, mode) {
434
+ const colorMap = /* @__PURE__ */ new Map();
435
+ const bucketSize = 16;
436
+ const centerWeight = 1.5;
437
+ const centerX = width / 2 || 1;
438
+ const centerY = height / 2 || 1;
439
+ let whiteWeight = 0;
440
+ for (let i = 0; i < data.length; i += 4) {
441
+ const alpha = data[i + 3];
442
+ if (alpha !== void 0 && alpha < 8) continue;
443
+ const r2 = data[i];
444
+ const g2 = data[i + 1];
445
+ const b2 = data[i + 2];
446
+ const brightness = (r2 + g2 + b2) / 3;
447
+ if (brightness < 10) continue;
448
+ const pixelIndex = i / 4;
449
+ const x = pixelIndex % width;
450
+ const y = Math.floor(pixelIndex / width);
451
+ const dx = (x - centerX) / centerX;
452
+ const dy = (y - centerY) / centerY;
453
+ const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
454
+ let weight = 1;
455
+ if (mode === "edge") {
456
+ const edgeThicknessX = Math.max(1, Math.floor(width * 0.12));
457
+ const edgeThicknessY = Math.max(1, Math.floor(height * 0.12));
458
+ const onLeftEdge = x < edgeThicknessX;
459
+ const onRightEdge = x >= width - edgeThicknessX;
460
+ const onTopEdge = y < edgeThicknessY;
461
+ const onBottomEdge = y >= height - edgeThicknessY;
462
+ if (!(onLeftEdge || onRightEdge || onTopEdge || onBottomEdge)) {
463
+ continue;
464
+ }
465
+ const distanceToNearestEdge = Math.min(
466
+ x,
467
+ y,
468
+ width - 1 - x,
469
+ height - 1 - y
470
+ );
471
+ const maxEdgeDistance = Math.max(edgeThicknessX, edgeThicknessY) || 1;
472
+ const edgeFactor = 1 - distanceToNearestEdge / maxEdgeDistance;
473
+ weight = 1 + edgeFactor * 3;
474
+ } else {
475
+ weight = distanceFromCenter < 0.5 ? centerWeight : 1;
476
+ }
477
+ const isNearWhite = r2 >= 245 && g2 >= 245 && b2 >= 245;
478
+ if (isNearWhite) {
479
+ whiteWeight += weight;
480
+ continue;
481
+ }
482
+ const bucketR = Math.floor(r2 / bucketSize) * bucketSize;
483
+ const bucketG = Math.floor(g2 / bucketSize) * bucketSize;
484
+ const bucketB = Math.floor(b2 / bucketSize) * bucketSize;
485
+ const key = `${bucketR},${bucketG},${bucketB}`;
486
+ colorMap.set(key, (colorMap.get(key) || 0) + weight);
487
+ }
488
+ if (!colorMap.size) {
489
+ return whiteWeight > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
490
+ }
491
+ let maxCount = 0;
492
+ let dominantColor = "64,64,80";
493
+ let totalColorWeight = 0;
494
+ colorMap.forEach((count, color) => {
495
+ totalColorWeight += count;
496
+ if (count > maxCount) {
497
+ maxCount = count;
498
+ dominantColor = color;
499
+ }
500
+ });
501
+ const whiteDominates = whiteWeight > 0 && whiteWeight >= totalColorWeight * WHITE_DOMINANCE_RATIO;
502
+ if (whiteDominates) {
503
+ return EDGE_WHITE_FALLBACK;
504
+ }
505
+ const [r, g, b] = dominantColor.split(",").map(Number);
506
+ return rgbToHex(r, g, b);
507
+ }
508
+ function extractDominantColorHistogram(data) {
509
+ const colorMap = /* @__PURE__ */ new Map();
510
+ const bucketSize = 16;
511
+ let whiteWeight = 0;
512
+ for (let i = 0; i < data.length; i += 4) {
513
+ const alpha = data[i + 3];
514
+ if (alpha !== void 0 && alpha < 8) continue;
515
+ const r2 = Math.floor(data[i] / bucketSize) * bucketSize;
516
+ const g2 = Math.floor(data[i + 1] / bucketSize) * bucketSize;
517
+ const b2 = Math.floor(data[i + 2] / bucketSize) * bucketSize;
518
+ if (r2 >= 240 && g2 >= 240 && b2 >= 240) {
519
+ whiteWeight += 1;
520
+ continue;
521
+ }
522
+ const key = `${r2},${g2},${b2}`;
523
+ colorMap.set(key, (colorMap.get(key) || 0) + 1);
524
+ }
525
+ if (!colorMap.size) {
526
+ return whiteWeight > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
527
+ }
528
+ let maxCount = 0;
529
+ let dominantColor = "64,64,80";
530
+ let totalColorWeight = 0;
531
+ colorMap.forEach((count, color) => {
532
+ totalColorWeight += count;
533
+ if (count > maxCount) {
534
+ maxCount = count;
535
+ dominantColor = color;
536
+ }
537
+ });
538
+ const whiteDominates = whiteWeight > 0 && whiteWeight >= totalColorWeight * WHITE_DOMINANCE_RATIO;
539
+ if (whiteDominates) {
540
+ return EDGE_WHITE_FALLBACK;
541
+ }
542
+ const [r, g, b] = dominantColor.split(",").map(Number);
543
+ return rgbToHex(r, g, b);
544
+ }
545
+
546
+ // src/server/optimization/edgeColorExtractor.ts
547
+ async function extractEdgeColorsFromBuffer(buffer) {
548
+ const sharp = await loadSharp();
549
+ try {
550
+ const sample = await sharp(buffer).resize(400, 400, {
551
+ fit: "inside",
552
+ kernel: "lanczos3",
553
+ withoutEnlargement: true
554
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
555
+ return computeEdgeColorSet(
556
+ sample.data,
557
+ sample.info.width,
558
+ sample.info.height
559
+ );
560
+ } catch {
561
+ try {
562
+ const fallback = await extractEnhancedColorFromBuffer(buffer, "edge");
563
+ return {
564
+ top: fallback,
565
+ right: fallback,
566
+ bottom: fallback,
567
+ left: fallback
568
+ };
569
+ } catch {
570
+ return {
571
+ top: EDGE_COLOR_FALLBACK,
572
+ right: EDGE_COLOR_FALLBACK,
573
+ bottom: EDGE_COLOR_FALLBACK,
574
+ left: EDGE_COLOR_FALLBACK
575
+ };
576
+ }
577
+ }
578
+ }
579
+ function computeEdgeColorSet(data, width, height) {
580
+ const thicknessX = Math.max(2, Math.floor(width * 0.12));
581
+ const thicknessY = Math.max(2, Math.floor(height * 0.12));
582
+ const bucketSize = 16;
583
+ const edgeMaps = {
584
+ top: /* @__PURE__ */ new Map(),
585
+ right: /* @__PURE__ */ new Map(),
586
+ bottom: /* @__PURE__ */ new Map(),
587
+ left: /* @__PURE__ */ new Map()
588
+ };
589
+ const whiteEdgeStrength = {
590
+ top: 0,
591
+ right: 0,
592
+ bottom: 0,
593
+ left: 0
594
+ };
595
+ const accumulateEdgeSample = (edge, weight, key, isWhiteSample) => {
596
+ if (weight <= 0) return;
597
+ if (isWhiteSample) {
598
+ whiteEdgeStrength[edge] += weight;
599
+ return;
600
+ }
601
+ edgeMaps[edge].set(key, (edgeMaps[edge].get(key) || 0) + weight);
602
+ };
603
+ for (let i = 0; i < data.length; i += 4) {
604
+ const r = data[i];
605
+ const g = data[i + 1];
606
+ const b = data[i + 2];
607
+ const alpha = data[i + 3];
608
+ if (alpha !== void 0 && alpha < 8) continue;
609
+ const brightness = (r + g + b) / 3;
610
+ if (brightness < 12) continue;
611
+ const isNearWhite = r >= 245 && g >= 245 && b >= 245;
612
+ const pixelIndex = i / 4;
613
+ const x = pixelIndex % width;
614
+ const y = Math.floor(pixelIndex / width);
615
+ const bucketR = Math.floor(r / bucketSize) * bucketSize;
616
+ const bucketG = Math.floor(g / bucketSize) * bucketSize;
617
+ const bucketB = Math.floor(b / bucketSize) * bucketSize;
618
+ const key = `${bucketR},${bucketG},${bucketB}`;
619
+ const topWeight = y < thicknessY ? 1 + (thicknessY - y) / thicknessY : 0;
620
+ if (topWeight > 0) {
621
+ accumulateEdgeSample("top", topWeight, key, isNearWhite);
622
+ }
623
+ const bottomWeight = y >= height - thicknessY ? 1 + (y - (height - thicknessY)) / thicknessY : 0;
624
+ if (bottomWeight > 0) {
625
+ accumulateEdgeSample("bottom", bottomWeight, key, isNearWhite);
626
+ }
627
+ const leftWeight = x < thicknessX ? 1 + (thicknessX - x) / thicknessX : 0;
628
+ if (leftWeight > 0) {
629
+ accumulateEdgeSample("left", leftWeight, key, isNearWhite);
630
+ }
631
+ const rightWeight = x >= width - thicknessX ? 1 + (x - (width - thicknessX)) / thicknessX : 0;
632
+ if (rightWeight > 0) {
633
+ accumulateEdgeSample("right", rightWeight, key, isNearWhite);
634
+ }
635
+ }
636
+ return {
637
+ top: resolveEdgeColor(edgeMaps.top, whiteEdgeStrength.top),
638
+ right: resolveEdgeColor(edgeMaps.right, whiteEdgeStrength.right),
639
+ bottom: resolveEdgeColor(edgeMaps.bottom, whiteEdgeStrength.bottom),
640
+ left: resolveEdgeColor(edgeMaps.left, whiteEdgeStrength.left)
641
+ };
642
+ }
643
+ function resolveEdgeColor(map, whiteStrength) {
644
+ if (!map.size) {
645
+ return whiteStrength > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
646
+ }
647
+ let dominantColor = "";
648
+ let maxCount = 0;
649
+ let totalColorWeight = 0;
650
+ map.forEach((count, color) => {
651
+ totalColorWeight += count;
652
+ if (count > maxCount) {
653
+ maxCount = count;
654
+ dominantColor = color;
655
+ }
656
+ });
657
+ if (!dominantColor) {
658
+ return whiteStrength > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
659
+ }
660
+ const whiteDominates = whiteStrength > 0 && (totalColorWeight === 0 || whiteStrength >= totalColorWeight * WHITE_DOMINANCE_RATIO);
661
+ if (whiteDominates) {
662
+ return EDGE_WHITE_FALLBACK;
663
+ }
664
+ if (dominantColor.startsWith("#")) {
665
+ return dominantColor.toUpperCase();
666
+ }
667
+ const [r, g, b] = dominantColor.split(",").map(Number);
668
+ return rgbToHex(r, g, b);
669
+ }
670
+ function blendEdgeColors(edgeColors) {
671
+ const colors = Object.values(edgeColors);
672
+ if (!colors.length) {
673
+ return EDGE_COLOR_FALLBACK;
674
+ }
675
+ const totals = colors.reduce(
676
+ (acc, hex) => {
677
+ const { r, g, b } = hexToRgb(hex || EDGE_COLOR_FALLBACK);
678
+ acc.r += r;
679
+ acc.g += g;
680
+ acc.b += b;
681
+ return acc;
682
+ },
683
+ { r: 0, g: 0, b: 0 }
684
+ );
685
+ const count = colors.length;
686
+ return rgbToHex(totals.r / count, totals.g / count, totals.b / count);
687
+ }
688
+
689
+ // src/server/optimization/blurGenerator.ts
690
+ function encodeRawBlurHash(data, width, height, componentX = 4, componentY = 3) {
691
+ const clampedX = Math.max(1, Math.min(Math.round(componentX), width, 9));
692
+ const clampedY = Math.max(1, Math.min(Math.round(componentY), height, 9));
693
+ return encodeBlurHash(
694
+ new Uint8ClampedArray(data),
695
+ width,
696
+ height,
697
+ clampedX,
698
+ clampedY
699
+ );
700
+ }
701
+ async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
702
+ const sharp = await loadSharp();
703
+ const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
704
+ return encodeRawBlurHash(
705
+ data,
706
+ info.width,
707
+ info.height,
708
+ componentX,
709
+ componentY
710
+ );
711
+ }
712
+ async function generateBlurPlaceholder(buffer, options = {}) {
713
+ const targetWidth = options.width ?? BLUR_PLACEHOLDER_WIDTH;
714
+ const fallbackWidth = options.fallbackWidth ?? BLUR_PLACEHOLDER_FALLBACK_WIDTH;
715
+ const quality = options.quality ?? BLUR_PLACEHOLDER_QUALITY;
716
+ const maxChars = options.maxDataUrlChars ?? BLUR_PLACEHOLDER_MAX_CHARS;
717
+ const sharp = await loadSharp();
718
+ try {
719
+ let processBuffer = buffer;
720
+ const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
721
+ if (isSvg) {
722
+ const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
723
+ processBuffer = Buffer.from(normalizedSvg, "utf8");
724
+ }
725
+ const baseSize = Math.max(targetWidth, 128);
726
+ const rawSample = await sharp(processBuffer).rotate().resize(baseSize, baseSize, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
727
+ const rawInput = {
728
+ raw: {
729
+ width: rawSample.info.width,
730
+ height: rawSample.info.height,
731
+ channels: 4
732
+ }
733
+ };
734
+ const buildPlaceholderDataUrl = async (width) => {
735
+ const webpBuffer = await sharp(rawSample.data, rawInput).resize(width, width, { fit: "inside", withoutEnlargement: true }).webp({ quality, effort: 6, smartSubsample: true }).toBuffer();
736
+ return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
737
+ };
738
+ let dataURL = await buildPlaceholderDataUrl(targetWidth);
739
+ if (dataURL.length > maxChars) {
740
+ dataURL = await buildPlaceholderDataUrl(fallbackWidth);
741
+ }
742
+ const edgeColors = computeEdgeColorSet(
743
+ rawSample.data,
744
+ rawSample.info.width,
745
+ rawSample.info.height
746
+ );
747
+ const color = blendEdgeColors(edgeColors);
748
+ let blurHash;
749
+ if (options.includeBlurHash) {
750
+ try {
751
+ const hashSample = rawSample.info.width <= 32 && rawSample.info.height <= 32 ? rawSample : await sharp(rawSample.data, rawInput).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
752
+ blurHash = encodeRawBlurHash(
753
+ hashSample.data,
754
+ hashSample.info.width,
755
+ hashSample.info.height
756
+ );
757
+ } catch {
758
+ blurHash = void 0;
759
+ }
760
+ }
761
+ return {
762
+ dataURL,
763
+ color,
764
+ ...blurHash && { blurHash }
765
+ };
766
+ } catch {
767
+ return generateFallbackBlur(buffer, options.includeBlurHash);
768
+ }
769
+ }
770
+ var generateBlurHash = generateBlurPlaceholder;
771
+ async function generateFallbackBlur(buffer, includeBlurHash) {
772
+ const sharp = await loadSharp();
773
+ const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
774
+ quality: 75,
775
+ effort: 6,
776
+ smartSubsample: false
777
+ }).toBuffer();
778
+ const dataURL = `data:image/webp;base64,${tinyBuffer.toString("base64")}`;
779
+ const edgeColors = await extractEdgeColorsFromBuffer(buffer);
780
+ const color = blendEdgeColors(edgeColors);
781
+ let blurHash;
782
+ if (includeBlurHash) {
783
+ try {
784
+ blurHash = await encodeBlurHashString(tinyBuffer);
785
+ } catch {
786
+ blurHash = void 0;
787
+ }
788
+ }
789
+ return { dataURL, color, ...blurHash && { blurHash } };
790
+ }
791
+
792
+ // src/server/security/sniffFormat.ts
793
+ var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
794
+ var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "mif1", "msf1"]);
795
+ function readAscii(bytes, start, end) {
796
+ let out = "";
797
+ const limit = Math.min(bytes.length, end);
798
+ for (let i = start; i < limit; i++) {
799
+ out += String.fromCharCode(bytes[i]);
800
+ }
801
+ return out;
802
+ }
803
+ function sniffIsoBmff(bytes) {
804
+ if (bytes.length < 12) return null;
805
+ if (readAscii(bytes, 4, 8) !== "ftyp") return null;
806
+ const boxSize = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
807
+ const maxOffset = Math.min(
808
+ bytes.length,
809
+ boxSize >= 16 ? Math.min(boxSize, 128) : 64
810
+ );
811
+ const majorBrand = readAscii(bytes, 8, 12).toLowerCase();
812
+ if (AVIF_BRANDS.has(majorBrand)) {
813
+ return { format: "avif", mime: "image/avif" };
814
+ }
815
+ const brands = [majorBrand];
816
+ for (let offset = 16; offset + 4 <= maxOffset; offset += 4) {
817
+ brands.push(readAscii(bytes, offset, offset + 4).toLowerCase());
818
+ }
819
+ if (brands.some((b) => AVIF_BRANDS.has(b))) {
820
+ return { format: "avif", mime: "image/avif" };
821
+ }
822
+ if (brands.some((b) => HEIF_BRANDS.has(b))) {
823
+ return { format: "heif", mime: "image/heif" };
824
+ }
825
+ return null;
826
+ }
827
+ function sniffTextSvg(bytes) {
828
+ const sampleLength = Math.min(bytes.length, 4096);
829
+ for (let i = 0; i < Math.min(sampleLength, 512); i++) {
830
+ if (bytes[i] === 0) return null;
831
+ }
832
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, sampleLength)).replace(/^\uFEFF/, "").trimStart();
833
+ if (!text.startsWith("<")) return null;
834
+ const stripped = text.replace(/^<\?xml[\s\S]*?\?>\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").replace(/<!DOCTYPE\s+svg[\s\S]*?(?:\]>|>)\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").trimStart();
835
+ if (/^<svg[\s>/]/i.test(stripped) || /<svg[\s>/]/i.test(text)) {
836
+ if (/^<!doctype\s+html\b/i.test(text) || /^<html[\s>]/i.test(stripped)) {
837
+ return null;
838
+ }
839
+ return { format: "svg", mime: "image/svg+xml" };
840
+ }
841
+ return null;
842
+ }
843
+ function sniffImageFormat(input) {
844
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
845
+ if (bytes.length < 4) return null;
846
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
847
+ return { format: "jpeg", mime: "image/jpeg" };
848
+ }
849
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
850
+ return { format: "png", mime: "image/png" };
851
+ }
852
+ if (bytes.length >= 6) {
853
+ const header = readAscii(bytes, 0, 6);
854
+ if (header === "GIF87a" || header === "GIF89a") {
855
+ return { format: "gif", mime: "image/gif" };
856
+ }
857
+ }
858
+ if (bytes.length >= 12 && readAscii(bytes, 0, 4) === "RIFF" && readAscii(bytes, 8, 12) === "WEBP") {
859
+ return { format: "webp", mime: "image/webp" };
860
+ }
861
+ const bmff = sniffIsoBmff(bytes);
862
+ if (bmff) return bmff;
863
+ if (bytes[0] === 73 && bytes[1] === 73 && bytes[2] === 42 && bytes[3] === 0 || bytes[0] === 77 && bytes[1] === 77 && bytes[2] === 0 && bytes[3] === 42) {
864
+ return { format: "tiff", mime: "image/tiff" };
865
+ }
866
+ if (bytes.length >= 14 && bytes[0] === 66 && bytes[1] === 77) {
867
+ return { format: "bmp", mime: "image/bmp" };
868
+ }
869
+ if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0) {
870
+ return { format: "ico", mime: "image/x-icon" };
871
+ }
872
+ return sniffTextSvg(bytes);
873
+ }
874
+ function assertMagicBytesAllowed(buffer, allowedFormats, allowSvg = false) {
875
+ const sniffed = sniffImageFormat(buffer);
876
+ if (!sniffed) {
877
+ throw new UploadValidationError(
878
+ "Unsupported image format: file content is not a recognized image"
879
+ );
880
+ }
881
+ if (sniffed.format === "svg" && !allowSvg) {
882
+ throw new UploadValidationError(
883
+ "File type image/svg+xml is not allowed unless allowSvg: true is set"
884
+ );
885
+ }
886
+ if (allowedFormats.length > 0 && !allowedFormats.includes(sniffed.mime)) {
887
+ throw new UploadValidationError(
888
+ `File type ${sniffed.mime} is not allowed. Allowed: ${allowedFormats.join(", ")}`
889
+ );
890
+ }
891
+ return sniffed;
892
+ }
893
+
894
+ // src/server/semaphore.ts
895
+ var SemaphoreQueueFullError = class extends Error {
896
+ status = 503;
897
+ constructor(message = "Server upload queue is full. Please try again later.") {
898
+ super(message);
899
+ this.name = "SemaphoreQueueFullError";
900
+ }
901
+ };
902
+ function createAbortError(signal) {
903
+ if (signal?.reason instanceof Error) {
904
+ return signal.reason;
905
+ }
906
+ const err = new Error(
907
+ typeof signal?.reason === "string" ? signal.reason : "Operation aborted"
908
+ );
909
+ err.name = "AbortError";
910
+ return err;
911
+ }
912
+ var AsyncSemaphore = class {
913
+ constructor(concurrency, options) {
914
+ this.concurrency = concurrency;
915
+ if (concurrency < 1) {
916
+ throw new Error("AsyncSemaphore concurrency must be >= 1");
917
+ }
918
+ if (typeof options === "number") {
919
+ this.maxQueue = options;
920
+ } else if (options && typeof options === "object") {
921
+ this.maxQueue = options.maxQueue;
922
+ }
923
+ }
924
+ concurrency;
925
+ activeCount = 0;
926
+ maxQueue;
927
+ queue = [];
928
+ get active() {
929
+ return this.activeCount;
930
+ }
931
+ get pending() {
932
+ return this.queue.length;
933
+ }
934
+ async run(task, options = {}) {
935
+ const { signal, timeoutMs, label = "Semaphore task" } = options;
936
+ await this.acquire(signal);
937
+ let rawTaskPromise;
938
+ try {
939
+ rawTaskPromise = Promise.resolve().then(task);
940
+ } catch (syncError) {
941
+ this.release();
942
+ throw syncError;
943
+ }
944
+ let released = false;
945
+ const releaseOnce = () => {
946
+ if (!released) {
947
+ released = true;
948
+ this.release();
949
+ }
950
+ };
951
+ void rawTaskPromise.then(
952
+ () => releaseOnce(),
953
+ (err) => {
954
+ const pending = err && typeof err === "object" && "pendingTask" in err ? err.pendingTask : void 0;
955
+ if (pending && typeof pending.then === "function") {
956
+ void pending.then(releaseOnce, releaseOnce);
957
+ } else {
958
+ releaseOnce();
959
+ }
960
+ }
961
+ );
962
+ let callerPromise = rawTaskPromise;
963
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
964
+ callerPromise = withTimeout(callerPromise, timeoutMs, label);
965
+ }
966
+ if (signal) {
967
+ callerPromise = raceWithAbort(callerPromise, signal);
968
+ }
969
+ return callerPromise;
970
+ }
971
+ acquire(signal) {
972
+ if (signal?.aborted) {
973
+ return Promise.reject(createAbortError(signal));
974
+ }
975
+ if (this.activeCount < this.concurrency) {
976
+ this.activeCount += 1;
977
+ return Promise.resolve();
978
+ }
979
+ if (this.maxQueue !== void 0 && this.queue.length >= this.maxQueue) {
980
+ return Promise.reject(new SemaphoreQueueFullError());
981
+ }
982
+ return new Promise((resolve, reject) => {
983
+ const onAbort = () => {
984
+ const idx = this.queue.indexOf(entry);
985
+ if (idx !== -1) {
986
+ this.queue.splice(idx, 1);
987
+ }
988
+ entry.cancel(createAbortError(signal));
989
+ };
990
+ const entry = {
991
+ grant: () => {
992
+ signal?.removeEventListener("abort", onAbort);
993
+ this.activeCount += 1;
994
+ resolve();
995
+ },
996
+ cancel: (reason) => {
997
+ signal?.removeEventListener("abort", onAbort);
998
+ reject(reason);
999
+ }
1000
+ };
1001
+ signal?.addEventListener("abort", onAbort, { once: true });
1002
+ this.queue.push(entry);
1003
+ });
1004
+ }
1005
+ release() {
1006
+ this.activeCount = Math.max(0, this.activeCount - 1);
1007
+ const next = this.queue.shift();
1008
+ next?.grant();
1009
+ }
1010
+ };
1011
+ function raceWithAbort(task, signal) {
1012
+ if (signal.aborted) {
1013
+ const err = createAbortError(signal);
1014
+ err.pendingTask = task.then(
1015
+ () => {
1016
+ },
1017
+ () => {
1018
+ }
1019
+ );
1020
+ return Promise.reject(err);
1021
+ }
1022
+ return new Promise((resolve, reject) => {
1023
+ const onAbort = () => {
1024
+ const err = createAbortError(signal);
1025
+ err.pendingTask = task.then(
1026
+ () => {
1027
+ },
1028
+ () => {
1029
+ }
1030
+ );
1031
+ reject(err);
1032
+ };
1033
+ signal.addEventListener("abort", onAbort, { once: true });
1034
+ task.then(
1035
+ (val) => {
1036
+ signal.removeEventListener("abort", onAbort);
1037
+ resolve(val);
1038
+ },
1039
+ (err) => {
1040
+ signal.removeEventListener("abort", onAbort);
1041
+ reject(err);
1042
+ }
1043
+ );
1044
+ });
1045
+ }
1046
+ async function withTimeout(task, timeoutMs, label) {
1047
+ let timeoutHandle = null;
1048
+ const settledTask = task.then(
1049
+ () => {
1050
+ },
1051
+ () => {
1052
+ }
1053
+ );
1054
+ try {
1055
+ return await Promise.race([
1056
+ task,
1057
+ new Promise((_, reject) => {
1058
+ timeoutHandle = setTimeout(() => {
1059
+ const err = new Error(
1060
+ `${label} timed out after ${timeoutMs}ms`
1061
+ );
1062
+ err.pendingTask = settledTask;
1063
+ reject(err);
1064
+ }, timeoutMs);
1065
+ })
1066
+ ]);
1067
+ } finally {
1068
+ if (timeoutHandle) {
1069
+ clearTimeout(timeoutHandle);
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ // src/server/uploader/bufferProcessor.ts
1075
+ async function computeSha256Hex(buffer) {
1076
+ const bytes = new Uint8Array(buffer.byteLength);
1077
+ bytes.set(buffer);
1078
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
1079
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
1080
+ }
1081
+ function throwIfAborted(signal) {
1082
+ if (!signal?.aborted) return;
1083
+ if (signal.reason instanceof Error) throw signal.reason;
1084
+ const err = new Error(
1085
+ typeof signal.reason === "string" ? signal.reason : "Operation aborted"
1086
+ );
1087
+ err.name = "AbortError";
1088
+ throw err;
1089
+ }
1090
+ async function processImageBuffer(originalBuffer, options = {}) {
1091
+ const optTimeout = options.optimizationTimeoutMs ?? 6e4;
1092
+ const blurTimeout = options.blurTimeoutMs ?? 3e4;
1093
+ throwIfAborted(options.signal);
1094
+ const contentHash = await computeSha256Hex(originalBuffer);
1095
+ throwIfAborted(options.signal);
1096
+ const optimized = await withTimeout(
1097
+ optimizeImage(originalBuffer, {
1098
+ maxDimensions: options.maxDimensions,
1099
+ outputFormat: options.outputFormat,
1100
+ quality: options.quality,
1101
+ limitInputPixels: options.limitInputPixels,
1102
+ effort: options.effort
1103
+ }),
1104
+ optTimeout,
1105
+ "Image optimization"
1106
+ );
1107
+ if (!optimized || !optimized.buffer) {
1108
+ throw new Error("Image optimization returned invalid result");
1109
+ }
1110
+ if (!optimized.width || !optimized.height) {
1111
+ throw new Error(
1112
+ `Image dimensions missing: width=${optimized.width}, height=${optimized.height}`
1113
+ );
1114
+ }
1115
+ throwIfAborted(options.signal);
1116
+ const blurData = await withTimeout(
1117
+ generateBlurPlaceholder(optimized.buffer, {
1118
+ includeBlurHash: options.includeBlurHash
1119
+ }),
1120
+ blurTimeout,
1121
+ "Blur generation"
1122
+ );
1123
+ return {
1124
+ optimized,
1125
+ blurData,
1126
+ contentHash,
1127
+ originalSize: originalBuffer.length
1128
+ };
1129
+ }
1130
+
1131
+ // src/server/uploader/pathGenerator.ts
1132
+ function randomShortId() {
1133
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
1134
+ return globalThis.crypto.randomUUID().split("-")[0];
1135
+ }
1136
+ return Math.random().toString(16).slice(2, 10).padEnd(8, "0");
1137
+ }
1138
+ function formatToExtension(format) {
1139
+ switch (format) {
1140
+ case "svg":
1141
+ return "svg";
1142
+ case "png":
1143
+ return "png";
1144
+ case "webp":
1145
+ return "webp";
1146
+ case "jpeg":
1147
+ return "jpg";
1148
+ default:
1149
+ return "avif";
1150
+ }
1151
+ }
1152
+ function sanitizeSegment(value) {
1153
+ return value.replace(/\\/g, "/").split("/").map((part) => part.trim().replace(/[^a-zA-Z0-9._-]/g, "-")).filter((part) => part && part !== "." && part !== "..").join("/");
1154
+ }
1155
+ function stripFilenameExtension(rawFilename) {
1156
+ const leaf = rawFilename.replace(/^.*[\\/]/, "").trim();
1157
+ const dotIdx = leaf.lastIndexOf(".");
1158
+ const base = dotIdx > 0 ? leaf.slice(0, dotIdx) : leaf;
1159
+ const cleaned = base.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^[.-]+|[.-]+$/g, "");
1160
+ return cleaned || "image";
1161
+ }
1162
+ function createDefaultPathGenerator() {
1163
+ let imageCounter = 0;
1164
+ return (ctx) => {
1165
+ const cleanFolder = ctx.folder ? sanitizeSegment(ctx.folder) : "";
1166
+ if (ctx.customFilename?.trim()) {
1167
+ const baseName = stripFilenameExtension(ctx.customFilename);
1168
+ const cleanName = `${baseName}.${ctx.extension}`;
1169
+ const fullPath2 = cleanFolder ? `${cleanFolder}/${cleanName}` : cleanName;
1170
+ return {
1171
+ bucket: ctx.bucket,
1172
+ path: fullPath2,
1173
+ filename: cleanName
1174
+ };
1175
+ }
1176
+ const date = /* @__PURE__ */ new Date();
1177
+ const year = date.getFullYear();
1178
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1179
+ const day = String(date.getDate()).padStart(2, "0");
1180
+ imageCounter = (imageCounter + 1) % 1e4;
1181
+ const seq = String(imageCounter).padStart(4, "0");
1182
+ const timestamp = Date.now();
1183
+ const randomSuffix = randomShortId();
1184
+ const prefix = sanitizeSegment(
1185
+ ctx.preset || cleanFolder.split("/")[0] || "img"
1186
+ );
1187
+ const filename = `${prefix}-${year}${month}${day}-${seq}-${timestamp}-${randomSuffix}.${ctx.extension}`;
1188
+ const fullPath = cleanFolder ? `${cleanFolder}/${year}/${month}/${filename}` : `${year}/${month}/${filename}`;
1189
+ return {
1190
+ bucket: ctx.bucket,
1191
+ path: fullPath,
1192
+ filename
1193
+ };
1194
+ };
1195
+ }
1196
+
1197
+ // src/server/uploader/uploadRetry.ts
1198
+ import fs from "fs/promises";
1199
+ import path from "path";
1200
+ function randomRetrySuffix() {
1201
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
1202
+ return globalThis.crypto.randomUUID().split("-")[0];
1203
+ }
1204
+ return Math.random().toString(16).slice(2, 10);
1205
+ }
1206
+ function isCollisionError(e) {
1207
+ if (!e) return false;
1208
+ const code = typeof e === "object" && e !== null && "code" in e ? String(e.code).toUpperCase() : "";
1209
+ const statusCode = typeof e === "object" && e !== null && "$metadata" in e ? e.$metadata?.httpStatusCode : typeof e === "object" && e !== null && "statusCode" in e ? Number(e.statusCode) : void 0;
1210
+ if (code === "EEXIST" || code === "PRECONDITIONFAILED" || statusCode === 412) {
1211
+ return true;
1212
+ }
1213
+ if (code === "ENOENT") {
1214
+ return false;
1215
+ }
1216
+ const msg = e instanceof Error ? e.message.toLowerCase() : String(e).toLowerCase();
1217
+ if (msg.includes("does not exist") || msg.includes("doesn't exist") || msg.includes("not exist") || msg.includes("enoent")) {
1218
+ return false;
1219
+ }
1220
+ return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists") || msg.includes("precondition failed");
1221
+ }
1222
+ async function uploadWithCollisionRetry(params) {
1223
+ const {
1224
+ storage,
1225
+ buildPath,
1226
+ pathContext,
1227
+ data,
1228
+ contentType,
1229
+ maxRetries = 3
1230
+ } = params;
1231
+ const isServerCustomName = Boolean(pathContext.customFilename) && !pathContext.isClientFilename;
1232
+ let storageTarget = buildPath(pathContext);
1233
+ let uploadResult;
1234
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1235
+ try {
1236
+ uploadResult = await storage.upload({
1237
+ bucket: storageTarget.bucket,
1238
+ path: storageTarget.path,
1239
+ data,
1240
+ contentType,
1241
+ upsert: isServerCustomName,
1242
+ contentHash: pathContext.contentHash,
1243
+ metadata: pathContext.metadata
1244
+ });
1245
+ break;
1246
+ } catch (e) {
1247
+ if (isCollisionError(e)) {
1248
+ if (isServerCustomName) {
1249
+ uploadResult = await storage.upload({
1250
+ bucket: storageTarget.bucket,
1251
+ path: storageTarget.path,
1252
+ data,
1253
+ contentType,
1254
+ upsert: true,
1255
+ contentHash: pathContext.contentHash,
1256
+ metadata: pathContext.metadata
1257
+ });
1258
+ break;
1259
+ }
1260
+ if (pathContext.customFilename && pathContext.isClientFilename) {
1261
+ const base = stripFilenameExtension(pathContext.customFilename);
1262
+ storageTarget = buildPath({
1263
+ ...pathContext,
1264
+ customFilename: `${base}-${randomRetrySuffix()}`
1265
+ });
1266
+ continue;
1267
+ }
1268
+ storageTarget = buildPath({
1269
+ ...pathContext,
1270
+ customFilename: void 0
1271
+ });
1272
+ continue;
1273
+ }
1274
+ throw e;
1275
+ }
1276
+ }
1277
+ if (!uploadResult) {
1278
+ throw new Error("Failed to upload image after multiple retries");
1279
+ }
1280
+ return { uploadResult, storageTarget };
1281
+ }
1282
+ async function mirrorBufferLocally(options) {
1283
+ const {
1284
+ buffer,
1285
+ filename,
1286
+ storagePath,
1287
+ fallbackUrl,
1288
+ mirrorDir,
1289
+ publicUrlBase
1290
+ } = options;
1291
+ if (!mirrorDir) {
1292
+ return fallbackUrl;
1293
+ }
1294
+ const relativeTarget = (storagePath || filename).replace(/^\/+/, "");
1295
+ try {
1296
+ const resolvedBase = path.resolve(mirrorDir);
1297
+ const destPath = path.resolve(resolvedBase, relativeTarget);
1298
+ const rel = path.relative(resolvedBase, destPath);
1299
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
1300
+ return fallbackUrl;
1301
+ }
1302
+ const destDir = path.dirname(destPath);
1303
+ await fs.mkdir(destDir, { recursive: true });
1304
+ await fs.writeFile(destPath, buffer);
1305
+ if (publicUrlBase) {
1306
+ return `${publicUrlBase.replace(/\/+$/, "")}/${relativeTarget}`;
1307
+ }
1308
+ } catch {
1309
+ }
1310
+ return fallbackUrl;
1311
+ }
1312
+ async function deleteMirroredFiles(mirrorDirs, storagePath) {
1313
+ if (!mirrorDirs.length || !storagePath) return;
1314
+ const relativeTarget = storagePath.replace(/^\/+/, "");
1315
+ if (!relativeTarget) return;
1316
+ await Promise.all(
1317
+ mirrorDirs.map(async (dir) => {
1318
+ try {
1319
+ const resolvedBase = path.resolve(dir);
1320
+ const destPath = path.resolve(resolvedBase, relativeTarget);
1321
+ const rel = path.relative(resolvedBase, destPath);
1322
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return;
1323
+ await fs.unlink(destPath);
1324
+ } catch {
1325
+ }
1326
+ })
1327
+ );
1328
+ }
1329
+
1330
+ // src/server/uploader/variantGenerator.ts
1331
+ function buildVariantStoragePath(basePath, width) {
1332
+ const dotIdx = basePath.lastIndexOf(".");
1333
+ const slashIdx = basePath.lastIndexOf("/");
1334
+ if (dotIdx > slashIdx && dotIdx > 0) {
1335
+ return `${basePath.slice(0, dotIdx)}-${width}w${basePath.slice(dotIdx)}`;
1336
+ }
1337
+ return `${basePath}-${width}w`;
1338
+ }
1339
+ function resolveEffectiveAllowedFormats(baseFormats, allowSvg, explicitAllowSvg) {
1340
+ if (!allowSvg) return baseFormats.filter((fmt) => fmt !== SVG_MIME_TYPE);
1341
+ if (explicitAllowSvg && !baseFormats.includes(SVG_MIME_TYPE)) {
1342
+ return [...baseFormats, SVG_MIME_TYPE];
1343
+ }
1344
+ return baseFormats;
1345
+ }
1346
+ async function generateImageVariants(params) {
1347
+ const {
1348
+ sourceBuffer,
1349
+ format,
1350
+ quality,
1351
+ effort,
1352
+ limitInputPixels,
1353
+ mainWidth,
1354
+ mainHeight,
1355
+ mainUrl,
1356
+ baseStoragePath,
1357
+ baseFilename,
1358
+ bucket,
1359
+ storage,
1360
+ variantWidths,
1361
+ upsert = false,
1362
+ contentHash,
1363
+ metadata,
1364
+ mirrorDir,
1365
+ publicUrlBase
1366
+ } = params;
1367
+ if (format === "svg" || !variantWidths?.length) {
1368
+ return { variants: [] };
1369
+ }
1370
+ const targetWidths = [
1371
+ ...new Set(
1372
+ variantWidths.map((w) => Math.floor(w)).filter((w) => Number.isFinite(w) && w > 0 && w < mainWidth)
1373
+ )
1374
+ ].sort((a, b) => a - b);
1375
+ if (targetWidths.length === 0) {
1376
+ return { variants: [] };
1377
+ }
1378
+ const sharp = await loadSharp();
1379
+ const baseQuality = Math.max(1, Math.min(100, quality ?? 85));
1380
+ const avifEffort = effort ?? PRIMARY_AVIF_EFFORT;
1381
+ const basePipeline = sharp(sourceBuffer, {
1382
+ failOn: "error",
1383
+ limitInputPixels: limitInputPixels ?? DEFAULT_LIMIT_INPUT_PIXELS
1384
+ }).rotate();
1385
+ const variants = await Promise.all(
1386
+ targetWidths.map(async (w) => {
1387
+ let branch = basePipeline.clone().resize({
1388
+ width: w,
1389
+ withoutEnlargement: true,
1390
+ kernel: "lanczos3"
1391
+ });
1392
+ if (format === "jpeg") {
1393
+ branch = branch.jpeg({ quality: baseQuality, mozjpeg: true });
1394
+ } else if (format === "png") {
1395
+ branch = branch.png({ compressionLevel: 9 });
1396
+ } else if (format === "webp") {
1397
+ branch = branch.webp({ quality: baseQuality, effort: 4 });
1398
+ } else {
1399
+ branch = branch.avif({
1400
+ quality: baseQuality,
1401
+ effort: avifEffort,
1402
+ chromaSubsampling: "4:2:0",
1403
+ bitdepth: 8
1404
+ });
1405
+ }
1406
+ const out = await branch.toBuffer({ resolveWithObject: true });
1407
+ const variantPath = buildVariantStoragePath(baseStoragePath, w);
1408
+ const variantFilename = buildVariantStoragePath(baseFilename, w);
1409
+ const uploaded = await storage.upload({
1410
+ bucket,
1411
+ path: variantPath,
1412
+ data: out.data,
1413
+ contentType: `image/${format}`,
1414
+ upsert,
1415
+ contentHash,
1416
+ metadata
1417
+ });
1418
+ const resolvedUrl = await mirrorBufferLocally({
1419
+ buffer: out.data,
1420
+ filename: variantFilename,
1421
+ storagePath: uploaded.path,
1422
+ fallbackUrl: uploaded.url,
1423
+ mirrorDir,
1424
+ publicUrlBase
1425
+ });
1426
+ return {
1427
+ width: out.info.width || w,
1428
+ height: out.info.height || Math.max(1, Math.round(mainHeight / Math.max(1, mainWidth) * w)),
1429
+ url: resolvedUrl,
1430
+ size: out.info.size,
1431
+ storagePath: uploaded.path
1432
+ };
1433
+ })
1434
+ );
1435
+ const srcSet = [
1436
+ ...variants.map((v) => `${v.url} ${v.width}w`),
1437
+ `${mainUrl} ${mainWidth}w`
1438
+ ].join(", ");
1439
+ return { variants, srcSet };
1440
+ }
1441
+ function collectCandidateVariantWidths(presets, extraWidths) {
1442
+ const widths = new Set(extraWidths);
1443
+ for (const preset of Object.values(presets)) {
1444
+ if (preset?.variantWidths) {
1445
+ for (const w of preset.variantWidths) {
1446
+ if (Number.isFinite(w) && w > 0) widths.add(Math.floor(w));
1447
+ }
1448
+ }
1449
+ }
1450
+ return [...widths];
1451
+ }
1452
+ async function deleteStoredVariants(params) {
1453
+ const {
1454
+ storage,
1455
+ bucket,
1456
+ basePath,
1457
+ mirrorDirs,
1458
+ knownVariantPaths = [],
1459
+ candidateWidths = []
1460
+ } = params;
1461
+ const paths = new Set(knownVariantPaths);
1462
+ for (const w of candidateWidths) {
1463
+ paths.add(buildVariantStoragePath(basePath, w));
1464
+ }
1465
+ if (paths.size === 0) return;
1466
+ await Promise.all(
1467
+ [...paths].map(async (vPath) => {
1468
+ await storage.delete(bucket, vPath).catch(() => {
1469
+ });
1470
+ await deleteMirroredFiles(mirrorDirs, vPath);
1471
+ })
1472
+ );
1473
+ }
1474
+ async function invokeRecordHookWithRollback(params) {
1475
+ const {
1476
+ onUploadRecord,
1477
+ awaitRecordHook,
1478
+ record,
1479
+ storage,
1480
+ bucket,
1481
+ mainPath,
1482
+ variantPaths,
1483
+ mirrorDirs,
1484
+ onError
1485
+ } = params;
1486
+ if (!onUploadRecord) return;
1487
+ if (awaitRecordHook) {
1488
+ try {
1489
+ await onUploadRecord(record);
1490
+ } catch (hookError) {
1491
+ await storage.delete(bucket, mainPath).catch(() => {
1492
+ });
1493
+ await deleteMirroredFiles(mirrorDirs, mainPath);
1494
+ await deleteStoredVariants({
1495
+ storage,
1496
+ bucket,
1497
+ basePath: mainPath,
1498
+ mirrorDirs,
1499
+ knownVariantPaths: variantPaths
1500
+ });
1501
+ throw hookError;
1502
+ }
1503
+ } else {
1504
+ Promise.resolve(onUploadRecord(record)).catch((err) => {
1505
+ onError?.(err, { action: "recordHook" });
1506
+ });
1507
+ }
1508
+ }
1509
+
1510
+ // src/server/uploader/createImageUploader.ts
1511
+ function createImageUploader(options) {
1512
+ const {
1513
+ storage,
1514
+ defaultBucket = "images",
1515
+ defaultFolder = "",
1516
+ defaultMaxSizeMB = 10,
1517
+ defaultAllowedFormats = DEFAULT_ALLOWED_FORMATS,
1518
+ defaultMaxDimensions = DEFAULT_IMAGE_DIMENSIONS,
1519
+ allowSvg: defaultAllowSvg = false,
1520
+ limitInputPixels: defaultLimitInputPixels,
1521
+ effort: defaultEffort,
1522
+ presets: customPresets,
1523
+ includeStandardPresets = true,
1524
+ pathGenerator,
1525
+ onUploadRecord,
1526
+ awaitRecordHook = false,
1527
+ onError,
1528
+ concurrency = 2,
1529
+ maxQueue,
1530
+ optimizationTimeoutMs = 6e4,
1531
+ blurTimeoutMs = 3e4,
1532
+ includeBlurHash: defaultIncludeBlurHash = false
1533
+ } = options;
1534
+ const mergedPresets = {
1535
+ ...includeStandardPresets ? STANDARD_PRESETS : {},
1536
+ ...customPresets || {}
1537
+ };
1538
+ const semaphore = new AsyncSemaphore(concurrency, { maxQueue });
1539
+ const buildPath = pathGenerator || createDefaultPathGenerator();
1540
+ const trackedVariants = /* @__PURE__ */ new Map();
1541
+ const observedWidths = /* @__PURE__ */ new Set();
1542
+ const resolvePreset = (name) => name ? mergedPresets[name] : void 0;
1543
+ const getAllowedBuckets = () => {
1544
+ const buckets = /* @__PURE__ */ new Set([defaultBucket]);
1545
+ for (const preset of Object.values(mergedPresets)) {
1546
+ if (preset?.bucket) buckets.add(preset.bucket);
1547
+ }
1548
+ return [...buckets];
1549
+ };
1550
+ const getMirrorDirs = () => [...new Set(Object.values(mergedPresets).map((p) => p?.mirrorDir).filter(Boolean))];
1551
+ const getMaxSizeMB = (allowedPresets) => {
1552
+ let maxMB = defaultMaxSizeMB;
1553
+ const keys = allowedPresets?.length ? allowedPresets : Object.keys(mergedPresets);
1554
+ for (const key of keys) {
1555
+ const p = mergedPresets[key];
1556
+ if (p?.maxSizeMB && p.maxSizeMB > maxMB) maxMB = p.maxSizeMB;
1557
+ }
1558
+ return maxMB;
1559
+ };
1560
+ const cleanupVariantsFor = async (bucket, basePath) => {
1561
+ const key = `${bucket}:${basePath}`;
1562
+ const knownVariantPaths = trackedVariants.get(key);
1563
+ trackedVariants.delete(key);
1564
+ await deleteStoredVariants({
1565
+ storage,
1566
+ bucket,
1567
+ basePath,
1568
+ mirrorDirs: getMirrorDirs(),
1569
+ knownVariantPaths,
1570
+ candidateWidths: collectCandidateVariantWidths(mergedPresets, observedWidths)
1571
+ });
1572
+ };
1573
+ return {
1574
+ defaultBucket,
1575
+ resolvePreset,
1576
+ getAllowedBuckets,
1577
+ getMaxSizeMB,
1578
+ parseUrl: (url) => storage.parseUrl ? storage.parseUrl(url) : null,
1579
+ async processBuffer(buffer, procOptions = {}) {
1580
+ return semaphore.run(
1581
+ () => processImageBuffer(buffer, {
1582
+ maxDimensions: defaultMaxDimensions,
1583
+ optimizationTimeoutMs,
1584
+ blurTimeoutMs,
1585
+ includeBlurHash: defaultIncludeBlurHash,
1586
+ limitInputPixels: defaultLimitInputPixels,
1587
+ effort: defaultEffort,
1588
+ ...procOptions
1589
+ }),
1590
+ { signal: procOptions.signal }
1591
+ );
1592
+ },
1593
+ async uploadImage(input, uploadOptions = {}) {
1594
+ const presetConfig = resolvePreset(uploadOptions.preset);
1595
+ if (uploadOptions.preset && !presetConfig) {
1596
+ throw new UploadValidationError(`Invalid preset "${uploadOptions.preset}".`);
1597
+ }
1598
+ const bucket = uploadOptions.bucket || presetConfig?.bucket || defaultBucket;
1599
+ const folder = uploadOptions.folder ?? presetConfig?.folder ?? defaultFolder;
1600
+ const maxSizeMB = uploadOptions.maxSizeMB ?? presetConfig?.maxSizeMB ?? defaultMaxSizeMB;
1601
+ const allowSvg = uploadOptions.allowSvg ?? presetConfig?.allowSvg ?? defaultAllowSvg;
1602
+ const explicitAllowSvg = Boolean(
1603
+ uploadOptions.allowSvg ?? presetConfig?.allowSvg ?? (defaultAllowSvg && !uploadOptions.allowedFormats && !presetConfig?.allowedFormats)
1604
+ );
1605
+ const allowedFormats = resolveEffectiveAllowedFormats(
1606
+ uploadOptions.allowedFormats ?? presetConfig?.allowedFormats ?? defaultAllowedFormats,
1607
+ allowSvg,
1608
+ explicitAllowSvg
1609
+ );
1610
+ const maxDimensions = uploadOptions.maxDimensions ?? presetConfig?.maxDimensions ?? defaultMaxDimensions;
1611
+ const outputFormat = uploadOptions.outputFormat ?? presetConfig?.outputFormat;
1612
+ const quality = uploadOptions.quality ?? presetConfig?.quality;
1613
+ const includeBlurHash = uploadOptions.includeBlurHash ?? presetConfig?.includeBlurHash ?? defaultIncludeBlurHash;
1614
+ const limitInputPixels = uploadOptions.limitInputPixels ?? presetConfig?.limitInputPixels ?? defaultLimitInputPixels;
1615
+ const variantWidths = uploadOptions.variantWidths ?? presetConfig?.variantWidths;
1616
+ const isWebFile = typeof input.arrayBuffer === "function";
1617
+ if (isWebFile) {
1618
+ const pre = validateImageFileStrict({ size: input.size, type: input.type }, maxSizeMB, allowedFormats);
1619
+ if (!pre.valid) throw new UploadValidationError(pre.error);
1620
+ }
1621
+ try {
1622
+ let originalBuffer = isWebFile ? Buffer.from(await input.arrayBuffer()) : input.buffer;
1623
+ if (!originalBuffer || originalBuffer.length === 0) throw new UploadValidationError("File is empty");
1624
+ const validation = validateImageFileStrict({ size: originalBuffer.length, type: input.type }, maxSizeMB, allowedFormats);
1625
+ if (!validation.valid) throw new UploadValidationError(validation.error);
1626
+ const sniffed = assertMagicBytesAllowed(originalBuffer, allowedFormats, allowSvg);
1627
+ if (sniffed.format === "svg") originalBuffer = sanitizeSvg(originalBuffer);
1628
+ const { optimized, blurData, contentHash, originalSize } = await semaphore.run(
1629
+ () => processImageBuffer(originalBuffer, {
1630
+ maxDimensions,
1631
+ outputFormat,
1632
+ quality,
1633
+ includeBlurHash,
1634
+ optimizationTimeoutMs,
1635
+ blurTimeoutMs,
1636
+ limitInputPixels,
1637
+ effort: defaultEffort,
1638
+ allowSvg,
1639
+ signal: uploadOptions.signal
1640
+ }),
1641
+ { signal: uploadOptions.signal }
1642
+ );
1643
+ const mainFormat = optimized.format;
1644
+ const { uploadResult, storageTarget } = await uploadWithCollisionRetry({
1645
+ storage,
1646
+ buildPath,
1647
+ pathContext: {
1648
+ bucket,
1649
+ folder,
1650
+ preset: uploadOptions.preset,
1651
+ format: mainFormat,
1652
+ extension: formatToExtension(mainFormat),
1653
+ customFilename: uploadOptions.customFilename,
1654
+ isClientFilename: uploadOptions.isClientFilename,
1655
+ contentHash,
1656
+ originalName: input.name,
1657
+ metadata: uploadOptions.metadata
1658
+ },
1659
+ data: optimized.buffer,
1660
+ contentType: mainFormat === "svg" ? SVG_MIME_TYPE : `image/${mainFormat}`
1661
+ });
1662
+ const resolvedUrl = await mirrorBufferLocally({
1663
+ buffer: optimized.buffer,
1664
+ filename: storageTarget.filename,
1665
+ storagePath: uploadResult.path,
1666
+ fallbackUrl: uploadResult.url,
1667
+ mirrorDir: presetConfig?.mirrorDir,
1668
+ publicUrlBase: presetConfig?.publicUrlBase
1669
+ });
1670
+ const { variants, srcSet } = variantWidths?.length && mainFormat !== "svg" ? await semaphore.run(
1671
+ () => generateImageVariants({
1672
+ sourceBuffer: originalBuffer,
1673
+ format: mainFormat,
1674
+ quality: optimized.quality ?? quality,
1675
+ effort: defaultEffort,
1676
+ limitInputPixels,
1677
+ mainWidth: optimized.width,
1678
+ mainHeight: optimized.height,
1679
+ mainUrl: resolvedUrl,
1680
+ baseStoragePath: uploadResult.path,
1681
+ baseFilename: storageTarget.filename,
1682
+ bucket: storageTarget.bucket,
1683
+ storage,
1684
+ variantWidths,
1685
+ upsert: Boolean(uploadOptions.customFilename) && !uploadOptions.isClientFilename,
1686
+ contentHash,
1687
+ metadata: uploadOptions.metadata,
1688
+ mirrorDir: presetConfig?.mirrorDir,
1689
+ publicUrlBase: presetConfig?.publicUrlBase
1690
+ }),
1691
+ { signal: uploadOptions.signal }
1692
+ ) : { variants: [], srcSet: void 0 };
1693
+ const variantPaths = variants.map((v) => v.storagePath).filter(Boolean);
1694
+ if (variantPaths.length > 0) {
1695
+ trackedVariants.set(`${storageTarget.bucket}:${uploadResult.path}`, variantPaths);
1696
+ variants.forEach((v) => observedWidths.add(v.width));
1697
+ }
1698
+ const result = {
1699
+ url: resolvedUrl,
1700
+ blurDataUrl: blurData.dataURL,
1701
+ blurColor: blurData.color,
1702
+ ...blurData.blurHash && { blurHash: blurData.blurHash },
1703
+ altText: uploadOptions.altText || "",
1704
+ width: optimized.width,
1705
+ height: optimized.height,
1706
+ format: optimized.format,
1707
+ size: optimized.size,
1708
+ originalSize,
1709
+ contentHash,
1710
+ storagePath: uploadResult.path,
1711
+ bucket: storageTarget.bucket,
1712
+ ...uploadOptions.preset && { preset: uploadOptions.preset },
1713
+ ...folder && { folder },
1714
+ ...uploadOptions.metadata && { metadata: uploadOptions.metadata },
1715
+ ...variants.length > 0 && { variants },
1716
+ ...srcSet && { srcSet },
1717
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
1718
+ };
1719
+ const record = { ...result, quality: optimized.quality ?? quality ?? 85 };
1720
+ await invokeRecordHookWithRollback({
1721
+ onUploadRecord,
1722
+ awaitRecordHook,
1723
+ record,
1724
+ storage,
1725
+ bucket: storageTarget.bucket,
1726
+ mainPath: uploadResult.path,
1727
+ variantPaths,
1728
+ mirrorDirs: getMirrorDirs(),
1729
+ onError
1730
+ });
1731
+ return result;
1732
+ } catch (error) {
1733
+ if (error instanceof UploadValidationError || error instanceof SemaphoreQueueFullError) {
1734
+ throw error;
1735
+ }
1736
+ throw new Error(`Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1737
+ }
1738
+ },
1739
+ async deleteImage(targetPath, bucket) {
1740
+ const targetBucket = bucket || defaultBucket;
1741
+ try {
1742
+ await storage.delete(targetBucket, targetPath);
1743
+ await deleteMirroredFiles(getMirrorDirs(), targetPath);
1744
+ await cleanupVariantsFor(targetBucket, targetPath);
1745
+ } catch (error) {
1746
+ throw new Error(`Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1747
+ }
1748
+ },
1749
+ async deleteByUrl(url) {
1750
+ const parsed = storage.parseUrl?.(url);
1751
+ const deleted = storage.deleteByUrl ? await storage.deleteByUrl(url) : false;
1752
+ if (deleted && parsed?.path) {
1753
+ await deleteMirroredFiles(getMirrorDirs(), parsed.path);
1754
+ await cleanupVariantsFor(parsed.bucket || defaultBucket, parsed.path);
1755
+ }
1756
+ return deleted;
1757
+ }
1758
+ };
1759
+ }
1760
+ export {
1761
+ AsyncSemaphore,
1762
+ BLUR_PLACEHOLDER_FALLBACK_WIDTH,
1763
+ BLUR_PLACEHOLDER_MAX_CHARS,
1764
+ BLUR_PLACEHOLDER_QUALITY,
1765
+ BLUR_PLACEHOLDER_WIDTH,
1766
+ DEFAULT_LIMIT_INPUT_PIXELS,
1767
+ DEFAULT_MAX_SVG_BYTES,
1768
+ EDGE_COLOR_FALLBACK,
1769
+ EDGE_WHITE_FALLBACK,
1770
+ PRIMARY_AVIF_EFFORT,
1771
+ RETRY_AVIF_EFFORT,
1772
+ SemaphoreQueueFullError,
1773
+ UploadValidationError,
1774
+ WHITE_DOMINANCE_RATIO,
1775
+ assertMagicBytesAllowed,
1776
+ blendEdgeColors,
1777
+ buildVariantStoragePath,
1778
+ computeEdgeColorSet,
1779
+ createDefaultPathGenerator,
1780
+ createImageUploader,
1781
+ createUploadHandler,
1782
+ createUploadHttpHandler,
1783
+ encodeBlurHashString,
1784
+ extractEdgeColorsFromBuffer,
1785
+ extractEnhancedColorFromBuffer,
1786
+ formatToExtension,
1787
+ generateBlurHash,
1788
+ generateBlurPlaceholder,
1789
+ generateImageVariants,
1790
+ isClientValidationError,
1791
+ isCollisionError,
1792
+ mirrorBufferLocally,
1793
+ optimizeImage,
1794
+ processImageBuffer,
1795
+ sanitizeSegment,
1796
+ sanitizeSvg,
1797
+ sniffImageFormat,
1798
+ uploadWithCollisionRetry,
1799
+ withTimeout
1800
+ };
1801
+ //# sourceMappingURL=server.js.map