img-upload-kit 1.1.1 → 2.0.1

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 (111) hide show
  1. package/CHANGELOG.md +89 -74
  2. package/MIGRATION.md +192 -0
  3. package/README.md +243 -259
  4. package/dist/bin.js +1186 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/blur.cjs +40 -412
  7. package/dist/blur.cjs.map +1 -1
  8. package/dist/blur.d.cts +5 -5
  9. package/dist/blur.d.ts +5 -5
  10. package/dist/blur.js +39 -393
  11. package/dist/blur.js.map +1 -1
  12. package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
  13. package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
  14. package/dist/blurService-D5TpMNji.d.ts +83 -0
  15. package/dist/blurService-DacwylJx.d.cts +83 -0
  16. package/dist/chunk-25GYKQ6J.js +158 -0
  17. package/dist/chunk-25GYKQ6J.js.map +1 -0
  18. package/dist/chunk-34L4TEE3.cjs +226 -0
  19. package/dist/chunk-34L4TEE3.cjs.map +1 -0
  20. package/dist/chunk-DFCSN3X7.cjs +158 -0
  21. package/dist/chunk-DFCSN3X7.cjs.map +1 -0
  22. package/dist/chunk-EWVIL57J.js +294 -0
  23. package/dist/chunk-EWVIL57J.js.map +1 -0
  24. package/dist/chunk-FP5YIWG5.cjs +135 -0
  25. package/dist/chunk-FP5YIWG5.cjs.map +1 -0
  26. package/dist/chunk-HILELQGS.js +226 -0
  27. package/dist/chunk-HILELQGS.js.map +1 -0
  28. package/dist/chunk-IDLLCVGB.js +2370 -0
  29. package/dist/chunk-IDLLCVGB.js.map +1 -0
  30. package/dist/chunk-J6A44Y3W.js +514 -0
  31. package/dist/chunk-J6A44Y3W.js.map +1 -0
  32. package/dist/chunk-LOSVFO56.cjs +145 -0
  33. package/dist/chunk-LOSVFO56.cjs.map +1 -0
  34. package/dist/chunk-MBQJ6NVJ.cjs +294 -0
  35. package/dist/chunk-MBQJ6NVJ.cjs.map +1 -0
  36. package/dist/chunk-MHGQEBPW.js +145 -0
  37. package/dist/chunk-MHGQEBPW.js.map +1 -0
  38. package/dist/chunk-QHU7FUO4.cjs +37 -0
  39. package/dist/chunk-QHU7FUO4.cjs.map +1 -0
  40. package/dist/chunk-S2IFAWJK.js +37 -0
  41. package/dist/chunk-S2IFAWJK.js.map +1 -0
  42. package/dist/chunk-TA5Y64JM.cjs +2370 -0
  43. package/dist/chunk-TA5Y64JM.cjs.map +1 -0
  44. package/dist/chunk-VYVZVUNI.js +135 -0
  45. package/dist/chunk-VYVZVUNI.js.map +1 -0
  46. package/dist/chunk-XKQMCY5Z.cjs +514 -0
  47. package/dist/chunk-XKQMCY5Z.cjs.map +1 -0
  48. package/dist/cli.cjs +273 -76
  49. package/dist/cli.cjs.map +1 -1
  50. package/dist/cli.js +269 -73
  51. package/dist/cli.js.map +1 -1
  52. package/dist/headless.cjs +8 -1880
  53. package/dist/headless.cjs.map +1 -1
  54. package/dist/headless.d.cts +3 -1
  55. package/dist/headless.d.ts +3 -1
  56. package/dist/headless.js +25 -1886
  57. package/dist/headless.js.map +1 -1
  58. package/dist/index-BIdnqtW8.d.ts +234 -0
  59. package/dist/index-Brk_uHs6.d.cts +234 -0
  60. package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
  61. package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
  62. package/dist/index.cjs +64 -532
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +7 -4
  65. package/dist/index.d.ts +7 -4
  66. package/dist/index.js +63 -504
  67. package/dist/index.js.map +1 -1
  68. package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
  69. package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
  70. package/dist/placeholders-C4ftwaII.d.ts +5 -0
  71. package/dist/placeholders-x_3rt65M.d.cts +5 -0
  72. package/dist/react.cjs +1952 -3257
  73. package/dist/react.cjs.map +1 -1
  74. package/dist/react.d.cts +188 -50
  75. package/dist/react.d.ts +188 -50
  76. package/dist/react.js +1763 -3013
  77. package/dist/react.js.map +1 -1
  78. package/dist/server-handler.cjs +14 -0
  79. package/dist/server-handler.cjs.map +1 -0
  80. package/dist/server-handler.d.cts +3 -0
  81. package/dist/server-handler.d.ts +3 -0
  82. package/dist/server-handler.js +14 -0
  83. package/dist/server-handler.js.map +1 -0
  84. package/dist/server.cjs +1096 -532
  85. package/dist/server.cjs.map +1 -1
  86. package/dist/server.d.cts +77 -154
  87. package/dist/server.d.ts +77 -154
  88. package/dist/server.js +1050 -448
  89. package/dist/server.js.map +1 -1
  90. package/dist/storage.cjs +239 -232
  91. package/dist/storage.cjs.map +1 -1
  92. package/dist/storage.d.cts +21 -5
  93. package/dist/storage.d.ts +21 -5
  94. package/dist/storage.js +227 -207
  95. package/dist/storage.js.map +1 -1
  96. package/dist/styles/animated-image.css +112 -0
  97. package/dist/styles/dropzone-and-tiles.css +247 -0
  98. package/dist/styles/modals-and-dock.css +248 -0
  99. package/dist/styles/primitives.css +197 -0
  100. package/dist/styles/quality-and-inputs.css +184 -0
  101. package/dist/styles/tokens.css +123 -0
  102. package/dist/styles.css +235 -220
  103. package/dist/types-BrP-o9nd.d.cts +85 -0
  104. package/dist/types-sZ3KoFQY.d.ts +85 -0
  105. package/dist/types-yShGNQnW.d.cts +61 -0
  106. package/dist/types-yShGNQnW.d.ts +61 -0
  107. package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
  108. package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
  109. package/package.json +92 -32
  110. package/dist/blurService-DqsnfPTd.d.cts +0 -172
  111. package/dist/blurService-DqsnfPTd.d.ts +0 -172
package/dist/server.cjs CHANGED
@@ -1,201 +1,22 @@
1
- 'use strict';
2
-
3
- var sharp = require('sharp');
4
- var blurhash = require('blurhash');
5
- var lqip = require('lqip-modern');
6
- var crypto = require('crypto');
7
- var fs = require('fs');
8
- var path = require('path');
9
-
10
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
-
12
- var sharp__default = /*#__PURE__*/_interopDefault(sharp);
13
- var lqip__default = /*#__PURE__*/_interopDefault(lqip);
14
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
15
- var fs__default = /*#__PURE__*/_interopDefault(fs);
16
- var path__default = /*#__PURE__*/_interopDefault(path);
17
-
18
- // src/server/handler.ts
19
- var UploadValidationError = class extends Error {
20
- status = 400;
21
- constructor(message) {
22
- super(message);
23
- this.name = "UploadValidationError";
24
- }
25
- };
26
- var VALIDATION_ERROR_SUBSTRINGS = [
27
- "exceeds maximum",
28
- "file size",
29
- "too large",
30
- "is not allowed",
31
- "not allowed",
32
- "unsupported image format",
33
- "unsupported format",
34
- "invalid format",
35
- "invalid image",
36
- "not a valid image",
37
- "file type",
38
- "invalid preset",
39
- "no file provided",
40
- "file is empty"
41
- ];
42
- function isClientValidationError(error) {
43
- if (!error) return false;
44
- if (error instanceof UploadValidationError) return true;
45
- if (typeof error === "object" && error !== null && "status" in error && error.status === 400) {
46
- return true;
47
- }
48
- if (error instanceof Error && (error.name === "UploadValidationError" || error.name === "ValidationError")) {
49
- return true;
50
- }
51
- const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
52
- return VALIDATION_ERROR_SUBSTRINGS.some((sub) => msg.includes(sub));
53
- }
54
- function jsonResponse(body, status = 200) {
55
- return new Response(JSON.stringify(body), {
56
- status,
57
- headers: { "Content-Type": "application/json" }
58
- });
59
- }
60
- function parseJsonRecord(raw) {
61
- if (!raw) return void 0;
62
- try {
63
- const parsed = JSON.parse(raw);
64
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
65
- const clean = {};
66
- for (const [k, v] of Object.entries(parsed)) {
67
- if (v !== void 0 && v !== null) {
68
- clean[k] = String(v);
69
- }
70
- }
71
- return Object.keys(clean).length > 0 ? clean : void 0;
72
- }
73
- } catch {
74
- }
75
- return void 0;
76
- }
77
- function createUploadHttpHandler(options) {
78
- const {
79
- uploader,
80
- authorize,
81
- allowedPresets,
82
- allowClientBucketOverride = false
83
- } = options;
84
- async function POST(request) {
85
- try {
86
- if (authorize) {
87
- const allowed = await authorize(request);
88
- if (!allowed) {
89
- return jsonResponse({ error: "Unauthorized" }, 401);
90
- }
91
- }
92
- const formData = await request.formData();
93
- const file = formData.get("file");
94
- if (!file || typeof file === "string" || typeof file.size === "number" && file.size === 0 && !file.name) {
95
- return jsonResponse({ error: "No file provided" }, 400);
96
- }
97
- if (typeof file.size === "number" && file.size === 0) {
98
- return jsonResponse({ error: "File is empty" }, 400);
99
- }
100
- const preset = formData.get("preset") || void 0;
101
- if (preset) {
102
- if (allowedPresets && allowedPresets.length > 0 && !allowedPresets.includes(preset)) {
103
- return jsonResponse(
104
- {
105
- error: `Invalid preset "${preset}". Allowed presets: ${allowedPresets.join(", ")}`
106
- },
107
- 400
108
- );
109
- }
110
- if (typeof uploader.resolvePreset === "function" && !uploader.resolvePreset(preset)) {
111
- return jsonResponse(
112
- {
113
- error: `Invalid preset "${preset}".`
114
- },
115
- 400
116
- );
117
- }
118
- }
119
- const folder = formData.get("folder") || void 0;
120
- const bucket = allowClientBucketOverride ? formData.get("bucket") || void 0 : void 0;
121
- const altText = formData.get("altText") || void 0;
122
- const customFilename = formData.get("customFilename") || void 0;
123
- const outputFormatRaw = formData.get("outputFormat");
124
- if (outputFormatRaw && !["jpeg", "png", "webp", "avif"].includes(outputFormatRaw)) {
125
- return jsonResponse(
126
- {
127
- error: `Invalid output format "${outputFormatRaw}". Allowed formats: jpeg, png, webp, avif`
128
- },
129
- 400
130
- );
131
- }
132
- const outputFormat = outputFormatRaw || void 0;
133
- const qualityRaw = formData.get("quality");
134
- const quality = qualityRaw !== null ? Math.max(1, Math.min(100, parseInt(qualityRaw, 10) || 85)) : void 0;
135
- const maxWidthRaw = formData.get("maxWidth");
136
- const maxHeightRaw = formData.get("maxHeight");
137
- let maxDimensions;
138
- if (maxWidthRaw && maxHeightRaw) {
139
- const w = parseInt(maxWidthRaw, 10);
140
- const h = parseInt(maxHeightRaw, 10);
141
- if (w > 0 && h > 0) {
142
- maxDimensions = { width: w, height: h };
143
- }
144
- }
145
- const metadata = parseJsonRecord(
146
- formData.get("metadata")
147
- );
148
- const result = await uploader.uploadImage(file, {
149
- preset,
150
- folder,
151
- bucket,
152
- altText,
153
- customFilename,
154
- outputFormat,
155
- quality,
156
- maxDimensions,
157
- metadata
158
- });
159
- return jsonResponse(result, 200);
160
- } catch (error) {
161
- const errorMessage = error instanceof Error ? error.message : "Internal server error";
162
- const status = isClientValidationError(error) ? 400 : 500;
163
- return jsonResponse({ error: errorMessage }, status);
164
- }
165
- }
166
- async function DELETE(request) {
167
- try {
168
- if (authorize) {
169
- const allowed = await authorize(request);
170
- if (!allowed) {
171
- return jsonResponse({ error: "Unauthorized" }, 401);
172
- }
173
- }
174
- const { searchParams } = new URL(request.url);
175
- const url = searchParams.get("url");
176
- if (url) {
177
- const deleted = await uploader.deleteByUrl(url);
178
- return jsonResponse({ success: deleted }, deleted ? 200 : 400);
179
- }
180
- const storagePath = searchParams.get("path");
181
- const bucket = allowClientBucketOverride ? searchParams.get("bucket") || void 0 : void 0;
182
- if (!storagePath) {
183
- return jsonResponse(
184
- { error: 'Missing required "path" or "url" query parameter' },
185
- 400
186
- );
187
- }
188
- await uploader.deleteImage(storagePath, bucket);
189
- return jsonResponse({ success: true }, 200);
190
- } catch (error) {
191
- const errorMessage = error instanceof Error ? error.message : "Internal server error";
192
- return jsonResponse({ error: errorMessage }, 500);
193
- }
194
- }
195
- return { POST, DELETE };
196
- }
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
2
+
3
+
4
+
5
+
6
+ var _chunkXKQMCY5Zcjs = require('./chunk-XKQMCY5Z.cjs');
7
+ require('./chunk-QHU7FUO4.cjs');
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+ var _chunk34L4TEE3cjs = require('./chunk-34L4TEE3.cjs');
197
17
 
198
18
  // src/server/optimization/types.ts
19
+ var DEFAULT_LIMIT_INPUT_PIXELS = 5e7;
199
20
  var PRIMARY_AVIF_EFFORT = 6;
200
21
  var RETRY_AVIF_EFFORT = 5;
201
22
  var BLUR_PLACEHOLDER_WIDTH = 128;
@@ -206,132 +27,252 @@ var EDGE_COLOR_FALLBACK = "#6B7280";
206
27
  var EDGE_WHITE_FALLBACK = "#F9FAFB";
207
28
  var WHITE_DOMINANCE_RATIO = 1.35;
208
29
 
209
- // src/types/presets.ts
210
- var DEFAULT_ALLOWED_FORMATS = [
211
- "image/jpeg",
212
- "image/png",
213
- "image/webp",
214
- "image/avif",
215
- "image/svg+xml"
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"
216
52
  ];
217
- var RASTER_ALLOWED_FORMATS = [
218
- "image/jpeg",
219
- "image/png",
220
- "image/webp",
221
- "image/avif"
222
- ];
223
- var DEFAULT_IMAGE_DIMENSIONS = {
224
- width: 1920,
225
- height: 1080
226
- };
227
- var STANDARD_PRESETS = {
228
- thumbnail: {
229
- folder: "thumbnails",
230
- maxDimensions: { width: 600, height: 600 },
231
- maxSizeMB: 5,
232
- quality: 78,
233
- recommendedQualityLevel: "medium",
234
- allowedFormats: RASTER_ALLOWED_FORMATS,
235
- recommendationNote: {
236
- en: "Thumbnails display at compact sizes \u2014 Medium quality keeps pages fast",
237
- ar: "\u0627\u0644\u0645\u0635\u063A\u0631\u0627\u062A \u062A\u0639\u0631\u0636 \u0628\u0623\u062D\u062C\u0627\u0645 \u0635\u063A\u064A\u0631\u0629 \u2014 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0645\u062A\u0648\u0633\u0637\u0629 \u062A\u062D\u0627\u0641\u0638 \u0639\u0644\u0649 \u0633\u0631\u0639\u0629 \u0627\u0644\u0635\u0641\u062D\u0629"
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 = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(match[2], () => ( match[3])), () => ( match[4])), () => ( ""));
97
+ if (lowerName.startsWith("on") || localName.startsWith("on")) {
98
+ continue;
238
99
  }
239
- },
240
- avatar: {
241
- folder: "avatars",
242
- maxDimensions: { width: 512, height: 512 },
243
- maxSizeMB: 5,
244
- quality: 80,
245
- recommendedQualityLevel: "medium",
246
- allowedFormats: RASTER_ALLOWED_FORMATS,
247
- recommendationNote: {
248
- en: "Avatars display small \u2014 Medium quality balances sharpness and speed",
249
- ar: "\u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u0634\u062E\u0635\u064A\u0629 \u0635\u063A\u064A\u0631\u0629 \u2014 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0645\u062A\u0648\u0633\u0637\u0629 \u062A\u0648\u0627\u0632\u0646 \u0628\u064A\u0646 \u0627\u0644\u0648\u0636\u0648\u062D \u0648\u0627\u0644\u0633\u0631\u0639\u0629"
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;
250
107
  }
251
- },
252
- card: {
253
- folder: "cards",
254
- maxDimensions: { width: 1200, height: 900 },
255
- maxSizeMB: 8,
256
- quality: 85,
257
- recommendedQualityLevel: "high",
258
- allowedFormats: DEFAULT_ALLOWED_FORMATS
259
- },
260
- cover: {
261
- folder: "covers",
262
- maxDimensions: { width: 1920, height: 1080 },
263
- maxSizeMB: 10,
264
- quality: 88,
265
- recommendedQualityLevel: "high",
266
- allowedFormats: DEFAULT_ALLOWED_FORMATS
267
- },
268
- hero: {
269
- folder: "hero",
270
- maxDimensions: { width: 2560, height: 1440 },
271
- maxSizeMB: 15,
272
- quality: 92,
273
- recommendedQualityLevel: "maximum",
274
- allowedFormats: DEFAULT_ALLOWED_FORMATS,
275
- recommendationNote: {
276
- en: "Full-width hero visuals benefit from Maximum quality to preserve fine detail",
277
- ar: "\u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629 \u0628\u0639\u0631\u0636 \u0643\u0627\u0645\u0644 \u062A\u0633\u062A\u0641\u064A\u062F \u0645\u0646 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0644\u062D\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062A\u0641\u0627\u0635\u064A\u0644 \u0627\u0644\u062F\u0642\u064A\u0642\u0629"
108
+ if (localName === "style") {
109
+ const cleanStyle = sanitizeCssBlock(rawValue).replace(/"/g, "'");
110
+ if (cleanStyle.trim()) {
111
+ kept.push(`style="${cleanStyle}"`);
112
+ }
113
+ continue;
278
114
  }
279
- },
280
- logo: {
281
- folder: "logos",
282
- maxDimensions: { width: 800, height: 800 },
283
- maxSizeMB: 5,
284
- quality: 90,
285
- recommendedQualityLevel: "high",
286
- allowedFormats: DEFAULT_ALLOWED_FORMATS,
287
- recommendationNote: {
288
- en: "Prefer SVG for vector logos \u2014 High quality preserves crisp edges on raster logos",
289
- ar: "\u064A\u0641\u0636\u0644 SVG \u0644\u0644\u0634\u0639\u0627\u0631\u0627\u062A \u0627\u0644\u0645\u062A\u062C\u0647\u0629 \u2014 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0639\u0627\u0644\u064A\u0629 \u062A\u062D\u0627\u0641\u0638 \u0639\u0644\u0649 \u062D\u0648\u0627\u0641 \u0648\u0627\u0636\u062D\u0629"
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);
290
128
  }
291
- },
292
- original: {
293
- folder: "originals",
294
- maxDimensions: { width: 4096, height: 4096 },
295
- maxSizeMB: 25,
296
- quality: 95,
297
- recommendedQualityLevel: "maximum",
298
- allowedFormats: DEFAULT_ALLOWED_FORMATS
299
129
  }
300
- };
130
+ return kept.length > 0 ? ` ${kept.join(" ")}` : "";
131
+ }
132
+ function sanitizeSvg(input, options = {}) {
133
+ const maxBytes = _nullishCoalesce(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 (0, _chunkXKQMCY5Zcjs.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 (0, _chunkXKQMCY5Zcjs.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 (0, _chunkXKQMCY5Zcjs.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 = Promise.resolve().then(() => _interopRequireWildcard(require("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
+ }
301
209
 
302
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
+ }
303
239
  async function optimizeImage(buffer, options = {}) {
304
- const maxDimensions = options.maxDimensions || DEFAULT_IMAGE_DIMENSIONS;
240
+ const sharp = await loadSharp();
241
+ const maxDimensions = options.maxDimensions || _chunk34L4TEE3cjs.DEFAULT_IMAGE_DIMENSIONS;
305
242
  const { outputFormat, quality } = options;
306
- let pipeline = sharp__default.default(buffer, {
307
- failOnError: false,
308
- limitInputPixels: 268402689
243
+ const limitInputPixels = _nullishCoalesce(options.limitInputPixels, () => ( DEFAULT_LIMIT_INPUT_PIXELS));
244
+ const primaryEffort = _nullishCoalesce(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
309
249
  }).rotate();
310
- const metadata = await pipeline.metadata();
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 (0, _chunkXKQMCY5Zcjs.UploadValidationError)(msg);
257
+ }
258
+ throw err;
259
+ }
311
260
  const originalFormat = metadata.format;
312
261
  if (originalFormat === "svg") {
313
- let width = metadata.width;
314
- let height = metadata.height;
315
- if (!width || !height) {
316
- const svgText = buffer.toString("utf8");
317
- const viewBoxMatch = svgText.match(
318
- /viewBox\s*=\s*["']\s*([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s*["']/i
319
- );
320
- if (viewBoxMatch) {
321
- width = Math.round(parseFloat(viewBoxMatch[3]));
322
- height = Math.round(parseFloat(viewBoxMatch[4]));
323
- }
324
- }
262
+ const sanitizedBuffer = sanitizeSvg(buffer);
263
+ const svgText = sanitizedBuffer.toString("utf8");
264
+ const dims = extractSvgDimensions(svgText, metadata.width, metadata.height);
325
265
  return {
326
- buffer,
327
- width: width || maxDimensions.width || 600,
328
- height: height || maxDimensions.height || 600,
266
+ buffer: sanitizedBuffer,
267
+ width: dims.width,
268
+ height: dims.height,
329
269
  format: "svg",
330
- size: buffer.length
270
+ size: sanitizedBuffer.length,
271
+ ...quality !== void 0 ? { quality } : {}
331
272
  };
332
273
  }
333
274
  const isPhoto = originalFormat === "jpeg" || Boolean(metadata.density && metadata.density > 150);
334
- let baseQuality = quality ?? (isPhoto ? 85 : 88);
275
+ let baseQuality = _nullishCoalesce(quality, () => ( (isPhoto ? 85 : 88)));
335
276
  baseQuality = Math.max(1, Math.min(100, baseQuality));
336
277
  pipeline = pipeline.resize(maxDimensions.width, maxDimensions.height, {
337
278
  fit: "inside",
@@ -353,25 +294,26 @@ async function optimizeImage(buffer, options = {}) {
353
294
  width: outputBuffer.info.width || maxDimensions.width,
354
295
  height: outputBuffer.info.height || maxDimensions.height,
355
296
  format: outputFormat,
356
- size: outputBuffer.info.size
297
+ size: outputBuffer.info.size,
298
+ quality: baseQuality
357
299
  };
358
300
  }
359
301
  const avifBuffer = await pipeline.clone().avif({
360
302
  quality: baseQuality,
361
- effort: PRIMARY_AVIF_EFFORT,
303
+ effort: primaryEffort,
362
304
  chromaSubsampling: "4:2:0",
363
305
  bitdepth: 8
364
306
  }).toBuffer({ resolveWithObject: true });
365
307
  const avifSize = avifBuffer.info.size;
366
308
  const originalSize = buffer.length;
367
309
  if (avifSize <= originalSize || outputFormat === "avif") {
368
- const finalMetadata = await sharp__default.default(avifBuffer.data).metadata();
369
310
  return {
370
311
  buffer: avifBuffer.data,
371
- width: finalMetadata.width ?? maxDimensions.width,
372
- height: finalMetadata.height ?? maxDimensions.height,
312
+ width: avifBuffer.info.width || maxDimensions.width,
313
+ height: avifBuffer.info.height || maxDimensions.height,
373
314
  format: "avif",
374
- size: avifSize
315
+ size: avifSize,
316
+ quality: baseQuality
375
317
  };
376
318
  }
377
319
  const scaledQuality = Math.max(
@@ -380,72 +322,91 @@ async function optimizeImage(buffer, options = {}) {
380
322
  );
381
323
  const avifRetry = await pipeline.clone().avif({
382
324
  quality: scaledQuality,
383
- effort: RETRY_AVIF_EFFORT,
325
+ effort: retryEffort,
384
326
  chromaSubsampling: "4:2:0",
385
327
  bitdepth: 8
386
328
  }).toBuffer({ resolveWithObject: true });
387
329
  if (avifRetry.info.size <= originalSize) {
388
- const retryMeta = await sharp__default.default(avifRetry.data).metadata();
389
330
  return {
390
331
  buffer: avifRetry.data,
391
- width: retryMeta.width ?? maxDimensions.width,
392
- height: retryMeta.height ?? maxDimensions.height,
332
+ width: avifRetry.info.width || maxDimensions.width,
333
+ height: avifRetry.info.height || maxDimensions.height,
393
334
  format: "avif",
394
- size: avifRetry.info.size
335
+ size: avifRetry.info.size,
336
+ quality: scaledQuality
395
337
  };
396
338
  }
397
- const wasDimensionCapped = (metadata.width ?? 0) > maxDimensions.width || (metadata.height ?? 0) > maxDimensions.height;
398
- if (!wasDimensionCapped) {
399
- const PASSTHROUGH_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif"]);
400
- const srcFmt = PASSTHROUGH_FORMATS.has(originalFormat ?? "") ? originalFormat : "jpeg";
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 = (_nullishCoalesce(orientedWidth, () => ( 0))) > maxDimensions.width || (_nullishCoalesce(orientedHeight, () => ( 0))) > maxDimensions.height;
343
+ if (!wasDimensionCapped && PASSTHROUGH_FORMATS.has(_nullishCoalesce(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
+ }
401
362
  return {
402
- buffer,
403
- width: metadata.width ?? maxDimensions.width,
404
- height: metadata.height ?? maxDimensions.height,
363
+ buffer: passthroughOut.data,
364
+ width: passthroughOut.info.width || orientedWidth || maxDimensions.width,
365
+ height: passthroughOut.info.height || orientedHeight || maxDimensions.height,
405
366
  format: srcFmt,
406
- size: originalSize
367
+ size: passthroughOut.info.size,
368
+ quality: baseQuality
407
369
  };
408
370
  }
409
371
  const webpBuffer = await pipeline.clone().webp({ quality: baseQuality, effort: 6, smartSubsample: true }).toBuffer({ resolveWithObject: true });
410
372
  const useWebP = webpBuffer.info.size <= avifRetry.info.size;
411
- const winner = useWebP ? { buf: webpBuffer.data, fmt: "webp", sz: webpBuffer.info.size } : { buf: avifRetry.data, fmt: "avif", sz: avifRetry.info.size };
412
- const winnerMeta = await sharp__default.default(winner.buf).metadata();
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
+ };
413
388
  return {
414
389
  buffer: winner.buf,
415
- width: winnerMeta.width ?? maxDimensions.width,
416
- height: winnerMeta.height ?? maxDimensions.height,
390
+ width: winner.w || maxDimensions.width,
391
+ height: winner.h || maxDimensions.height,
417
392
  format: winner.fmt,
418
- size: winner.sz
393
+ size: winner.sz,
394
+ quality: winner.q
419
395
  };
420
396
  }
421
397
 
422
- // src/utils/color.ts
423
- function hexToRgb(hex) {
424
- const normalized = hex.replace("#", "");
425
- const expanded = normalized.length === 3 ? normalized.split("").map((char) => char + char).join("") : normalized;
426
- const padded = expanded.padEnd(6, "0").slice(0, 6);
427
- const value = Number.parseInt(padded, 16);
428
- if (Number.isNaN(value)) {
429
- return { r: 245, g: 245, b: 245 };
430
- }
431
- return {
432
- r: value >> 16 & 255,
433
- g: value >> 8 & 255,
434
- b: value & 255
435
- };
436
- }
437
- function rgbToHex(r, g, b) {
438
- const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
439
- const value = clamp(r) << 16 | clamp(g) << 8 | clamp(b);
440
- return `#${value.toString(16).padStart(6, "0").toUpperCase()}`;
441
- }
398
+ // src/server/optimization/blurGenerator.ts
399
+ var _blurhash = require('blurhash');
400
+
401
+ // src/server/optimization/colorHistogram.ts
442
402
  async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
403
+ const sharp = await loadSharp();
443
404
  try {
444
- const metadata = await sharp__default.default(buffer).metadata();
405
+ const metadata = await sharp(buffer).metadata();
445
406
  const maxSize = 400;
446
407
  const needsResize = metadata.width && metadata.height && (metadata.width > maxSize || metadata.height > maxSize);
447
408
  if (needsResize) {
448
- const resized = await sharp__default.default(buffer).resize(maxSize, maxSize, {
409
+ const resized = await sharp(buffer).resize(maxSize, maxSize, {
449
410
  fit: "inside",
450
411
  kernel: "lanczos3",
451
412
  withoutEnlargement: true
@@ -457,15 +418,15 @@ async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
457
418
  mode
458
419
  );
459
420
  }
460
- const raw = await sharp__default.default(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
421
+ const raw = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
461
422
  return extractDominantColorHistogramAdvanced(
462
423
  raw.data,
463
424
  raw.info.width,
464
425
  raw.info.height,
465
426
  mode
466
427
  );
467
- } catch {
468
- const stats = await sharp__default.default(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
428
+ } catch (e2) {
429
+ const stats = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
469
430
  return extractDominantColorHistogram(stats.data);
470
431
  }
471
432
  }
@@ -542,7 +503,7 @@ function extractDominantColorHistogramAdvanced(data, width, height, mode) {
542
503
  return EDGE_WHITE_FALLBACK;
543
504
  }
544
505
  const [r, g, b] = dominantColor.split(",").map(Number);
545
- return rgbToHex(r, g, b);
506
+ return _chunk34L4TEE3cjs.rgbToHex.call(void 0, r, g, b);
546
507
  }
547
508
  function extractDominantColorHistogram(data) {
548
509
  const colorMap = /* @__PURE__ */ new Map();
@@ -579,13 +540,14 @@ function extractDominantColorHistogram(data) {
579
540
  return EDGE_WHITE_FALLBACK;
580
541
  }
581
542
  const [r, g, b] = dominantColor.split(",").map(Number);
582
- return rgbToHex(r, g, b);
543
+ return _chunk34L4TEE3cjs.rgbToHex.call(void 0, r, g, b);
583
544
  }
584
545
 
585
546
  // src/server/optimization/edgeColorExtractor.ts
586
547
  async function extractEdgeColorsFromBuffer(buffer) {
548
+ const sharp = await loadSharp();
587
549
  try {
588
- const sample = await sharp__default.default(buffer).resize(400, 400, {
550
+ const sample = await sharp(buffer).resize(400, 400, {
589
551
  fit: "inside",
590
552
  kernel: "lanczos3",
591
553
  withoutEnlargement: true
@@ -595,7 +557,7 @@ async function extractEdgeColorsFromBuffer(buffer) {
595
557
  sample.info.width,
596
558
  sample.info.height
597
559
  );
598
- } catch {
560
+ } catch (e3) {
599
561
  try {
600
562
  const fallback = await extractEnhancedColorFromBuffer(buffer, "edge");
601
563
  return {
@@ -604,7 +566,7 @@ async function extractEdgeColorsFromBuffer(buffer) {
604
566
  bottom: fallback,
605
567
  left: fallback
606
568
  };
607
- } catch {
569
+ } catch (e4) {
608
570
  return {
609
571
  top: EDGE_COLOR_FALLBACK,
610
572
  right: EDGE_COLOR_FALLBACK,
@@ -703,7 +665,7 @@ function resolveEdgeColor(map, whiteStrength) {
703
665
  return dominantColor.toUpperCase();
704
666
  }
705
667
  const [r, g, b] = dominantColor.split(",").map(Number);
706
- return rgbToHex(r, g, b);
668
+ return _chunk34L4TEE3cjs.rgbToHex.call(void 0, r, g, b);
707
669
  }
708
670
  function blendEdgeColors(edgeColors) {
709
671
  const colors = Object.values(edgeColors);
@@ -712,7 +674,7 @@ function blendEdgeColors(edgeColors) {
712
674
  }
713
675
  const totals = colors.reduce(
714
676
  (acc, hex) => {
715
- const { r, g, b } = hexToRgb(hex || EDGE_COLOR_FALLBACK);
677
+ const { r, g, b } = _chunk34L4TEE3cjs.hexToRgb.call(void 0, hex || EDGE_COLOR_FALLBACK);
716
678
  acc.r += r;
717
679
  acc.g += g;
718
680
  acc.b += b;
@@ -721,51 +683,78 @@ function blendEdgeColors(edgeColors) {
721
683
  { r: 0, g: 0, b: 0 }
722
684
  );
723
685
  const count = colors.length;
724
- return rgbToHex(totals.r / count, totals.g / count, totals.b / count);
686
+ return _chunk34L4TEE3cjs.rgbToHex.call(void 0, totals.r / count, totals.g / count, totals.b / count);
725
687
  }
726
688
 
727
689
  // src/server/optimization/blurGenerator.ts
728
- async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
729
- const { data, info } = await sharp__default.default(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
730
- const clampedX = Math.max(1, Math.min(Math.round(componentX), info.width, 9));
731
- const clampedY = Math.max(1, Math.min(Math.round(componentY), info.height, 9));
732
- return blurhash.encode(
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 _blurhash.encode.call(void 0,
733
694
  new Uint8ClampedArray(data),
734
- info.width,
735
- info.height,
695
+ width,
696
+ height,
736
697
  clampedX,
737
698
  clampedY
738
699
  );
739
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
+ }
740
712
  async function generateBlurPlaceholder(buffer, options = {}) {
741
- const targetWidth = options.width ?? BLUR_PLACEHOLDER_WIDTH;
742
- const fallbackWidth = options.fallbackWidth ?? BLUR_PLACEHOLDER_FALLBACK_WIDTH;
743
- const quality = options.quality ?? BLUR_PLACEHOLDER_QUALITY;
744
- const maxChars = options.maxDataUrlChars ?? BLUR_PLACEHOLDER_MAX_CHARS;
713
+ const targetWidth = _nullishCoalesce(options.width, () => ( BLUR_PLACEHOLDER_WIDTH));
714
+ const fallbackWidth = _nullishCoalesce(options.fallbackWidth, () => ( BLUR_PLACEHOLDER_FALLBACK_WIDTH));
715
+ const quality = _nullishCoalesce(options.quality, () => ( BLUR_PLACEHOLDER_QUALITY));
716
+ const maxChars = _nullishCoalesce(options.maxDataUrlChars, () => ( BLUR_PLACEHOLDER_MAX_CHARS));
717
+ const sharp = await loadSharp();
745
718
  try {
746
719
  let processBuffer = buffer;
747
720
  const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
748
721
  if (isSvg) {
749
722
  const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
750
- processBuffer = await sharp__default.default(Buffer.from(normalizedSvg, "utf8")).resize(targetWidth, targetWidth, { fit: "inside" }).png().toBuffer();
723
+ processBuffer = Buffer.from(normalizedSvg, "utf8");
751
724
  }
752
- const buildPlaceholder = (width, q) => lqip__default.default(processBuffer, {
753
- outputFormat: "webp",
754
- resize: width,
755
- outputOptions: { quality: q, effort: 6, smartSubsample: true }
756
- });
757
- let result = await buildPlaceholder(targetWidth, quality);
758
- if (result.metadata.dataURIBase64.length > maxChars) {
759
- result = await buildPlaceholder(fallbackWidth, quality);
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);
760
741
  }
761
- const dataURL = result.metadata.dataURIBase64;
762
- const edgeColors = await extractEdgeColorsFromBuffer(processBuffer);
742
+ const edgeColors = computeEdgeColorSet(
743
+ rawSample.data,
744
+ rawSample.info.width,
745
+ rawSample.info.height
746
+ );
763
747
  const color = blendEdgeColors(edgeColors);
764
748
  let blurHash;
765
749
  if (options.includeBlurHash) {
766
750
  try {
767
- blurHash = await encodeBlurHashString(processBuffer);
768
- } catch {
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 (e5) {
769
758
  blurHash = void 0;
770
759
  }
771
760
  }
@@ -774,13 +763,14 @@ async function generateBlurPlaceholder(buffer, options = {}) {
774
763
  color,
775
764
  ...blurHash && { blurHash }
776
765
  };
777
- } catch {
766
+ } catch (e6) {
778
767
  return generateFallbackBlur(buffer, options.includeBlurHash);
779
768
  }
780
769
  }
781
770
  var generateBlurHash = generateBlurPlaceholder;
782
771
  async function generateFallbackBlur(buffer, includeBlurHash) {
783
- const tinyBuffer = await sharp__default.default(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
772
+ const sharp = await loadSharp();
773
+ const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
784
774
  quality: 75,
785
775
  effort: 6,
786
776
  smartSubsample: false
@@ -792,58 +782,285 @@ async function generateFallbackBlur(buffer, includeBlurHash) {
792
782
  if (includeBlurHash) {
793
783
  try {
794
784
  blurHash = await encodeBlurHashString(tinyBuffer);
795
- } catch {
785
+ } catch (e7) {
796
786
  blurHash = void 0;
797
787
  }
798
788
  }
799
789
  return { dataURL, color, ...blurHash && { blurHash } };
800
790
  }
801
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 (0, _chunkXKQMCY5Zcjs.UploadValidationError)(
878
+ "Unsupported image format: file content is not a recognized image"
879
+ );
880
+ }
881
+ if (sniffed.format === "svg" && !allowSvg) {
882
+ throw new (0, _chunkXKQMCY5Zcjs.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 (0, _chunkXKQMCY5Zcjs.UploadValidationError)(
888
+ `File type ${sniffed.mime} is not allowed. Allowed: ${allowedFormats.join(", ")}`
889
+ );
890
+ }
891
+ return sniffed;
892
+ }
893
+
802
894
  // src/server/semaphore.ts
803
- var AsyncSemaphore = class {
804
- constructor(concurrency) {
895
+ var SemaphoreQueueFullError = (_class = class extends Error {
896
+ __init() {this.status = 503}
897
+ constructor(message = "Server upload queue is full. Please try again later.") {
898
+ super(message);_class.prototype.__init.call(this);;
899
+ this.name = "SemaphoreQueueFullError";
900
+ }
901
+ }, _class);
902
+ function createAbortError(signal) {
903
+ if (_optionalChain([signal, 'optionalAccess', _2 => _2.reason]) instanceof Error) {
904
+ return signal.reason;
905
+ }
906
+ const err = new Error(
907
+ typeof _optionalChain([signal, 'optionalAccess', _3 => _3.reason]) === "string" ? signal.reason : "Operation aborted"
908
+ );
909
+ err.name = "AbortError";
910
+ return err;
911
+ }
912
+ var AsyncSemaphore = (_class2 = class {
913
+ constructor(concurrency, options) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);
805
914
  this.concurrency = concurrency;
806
915
  if (concurrency < 1) {
807
916
  throw new Error("AsyncSemaphore concurrency must be >= 1");
808
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
+
925
+ __init2() {this.activeCount = 0}
926
+
927
+ __init3() {this.queue = []}
928
+ get active() {
929
+ return this.activeCount;
809
930
  }
810
- concurrency;
811
- activeCount = 0;
812
- queue = [];
813
- async run(task) {
814
- await this.acquire();
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;
815
938
  try {
816
- return await task();
817
- } finally {
939
+ rawTaskPromise = Promise.resolve().then(task);
940
+ } catch (syncError) {
818
941
  this.release();
942
+ throw syncError;
819
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;
820
970
  }
821
- acquire() {
971
+ acquire(signal) {
972
+ if (_optionalChain([signal, 'optionalAccess', _4 => _4.aborted])) {
973
+ return Promise.reject(createAbortError(signal));
974
+ }
822
975
  if (this.activeCount < this.concurrency) {
823
976
  this.activeCount += 1;
824
977
  return Promise.resolve();
825
978
  }
826
- return new Promise((resolve) => {
827
- this.queue.push(() => {
828
- this.activeCount += 1;
829
- resolve();
830
- });
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
+ _optionalChain([signal, 'optionalAccess', _5 => _5.removeEventListener, 'call', _6 => _6("abort", onAbort)]);
993
+ this.activeCount += 1;
994
+ resolve();
995
+ },
996
+ cancel: (reason) => {
997
+ _optionalChain([signal, 'optionalAccess', _7 => _7.removeEventListener, 'call', _8 => _8("abort", onAbort)]);
998
+ reject(reason);
999
+ }
1000
+ };
1001
+ _optionalChain([signal, 'optionalAccess', _9 => _9.addEventListener, 'call', _10 => _10("abort", onAbort, { once: true })]);
1002
+ this.queue.push(entry);
831
1003
  });
832
1004
  }
833
1005
  release() {
834
1006
  this.activeCount = Math.max(0, this.activeCount - 1);
835
1007
  const next = this.queue.shift();
836
- next?.();
1008
+ _optionalChain([next, 'optionalAccess', _11 => _11.grant, 'call', _12 => _12()]);
1009
+ }
1010
+ }, _class2);
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);
837
1021
  }
838
- };
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
+ }
839
1046
  async function withTimeout(task, timeoutMs, label) {
840
1047
  let timeoutHandle = null;
1048
+ const settledTask = task.then(
1049
+ () => {
1050
+ },
1051
+ () => {
1052
+ }
1053
+ );
841
1054
  try {
842
1055
  return await Promise.race([
843
1056
  task,
844
1057
  new Promise((_, reject) => {
845
1058
  timeoutHandle = setTimeout(() => {
846
- reject(new Error(`${label} timed out after ${timeoutMs}ms`));
1059
+ const err = new Error(
1060
+ `${label} timed out after ${timeoutMs}ms`
1061
+ );
1062
+ err.pendingTask = settledTask;
1063
+ reject(err);
847
1064
  }, timeoutMs);
848
1065
  })
849
1066
  ]);
@@ -853,15 +1070,36 @@ async function withTimeout(task, timeoutMs, label) {
853
1070
  }
854
1071
  }
855
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 (!_optionalChain([signal, 'optionalAccess', _13 => _13.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
+ }
856
1090
  async function processImageBuffer(originalBuffer, options = {}) {
857
- const optTimeout = options.optimizationTimeoutMs ?? 6e4;
858
- const blurTimeout = options.blurTimeoutMs ?? 3e4;
859
- const contentHash = crypto__default.default.createHash("sha256").update(originalBuffer).digest("hex");
1091
+ const optTimeout = _nullishCoalesce(options.optimizationTimeoutMs, () => ( 6e4));
1092
+ const blurTimeout = _nullishCoalesce(options.blurTimeoutMs, () => ( 3e4));
1093
+ throwIfAborted(options.signal);
1094
+ const contentHash = await computeSha256Hex(originalBuffer);
1095
+ throwIfAborted(options.signal);
860
1096
  const optimized = await withTimeout(
861
1097
  optimizeImage(originalBuffer, {
862
1098
  maxDimensions: options.maxDimensions,
863
1099
  outputFormat: options.outputFormat,
864
- quality: options.quality
1100
+ quality: options.quality,
1101
+ limitInputPixels: options.limitInputPixels,
1102
+ effort: options.effort
865
1103
  }),
866
1104
  optTimeout,
867
1105
  "Image optimization"
@@ -874,6 +1112,7 @@ async function processImageBuffer(originalBuffer, options = {}) {
874
1112
  `Image dimensions missing: width=${optimized.width}, height=${optimized.height}`
875
1113
  );
876
1114
  }
1115
+ throwIfAborted(options.signal);
877
1116
  const blurData = await withTimeout(
878
1117
  generateBlurPlaceholder(optimized.buffer, {
879
1118
  includeBlurHash: options.includeBlurHash
@@ -889,22 +1128,12 @@ async function processImageBuffer(originalBuffer, options = {}) {
889
1128
  };
890
1129
  }
891
1130
 
892
- // src/utils/validators.ts
893
- function validateImageFileStrict(file, maxSizeMB, allowedFormats) {
894
- const maxBytes = maxSizeMB * 1024 * 1024;
895
- if (file.size > maxBytes) {
896
- return {
897
- valid: false,
898
- error: `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum of ${maxSizeMB}MB`
899
- };
900
- }
901
- if (allowedFormats.length > 0 && !allowedFormats.includes(file.type)) {
902
- return {
903
- valid: false,
904
- error: `File type ${file.type || "unknown"} is not allowed. Allowed: ${allowedFormats.join(", ")}`
905
- };
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];
906
1135
  }
907
- return { valid: true };
1136
+ return Math.random().toString(16).slice(2, 10).padEnd(8, "0");
908
1137
  }
909
1138
  function formatToExtension(format) {
910
1139
  switch (format) {
@@ -923,16 +1152,20 @@ function formatToExtension(format) {
923
1152
  function sanitizeSegment(value) {
924
1153
  return value.replace(/\\/g, "/").split("/").map((part) => part.trim().replace(/[^a-zA-Z0-9._-]/g, "-")).filter((part) => part && part !== "." && part !== "..").join("/");
925
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
+ }
926
1162
  function createDefaultPathGenerator() {
927
1163
  let imageCounter = 0;
928
1164
  return (ctx) => {
929
1165
  const cleanFolder = ctx.folder ? sanitizeSegment(ctx.folder) : "";
930
- if (ctx.customFilename?.trim()) {
931
- let cleanName = ctx.customFilename.replace(/^.*[\\/]/, "").trim();
932
- if (!cleanName.includes(".")) {
933
- cleanName = `${cleanName}.${ctx.extension}`;
934
- }
935
- cleanName = cleanName.replace(/[^a-zA-Z0-9._-]/g, "-");
1166
+ if (_optionalChain([ctx, 'access', _14 => _14.customFilename, 'optionalAccess', _15 => _15.trim, 'call', _16 => _16()])) {
1167
+ const baseName = stripFilenameExtension(ctx.customFilename);
1168
+ const cleanName = `${baseName}.${ctx.extension}`;
936
1169
  const fullPath2 = cleanFolder ? `${cleanFolder}/${cleanName}` : cleanName;
937
1170
  return {
938
1171
  bucket: ctx.bucket,
@@ -947,7 +1180,7 @@ function createDefaultPathGenerator() {
947
1180
  imageCounter = (imageCounter + 1) % 1e4;
948
1181
  const seq = String(imageCounter).padStart(4, "0");
949
1182
  const timestamp = Date.now();
950
- const randomSuffix = crypto__default.default.randomUUID().split("-")[0];
1183
+ const randomSuffix = randomShortId();
951
1184
  const prefix = sanitizeSegment(
952
1185
  ctx.preset || cleanFolder.split("/")[0] || "img"
953
1186
  );
@@ -960,10 +1193,21 @@ function createDefaultPathGenerator() {
960
1193
  };
961
1194
  };
962
1195
  }
1196
+
1197
+ // src/server/uploader/uploadRetry.ts
1198
+ var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
1199
+ var _path = require('path'); var _path2 = _interopRequireDefault(_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
+ }
963
1206
  function isCollisionError(e) {
964
1207
  if (!e) return false;
965
1208
  const code = typeof e === "object" && e !== null && "code" in e ? String(e.code).toUpperCase() : "";
966
- if (code === "EEXIST") {
1209
+ const statusCode = typeof e === "object" && e !== null && "$metadata" in e ? _optionalChain([e, 'access', _17 => _17.$metadata, 'optionalAccess', _18 => _18.httpStatusCode]) : typeof e === "object" && e !== null && "statusCode" in e ? Number(e.statusCode) : void 0;
1210
+ if (code === "EEXIST" || code === "PRECONDITIONFAILED" || statusCode === 412) {
967
1211
  return true;
968
1212
  }
969
1213
  if (code === "ENOENT") {
@@ -973,7 +1217,7 @@ function isCollisionError(e) {
973
1217
  if (msg.includes("does not exist") || msg.includes("doesn't exist") || msg.includes("not exist") || msg.includes("enoent")) {
974
1218
  return false;
975
1219
  }
976
- return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists");
1220
+ return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists") || msg.includes("precondition failed");
977
1221
  }
978
1222
  async function uploadWithCollisionRetry(params) {
979
1223
  const {
@@ -984,6 +1228,7 @@ async function uploadWithCollisionRetry(params) {
984
1228
  contentType,
985
1229
  maxRetries = 3
986
1230
  } = params;
1231
+ const isServerCustomName = Boolean(pathContext.customFilename) && !pathContext.isClientFilename;
987
1232
  let storageTarget = buildPath(pathContext);
988
1233
  let uploadResult;
989
1234
  for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -993,14 +1238,14 @@ async function uploadWithCollisionRetry(params) {
993
1238
  path: storageTarget.path,
994
1239
  data,
995
1240
  contentType,
996
- upsert: Boolean(pathContext.customFilename),
1241
+ upsert: isServerCustomName,
997
1242
  contentHash: pathContext.contentHash,
998
1243
  metadata: pathContext.metadata
999
1244
  });
1000
1245
  break;
1001
1246
  } catch (e) {
1002
1247
  if (isCollisionError(e)) {
1003
- if (pathContext.customFilename) {
1248
+ if (isServerCustomName) {
1004
1249
  uploadResult = await storage.upload({
1005
1250
  bucket: storageTarget.bucket,
1006
1251
  path: storageTarget.path,
@@ -1012,6 +1257,14 @@ async function uploadWithCollisionRetry(params) {
1012
1257
  });
1013
1258
  break;
1014
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
+ }
1015
1268
  storageTarget = buildPath({
1016
1269
  ...pathContext,
1017
1270
  customFilename: void 0
@@ -1026,23 +1279,233 @@ async function uploadWithCollisionRetry(params) {
1026
1279
  }
1027
1280
  return { uploadResult, storageTarget };
1028
1281
  }
1029
- function mirrorBufferLocally(options) {
1030
- const { buffer, filename, fallbackUrl, mirrorDir, publicUrlBase } = options;
1282
+ async function mirrorBufferLocally(options) {
1283
+ const {
1284
+ buffer,
1285
+ filename,
1286
+ storagePath,
1287
+ fallbackUrl,
1288
+ mirrorDir,
1289
+ publicUrlBase
1290
+ } = options;
1031
1291
  if (!mirrorDir) {
1032
1292
  return fallbackUrl;
1033
1293
  }
1294
+ const relativeTarget = (storagePath || filename).replace(/^\/+/, "");
1034
1295
  try {
1035
- if (!fs__default.default.existsSync(mirrorDir)) {
1036
- fs__default.default.mkdirSync(mirrorDir, { recursive: true });
1296
+ const resolvedBase = _path2.default.resolve(mirrorDir);
1297
+ const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
1298
+ const rel = _path2.default.relative(resolvedBase, destPath);
1299
+ if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) {
1300
+ return fallbackUrl;
1037
1301
  }
1038
- fs__default.default.writeFileSync(path__default.default.join(mirrorDir, filename), buffer);
1302
+ const destDir = _path2.default.dirname(destPath);
1303
+ await _promises2.default.mkdir(destDir, { recursive: true });
1304
+ await _promises2.default.writeFile(destPath, buffer);
1039
1305
  if (publicUrlBase) {
1040
- return `${publicUrlBase.replace(/\/+$/, "")}/${filename}`;
1306
+ return `${publicUrlBase.replace(/\/+$/, "")}/${relativeTarget}`;
1041
1307
  }
1042
- } catch {
1308
+ } catch (e8) {
1043
1309
  }
1044
1310
  return fallbackUrl;
1045
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 = _path2.default.resolve(dir);
1320
+ const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
1321
+ const rel = _path2.default.relative(resolvedBase, destPath);
1322
+ if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) return;
1323
+ await _promises2.default.unlink(destPath);
1324
+ } catch (e9) {
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 !== _chunk34L4TEE3cjs.SVG_MIME_TYPE);
1341
+ if (explicitAllowSvg && !baseFormats.includes(_chunk34L4TEE3cjs.SVG_MIME_TYPE)) {
1342
+ return [...baseFormats, _chunk34L4TEE3cjs.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" || !_optionalChain([variantWidths, 'optionalAccess', _19 => _19.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, _nullishCoalesce(quality, () => ( 85))));
1380
+ const avifEffort = _nullishCoalesce(effort, () => ( PRIMARY_AVIF_EFFORT));
1381
+ const basePipeline = sharp(sourceBuffer, {
1382
+ failOn: "error",
1383
+ limitInputPixels: _nullishCoalesce(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 (_optionalChain([preset, 'optionalAccess', _20 => _20.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
+ _optionalChain([onError, 'optionalCall', _21 => _21(err, { action: "recordHook" })]);
1506
+ });
1507
+ }
1508
+ }
1046
1509
 
1047
1510
  // src/server/uploader/createImageUploader.ts
1048
1511
  function createImageUploader(options) {
@@ -1051,27 +1514,68 @@ function createImageUploader(options) {
1051
1514
  defaultBucket = "images",
1052
1515
  defaultFolder = "",
1053
1516
  defaultMaxSizeMB = 10,
1054
- defaultAllowedFormats = DEFAULT_ALLOWED_FORMATS,
1055
- defaultMaxDimensions = DEFAULT_IMAGE_DIMENSIONS,
1517
+ defaultAllowedFormats = _chunk34L4TEE3cjs.DEFAULT_ALLOWED_FORMATS,
1518
+ defaultMaxDimensions = _chunk34L4TEE3cjs.DEFAULT_IMAGE_DIMENSIONS,
1519
+ allowSvg: defaultAllowSvg = false,
1520
+ limitInputPixels: defaultLimitInputPixels,
1521
+ effort: defaultEffort,
1056
1522
  presets: customPresets,
1057
1523
  includeStandardPresets = true,
1058
1524
  pathGenerator,
1059
1525
  onUploadRecord,
1060
1526
  awaitRecordHook = false,
1527
+ onError,
1061
1528
  concurrency = 2,
1529
+ maxQueue,
1062
1530
  optimizationTimeoutMs = 6e4,
1063
1531
  blurTimeoutMs = 3e4,
1064
1532
  includeBlurHash: defaultIncludeBlurHash = false
1065
1533
  } = options;
1066
1534
  const mergedPresets = {
1067
- ...includeStandardPresets ? STANDARD_PRESETS : {},
1535
+ ...includeStandardPresets ? _chunk34L4TEE3cjs.STANDARD_PRESETS : {},
1068
1536
  ...customPresets || {}
1069
1537
  };
1070
- const semaphore = new AsyncSemaphore(concurrency);
1538
+ const semaphore = new AsyncSemaphore(concurrency, { maxQueue });
1071
1539
  const buildPath = pathGenerator || createDefaultPathGenerator();
1072
- const resolvePreset = (presetName) => presetName ? mergedPresets[presetName] : void 0;
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 (_optionalChain([preset, 'optionalAccess', _22 => _22.bucket])) buckets.add(preset.bucket);
1547
+ }
1548
+ return [...buckets];
1549
+ };
1550
+ const getMirrorDirs = () => [...new Set(Object.values(mergedPresets).map((p) => _optionalChain([p, 'optionalAccess', _23 => _23.mirrorDir])).filter(Boolean))];
1551
+ const getMaxSizeMB = (allowedPresets) => {
1552
+ let maxMB = defaultMaxSizeMB;
1553
+ const keys = _optionalChain([allowedPresets, 'optionalAccess', _24 => _24.length]) ? allowedPresets : Object.keys(mergedPresets);
1554
+ for (const key of keys) {
1555
+ const p = mergedPresets[key];
1556
+ if (_optionalChain([p, 'optionalAccess', _25 => _25.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
+ };
1073
1573
  return {
1574
+ defaultBucket,
1074
1575
  resolvePreset,
1576
+ getAllowedBuckets,
1577
+ getMaxSizeMB,
1578
+ parseUrl: (url) => storage.parseUrl ? storage.parseUrl(url) : null,
1075
1579
  async processBuffer(buffer, procOptions = {}) {
1076
1580
  return semaphore.run(
1077
1581
  () => processImageBuffer(buffer, {
@@ -1079,34 +1583,48 @@ function createImageUploader(options) {
1079
1583
  optimizationTimeoutMs,
1080
1584
  blurTimeoutMs,
1081
1585
  includeBlurHash: defaultIncludeBlurHash,
1586
+ limitInputPixels: defaultLimitInputPixels,
1587
+ effort: defaultEffort,
1082
1588
  ...procOptions
1083
- })
1589
+ }),
1590
+ { signal: procOptions.signal }
1084
1591
  );
1085
1592
  },
1086
1593
  async uploadImage(input, uploadOptions = {}) {
1087
1594
  const presetConfig = resolvePreset(uploadOptions.preset);
1088
- const bucket = uploadOptions.bucket || presetConfig?.bucket || defaultBucket;
1089
- const folder = uploadOptions.folder ?? presetConfig?.folder ?? defaultFolder;
1090
- const maxSizeMB = uploadOptions.maxSizeMB ?? presetConfig?.maxSizeMB ?? defaultMaxSizeMB;
1091
- const allowedFormats = uploadOptions.allowedFormats ?? presetConfig?.allowedFormats ?? defaultAllowedFormats;
1092
- const maxDimensions = uploadOptions.maxDimensions ?? presetConfig?.maxDimensions ?? defaultMaxDimensions;
1093
- const outputFormat = uploadOptions.outputFormat ?? presetConfig?.outputFormat;
1094
- const quality = uploadOptions.quality ?? presetConfig?.quality;
1095
- const includeBlurHash = uploadOptions.includeBlurHash ?? presetConfig?.includeBlurHash ?? defaultIncludeBlurHash;
1096
- const isWebFile = typeof input.arrayBuffer === "function";
1097
- const fileType = input.type;
1098
- const originalName = input.name;
1099
- const fileSize = isWebFile ? input.size : input.size ?? input.buffer.length;
1100
- const validation = validateImageFileStrict(
1101
- { size: fileSize, type: fileType },
1102
- maxSizeMB,
1103
- allowedFormats
1595
+ if (uploadOptions.preset && !presetConfig) {
1596
+ throw new (0, _chunkXKQMCY5Zcjs.UploadValidationError)(`Invalid preset "${uploadOptions.preset}".`);
1597
+ }
1598
+ const bucket = uploadOptions.bucket || _optionalChain([presetConfig, 'optionalAccess', _26 => _26.bucket]) || defaultBucket;
1599
+ const folder = _nullishCoalesce(_nullishCoalesce(uploadOptions.folder, () => ( _optionalChain([presetConfig, 'optionalAccess', _27 => _27.folder]))), () => ( defaultFolder));
1600
+ const maxSizeMB = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxSizeMB, () => ( _optionalChain([presetConfig, 'optionalAccess', _28 => _28.maxSizeMB]))), () => ( defaultMaxSizeMB));
1601
+ const allowSvg = _nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _29 => _29.allowSvg]))), () => ( defaultAllowSvg));
1602
+ const explicitAllowSvg = Boolean(
1603
+ _nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _30 => _30.allowSvg]))), () => ( (defaultAllowSvg && !uploadOptions.allowedFormats && !_optionalChain([presetConfig, 'optionalAccess', _31 => _31.allowedFormats]))))
1604
+ );
1605
+ const allowedFormats = resolveEffectiveAllowedFormats(
1606
+ _nullishCoalesce(_nullishCoalesce(uploadOptions.allowedFormats, () => ( _optionalChain([presetConfig, 'optionalAccess', _32 => _32.allowedFormats]))), () => ( defaultAllowedFormats)),
1607
+ allowSvg,
1608
+ explicitAllowSvg
1104
1609
  );
1105
- if (!validation.valid) {
1106
- throw new UploadValidationError(validation.error);
1610
+ const maxDimensions = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxDimensions, () => ( _optionalChain([presetConfig, 'optionalAccess', _33 => _33.maxDimensions]))), () => ( defaultMaxDimensions));
1611
+ const outputFormat = _nullishCoalesce(uploadOptions.outputFormat, () => ( _optionalChain([presetConfig, 'optionalAccess', _34 => _34.outputFormat])));
1612
+ const quality = _nullishCoalesce(uploadOptions.quality, () => ( _optionalChain([presetConfig, 'optionalAccess', _35 => _35.quality])));
1613
+ const includeBlurHash = _nullishCoalesce(_nullishCoalesce(uploadOptions.includeBlurHash, () => ( _optionalChain([presetConfig, 'optionalAccess', _36 => _36.includeBlurHash]))), () => ( defaultIncludeBlurHash));
1614
+ const limitInputPixels = _nullishCoalesce(_nullishCoalesce(uploadOptions.limitInputPixels, () => ( _optionalChain([presetConfig, 'optionalAccess', _37 => _37.limitInputPixels]))), () => ( defaultLimitInputPixels));
1615
+ const variantWidths = _nullishCoalesce(uploadOptions.variantWidths, () => ( _optionalChain([presetConfig, 'optionalAccess', _38 => _38.variantWidths])));
1616
+ const isWebFile = typeof input.arrayBuffer === "function";
1617
+ if (isWebFile) {
1618
+ const pre = _chunk34L4TEE3cjs.validateImageFileStrict.call(void 0, { size: input.size, type: input.type }, maxSizeMB, allowedFormats);
1619
+ if (!pre.valid) throw new (0, _chunkXKQMCY5Zcjs.UploadValidationError)(pre.error);
1107
1620
  }
1108
1621
  try {
1109
- const originalBuffer = isWebFile ? Buffer.from(await input.arrayBuffer()) : input.buffer;
1622
+ let originalBuffer = isWebFile ? Buffer.from(await input.arrayBuffer()) : input.buffer;
1623
+ if (!originalBuffer || originalBuffer.length === 0) throw new (0, _chunkXKQMCY5Zcjs.UploadValidationError)("File is empty");
1624
+ const validation = _chunk34L4TEE3cjs.validateImageFileStrict.call(void 0, { size: originalBuffer.length, type: input.type }, maxSizeMB, allowedFormats);
1625
+ if (!validation.valid) throw new (0, _chunkXKQMCY5Zcjs.UploadValidationError)(validation.error);
1626
+ const sniffed = assertMagicBytesAllowed(originalBuffer, allowedFormats, allowSvg);
1627
+ if (sniffed.format === "svg") originalBuffer = sanitizeSvg(originalBuffer);
1110
1628
  const { optimized, blurData, contentHash, originalSize } = await semaphore.run(
1111
1629
  () => processImageBuffer(originalBuffer, {
1112
1630
  maxDimensions,
@@ -1114,12 +1632,15 @@ function createImageUploader(options) {
1114
1632
  quality,
1115
1633
  includeBlurHash,
1116
1634
  optimizationTimeoutMs,
1117
- blurTimeoutMs
1118
- })
1635
+ blurTimeoutMs,
1636
+ limitInputPixels,
1637
+ effort: defaultEffort,
1638
+ allowSvg,
1639
+ signal: uploadOptions.signal
1640
+ }),
1641
+ { signal: uploadOptions.signal }
1119
1642
  );
1120
1643
  const mainFormat = optimized.format;
1121
- const extension = formatToExtension(mainFormat);
1122
- const mainContentType = mainFormat === "svg" ? "image/svg+xml" : `image/${mainFormat}`;
1123
1644
  const { uploadResult, storageTarget } = await uploadWithCollisionRetry({
1124
1645
  storage,
1125
1646
  buildPath,
@@ -1128,23 +1649,52 @@ function createImageUploader(options) {
1128
1649
  folder,
1129
1650
  preset: uploadOptions.preset,
1130
1651
  format: mainFormat,
1131
- extension,
1652
+ extension: formatToExtension(mainFormat),
1132
1653
  customFilename: uploadOptions.customFilename,
1654
+ isClientFilename: uploadOptions.isClientFilename,
1133
1655
  contentHash,
1134
- originalName,
1656
+ originalName: input.name,
1135
1657
  metadata: uploadOptions.metadata
1136
1658
  },
1137
1659
  data: optimized.buffer,
1138
- contentType: mainContentType
1660
+ contentType: mainFormat === "svg" ? _chunk34L4TEE3cjs.SVG_MIME_TYPE : `image/${mainFormat}`
1139
1661
  });
1140
- const resolvedUrl = mirrorBufferLocally({
1662
+ const resolvedUrl = await mirrorBufferLocally({
1141
1663
  buffer: optimized.buffer,
1142
1664
  filename: storageTarget.filename,
1665
+ storagePath: uploadResult.path,
1143
1666
  fallbackUrl: uploadResult.url,
1144
- mirrorDir: presetConfig?.mirrorDir,
1145
- publicUrlBase: presetConfig?.publicUrlBase
1667
+ mirrorDir: _optionalChain([presetConfig, 'optionalAccess', _39 => _39.mirrorDir]),
1668
+ publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _40 => _40.publicUrlBase])
1146
1669
  });
1147
- const resolvedQuality = quality ?? 85;
1670
+ const { variants, srcSet } = _optionalChain([variantWidths, 'optionalAccess', _41 => _41.length]) && mainFormat !== "svg" ? await semaphore.run(
1671
+ () => generateImageVariants({
1672
+ sourceBuffer: originalBuffer,
1673
+ format: mainFormat,
1674
+ quality: _nullishCoalesce(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: _optionalChain([presetConfig, 'optionalAccess', _42 => _42.mirrorDir]),
1689
+ publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _43 => _43.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
+ }
1148
1698
  const result = {
1149
1699
  url: resolvedUrl,
1150
1700
  blurDataUrl: blurData.dataURL,
@@ -1162,76 +1712,90 @@ function createImageUploader(options) {
1162
1712
  ...uploadOptions.preset && { preset: uploadOptions.preset },
1163
1713
  ...folder && { folder },
1164
1714
  ...uploadOptions.metadata && { metadata: uploadOptions.metadata },
1715
+ ...variants.length > 0 && { variants },
1716
+ ...srcSet && { srcSet },
1165
1717
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
1166
1718
  };
1167
- if (onUploadRecord) {
1168
- const record = {
1169
- ...result,
1170
- quality: resolvedQuality
1171
- };
1172
- if (awaitRecordHook) {
1173
- await onUploadRecord(record);
1174
- } else {
1175
- Promise.resolve(onUploadRecord(record)).catch(() => {
1176
- });
1177
- }
1178
- }
1719
+ const record = { ...result, quality: _nullishCoalesce(_nullishCoalesce(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
+ });
1179
1731
  return result;
1180
1732
  } catch (error) {
1181
- if (error instanceof UploadValidationError) {
1733
+ if (error instanceof _chunkXKQMCY5Zcjs.UploadValidationError || error instanceof SemaphoreQueueFullError) {
1182
1734
  throw error;
1183
1735
  }
1184
- throw new Error(
1185
- `Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`
1186
- );
1736
+ throw new Error(`Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1187
1737
  }
1188
1738
  },
1189
1739
  async deleteImage(targetPath, bucket) {
1740
+ const targetBucket = bucket || defaultBucket;
1190
1741
  try {
1191
- await storage.delete(bucket || defaultBucket, targetPath);
1742
+ await storage.delete(targetBucket, targetPath);
1743
+ await deleteMirroredFiles(getMirrorDirs(), targetPath);
1744
+ await cleanupVariantsFor(targetBucket, targetPath);
1192
1745
  } catch (error) {
1193
- throw new Error(
1194
- `Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`
1195
- );
1746
+ throw new Error(`Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1196
1747
  }
1197
1748
  },
1198
1749
  async deleteByUrl(url) {
1199
- if (storage.deleteByUrl) {
1200
- return storage.deleteByUrl(url);
1750
+ const parsed = _optionalChain([storage, 'access', _44 => _44.parseUrl, 'optionalCall', _45 => _45(url)]);
1751
+ const deleted = storage.deleteByUrl ? await storage.deleteByUrl(url) : false;
1752
+ if (deleted && _optionalChain([parsed, 'optionalAccess', _46 => _46.path])) {
1753
+ await deleteMirroredFiles(getMirrorDirs(), parsed.path);
1754
+ await cleanupVariantsFor(parsed.bucket || defaultBucket, parsed.path);
1201
1755
  }
1202
- return false;
1756
+ return deleted;
1203
1757
  }
1204
1758
  };
1205
1759
  }
1206
1760
 
1207
- exports.AsyncSemaphore = AsyncSemaphore;
1208
- exports.BLUR_PLACEHOLDER_FALLBACK_WIDTH = BLUR_PLACEHOLDER_FALLBACK_WIDTH;
1209
- exports.BLUR_PLACEHOLDER_MAX_CHARS = BLUR_PLACEHOLDER_MAX_CHARS;
1210
- exports.BLUR_PLACEHOLDER_QUALITY = BLUR_PLACEHOLDER_QUALITY;
1211
- exports.BLUR_PLACEHOLDER_WIDTH = BLUR_PLACEHOLDER_WIDTH;
1212
- exports.EDGE_COLOR_FALLBACK = EDGE_COLOR_FALLBACK;
1213
- exports.EDGE_WHITE_FALLBACK = EDGE_WHITE_FALLBACK;
1214
- exports.PRIMARY_AVIF_EFFORT = PRIMARY_AVIF_EFFORT;
1215
- exports.RETRY_AVIF_EFFORT = RETRY_AVIF_EFFORT;
1216
- exports.UploadValidationError = UploadValidationError;
1217
- exports.WHITE_DOMINANCE_RATIO = WHITE_DOMINANCE_RATIO;
1218
- exports.blendEdgeColors = blendEdgeColors;
1219
- exports.createDefaultPathGenerator = createDefaultPathGenerator;
1220
- exports.createImageUploader = createImageUploader;
1221
- exports.createUploadHttpHandler = createUploadHttpHandler;
1222
- exports.encodeBlurHashString = encodeBlurHashString;
1223
- exports.extractEdgeColorsFromBuffer = extractEdgeColorsFromBuffer;
1224
- exports.extractEnhancedColorFromBuffer = extractEnhancedColorFromBuffer;
1225
- exports.formatToExtension = formatToExtension;
1226
- exports.generateBlurHash = generateBlurHash;
1227
- exports.generateBlurPlaceholder = generateBlurPlaceholder;
1228
- exports.isClientValidationError = isClientValidationError;
1229
- exports.isCollisionError = isCollisionError;
1230
- exports.mirrorBufferLocally = mirrorBufferLocally;
1231
- exports.optimizeImage = optimizeImage;
1232
- exports.processImageBuffer = processImageBuffer;
1233
- exports.sanitizeSegment = sanitizeSegment;
1234
- exports.uploadWithCollisionRetry = uploadWithCollisionRetry;
1235
- exports.withTimeout = withTimeout;
1236
- //# sourceMappingURL=server.cjs.map
1761
+
1762
+
1763
+
1764
+
1765
+
1766
+
1767
+
1768
+
1769
+
1770
+
1771
+
1772
+
1773
+
1774
+
1775
+
1776
+
1777
+
1778
+
1779
+
1780
+
1781
+
1782
+
1783
+
1784
+
1785
+
1786
+
1787
+
1788
+
1789
+
1790
+
1791
+
1792
+
1793
+
1794
+
1795
+
1796
+
1797
+
1798
+
1799
+
1800
+ exports.AsyncSemaphore = AsyncSemaphore; exports.BLUR_PLACEHOLDER_FALLBACK_WIDTH = BLUR_PLACEHOLDER_FALLBACK_WIDTH; exports.BLUR_PLACEHOLDER_MAX_CHARS = BLUR_PLACEHOLDER_MAX_CHARS; exports.BLUR_PLACEHOLDER_QUALITY = BLUR_PLACEHOLDER_QUALITY; exports.BLUR_PLACEHOLDER_WIDTH = BLUR_PLACEHOLDER_WIDTH; exports.DEFAULT_LIMIT_INPUT_PIXELS = DEFAULT_LIMIT_INPUT_PIXELS; exports.DEFAULT_MAX_SVG_BYTES = DEFAULT_MAX_SVG_BYTES; exports.EDGE_COLOR_FALLBACK = EDGE_COLOR_FALLBACK; exports.EDGE_WHITE_FALLBACK = EDGE_WHITE_FALLBACK; exports.PRIMARY_AVIF_EFFORT = PRIMARY_AVIF_EFFORT; exports.RETRY_AVIF_EFFORT = RETRY_AVIF_EFFORT; exports.SemaphoreQueueFullError = SemaphoreQueueFullError; exports.UploadValidationError = _chunkXKQMCY5Zcjs.UploadValidationError; exports.WHITE_DOMINANCE_RATIO = WHITE_DOMINANCE_RATIO; exports.assertMagicBytesAllowed = assertMagicBytesAllowed; exports.blendEdgeColors = blendEdgeColors; exports.buildVariantStoragePath = buildVariantStoragePath; exports.computeEdgeColorSet = computeEdgeColorSet; exports.createDefaultPathGenerator = createDefaultPathGenerator; exports.createImageUploader = createImageUploader; exports.createUploadHandler = _chunkXKQMCY5Zcjs.createUploadHandler; exports.createUploadHttpHandler = _chunkXKQMCY5Zcjs.createUploadHttpHandler; exports.encodeBlurHashString = encodeBlurHashString; exports.extractEdgeColorsFromBuffer = extractEdgeColorsFromBuffer; exports.extractEnhancedColorFromBuffer = extractEnhancedColorFromBuffer; exports.formatToExtension = formatToExtension; exports.generateBlurHash = generateBlurHash; exports.generateBlurPlaceholder = generateBlurPlaceholder; exports.generateImageVariants = generateImageVariants; exports.isClientValidationError = _chunkXKQMCY5Zcjs.isClientValidationError; exports.isCollisionError = isCollisionError; exports.mirrorBufferLocally = mirrorBufferLocally; exports.optimizeImage = optimizeImage; exports.processImageBuffer = processImageBuffer; exports.sanitizeSegment = sanitizeSegment; exports.sanitizeSvg = sanitizeSvg; exports.sniffImageFormat = sniffImageFormat; exports.uploadWithCollisionRetry = uploadWithCollisionRetry; exports.withTimeout = withTimeout;
1237
1801
  //# sourceMappingURL=server.cjs.map