@od-oneapp/storage 2026.1.1301

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 (69) hide show
  1. package/README.md +854 -0
  2. package/dist/client-next.d.mts +61 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +111 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client-utils-Dx6W25iz.d.mts +43 -0
  7. package/dist/client-utils-Dx6W25iz.d.mts.map +1 -0
  8. package/dist/client.d.mts +28 -0
  9. package/dist/client.d.mts.map +1 -0
  10. package/dist/client.mjs +183 -0
  11. package/dist/client.mjs.map +1 -0
  12. package/dist/env-BVHLmQdh.mjs +128 -0
  13. package/dist/env-BVHLmQdh.mjs.map +1 -0
  14. package/dist/env.mjs +3 -0
  15. package/dist/health-check-D7LnnDec.mjs +746 -0
  16. package/dist/health-check-D7LnnDec.mjs.map +1 -0
  17. package/dist/health-check-im_huJ59.d.mts +116 -0
  18. package/dist/health-check-im_huJ59.d.mts.map +1 -0
  19. package/dist/index.d.mts +60 -0
  20. package/dist/index.d.mts.map +1 -0
  21. package/dist/index.mjs +3 -0
  22. package/dist/keys.d.mts +37 -0
  23. package/dist/keys.d.mts.map +1 -0
  24. package/dist/keys.mjs +253 -0
  25. package/dist/keys.mjs.map +1 -0
  26. package/dist/server-edge.d.mts +28 -0
  27. package/dist/server-edge.d.mts.map +1 -0
  28. package/dist/server-edge.mjs +88 -0
  29. package/dist/server-edge.mjs.map +1 -0
  30. package/dist/server-next.d.mts +183 -0
  31. package/dist/server-next.d.mts.map +1 -0
  32. package/dist/server-next.mjs +1353 -0
  33. package/dist/server-next.mjs.map +1 -0
  34. package/dist/server.d.mts +70 -0
  35. package/dist/server.d.mts.map +1 -0
  36. package/dist/server.mjs +384 -0
  37. package/dist/server.mjs.map +1 -0
  38. package/dist/types.d.mts +321 -0
  39. package/dist/types.d.mts.map +1 -0
  40. package/dist/types.mjs +3 -0
  41. package/dist/validation.d.mts +101 -0
  42. package/dist/validation.d.mts.map +1 -0
  43. package/dist/validation.mjs +590 -0
  44. package/dist/validation.mjs.map +1 -0
  45. package/dist/vercel-blob-07Sx0Akn.d.mts +31 -0
  46. package/dist/vercel-blob-07Sx0Akn.d.mts.map +1 -0
  47. package/dist/vercel-blob-DA8HaYuw.mjs +158 -0
  48. package/dist/vercel-blob-DA8HaYuw.mjs.map +1 -0
  49. package/package.json +111 -0
  50. package/src/actions/blob-upload.ts +171 -0
  51. package/src/actions/index.ts +23 -0
  52. package/src/actions/mediaActions.ts +1071 -0
  53. package/src/actions/productMediaActions.ts +538 -0
  54. package/src/auth-helpers.ts +386 -0
  55. package/src/capabilities.ts +225 -0
  56. package/src/client-next.ts +184 -0
  57. package/src/client-utils.ts +292 -0
  58. package/src/client.ts +102 -0
  59. package/src/constants.ts +88 -0
  60. package/src/health-check.ts +81 -0
  61. package/src/multi-storage.ts +230 -0
  62. package/src/multipart.ts +497 -0
  63. package/src/retry-utils.test.ts +118 -0
  64. package/src/retry-utils.ts +59 -0
  65. package/src/server-edge.ts +129 -0
  66. package/src/server-next.ts +14 -0
  67. package/src/server.ts +666 -0
  68. package/src/validation.test.ts +312 -0
  69. package/src/validation.ts +827 -0
@@ -0,0 +1,590 @@
1
+ import { validateStorageKey } from "./keys.mjs";
2
+
3
+ //#region src/validation.ts
4
+ /**
5
+ * @fileoverview Storage Validation and Error Handling
6
+ *
7
+ * Provides comprehensive validation utilities and error types for storage operations.
8
+ *
9
+ * Features:
10
+ * - File size validation
11
+ * - MIME type validation
12
+ * - Storage key validation
13
+ * - Error types and handling
14
+ * - SSRF protection
15
+ *
16
+ * @module @od-oneapp/storage/validation
17
+ */
18
+ /**
19
+ * Storage error codes
20
+ */
21
+ let StorageErrorCode = /* @__PURE__ */ function(StorageErrorCode) {
22
+ StorageErrorCode["INVALID_KEY"] = "INVALID_KEY";
23
+ StorageErrorCode["INVALID_FILE_SIZE"] = "INVALID_FILE_SIZE";
24
+ StorageErrorCode["INVALID_MIME_TYPE"] = "INVALID_MIME_TYPE";
25
+ StorageErrorCode["INVALID_OPTIONS"] = "INVALID_OPTIONS";
26
+ StorageErrorCode["PROVIDER_ERROR"] = "PROVIDER_ERROR";
27
+ StorageErrorCode["PROVIDER_UNAVAILABLE"] = "PROVIDER_UNAVAILABLE";
28
+ StorageErrorCode["PROVIDER_QUOTA_EXCEEDED"] = "PROVIDER_QUOTA_EXCEEDED";
29
+ StorageErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
30
+ StorageErrorCode["TIMEOUT"] = "TIMEOUT";
31
+ StorageErrorCode["CONNECTION_FAILED"] = "CONNECTION_FAILED";
32
+ StorageErrorCode["UPLOAD_FAILED"] = "UPLOAD_FAILED";
33
+ StorageErrorCode["UPLOAD_CANCELLED"] = "UPLOAD_CANCELLED";
34
+ StorageErrorCode["UPLOAD_PARTIAL"] = "UPLOAD_PARTIAL";
35
+ StorageErrorCode["DOWNLOAD_FAILED"] = "DOWNLOAD_FAILED";
36
+ StorageErrorCode["FILE_NOT_FOUND"] = "FILE_NOT_FOUND";
37
+ StorageErrorCode["ACCESS_DENIED"] = "ACCESS_DENIED";
38
+ StorageErrorCode["CONFIG_ERROR"] = "CONFIG_ERROR";
39
+ StorageErrorCode["MISSING_CREDENTIALS"] = "MISSING_CREDENTIALS";
40
+ StorageErrorCode["INVALID_CONFIG"] = "INVALID_CONFIG";
41
+ return StorageErrorCode;
42
+ }({});
43
+ /**
44
+ * Base storage error class
45
+ */
46
+ var StorageError = class extends Error {
47
+ code;
48
+ details;
49
+ retryable;
50
+ constructor(message, code, details, retryable = false) {
51
+ super(message);
52
+ this.name = "StorageError";
53
+ this.code = code;
54
+ this.details = details;
55
+ this.retryable = retryable;
56
+ }
57
+ };
58
+ /**
59
+ * Validation error for invalid input
60
+ */
61
+ var ValidationError = class extends StorageError {
62
+ constructor(message, details) {
63
+ super(message, StorageErrorCode.INVALID_OPTIONS, details, false);
64
+ this.name = "ValidationError";
65
+ }
66
+ };
67
+ /**
68
+ * Provider-specific error
69
+ */
70
+ var ProviderError = class extends StorageError {
71
+ constructor(message, provider, details, retryable = false) {
72
+ super(message, StorageErrorCode.PROVIDER_ERROR, {
73
+ ...details,
74
+ provider
75
+ }, retryable);
76
+ this.name = "ProviderError";
77
+ }
78
+ };
79
+ /**
80
+ * Network-related error
81
+ */
82
+ var NetworkError = class extends StorageError {
83
+ constructor(message, details, retryable = true) {
84
+ super(message, StorageErrorCode.NETWORK_ERROR, details, retryable);
85
+ this.name = "NetworkError";
86
+ }
87
+ };
88
+ /**
89
+ * Upload-specific error
90
+ */
91
+ var UploadError = class extends StorageError {
92
+ constructor(message, details, retryable = true) {
93
+ super(message, StorageErrorCode.UPLOAD_FAILED, details, retryable);
94
+ this.name = "UploadError";
95
+ }
96
+ };
97
+ /**
98
+ * Download-specific error
99
+ */
100
+ var DownloadError = class extends StorageError {
101
+ constructor(message, details, retryable = true) {
102
+ super(message, StorageErrorCode.DOWNLOAD_FAILED, details, retryable);
103
+ this.name = "DownloadError";
104
+ }
105
+ };
106
+ /**
107
+ * Configuration error
108
+ */
109
+ var ConfigError = class extends StorageError {
110
+ constructor(message, details) {
111
+ super(message, StorageErrorCode.CONFIG_ERROR, details, false);
112
+ this.name = "ConfigError";
113
+ }
114
+ };
115
+ /**
116
+ * Validate file size
117
+ *
118
+ * @param size - File size in bytes
119
+ * @param maxSize - Maximum allowed size in bytes
120
+ * @returns Validation result
121
+ */
122
+ function validateFileSize(size, maxSize) {
123
+ if (size < 0) return {
124
+ valid: false,
125
+ error: "File size cannot be negative"
126
+ };
127
+ if (size > maxSize) return {
128
+ valid: false,
129
+ error: `File size ${size} bytes exceeds maximum ${maxSize} bytes`
130
+ };
131
+ return { valid: true };
132
+ }
133
+ /**
134
+ * Dangerous MIME types that should be blocked for security reasons
135
+ *
136
+ * These types can execute code or present security risks:
137
+ * - Executables (.exe, .dll, .app)
138
+ * - Scripts (.sh, .bat, .cmd, .js, .php, .py, .pl)
139
+ * - Archives with executables (.jar)
140
+ * - Windows system files (.com, .pif, .scr)
141
+ */
142
+ const DANGEROUS_MIME_TYPES = [
143
+ "application/x-msdownload",
144
+ "application/x-executable",
145
+ "application/x-dosexec",
146
+ "application/x-msdos-program",
147
+ "application/x-dll",
148
+ "application/x-app",
149
+ "application/x-sh",
150
+ "text/x-sh",
151
+ "application/x-bat",
152
+ "application/x-cmd",
153
+ "text/x-python",
154
+ "application/x-python-code",
155
+ "text/x-perl",
156
+ "application/x-perl",
157
+ "application/javascript",
158
+ "text/javascript",
159
+ "application/x-javascript",
160
+ "application/x-php",
161
+ "application/x-httpd-php",
162
+ "text/x-php",
163
+ "application/x-com",
164
+ "application/x-pif",
165
+ "application/x-scr",
166
+ "application/x-vbs",
167
+ "text/vbscript",
168
+ "application/java-archive",
169
+ "application/x-java-applet"
170
+ ];
171
+ /**
172
+ * MIME type to file extension mappings for validation
173
+ *
174
+ * Maps common MIME types to their expected file extensions to prevent
175
+ * extension spoofing attacks (e.g., executable disguised as image)
176
+ */
177
+ const MIME_TO_EXTENSIONS = {
178
+ "image/jpeg": [".jpg", ".jpeg"],
179
+ "image/png": [".png"],
180
+ "image/gif": [".gif"],
181
+ "image/webp": [".webp"],
182
+ "image/svg+xml": [".svg"],
183
+ "image/bmp": [".bmp"],
184
+ "image/tiff": [".tif", ".tiff"],
185
+ "image/avif": [".avif"],
186
+ "application/pdf": [".pdf"],
187
+ "text/plain": [".txt"],
188
+ "text/csv": [".csv"],
189
+ "text/html": [".html", ".htm"],
190
+ "text/xml": [".xml"],
191
+ "application/json": [".json"],
192
+ "application/msword": [".doc"],
193
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
194
+ "application/vnd.ms-excel": [".xls"],
195
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
196
+ "application/vnd.ms-powerpoint": [".ppt"],
197
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
198
+ "application/zip": [".zip"],
199
+ "application/x-gzip": [".gz"],
200
+ "application/x-tar": [".tar"],
201
+ "application/x-7z-compressed": [".7z"],
202
+ "audio/mpeg": [".mp3"],
203
+ "audio/wav": [".wav"],
204
+ "audio/ogg": [".ogg"],
205
+ "audio/mp4": [".m4a"],
206
+ "video/mp4": [".mp4"],
207
+ "video/mpeg": [".mpeg", ".mpg"],
208
+ "video/quicktime": [".mov"],
209
+ "video/webm": [".webm"],
210
+ "video/x-msvideo": [".avi"]
211
+ };
212
+ /**
213
+ * Validate MIME type against allowed types and dangerous content
214
+ *
215
+ * This function:
216
+ * 1. Blocks dangerous MIME types (executables, scripts) for security
217
+ * 2. Validates against allowed types list if provided
218
+ * 3. Supports wildcard patterns (e.g., image/*)
219
+ *
220
+ * @param mimeType - MIME type to validate
221
+ * @param allowedTypes - Array of allowed MIME types (optional)
222
+ * @returns Validation result with error message if invalid
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * // Block dangerous types
227
+ * validateMimeType('application/x-msdownload', ['image/*'])
228
+ * // => { valid: false, error: '...' }
229
+ *
230
+ * // Allow specific safe types
231
+ * validateMimeType('image/jpeg', ['image/*'])
232
+ * // => { valid: true }
233
+ * ```
234
+ */
235
+ function validateMimeType(mimeType, allowedTypes = []) {
236
+ if (!mimeType) return {
237
+ valid: false,
238
+ error: "MIME type is required"
239
+ };
240
+ const normalizedMimeType = mimeType.toLowerCase().trim();
241
+ if (DANGEROUS_MIME_TYPES.includes(normalizedMimeType)) return {
242
+ valid: false,
243
+ error: `Content type '${mimeType}' is not allowed for security reasons (executable or script content)`
244
+ };
245
+ if (allowedTypes.length === 0) return { valid: true };
246
+ const normalizedAllowed = allowedTypes.map((t) => t.toLowerCase().trim());
247
+ if (normalizedAllowed.includes(normalizedMimeType)) return { valid: true };
248
+ if (normalizedAllowed.some((allowed) => {
249
+ if (allowed.endsWith("/*")) {
250
+ const prefix = allowed.slice(0, -2);
251
+ return normalizedMimeType.startsWith(prefix);
252
+ }
253
+ return false;
254
+ })) return { valid: true };
255
+ return {
256
+ valid: false,
257
+ error: `MIME type '${mimeType}' is not allowed. Allowed types: ${allowedTypes.join(", ")}`
258
+ };
259
+ }
260
+ /**
261
+ * Validate file extension matches expected extension for MIME type
262
+ *
263
+ * Prevents extension spoofing attacks where malicious files are disguised
264
+ * with incorrect extensions (e.g., executable.exe renamed to image.jpg)
265
+ *
266
+ * @param filename - File name with extension
267
+ * @param contentType - MIME type of the file
268
+ * @returns Validation result with error message if mismatch detected
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * // Valid: JPEG file with .jpg extension
273
+ * validateFileExtension('photo.jpg', 'image/jpeg')
274
+ * // => { valid: true }
275
+ *
276
+ * // Invalid: JPEG MIME type with .exe extension (spoofing)
277
+ * validateFileExtension('photo.exe', 'image/jpeg')
278
+ * // => { valid: false, error: '...' }
279
+ *
280
+ * // Unknown MIME type: allowed with warning
281
+ * validateFileExtension('data.custom', 'application/x-custom')
282
+ * // => { valid: true } (with warning logged)
283
+ * ```
284
+ */
285
+ function validateFileExtension(filename, contentType) {
286
+ if (!filename || !contentType) return {
287
+ valid: false,
288
+ error: "Filename and content type are required"
289
+ };
290
+ const lastDotIndex = filename.lastIndexOf(".");
291
+ if (lastDotIndex === -1) return {
292
+ valid: false,
293
+ error: "File must have an extension"
294
+ };
295
+ const extension = filename.substring(lastDotIndex).toLowerCase();
296
+ const allowedExtensions = MIME_TO_EXTENSIONS[contentType.toLowerCase().trim()];
297
+ if (!allowedExtensions) return { valid: true };
298
+ if (!allowedExtensions.includes(extension)) return {
299
+ valid: false,
300
+ error: `File extension '${extension}' does not match content type '${contentType}'. Expected: ${allowedExtensions.join(", ")}`
301
+ };
302
+ return { valid: true };
303
+ }
304
+ /**
305
+ * Validate upload options
306
+ *
307
+ * @param options - Upload options to validate
308
+ * @param validationOptions - Validation constraints
309
+ * @returns Validation result
310
+ */
311
+ function validateUploadOptions(options, validationOptions = {}) {
312
+ const errors = [];
313
+ if (options?.fileSize && validationOptions.maxFileSize) {
314
+ const sizeValidation = validateFileSize(options.fileSize, validationOptions.maxFileSize);
315
+ if (!sizeValidation.valid) {
316
+ if (sizeValidation.error) errors.push(sizeValidation.error);
317
+ }
318
+ }
319
+ if (options?.contentType && validationOptions.allowedMimeTypes) {
320
+ const mimeValidation = validateMimeType(options.contentType, validationOptions.allowedMimeTypes);
321
+ if (!mimeValidation.valid) {
322
+ if (mimeValidation.error) errors.push(mimeValidation.error);
323
+ }
324
+ }
325
+ if (options?.key) {
326
+ const keyValidation = validateStorageKey(options.key, {
327
+ maxLength: validationOptions.maxKeyLength,
328
+ forbiddenPatterns: validationOptions.forbiddenKeyPatterns
329
+ });
330
+ if (!keyValidation.valid && keyValidation.errors.length > 0) errors.push(...keyValidation.errors);
331
+ }
332
+ if (validationOptions.requireContentType && !options?.contentType) errors.push("Content type is required");
333
+ return {
334
+ valid: errors.length === 0,
335
+ errors
336
+ };
337
+ }
338
+ /**
339
+ * Determines if an error should be retried based on error type and message
340
+ *
341
+ * Checks for common transient failures that are safe to retry:
342
+ * - Network timeouts
343
+ * - Connection failures (ECONNRESET, ENOTFOUND, ETIMEDOUT)
344
+ * - Rate limiting (429, 503 status codes)
345
+ * - Server errors (500-599 status codes)
346
+ *
347
+ * @param error - Error instance to analyze (any type accepted)
348
+ * @returns `true` if the error indicates a transient failure that should be retried
349
+ *
350
+ * @example
351
+ * ```typescript
352
+ * try {
353
+ * await storage.upload(key, data);
354
+ * } catch (error) {
355
+ * if (isRetryableError(error)) {
356
+ * // Retry the operation
357
+ * await retryWithBackoff(() => storage.upload(key, data));
358
+ * } else {
359
+ * // Permanent failure - don't retry
360
+ * throw error;
361
+ * }
362
+ * }
363
+ * ```
364
+ */
365
+ function isRetryableError(error) {
366
+ if (error instanceof StorageError) return error.retryable;
367
+ if (error instanceof Error) {
368
+ const message = error.message.toLowerCase();
369
+ return message.includes("timeout") || message.includes("network") || message.includes("connection") || message.includes("rate limit") || message.includes("temporary");
370
+ }
371
+ return false;
372
+ }
373
+ /**
374
+ * Extracts a standardized error code from any error type
375
+ *
376
+ * Maps common error messages to StorageErrorCode enum values:
377
+ * - "not found" → FILE_NOT_FOUND
378
+ * - "access denied" → ACCESS_DENIED
379
+ * - "timeout" → TIMEOUT
380
+ * - "network" → NETWORK_ERROR
381
+ * - Others → PROVIDER_ERROR
382
+ *
383
+ * @param error - Error of any type (StorageError, Error, string, unknown)
384
+ * @returns StorageErrorCode enum value for categorization
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * try {
389
+ * await storage.delete(key);
390
+ * } catch (error) {
391
+ * const code = getErrorCode(error);
392
+ * if (code === StorageErrorCode.FILE_NOT_FOUND) {
393
+ * // File already deleted - ignore
394
+ * return { success: true };
395
+ * }
396
+ * throw error;
397
+ * }
398
+ * ```
399
+ */
400
+ function getErrorCode(error) {
401
+ if (error instanceof StorageError) return error.code;
402
+ if (error instanceof Error) {
403
+ const message = error.message.toLowerCase();
404
+ if (message.includes("not found")) return StorageErrorCode.FILE_NOT_FOUND;
405
+ if (message.includes("access denied")) return StorageErrorCode.ACCESS_DENIED;
406
+ if (message.includes("timeout")) return StorageErrorCode.TIMEOUT;
407
+ if (message.includes("network")) return StorageErrorCode.NETWORK_ERROR;
408
+ }
409
+ return StorageErrorCode.PROVIDER_ERROR;
410
+ }
411
+ /**
412
+ * Wraps any error into a standardized StorageError with additional context
413
+ *
414
+ * Converts plain errors into structured StorageError instances with:
415
+ * - Proper error code classification
416
+ * - Operation context (upload, delete, etc.)
417
+ * - Provider information
418
+ * - Storage key reference
419
+ *
420
+ * @param error - Original error of any type
421
+ * @param context - Additional context to attach to the error
422
+ * @param context.operation - Storage operation being performed (e.g., "upload", "delete")
423
+ * @param context.provider - Storage provider name (e.g., "cloudflare-r2", "vercel-blob")
424
+ * @param context.key - Storage key being operated on
425
+ * @returns Standardized StorageError instance with full context
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * try {
430
+ * await provider.upload(key, data);
431
+ * } catch (error) {
432
+ * throw createStorageError(error, {
433
+ * operation: 'upload',
434
+ * provider: 'vercel-blob',
435
+ * key,
436
+ * });
437
+ * }
438
+ * ```
439
+ */
440
+ function createStorageError(error, context = {}) {
441
+ if (error instanceof StorageError) return error;
442
+ const message = error instanceof Error ? error.message : String(error);
443
+ const code = getErrorCode(error);
444
+ const retryable = isRetryableError(error);
445
+ return new StorageError(message, code, {
446
+ originalError: error,
447
+ ...context
448
+ }, retryable);
449
+ }
450
+ /**
451
+ * Check quota information
452
+ *
453
+ * @param used - Used quota in bytes
454
+ * @param limit - Quota limit in bytes
455
+ * @returns Quota information
456
+ */
457
+ function getQuotaInfo(used, limit, provider) {
458
+ return {
459
+ used,
460
+ limit,
461
+ remaining: Math.max(0, limit - used),
462
+ provider,
463
+ resetAt: void 0
464
+ };
465
+ }
466
+ /**
467
+ * Check if quota is exceeded
468
+ *
469
+ * @param used - Used quota in bytes
470
+ * @param limit - Quota limit in bytes
471
+ * @returns True if quota is exceeded
472
+ */
473
+ function isQuotaExceeded(used, limit) {
474
+ return used >= limit;
475
+ }
476
+ /**
477
+ * Format file size for display
478
+ *
479
+ * @param bytes - Size in bytes
480
+ * @returns Formatted size string
481
+ */
482
+ function formatFileSize(bytes) {
483
+ if (bytes === 0) return "0 B";
484
+ const k = 1024;
485
+ const sizes = [
486
+ "B",
487
+ "KB",
488
+ "MB",
489
+ "GB",
490
+ "TB"
491
+ ];
492
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
493
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
494
+ }
495
+ /**
496
+ * Parse file size from string
497
+ *
498
+ * @param sizeString - Size string (e.g., "10MB", "1.5GB")
499
+ * @returns Size in bytes
500
+ */
501
+ function parseFileSize(sizeString) {
502
+ const match = sizeString.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i);
503
+ if (!match) throw new ValidationError(`Invalid file size format: ${sizeString}`);
504
+ const valueStr = match[1];
505
+ const unitStr = match[2];
506
+ if (!valueStr || !unitStr) throw new ValidationError(`Invalid file size format: ${sizeString}`);
507
+ const value = parseFloat(valueStr);
508
+ const unit = unitStr.toUpperCase();
509
+ const multiplier = {
510
+ B: 1,
511
+ KB: 1024,
512
+ MB: 1024 * 1024,
513
+ GB: 1024 * 1024 * 1024,
514
+ TB: 1024 * 1024 * 1024 * 1024
515
+ }[unit];
516
+ if (multiplier === void 0) throw new ValidationError(`Unknown unit: ${unit}`);
517
+ return value * multiplier;
518
+ }
519
+ /**
520
+ * Blocked hostname patterns for SSRF prevention
521
+ * Comprehensive list covering IPv4, IPv6, and special addresses
522
+ */
523
+ const SSRF_BLOCKED_PATTERNS = [
524
+ /^localhost$/i,
525
+ /^127\./,
526
+ /^0\./,
527
+ /^10\./,
528
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
529
+ /^192\.168\./,
530
+ /^169\.254\./,
531
+ /^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\./,
532
+ /^240\./,
533
+ /^::1$/,
534
+ /^\[::1\]$/,
535
+ /^fe80:/i,
536
+ /^\[fe80:/i,
537
+ /^fc00:/i,
538
+ /^fd00:/i,
539
+ /^::ffff:0:0:/i,
540
+ /^ff00:/i,
541
+ /\.local$/i
542
+ ];
543
+ /**
544
+ * Validates URL safety to prevent SSRF (Server-Side Request OneAppry) attacks
545
+ *
546
+ * This function blocks:
547
+ * - Non-HTTPS URLs (only HTTPS is allowed)
548
+ * - Localhost and loopback addresses (127.0.0.1, ::1, etc.)
549
+ * - Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
550
+ * - Cloud metadata endpoints (169.254.169.254)
551
+ * - Link-local addresses (169.254.0.0/16, fe80::/10)
552
+ * - Carrier-grade NAT ranges (100.64.0.0/10)
553
+ * - IPv6 private/special addresses (fc00::/7, ff00::/8)
554
+ * - .local domain suffix
555
+ *
556
+ * @param url - The URL to validate
557
+ * @returns Object with valid boolean and optional error message
558
+ *
559
+ * @example
560
+ * ```typescript
561
+ * const result = validateUrlForSSRF('https://example.com/image.jpg');
562
+ * if (!result.valid) {
563
+ * throw new Error(result.error);
564
+ * }
565
+ * ```
566
+ */
567
+ function validateUrlForSSRF(url) {
568
+ try {
569
+ const parsed = new URL(url);
570
+ if (parsed.protocol !== "https:") return {
571
+ valid: false,
572
+ error: "Only HTTPS URLs are allowed"
573
+ };
574
+ const hostname = parsed.hostname.toLowerCase();
575
+ if (SSRF_BLOCKED_PATTERNS.some((pattern) => pattern.test(hostname))) return {
576
+ valid: false,
577
+ error: `URL hostname "${hostname}" is blocked for security reasons (private IP, localhost, or reserved address)`
578
+ };
579
+ return { valid: true };
580
+ } catch {
581
+ return {
582
+ valid: false,
583
+ error: "Invalid URL format"
584
+ };
585
+ }
586
+ }
587
+
588
+ //#endregion
589
+ export { ConfigError, DownloadError, NetworkError, ProviderError, StorageError, StorageErrorCode, UploadError, ValidationError, createStorageError, formatFileSize, getErrorCode, getQuotaInfo, isQuotaExceeded, isRetryableError, parseFileSize, validateFileExtension, validateFileSize, validateMimeType, validateStorageKey, validateUploadOptions, validateUrlForSSRF };
590
+ //# sourceMappingURL=validation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.mjs","names":[],"sources":["../src/validation.ts"],"sourcesContent":["/**\n * @fileoverview Storage Validation and Error Handling\n *\n * Provides comprehensive validation utilities and error types for storage operations.\n *\n * Features:\n * - File size validation\n * - MIME type validation\n * - Storage key validation\n * - Error types and handling\n * - SSRF protection\n *\n * @module @repo/storage/validation\n */\n\n/**\n * Storage error codes\n */\nexport enum StorageErrorCode {\n // Validation errors\n INVALID_KEY = 'INVALID_KEY',\n INVALID_FILE_SIZE = 'INVALID_FILE_SIZE',\n INVALID_MIME_TYPE = 'INVALID_MIME_TYPE',\n INVALID_OPTIONS = 'INVALID_OPTIONS',\n\n // Provider errors\n PROVIDER_ERROR = 'PROVIDER_ERROR',\n PROVIDER_UNAVAILABLE = 'PROVIDER_UNAVAILABLE',\n PROVIDER_QUOTA_EXCEEDED = 'PROVIDER_QUOTA_EXCEEDED',\n\n // Network errors\n NETWORK_ERROR = 'NETWORK_ERROR',\n TIMEOUT = 'TIMEOUT',\n CONNECTION_FAILED = 'CONNECTION_FAILED',\n\n // Upload errors\n UPLOAD_FAILED = 'UPLOAD_FAILED',\n UPLOAD_CANCELLED = 'UPLOAD_CANCELLED',\n UPLOAD_PARTIAL = 'UPLOAD_PARTIAL',\n\n // Download errors\n DOWNLOAD_FAILED = 'DOWNLOAD_FAILED',\n FILE_NOT_FOUND = 'FILE_NOT_FOUND',\n ACCESS_DENIED = 'ACCESS_DENIED',\n\n // Configuration errors\n CONFIG_ERROR = 'CONFIG_ERROR',\n MISSING_CREDENTIALS = 'MISSING_CREDENTIALS',\n INVALID_CONFIG = 'INVALID_CONFIG',\n}\n\n/**\n * Base storage error class\n */\nexport class StorageError extends Error {\n public readonly code: StorageErrorCode;\n public readonly details?: Record<string, any>;\n public readonly retryable: boolean;\n\n constructor(\n message: string,\n code: StorageErrorCode,\n details?: Record<string, any>,\n retryable: boolean = false,\n ) {\n super(message);\n this.name = 'StorageError';\n this.code = code;\n this.details = details;\n this.retryable = retryable;\n }\n}\n\n/**\n * Validation error for invalid input\n */\nexport class ValidationError extends StorageError {\n constructor(message: string, details?: Record<string, any>) {\n super(message, StorageErrorCode.INVALID_OPTIONS, details, false);\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Provider-specific error\n */\nexport class ProviderError extends StorageError {\n constructor(\n message: string,\n provider: string,\n details?: Record<string, any>,\n retryable: boolean = false,\n ) {\n super(message, StorageErrorCode.PROVIDER_ERROR, { ...details, provider }, retryable);\n this.name = 'ProviderError';\n }\n}\n\n/**\n * Network-related error\n */\nexport class NetworkError extends StorageError {\n constructor(message: string, details?: Record<string, any>, retryable: boolean = true) {\n super(message, StorageErrorCode.NETWORK_ERROR, details, retryable);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Upload-specific error\n */\nexport class UploadError extends StorageError {\n constructor(message: string, details?: Record<string, any>, retryable: boolean = true) {\n super(message, StorageErrorCode.UPLOAD_FAILED, details, retryable);\n this.name = 'UploadError';\n }\n}\n\n/**\n * Download-specific error\n */\nexport class DownloadError extends StorageError {\n constructor(message: string, details?: Record<string, any>, retryable: boolean = true) {\n super(message, StorageErrorCode.DOWNLOAD_FAILED, details, retryable);\n this.name = 'DownloadError';\n }\n}\n\n/**\n * Configuration error\n */\nexport class ConfigError extends StorageError {\n constructor(message: string, details?: Record<string, any>) {\n super(message, StorageErrorCode.CONFIG_ERROR, details, false);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * Validation options\n */\nexport interface ValidationOptions {\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n allowedExtensions?: string[];\n maxKeyLength?: number;\n forbiddenKeyPatterns?: RegExp[];\n requireContentType?: boolean;\n}\n\n/**\n * Quota information\n */\nexport interface QuotaInfo {\n used: number;\n limit: number;\n remaining: number;\n resetAt?: Date;\n provider: string;\n}\n\n/**\n * Validate file size\n *\n * @param size - File size in bytes\n * @param maxSize - Maximum allowed size in bytes\n * @returns Validation result\n */\nexport function validateFileSize(\n size: number,\n maxSize: number,\n): { valid: boolean; error?: string } {\n if (size < 0) {\n return { valid: false, error: 'File size cannot be negative' };\n }\n\n if (size > maxSize) {\n return {\n valid: false,\n error: `File size ${size} bytes exceeds maximum ${maxSize} bytes`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Dangerous MIME types that should be blocked for security reasons\n *\n * These types can execute code or present security risks:\n * - Executables (.exe, .dll, .app)\n * - Scripts (.sh, .bat, .cmd, .js, .php, .py, .pl)\n * - Archives with executables (.jar)\n * - Windows system files (.com, .pif, .scr)\n */\nconst DANGEROUS_MIME_TYPES = [\n // Executables\n 'application/x-msdownload', // .exe\n 'application/x-executable',\n 'application/x-dosexec',\n 'application/x-msdos-program',\n 'application/x-dll',\n 'application/x-app',\n\n // Scripts\n 'application/x-sh', // Shell scripts\n 'text/x-sh',\n 'application/x-bat', // Batch files\n 'application/x-cmd',\n 'text/x-python', // Python scripts\n 'application/x-python-code',\n 'text/x-perl', // Perl scripts\n 'application/x-perl',\n 'application/javascript', // JavaScript (unless explicitly allowed)\n 'text/javascript',\n 'application/x-javascript',\n 'application/x-php', // PHP\n 'application/x-httpd-php',\n 'text/x-php',\n\n // Windows system files\n 'application/x-com',\n 'application/x-pif',\n 'application/x-scr',\n 'application/x-vbs', // VBScript\n 'text/vbscript',\n\n // Java\n 'application/java-archive', // .jar (can contain malicious code)\n 'application/x-java-applet',\n] as const;\n\n/**\n * MIME type to file extension mappings for validation\n *\n * Maps common MIME types to their expected file extensions to prevent\n * extension spoofing attacks (e.g., executable disguised as image)\n */\nconst MIME_TO_EXTENSIONS: Record<string, readonly string[]> = {\n // Images\n 'image/jpeg': ['.jpg', '.jpeg'],\n 'image/png': ['.png'],\n 'image/gif': ['.gif'],\n 'image/webp': ['.webp'],\n 'image/svg+xml': ['.svg'],\n 'image/bmp': ['.bmp'],\n 'image/tiff': ['.tif', '.tiff'],\n 'image/avif': ['.avif'],\n\n // Documents\n 'application/pdf': ['.pdf'],\n 'text/plain': ['.txt'],\n 'text/csv': ['.csv'],\n 'text/html': ['.html', '.htm'],\n 'text/xml': ['.xml'],\n 'application/json': ['.json'],\n\n // Microsoft Office\n 'application/msword': ['.doc'],\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],\n 'application/vnd.ms-excel': ['.xls'],\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],\n 'application/vnd.ms-powerpoint': ['.ppt'],\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],\n\n // Archives (safe ones)\n 'application/zip': ['.zip'],\n 'application/x-gzip': ['.gz'],\n 'application/x-tar': ['.tar'],\n 'application/x-7z-compressed': ['.7z'],\n\n // Audio\n 'audio/mpeg': ['.mp3'],\n 'audio/wav': ['.wav'],\n 'audio/ogg': ['.ogg'],\n 'audio/mp4': ['.m4a'],\n\n // Video\n 'video/mp4': ['.mp4'],\n 'video/mpeg': ['.mpeg', '.mpg'],\n 'video/quicktime': ['.mov'],\n 'video/webm': ['.webm'],\n 'video/x-msvideo': ['.avi'],\n} as const;\n\n/**\n * Validate MIME type against allowed types and dangerous content\n *\n * This function:\n * 1. Blocks dangerous MIME types (executables, scripts) for security\n * 2. Validates against allowed types list if provided\n * 3. Supports wildcard patterns (e.g., image/*)\n *\n * @param mimeType - MIME type to validate\n * @param allowedTypes - Array of allowed MIME types (optional)\n * @returns Validation result with error message if invalid\n *\n * @example\n * ```typescript\n * // Block dangerous types\n * validateMimeType('application/x-msdownload', ['image/*'])\n * // => { valid: false, error: '...' }\n *\n * // Allow specific safe types\n * validateMimeType('image/jpeg', ['image/*'])\n * // => { valid: true }\n * ```\n */\nexport function validateMimeType(\n mimeType: string,\n allowedTypes: string[] = [],\n): { valid: boolean; error?: string } {\n if (!mimeType) {\n return { valid: false, error: 'MIME type is required' };\n }\n\n const normalizedMimeType = mimeType.toLowerCase().trim();\n\n // Block dangerous MIME types first (security check)\n if (DANGEROUS_MIME_TYPES.includes(normalizedMimeType as (typeof DANGEROUS_MIME_TYPES)[number])) {\n return {\n valid: false,\n error: `Content type '${mimeType}' is not allowed for security reasons (executable or script content)`,\n };\n }\n\n // If no allowed types specified, allow all safe types\n if (allowedTypes.length === 0) {\n return { valid: true };\n }\n\n const normalizedAllowed = allowedTypes.map(t => t.toLowerCase().trim());\n\n // Check exact match\n if (normalizedAllowed.includes(normalizedMimeType)) {\n return { valid: true };\n }\n\n // Check wildcard patterns (e.g., image/*)\n const wildcardMatch = normalizedAllowed.some(allowed => {\n if (allowed.endsWith('/*')) {\n const prefix = allowed.slice(0, -2);\n return normalizedMimeType.startsWith(prefix);\n }\n return false;\n });\n\n if (wildcardMatch) {\n return { valid: true };\n }\n\n return {\n valid: false,\n error: `MIME type '${mimeType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,\n };\n}\n\n/**\n * Validate file extension matches expected extension for MIME type\n *\n * Prevents extension spoofing attacks where malicious files are disguised\n * with incorrect extensions (e.g., executable.exe renamed to image.jpg)\n *\n * @param filename - File name with extension\n * @param contentType - MIME type of the file\n * @returns Validation result with error message if mismatch detected\n *\n * @example\n * ```typescript\n * // Valid: JPEG file with .jpg extension\n * validateFileExtension('photo.jpg', 'image/jpeg')\n * // => { valid: true }\n *\n * // Invalid: JPEG MIME type with .exe extension (spoofing)\n * validateFileExtension('photo.exe', 'image/jpeg')\n * // => { valid: false, error: '...' }\n *\n * // Unknown MIME type: allowed with warning\n * validateFileExtension('data.custom', 'application/x-custom')\n * // => { valid: true } (with warning logged)\n * ```\n */\nexport function validateFileExtension(\n filename: string,\n contentType: string,\n): { valid: boolean; error?: string } {\n if (!filename || !contentType) {\n return { valid: false, error: 'Filename and content type are required' };\n }\n\n // Extract file extension\n const lastDotIndex = filename.lastIndexOf('.');\n if (lastDotIndex === -1) {\n return { valid: false, error: 'File must have an extension' };\n }\n\n const extension = filename.substring(lastDotIndex).toLowerCase();\n const normalizedMimeType = contentType.toLowerCase().trim();\n\n // Check if we have extension mapping for this MIME type\n const allowedExtensions = MIME_TO_EXTENSIONS[normalizedMimeType];\n\n if (!allowedExtensions) {\n // Unknown MIME type - allow but this should be logged upstream\n // Don't fail validation for unknown types (extensibility)\n return { valid: true };\n }\n\n // Validate extension matches expected extensions for this MIME type\n if (!allowedExtensions.includes(extension)) {\n return {\n valid: false,\n error: `File extension '${extension}' does not match content type '${contentType}'. Expected: ${allowedExtensions.join(', ')}`,\n };\n }\n\n return { valid: true };\n}\n\n// Import validateStorageKey for internal use in this file\nimport { validateStorageKey } from '../keys';\n\n/**\n * Validate storage key\n *\n * Re-exports from keys.ts for backwards compatibility.\n * Use validateStorageKey from '../keys' for the canonical implementation.\n *\n * @param key - Storage key to validate\n * @param options - Validation options\n * @returns Validation result\n */\nexport { validateStorageKey };\n\n/**\n * Validate upload options\n *\n * @param options - Upload options to validate\n * @param validationOptions - Validation constraints\n * @returns Validation result\n */\nexport function validateUploadOptions(\n options: {\n fileSize?: number;\n contentType?: string;\n key?: string;\n },\n validationOptions: ValidationOptions = {},\n): { valid: boolean; errors: string[] } {\n const errors: string[] = [];\n\n // Validate file size\n if (options?.fileSize && validationOptions.maxFileSize) {\n const sizeValidation = validateFileSize(options.fileSize, validationOptions.maxFileSize);\n if (!sizeValidation.valid) {\n if (sizeValidation.error) {\n errors.push(sizeValidation.error);\n }\n }\n }\n\n // Validate MIME type\n if (options?.contentType && validationOptions.allowedMimeTypes) {\n const mimeValidation = validateMimeType(\n options.contentType,\n validationOptions.allowedMimeTypes,\n );\n if (!mimeValidation.valid) {\n if (mimeValidation.error) {\n errors.push(mimeValidation.error);\n }\n }\n }\n\n // Validate key\n if (options?.key) {\n const keyValidation = validateStorageKey(options.key, {\n maxLength: validationOptions.maxKeyLength,\n forbiddenPatterns: validationOptions.forbiddenKeyPatterns,\n });\n if (!keyValidation.valid && keyValidation.errors.length > 0) {\n errors.push(...keyValidation.errors);\n }\n }\n\n // Require content type\n if (validationOptions.requireContentType && !options?.contentType) {\n errors.push('Content type is required');\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Determines if an error should be retried based on error type and message\n *\n * Checks for common transient failures that are safe to retry:\n * - Network timeouts\n * - Connection failures (ECONNRESET, ENOTFOUND, ETIMEDOUT)\n * - Rate limiting (429, 503 status codes)\n * - Server errors (500-599 status codes)\n *\n * @param error - Error instance to analyze (any type accepted)\n * @returns `true` if the error indicates a transient failure that should be retried\n *\n * @example\n * ```typescript\n * try {\n * await storage.upload(key, data);\n * } catch (error) {\n * if (isRetryableError(error)) {\n * // Retry the operation\n * await retryWithBackoff(() => storage.upload(key, data));\n * } else {\n * // Permanent failure - don't retry\n * throw error;\n * }\n * }\n * ```\n */\nexport function isRetryableError(error: unknown): boolean {\n if (error instanceof StorageError) {\n return error.retryable;\n }\n\n // Check for common retryable error patterns\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n return (\n message.includes('timeout') ||\n message.includes('network') ||\n message.includes('connection') ||\n message.includes('rate limit') ||\n message.includes('temporary')\n );\n }\n\n return false;\n}\n\n/**\n * Extracts a standardized error code from any error type\n *\n * Maps common error messages to StorageErrorCode enum values:\n * - \"not found\" → FILE_NOT_FOUND\n * - \"access denied\" → ACCESS_DENIED\n * - \"timeout\" → TIMEOUT\n * - \"network\" → NETWORK_ERROR\n * - Others → PROVIDER_ERROR\n *\n * @param error - Error of any type (StorageError, Error, string, unknown)\n * @returns StorageErrorCode enum value for categorization\n *\n * @example\n * ```typescript\n * try {\n * await storage.delete(key);\n * } catch (error) {\n * const code = getErrorCode(error);\n * if (code === StorageErrorCode.FILE_NOT_FOUND) {\n * // File already deleted - ignore\n * return { success: true };\n * }\n * throw error;\n * }\n * ```\n */\nexport function getErrorCode(error: unknown): StorageErrorCode {\n if (error instanceof StorageError) {\n return error.code;\n }\n\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n if (message.includes('not found')) return StorageErrorCode.FILE_NOT_FOUND;\n if (message.includes('access denied')) return StorageErrorCode.ACCESS_DENIED;\n if (message.includes('timeout')) return StorageErrorCode.TIMEOUT;\n if (message.includes('network')) return StorageErrorCode.NETWORK_ERROR;\n }\n\n return StorageErrorCode.PROVIDER_ERROR;\n}\n\n/**\n * Wraps any error into a standardized StorageError with additional context\n *\n * Converts plain errors into structured StorageError instances with:\n * - Proper error code classification\n * - Operation context (upload, delete, etc.)\n * - Provider information\n * - Storage key reference\n *\n * @param error - Original error of any type\n * @param context - Additional context to attach to the error\n * @param context.operation - Storage operation being performed (e.g., \"upload\", \"delete\")\n * @param context.provider - Storage provider name (e.g., \"cloudflare-r2\", \"vercel-blob\")\n * @param context.key - Storage key being operated on\n * @returns Standardized StorageError instance with full context\n *\n * @example\n * ```typescript\n * try {\n * await provider.upload(key, data);\n * } catch (error) {\n * throw createStorageError(error, {\n * operation: 'upload',\n * provider: 'vercel-blob',\n * key,\n * });\n * }\n * ```\n */\nexport function createStorageError(\n error: unknown,\n context: {\n operation?: string;\n provider?: string;\n key?: string;\n } = {},\n): StorageError {\n if (error instanceof StorageError) {\n return error;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n const code = getErrorCode(error);\n const retryable = isRetryableError(error);\n\n return new StorageError(\n message,\n code,\n {\n originalError: error,\n ...context,\n },\n retryable,\n );\n}\n\n/**\n * Check quota information\n *\n * @param used - Used quota in bytes\n * @param limit - Quota limit in bytes\n * @returns Quota information\n */\nexport function getQuotaInfo(used: number, limit: number, provider: string): QuotaInfo {\n return {\n used,\n limit,\n remaining: Math.max(0, limit - used),\n provider,\n resetAt: undefined, // Provider-specific\n };\n}\n\n/**\n * Check if quota is exceeded\n *\n * @param used - Used quota in bytes\n * @param limit - Quota limit in bytes\n * @returns True if quota is exceeded\n */\nexport function isQuotaExceeded(used: number, limit: number): boolean {\n return used >= limit;\n}\n\n/**\n * Format file size for display\n *\n * @param bytes - Size in bytes\n * @returns Formatted size string\n */\nexport function formatFileSize(bytes: number): string {\n if (bytes === 0) return '0 B';\n\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;\n}\n\n/**\n * Parse file size from string\n *\n * @param sizeString - Size string (e.g., \"10MB\", \"1.5GB\")\n * @returns Size in bytes\n */\nexport function parseFileSize(sizeString: string): number {\n // eslint-disable-next-line security/detect-unsafe-regex\n const match = sizeString.match(/^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)$/i);\n if (!match) {\n throw new ValidationError(`Invalid file size format: ${sizeString}`);\n }\n\n const valueStr = match[1];\n const unitStr = match[2];\n if (!valueStr || !unitStr) {\n throw new ValidationError(`Invalid file size format: ${sizeString}`);\n }\n\n const value = parseFloat(valueStr);\n const unit = unitStr.toUpperCase();\n\n const multipliers: Record<string, number> = {\n B: 1,\n KB: 1024,\n MB: 1024 * 1024,\n GB: 1024 * 1024 * 1024,\n TB: 1024 * 1024 * 1024 * 1024,\n };\n\n const multiplier = multipliers[unit];\n if (multiplier === undefined) {\n throw new ValidationError(`Unknown unit: ${unit}`);\n }\n\n return value * multiplier;\n}\n\n/**\n * Blocked hostname patterns for SSRF prevention\n * Comprehensive list covering IPv4, IPv6, and special addresses\n */\nconst SSRF_BLOCKED_PATTERNS: RegExp[] = [\n // IPv4 localhost and loopback\n /^localhost$/i,\n /^127\\./,\n /^0\\./,\n\n // IPv4 private ranges (RFC 1918)\n /^10\\./,\n /^172\\.(1[6-9]|2[0-9]|3[0-1])\\./,\n /^192\\.168\\./,\n\n // Link-local IPv4 (includes cloud metadata endpoints)\n /^169\\.254\\./,\n\n // Carrier-grade NAT (RFC 6598)\n /^100\\.(6[4-9]|[7-9]\\d|1[0-1]\\d|12[0-7])\\./,\n\n // Class E reserved\n /^240\\./,\n\n // IPv6 localhost\n /^::1$/,\n /^\\[::1\\]$/,\n\n // IPv6 link-local\n /^fe80:/i,\n /^\\[fe80:/i,\n\n // IPv6 unique local (private)\n /^fc00:/i,\n /^fd00:/i,\n\n // IPv4-mapped IPv6\n /^::ffff:0:0:/i,\n\n // IPv6 multicast\n /^ff00:/i,\n\n // Domain patterns\n /\\.local$/i,\n];\n\n/**\n * Validates URL safety to prevent SSRF (Server-Side Request OneAppry) attacks\n *\n * This function blocks:\n * - Non-HTTPS URLs (only HTTPS is allowed)\n * - Localhost and loopback addresses (127.0.0.1, ::1, etc.)\n * - Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\n * - Cloud metadata endpoints (169.254.169.254)\n * - Link-local addresses (169.254.0.0/16, fe80::/10)\n * - Carrier-grade NAT ranges (100.64.0.0/10)\n * - IPv6 private/special addresses (fc00::/7, ff00::/8)\n * - .local domain suffix\n *\n * @param url - The URL to validate\n * @returns Object with valid boolean and optional error message\n *\n * @example\n * ```typescript\n * const result = validateUrlForSSRF('https://example.com/image.jpg');\n * if (!result.valid) {\n * throw new Error(result.error);\n * }\n * ```\n */\nexport function validateUrlForSSRF(url: string): {\n valid: boolean;\n error?: string;\n} {\n try {\n const parsed = new URL(url);\n\n // Only allow HTTPS (not HTTP or other protocols)\n if (parsed.protocol !== 'https:') {\n return {\n valid: false,\n error: 'Only HTTPS URLs are allowed',\n };\n }\n\n const hostname = parsed.hostname.toLowerCase();\n\n // Check against all blocked patterns\n if (SSRF_BLOCKED_PATTERNS.some(pattern => pattern.test(hostname))) {\n return {\n valid: false,\n error: `URL hostname \"${hostname}\" is blocked for security reasons (private IP, localhost, or reserved address)`,\n };\n }\n\n return { valid: true };\n } catch {\n return {\n valid: false,\n error: 'Invalid URL format',\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkBA,IAAY,8DAAL;AAEL;AACA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;;;;;;AAMF,IAAa,eAAb,cAAkC,MAAM;CACtC,AAAgB;CAChB,AAAgB;CAChB,AAAgB;CAEhB,YACE,SACA,MACA,SACA,YAAqB,OACrB;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,UAAU;AACf,OAAK,YAAY;;;;;;AAOrB,IAAa,kBAAb,cAAqC,aAAa;CAChD,YAAY,SAAiB,SAA+B;AAC1D,QAAM,SAAS,iBAAiB,iBAAiB,SAAS,MAAM;AAChE,OAAK,OAAO;;;;;;AAOhB,IAAa,gBAAb,cAAmC,aAAa;CAC9C,YACE,SACA,UACA,SACA,YAAqB,OACrB;AACA,QAAM,SAAS,iBAAiB,gBAAgB;GAAE,GAAG;GAAS;GAAU,EAAE,UAAU;AACpF,OAAK,OAAO;;;;;;AAOhB,IAAa,eAAb,cAAkC,aAAa;CAC7C,YAAY,SAAiB,SAA+B,YAAqB,MAAM;AACrF,QAAM,SAAS,iBAAiB,eAAe,SAAS,UAAU;AAClE,OAAK,OAAO;;;;;;AAOhB,IAAa,cAAb,cAAiC,aAAa;CAC5C,YAAY,SAAiB,SAA+B,YAAqB,MAAM;AACrF,QAAM,SAAS,iBAAiB,eAAe,SAAS,UAAU;AAClE,OAAK,OAAO;;;;;;AAOhB,IAAa,gBAAb,cAAmC,aAAa;CAC9C,YAAY,SAAiB,SAA+B,YAAqB,MAAM;AACrF,QAAM,SAAS,iBAAiB,iBAAiB,SAAS,UAAU;AACpE,OAAK,OAAO;;;;;;AAOhB,IAAa,cAAb,cAAiC,aAAa;CAC5C,YAAY,SAAiB,SAA+B;AAC1D,QAAM,SAAS,iBAAiB,cAAc,SAAS,MAAM;AAC7D,OAAK,OAAO;;;;;;;;;;AAkChB,SAAgB,iBACd,MACA,SACoC;AACpC,KAAI,OAAO,EACT,QAAO;EAAE,OAAO;EAAO,OAAO;EAAgC;AAGhE,KAAI,OAAO,QACT,QAAO;EACL,OAAO;EACP,OAAO,aAAa,KAAK,yBAAyB,QAAQ;EAC3D;AAGH,QAAO,EAAE,OAAO,MAAM;;;;;;;;;;;AAYxB,MAAM,uBAAuB;CAE3B;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACD;;;;;;;AAQD,MAAM,qBAAwD;CAE5D,cAAc,CAAC,QAAQ,QAAQ;CAC/B,aAAa,CAAC,OAAO;CACrB,aAAa,CAAC,OAAO;CACrB,cAAc,CAAC,QAAQ;CACvB,iBAAiB,CAAC,OAAO;CACzB,aAAa,CAAC,OAAO;CACrB,cAAc,CAAC,QAAQ,QAAQ;CAC/B,cAAc,CAAC,QAAQ;CAGvB,mBAAmB,CAAC,OAAO;CAC3B,cAAc,CAAC,OAAO;CACtB,YAAY,CAAC,OAAO;CACpB,aAAa,CAAC,SAAS,OAAO;CAC9B,YAAY,CAAC,OAAO;CACpB,oBAAoB,CAAC,QAAQ;CAG7B,sBAAsB,CAAC,OAAO;CAC9B,2EAA2E,CAAC,QAAQ;CACpF,4BAA4B,CAAC,OAAO;CACpC,qEAAqE,CAAC,QAAQ;CAC9E,iCAAiC,CAAC,OAAO;CACzC,6EAA6E,CAAC,QAAQ;CAGtF,mBAAmB,CAAC,OAAO;CAC3B,sBAAsB,CAAC,MAAM;CAC7B,qBAAqB,CAAC,OAAO;CAC7B,+BAA+B,CAAC,MAAM;CAGtC,cAAc,CAAC,OAAO;CACtB,aAAa,CAAC,OAAO;CACrB,aAAa,CAAC,OAAO;CACrB,aAAa,CAAC,OAAO;CAGrB,aAAa,CAAC,OAAO;CACrB,cAAc,CAAC,SAAS,OAAO;CAC/B,mBAAmB,CAAC,OAAO;CAC3B,cAAc,CAAC,QAAQ;CACvB,mBAAmB,CAAC,OAAO;CAC5B;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,iBACd,UACA,eAAyB,EAAE,EACS;AACpC,KAAI,CAAC,SACH,QAAO;EAAE,OAAO;EAAO,OAAO;EAAyB;CAGzD,MAAM,qBAAqB,SAAS,aAAa,CAAC,MAAM;AAGxD,KAAI,qBAAqB,SAAS,mBAA4D,CAC5F,QAAO;EACL,OAAO;EACP,OAAO,iBAAiB,SAAS;EAClC;AAIH,KAAI,aAAa,WAAW,EAC1B,QAAO,EAAE,OAAO,MAAM;CAGxB,MAAM,oBAAoB,aAAa,KAAI,MAAK,EAAE,aAAa,CAAC,MAAM,CAAC;AAGvE,KAAI,kBAAkB,SAAS,mBAAmB,CAChD,QAAO,EAAE,OAAO,MAAM;AAYxB,KARsB,kBAAkB,MAAK,YAAW;AACtD,MAAI,QAAQ,SAAS,KAAK,EAAE;GAC1B,MAAM,SAAS,QAAQ,MAAM,GAAG,GAAG;AACnC,UAAO,mBAAmB,WAAW,OAAO;;AAE9C,SAAO;GACP,CAGA,QAAO,EAAE,OAAO,MAAM;AAGxB,QAAO;EACL,OAAO;EACP,OAAO,cAAc,SAAS,mCAAmC,aAAa,KAAK,KAAK;EACzF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BH,SAAgB,sBACd,UACA,aACoC;AACpC,KAAI,CAAC,YAAY,CAAC,YAChB,QAAO;EAAE,OAAO;EAAO,OAAO;EAA0C;CAI1E,MAAM,eAAe,SAAS,YAAY,IAAI;AAC9C,KAAI,iBAAiB,GACnB,QAAO;EAAE,OAAO;EAAO,OAAO;EAA+B;CAG/D,MAAM,YAAY,SAAS,UAAU,aAAa,CAAC,aAAa;CAIhE,MAAM,oBAAoB,mBAHC,YAAY,aAAa,CAAC,MAAM;AAK3D,KAAI,CAAC,kBAGH,QAAO,EAAE,OAAO,MAAM;AAIxB,KAAI,CAAC,kBAAkB,SAAS,UAAU,CACxC,QAAO;EACL,OAAO;EACP,OAAO,mBAAmB,UAAU,iCAAiC,YAAY,eAAe,kBAAkB,KAAK,KAAK;EAC7H;AAGH,QAAO,EAAE,OAAO,MAAM;;;;;;;;;AAyBxB,SAAgB,sBACd,SAKA,oBAAuC,EAAE,EACH;CACtC,MAAM,SAAmB,EAAE;AAG3B,KAAI,SAAS,YAAY,kBAAkB,aAAa;EACtD,MAAM,iBAAiB,iBAAiB,QAAQ,UAAU,kBAAkB,YAAY;AACxF,MAAI,CAAC,eAAe,OAClB;OAAI,eAAe,MACjB,QAAO,KAAK,eAAe,MAAM;;;AAMvC,KAAI,SAAS,eAAe,kBAAkB,kBAAkB;EAC9D,MAAM,iBAAiB,iBACrB,QAAQ,aACR,kBAAkB,iBACnB;AACD,MAAI,CAAC,eAAe,OAClB;OAAI,eAAe,MACjB,QAAO,KAAK,eAAe,MAAM;;;AAMvC,KAAI,SAAS,KAAK;EAChB,MAAM,gBAAgB,mBAAmB,QAAQ,KAAK;GACpD,WAAW,kBAAkB;GAC7B,mBAAmB,kBAAkB;GACtC,CAAC;AACF,MAAI,CAAC,cAAc,SAAS,cAAc,OAAO,SAAS,EACxD,QAAO,KAAK,GAAG,cAAc,OAAO;;AAKxC,KAAI,kBAAkB,sBAAsB,CAAC,SAAS,YACpD,QAAO,KAAK,2BAA2B;AAGzC,QAAO;EACL,OAAO,OAAO,WAAW;EACzB;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,SAAgB,iBAAiB,OAAyB;AACxD,KAAI,iBAAiB,aACnB,QAAO,MAAM;AAIf,KAAI,iBAAiB,OAAO;EAC1B,MAAM,UAAU,MAAM,QAAQ,aAAa;AAC3C,SACE,QAAQ,SAAS,UAAU,IAC3B,QAAQ,SAAS,UAAU,IAC3B,QAAQ,SAAS,aAAa,IAC9B,QAAQ,SAAS,aAAa,IAC9B,QAAQ,SAAS,YAAY;;AAIjC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT,SAAgB,aAAa,OAAkC;AAC7D,KAAI,iBAAiB,aACnB,QAAO,MAAM;AAGf,KAAI,iBAAiB,OAAO;EAC1B,MAAM,UAAU,MAAM,QAAQ,aAAa;AAC3C,MAAI,QAAQ,SAAS,YAAY,CAAE,QAAO,iBAAiB;AAC3D,MAAI,QAAQ,SAAS,gBAAgB,CAAE,QAAO,iBAAiB;AAC/D,MAAI,QAAQ,SAAS,UAAU,CAAE,QAAO,iBAAiB;AACzD,MAAI,QAAQ,SAAS,UAAU,CAAE,QAAO,iBAAiB;;AAG3D,QAAO,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC1B,SAAgB,mBACd,OACA,UAII,EAAE,EACQ;AACd,KAAI,iBAAiB,aACnB,QAAO;CAGT,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;CACtE,MAAM,OAAO,aAAa,MAAM;CAChC,MAAM,YAAY,iBAAiB,MAAM;AAEzC,QAAO,IAAI,aACT,SACA,MACA;EACE,eAAe;EACf,GAAG;EACJ,EACD,UACD;;;;;;;;;AAUH,SAAgB,aAAa,MAAc,OAAe,UAA6B;AACrF,QAAO;EACL;EACA;EACA,WAAW,KAAK,IAAI,GAAG,QAAQ,KAAK;EACpC;EACA,SAAS;EACV;;;;;;;;;AAUH,SAAgB,gBAAgB,MAAc,OAAwB;AACpE,QAAO,QAAQ;;;;;;;;AASjB,SAAgB,eAAe,OAAuB;AACpD,KAAI,UAAU,EAAG,QAAO;CAExB,MAAM,IAAI;CACV,MAAM,QAAQ;EAAC;EAAK;EAAM;EAAM;EAAM;EAAK;CAC3C,MAAM,IAAI,KAAK,MAAM,KAAK,IAAI,MAAM,GAAG,KAAK,IAAI,EAAE,CAAC;AAEnD,QAAO,GAAG,YAAY,QAAQ,KAAK,IAAI,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,GAAG,MAAM;;;;;;;;AASrE,SAAgB,cAAc,YAA4B;CAExD,MAAM,QAAQ,WAAW,MAAM,uCAAuC;AACtE,KAAI,CAAC,MACH,OAAM,IAAI,gBAAgB,6BAA6B,aAAa;CAGtE,MAAM,WAAW,MAAM;CACvB,MAAM,UAAU,MAAM;AACtB,KAAI,CAAC,YAAY,CAAC,QAChB,OAAM,IAAI,gBAAgB,6BAA6B,aAAa;CAGtE,MAAM,QAAQ,WAAW,SAAS;CAClC,MAAM,OAAO,QAAQ,aAAa;CAUlC,MAAM,aARsC;EAC1C,GAAG;EACH,IAAI;EACJ,IAAI,OAAO;EACX,IAAI,OAAO,OAAO;EAClB,IAAI,OAAO,OAAO,OAAO;EAC1B,CAE8B;AAC/B,KAAI,eAAe,OACjB,OAAM,IAAI,gBAAgB,iBAAiB,OAAO;AAGpD,QAAO,QAAQ;;;;;;AAOjB,MAAM,wBAAkC;CAEtC;CACA;CACA;CAGA;CACA;CACA;CAGA;CAGA;CAGA;CAGA;CACA;CAGA;CACA;CAGA;CACA;CAGA;CAGA;CAGA;CACD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,mBAAmB,KAGjC;AACA,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAG3B,MAAI,OAAO,aAAa,SACtB,QAAO;GACL,OAAO;GACP,OAAO;GACR;EAGH,MAAM,WAAW,OAAO,SAAS,aAAa;AAG9C,MAAI,sBAAsB,MAAK,YAAW,QAAQ,KAAK,SAAS,CAAC,CAC/D,QAAO;GACL,OAAO;GACP,OAAO,iBAAiB,SAAS;GAClC;AAGH,SAAO,EAAE,OAAO,MAAM;SAChB;AACN,SAAO;GACL,OAAO;GACP,OAAO;GACR"}
@@ -0,0 +1,31 @@
1
+ import { ListOptions, PresignedUploadUrl, StorageCapabilities, StorageObject, StorageProvider, UploadOptions } from "./types.mjs";
2
+
3
+ //#region providers/vercel-blob.d.ts
4
+ declare class VercelBlobProvider implements StorageProvider {
5
+ private token;
6
+ constructor(token: string);
7
+ delete(key: string): Promise<void>;
8
+ download(key: string): Promise<Blob>;
9
+ exists(key: string): Promise<boolean>;
10
+ getMetadata(key: string): Promise<StorageObject>;
11
+ getUrl(key: string, _options?: {
12
+ expiresIn?: number;
13
+ }): Promise<string>;
14
+ list(options?: ListOptions): Promise<StorageObject[]>;
15
+ upload(key: string, data: ArrayBuffer | Blob | Buffer | File | ReadableStream, options?: UploadOptions): Promise<StorageObject>;
16
+ createMultipartUpload(key: string, _options?: UploadOptions): Promise<{
17
+ uploadId: string;
18
+ key: string;
19
+ }>;
20
+ uploadPart(): Promise<{
21
+ etag: string;
22
+ partNumber: number;
23
+ }>;
24
+ completeMultipartUpload(): Promise<StorageObject>;
25
+ abortMultipartUpload(): Promise<void>;
26
+ getPresignedUploadUrl(): Promise<PresignedUploadUrl>;
27
+ getCapabilities(): StorageCapabilities;
28
+ }
29
+ //#endregion
30
+ export { VercelBlobProvider as t };
31
+ //# sourceMappingURL=vercel-blob-07Sx0Akn.d.mts.map