@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.
- package/README.md +854 -0
- package/dist/client-next.d.mts +61 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +111 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client-utils-Dx6W25iz.d.mts +43 -0
- package/dist/client-utils-Dx6W25iz.d.mts.map +1 -0
- package/dist/client.d.mts +28 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +183 -0
- package/dist/client.mjs.map +1 -0
- package/dist/env-BVHLmQdh.mjs +128 -0
- package/dist/env-BVHLmQdh.mjs.map +1 -0
- package/dist/env.mjs +3 -0
- package/dist/health-check-D7LnnDec.mjs +746 -0
- package/dist/health-check-D7LnnDec.mjs.map +1 -0
- package/dist/health-check-im_huJ59.d.mts +116 -0
- package/dist/health-check-im_huJ59.d.mts.map +1 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/keys.d.mts +37 -0
- package/dist/keys.d.mts.map +1 -0
- package/dist/keys.mjs +253 -0
- package/dist/keys.mjs.map +1 -0
- package/dist/server-edge.d.mts +28 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +88 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +183 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +1353 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +70 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +384 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types.d.mts +321 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +3 -0
- package/dist/validation.d.mts +101 -0
- package/dist/validation.d.mts.map +1 -0
- package/dist/validation.mjs +590 -0
- package/dist/validation.mjs.map +1 -0
- package/dist/vercel-blob-07Sx0Akn.d.mts +31 -0
- package/dist/vercel-blob-07Sx0Akn.d.mts.map +1 -0
- package/dist/vercel-blob-DA8HaYuw.mjs +158 -0
- package/dist/vercel-blob-DA8HaYuw.mjs.map +1 -0
- package/package.json +111 -0
- package/src/actions/blob-upload.ts +171 -0
- package/src/actions/index.ts +23 -0
- package/src/actions/mediaActions.ts +1071 -0
- package/src/actions/productMediaActions.ts +538 -0
- package/src/auth-helpers.ts +386 -0
- package/src/capabilities.ts +225 -0
- package/src/client-next.ts +184 -0
- package/src/client-utils.ts +292 -0
- package/src/client.ts +102 -0
- package/src/constants.ts +88 -0
- package/src/health-check.ts +81 -0
- package/src/multi-storage.ts +230 -0
- package/src/multipart.ts +497 -0
- package/src/retry-utils.test.ts +118 -0
- package/src/retry-utils.ts +59 -0
- package/src/server-edge.ts +129 -0
- package/src/server-next.ts +14 -0
- package/src/server.ts +666 -0
- package/src/validation.test.ts +312 -0
- 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":"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"}
|
|
@@ -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
|