@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
package/README.md ADDED
@@ -0,0 +1,854 @@
1
+ # @repo/storage
2
+
3
+ **Production-grade, multi-provider storage abstraction layer** for Next.js applications with full TypeScript support.
4
+
5
+ [![Type Safety](https://img.shields.io/badge/TypeScript-Strict-blue)](./tsconfig.json)
6
+ [![Test Coverage](https://img.shields.io/badge/Tests-38_files-green)](#testing)
7
+ [![Edge Compatible](https://img.shields.io/badge/Edge-Compatible-purple)](#edge-runtime-support)
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [Overview](#overview)
14
+ - [Features](#features)
15
+ - [Installation](#installation)
16
+ - [Quick Start](#quick-start)
17
+ - [Usage Patterns](#usage-patterns)
18
+ - [Provider Configuration](#provider-configuration)
19
+ - [Security Best Practices](#security-best-practices)
20
+ - [API Reference](#api-reference)
21
+ - [Migration Guide](#migration-guide)
22
+ - [Troubleshooting](#troubleshooting)
23
+ - [Contributing](#contributing)
24
+
25
+ ---
26
+
27
+ ## Overview
28
+
29
+ The `@repo/storage` package provides a unified interface for cloud storage operations across multiple providers:
30
+
31
+ - **Vercel Blob**: Optimized for Vercel deployments with edge support
32
+ - **Cloudflare R2**: S3-compatible object storage with global distribution
33
+ - **Cloudflare Images**: Specialized image optimization and delivery
34
+
35
+ ### Architecture
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────────┐
39
+ │ Application Code │
40
+ ├─────────────────────────────────────────────────┤
41
+ │ @repo/storage (Unified Interface) │
42
+ ├──────────────┬──────────────┬───────────────────┤
43
+ │ Vercel Blob │ Cloudflare R2│ Cloudflare Images │
44
+ ├──────────────┴──────────────┴───────────────────┤
45
+ │ Cloud Storage Providers │
46
+ └─────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Features
52
+
53
+ ### Core Capabilities
54
+
55
+ ✅ **Multi-Provider Support**: Switch between providers with configuration change ✅ **Type-Safe**: Full TypeScript with
56
+ strict mode enabled ✅ **Edge Compatible**: Works in Vercel Edge Runtime and Cloudflare Workers ✅ **Server Actions**:
57
+ 24+ Next.js server actions for App Router ✅ **Multipart Uploads**: Automatic chunking for large files (>100MB) ✅
58
+ **Progress Tracking**: Real-time upload progress with callbacks ✅ **Abort Support**: Cancel in-progress uploads ✅
59
+ **Retry Logic**: Automatic retry with exponential backoff ✅ **Health Checks**: Monitor provider availability ✅
60
+ **Structured Errors**: Comprehensive error types with context
61
+
62
+ ### Security Features
63
+
64
+ 🔒 **Authentication**: Built-in auth helpers with session management 🔒 **Rate Limiting**: Configurable rate limits per
65
+ operation 🔒 **Input Validation**: File size, MIME type, and path validation 🔒 **CSRF Protection**: Origin checking and
66
+ token validation 🔒 **Path Sanitization**: Prevent directory traversal attacks
67
+
68
+ ### Developer Experience
69
+
70
+ 🎯 **Comprehensive Tests**: 38 test files with extensive coverage 🎯 **Detailed Examples**: Real-world usage patterns in
71
+ `/examples` 🎯 **JSDoc Documentation**: Inline documentation for all public APIs 🎯 **Error Messages**: Clear,
72
+ actionable error messages 🎯 **TypeScript First**: Designed for TypeScript with excellent type inference
73
+
74
+ ---
75
+
76
+ ## Installation
77
+
78
+ This is an internal workspace package. Add it to your package dependencies:
79
+
80
+ ```json
81
+ {
82
+ "dependencies": {
83
+ "@repo/storage": "workspace:*"
84
+ }
85
+ }
86
+ ```
87
+
88
+ Then install:
89
+
90
+ ```bash
91
+ pnpm install
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Quick Start
97
+
98
+ ### 1. Configure Environment Variables
99
+
100
+ Create a `.env` file:
101
+
102
+ ```bash
103
+ # Required: Choose your provider
104
+ STORAGE_PROVIDER=vercel-blob # or cloudflare-r2, cloudflare-images, multi
105
+
106
+ # Vercel Blob configuration
107
+ VERCEL_BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxx
108
+
109
+ # Cloudflare R2 configuration
110
+ R2_ACCOUNT_ID=your-account-id
111
+ R2_ACCESS_KEY_ID=your-access-key
112
+ R2_SECRET_ACCESS_KEY=your-secret-key
113
+ R2_BUCKET=my-bucket
114
+
115
+ # Cloudflare Images configuration
116
+ CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
117
+ CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
118
+
119
+ # Security settings (recommended for production)
120
+ STORAGE_ENFORCE_AUTH=true
121
+ STORAGE_ENABLE_RATE_LIMIT=true
122
+ STORAGE_ENFORCE_CSRF=true
123
+
124
+ # File limits
125
+ STORAGE_MAX_FILE_SIZE=104857600 # 100MB
126
+ STORAGE_MAX_FILES_PER_UPLOAD=10
127
+
128
+ # Rate limiting
129
+ STORAGE_RATE_LIMIT_REQUESTS=100
130
+ STORAGE_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
131
+ ```
132
+
133
+ ### 2. Basic Upload (Client-Side)
134
+
135
+ ```typescript
136
+ 'use client';
137
+
138
+ import { upload } from '@repo/storage/client';
139
+ import { useState } from 'react';
140
+
141
+ export function FileUploader() {
142
+ const [uploading, setUploading] = useState(false);
143
+
144
+ async function handleUpload(file: File) {
145
+ setUploading(true);
146
+ try {
147
+ const blob = await upload(file.name, file, {
148
+ access: 'public',
149
+ handleUploadUrl: '/api/upload',
150
+ });
151
+ console.log('Uploaded:', blob.url);
152
+ } catch (error) {
153
+ console.error('Upload failed:', error);
154
+ } finally {
155
+ setUploading(false);
156
+ }
157
+ }
158
+
159
+ return (
160
+ <input
161
+ type="file"
162
+ onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
163
+ disabled={uploading}
164
+ />
165
+ );
166
+ }
167
+ ```
168
+
169
+ ### 3. Basic Upload (Server-Side)
170
+
171
+ ```typescript
172
+ "use server";
173
+
174
+ import { uploadMediaAction } from "@repo/storage/server/next";
175
+
176
+ export async function uploadFile(file: File) {
177
+ const result = await uploadMediaAction(`uploads/${Date.now()}-${file.name}`, file, {
178
+ contentType: file.type,
179
+ maxFileSize: 10 * 1024 * 1024, // 10MB
180
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
181
+ });
182
+
183
+ if (!result.success) {
184
+ throw new Error(result.error);
185
+ }
186
+
187
+ return result.data;
188
+ }
189
+ ```
190
+
191
+ ### 4. List Files
192
+
193
+ ```typescript
194
+ import { storage } from "@repo/storage/server";
195
+
196
+ export async function listFiles(prefix: string) {
197
+ const files = await storage.list({
198
+ prefix,
199
+ limit: 100
200
+ });
201
+
202
+ return files.map((file) => ({
203
+ key: file.key,
204
+ url: file.url,
205
+ size: file.size,
206
+ lastModified: file.lastModified
207
+ }));
208
+ }
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Usage Patterns
214
+
215
+ ### Client-Side Upload with Progress
216
+
217
+ ```typescript
218
+ 'use client';
219
+
220
+ import { upload } from '@repo/storage/client/next';
221
+ import { useState } from 'react';
222
+
223
+ export function ProgressUploader() {
224
+ const [progress, setProgress] = useState(0);
225
+
226
+ async function handleUpload(file: File) {
227
+ const blob = await upload(file.name, file, {
228
+ access: 'public',
229
+ handleUploadUrl: '/api/upload',
230
+ onUploadProgress: ({ percentage }) => {
231
+ setProgress(percentage ?? 0);
232
+ },
233
+ });
234
+
235
+ return blob;
236
+ }
237
+
238
+ return (
239
+ <div>
240
+ <input type="file" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} />
241
+ {progress > 0 && <progress value={progress} max={100} />}
242
+ </div>
243
+ );
244
+ }
245
+ ```
246
+
247
+ ### Multiple File Upload
248
+
249
+ ```typescript
250
+ "use client";
251
+
252
+ import { uploadMultipleFiles } from "@repo/storage/client/next";
253
+
254
+ export async function uploadFiles(files: File[]) {
255
+ const results = await uploadMultipleFiles(
256
+ files.map((file) => ({
257
+ file,
258
+ key: `uploads/${Date.now()}-${file.name}`
259
+ })),
260
+ {
261
+ onProgress: (key, progress) => {
262
+ console.log(`${key}: ${progress.percentage}%`);
263
+ },
264
+ onError: (key, error) => {
265
+ console.error(`${key} failed:`, error);
266
+ }
267
+ }
268
+ );
269
+
270
+ return results;
271
+ }
272
+ ```
273
+
274
+ ### Multipart Upload (Large Files)
275
+
276
+ ```typescript
277
+ import { createMultipartUploadManager } from "@repo/storage/server";
278
+ import { storage } from "@repo/storage/server";
279
+
280
+ export async function uploadLargeFile(file: File) {
281
+ const manager = createMultipartUploadManager(storage, `large-files/${file.name}`, file, {
282
+ partSize: 10 * 1024 * 1024, // 10MB parts
283
+ queueSize: 8, // 8 concurrent uploads
284
+ onProgress: (progress) => {
285
+ console.log(`Progress: ${progress.percentage}%`);
286
+ }
287
+ });
288
+
289
+ const result = await manager.upload();
290
+ return result;
291
+ }
292
+ ```
293
+
294
+ ### Presigned URLs (Direct Upload)
295
+
296
+ ```typescript
297
+ import { storage } from "@repo/storage/server";
298
+
299
+ export async function getUploadUrl(filename: string) {
300
+ const presigned = await storage.getPresignedUploadUrl(`uploads/${filename}`, {
301
+ expiresIn: 3600, // 1 hour
302
+ contentType: "image/jpeg"
303
+ });
304
+
305
+ return presigned;
306
+ }
307
+
308
+ // Client-side: Upload directly to presigned URL
309
+ async function uploadWithPresignedUrl(file: File, presigned: PresignedUploadUrl) {
310
+ const formData = new FormData();
311
+ Object.entries(presigned.fields).forEach(([key, value]) => {
312
+ formData.append(key, value);
313
+ });
314
+ formData.append("file", file);
315
+
316
+ const response = await fetch(presigned.url, {
317
+ method: "POST",
318
+ body: formData
319
+ });
320
+
321
+ return response.ok;
322
+ }
323
+ ```
324
+
325
+ ### Delete Files
326
+
327
+ ```typescript
328
+ import { deleteMediaAction } from "@repo/storage/server/next";
329
+
330
+ export async function deleteFile(key: string) {
331
+ const result = await deleteMediaAction(key);
332
+
333
+ if (!result.success) {
334
+ throw new Error(result.error);
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### Bulk Operations
340
+
341
+ ```typescript
342
+ import { bulkDeleteMediaAction } from "@repo/storage/server/next";
343
+
344
+ export async function deleteMultipleFiles(keys: string[]) {
345
+ const result = await bulkDeleteMediaAction(keys);
346
+
347
+ console.log("Succeeded:", result.data?.succeeded);
348
+ console.log("Failed:", result.data?.failed);
349
+
350
+ return result;
351
+ }
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Provider Configuration
357
+
358
+ ### Vercel Blob
359
+
360
+ **Best for**: Vercel deployments, edge runtime, simple use cases
361
+
362
+ ```bash
363
+ STORAGE_PROVIDER=vercel-blob
364
+ VERCEL_BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxx
365
+ ```
366
+
367
+ **Features**:
368
+
369
+ - ✅ Edge compatible
370
+ - ✅ Automatic CDN distribution
371
+ - ✅ Multipart uploads
372
+ - ✅ Progress tracking
373
+ - ❌ No presigned URLs
374
+
375
+ ### Cloudflare R2
376
+
377
+ **Best for**: Large file storage, S3 compatibility, cost optimization
378
+
379
+ ```bash
380
+ STORAGE_PROVIDER=cloudflare-r2
381
+ R2_ACCOUNT_ID=your-account-id
382
+ R2_ACCESS_KEY_ID=your-access-key
383
+ R2_SECRET_ACCESS_KEY=your-secret-key
384
+ R2_BUCKET=my-bucket
385
+ ```
386
+
387
+ **Features**:
388
+
389
+ - ✅ S3-compatible API
390
+ - ✅ Presigned URLs
391
+ - ✅ Multipart uploads
392
+ - ✅ Custom domains
393
+ - ❌ Not edge compatible (uses Node.js AWS SDK)
394
+
395
+ ### Cloudflare Images
396
+
397
+ **Best for**: Image optimization, automatic format conversion, variants
398
+
399
+ ```bash
400
+ STORAGE_PROVIDER=cloudflare-images
401
+ CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
402
+ CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
403
+ CLOUDFLARE_IMAGES_SIGNING_KEY=your-signing-key # Optional
404
+ CLOUDFLARE_IMAGES_DELIVERY_URL=https://imagedelivery.net/xxx # Optional
405
+ ```
406
+
407
+ **Features**:
408
+
409
+ - ✅ Automatic image optimization
410
+ - ✅ Format conversion (WebP, AVIF)
411
+ - ✅ Variants (different sizes)
412
+ - ✅ Signed URLs (with signing key)
413
+ - ❌ Images only
414
+ - ❌ No multipart uploads
415
+
416
+ ### Multi-Provider Setup
417
+
418
+ **Best for**: Using different providers for different file types
419
+
420
+ ```bash
421
+ STORAGE_PROVIDER=multi
422
+
423
+ # Configure multiple providers
424
+ VERCEL_BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxx
425
+ R2_ACCOUNT_ID=your-account-id
426
+ R2_ACCESS_KEY_ID=your-access-key
427
+ R2_SECRET_ACCESS_KEY=your-secret-key
428
+ R2_BUCKET=my-bucket
429
+ CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
430
+ CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
431
+ ```
432
+
433
+ ```typescript
434
+ import { multiStorage } from "@repo/storage/server";
435
+
436
+ // Upload to specific provider
437
+ await multiStorage.upload("images/photo.jpg", imageFile, {
438
+ provider: "cloudflare-images" // Route to Cloudflare Images
439
+ });
440
+
441
+ await multiStorage.upload("documents/file.pdf", pdfFile, {
442
+ provider: "r2-legacy" // Route to Cloudflare R2
443
+ });
444
+
445
+ // Or use automatic routing (configured in env)
446
+ await multiStorage.upload("images/photo.jpg", imageFile); // → Cloudflare Images
447
+ await multiStorage.upload("documents/file.pdf", pdfFile); // → Cloudflare R2
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Security Best Practices
453
+
454
+ ### 1. Enable Authentication
455
+
456
+ **Always enable authentication in production:**
457
+
458
+ ```bash
459
+ STORAGE_ENFORCE_AUTH=true
460
+ ```
461
+
462
+ ```typescript
463
+ // Server action automatically checks authentication
464
+ export async function uploadUserFile(file: File) {
465
+ // If not authenticated, returns { success: false, error: 'Unauthorized' }
466
+ const result = await uploadMediaAction(`user-files/${file.name}`, file);
467
+ return result;
468
+ }
469
+ ```
470
+
471
+ ### 2. Implement Authorization
472
+
473
+ **Check resource ownership:**
474
+
475
+ ```typescript
476
+ import { getSession } from "@repo/storage/server/next";
477
+
478
+ export async function uploadProductImage(productId: string, file: File) {
479
+ const session = await getSession();
480
+ if (!session?.user) {
481
+ throw new Error("Unauthorized");
482
+ }
483
+
484
+ // Check if user owns this product
485
+ const hasAccess = await checkProductAccess(session.user.id, productId, "write");
486
+ if (!hasAccess) {
487
+ throw new Error("Forbidden");
488
+ }
489
+
490
+ // Proceed with upload
491
+ const result = await uploadMediaAction(`products/${productId}/${file.name}`, file);
492
+
493
+ return result;
494
+ }
495
+ ```
496
+
497
+ ### 3. Validate All Inputs
498
+
499
+ **Always validate file size and type:**
500
+
501
+ ```typescript
502
+ const result = await uploadMediaAction(key, file, {
503
+ maxFileSize: 10 * 1024 * 1024, // 10MB
504
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"]
505
+ });
506
+ ```
507
+
508
+ ### 4. Enable Rate Limiting
509
+
510
+ ```bash
511
+ STORAGE_ENABLE_RATE_LIMIT=true
512
+ STORAGE_RATE_LIMIT_REQUESTS=100
513
+ STORAGE_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
514
+ ```
515
+
516
+ ### 5. Sanitize File Paths
517
+
518
+ **Never trust user-provided filenames:**
519
+
520
+ ```typescript
521
+ import { sanitizeStorageKey } from "@repo/storage/keys";
522
+
523
+ const userFilename = req.query.filename; // Untrusted input
524
+ const safeFilename = sanitizeStorageKey(userFilename);
525
+ const key = `uploads/${safeFilename}`;
526
+
527
+ await storage.upload(key, file);
528
+ ```
529
+
530
+ ### 6. Use HTTPS Only
531
+
532
+ **For bulk imports, only allow HTTPS URLs:**
533
+
534
+ ```typescript
535
+ // SSRF protection built into bulkImportFromUrlsAction
536
+ const result = await bulkImportFromUrlsAction([
537
+ {
538
+ sourceUrl: "https://example.com/file.jpg", // ✅ HTTPS only
539
+ destinationKey: "imports/file.jpg"
540
+ }
541
+ ]);
542
+ ```
543
+
544
+ ---
545
+
546
+ ## API Reference
547
+
548
+ ### Client Exports
549
+
550
+ ```typescript
551
+ import { upload, handleUpload } from "@repo/storage/client";
552
+ import { uploadMultipleFiles } from "@repo/storage/client/next";
553
+ ```
554
+
555
+ - `upload(pathname, body, options)` - Upload file to storage
556
+ - `handleUpload(options)` - Get upload handler for form submissions
557
+ - `uploadMultipleFiles(files, options)` - Upload multiple files with progress
558
+
559
+ ### Server Exports
560
+
561
+ ```typescript
562
+ import {
563
+ storage,
564
+ multiStorage,
565
+ uploadMediaAction,
566
+ deleteMediaAction,
567
+ bulkDeleteMediaAction,
568
+ getStorage
569
+ } from "@repo/storage/server/next";
570
+ ```
571
+
572
+ - `storage` - Primary storage provider instance
573
+ - `multiStorage` - Multi-provider manager
574
+ - `uploadMediaAction()` - Server action for uploads
575
+ - `deleteMediaAction()` - Server action for deletion
576
+ - `bulkDeleteMediaAction()` - Bulk delete operation
577
+ - `getStorage()` - Get storage provider instance
578
+
579
+ ### Edge Runtime Exports
580
+
581
+ ```typescript
582
+ import { storage } from "@repo/storage/server/edge";
583
+ ```
584
+
585
+ **Note**: Edge runtime only supports Vercel Blob and Cloudflare Images (no R2 due to Node.js dependency).
586
+
587
+ ### Utility Exports
588
+
589
+ ```typescript
590
+ import { sanitizeStorageKey, validateStorageKey } from "@repo/storage/keys";
591
+ import { env } from "@repo/storage/env";
592
+ import type { StorageObject, UploadOptions } from "@repo/storage/types";
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Migration Guide
598
+
599
+ ### Migrating from Vercel Blob to Cloudflare R2
600
+
601
+ **1. Export existing files:**
602
+
603
+ ```typescript
604
+ import { storage } from "@repo/storage/server";
605
+
606
+ async function exportFiles() {
607
+ const files = await storage.list({ prefix: "uploads/" });
608
+
609
+ for (const file of files) {
610
+ const blob = await storage.download(file.key);
611
+ // Save locally or upload to new provider
612
+ await fs.writeFile(`./exports/${file.key}`, blob);
613
+ }
614
+ }
615
+ ```
616
+
617
+ **2. Update environment variables:**
618
+
619
+ ```bash
620
+ # Before
621
+ STORAGE_PROVIDER=vercel-blob
622
+ VERCEL_BLOB_READ_WRITE_TOKEN=xxx
623
+
624
+ # After
625
+ STORAGE_PROVIDER=cloudflare-r2
626
+ R2_ACCOUNT_ID=xxx
627
+ R2_ACCESS_KEY_ID=xxx
628
+ R2_SECRET_ACCESS_KEY=xxx
629
+ R2_BUCKET=my-bucket
630
+ ```
631
+
632
+ **3. Import files to new provider:**
633
+
634
+ ```typescript
635
+ import { storage } from "@repo/storage/server";
636
+ import fs from "fs/promises";
637
+
638
+ async function importFiles() {
639
+ const files = await fs.readdir("./exports");
640
+
641
+ for (const filename of files) {
642
+ const data = await fs.readFile(`./exports/${filename}`);
643
+ await storage.upload(filename, data);
644
+ }
645
+ }
646
+ ```
647
+
648
+ **4. Update database records:**
649
+
650
+ ```typescript
651
+ // Update URLs in database
652
+ await db.media.updateMany({
653
+ data: {
654
+ url: db.raw("REPLACE(url, 'blob.vercel-storage.com', 'r2.cloudflarestorage.com')")
655
+ }
656
+ });
657
+ ```
658
+
659
+ ---
660
+
661
+ ## Troubleshooting
662
+
663
+ ### "Storage provider configuration is incomplete"
664
+
665
+ **Cause**: Missing required environment variables for selected provider.
666
+
667
+ **Solution**: Check that all required variables are set:
668
+
669
+ ```bash
670
+ # For Vercel Blob
671
+ STORAGE_PROVIDER=vercel-blob
672
+ VERCEL_BLOB_READ_WRITE_TOKEN=xxx
673
+
674
+ # For Cloudflare R2
675
+ STORAGE_PROVIDER=cloudflare-r2
676
+ R2_ACCOUNT_ID=xxx
677
+ R2_ACCESS_KEY_ID=xxx
678
+ R2_SECRET_ACCESS_KEY=xxx
679
+ R2_BUCKET=xxx
680
+ ```
681
+
682
+ ### "Rate limit exceeded"
683
+
684
+ **Cause**: Too many requests in configured time window.
685
+
686
+ **Solution**: Increase rate limit or add backoff logic:
687
+
688
+ ```bash
689
+ STORAGE_RATE_LIMIT_REQUESTS=200 # Increase from 100
690
+ STORAGE_RATE_LIMIT_WINDOW_MS=60000
691
+ ```
692
+
693
+ ### "Invalid storage key"
694
+
695
+ **Cause**: Key contains invalid characters or path traversal.
696
+
697
+ **Solution**: Use `sanitizeStorageKey`:
698
+
699
+ ```typescript
700
+ import { sanitizeStorageKey } from "@repo/storage/keys";
701
+
702
+ const key = sanitizeStorageKey(userInput);
703
+ ```
704
+
705
+ ### Edge runtime errors with R2
706
+
707
+ **Cause**: Cloudflare R2 uses Node.js AWS SDK which is not edge-compatible.
708
+
709
+ **Solution**: Use `@repo/storage/server/edge` which only includes edge-compatible providers:
710
+
711
+ ```typescript
712
+ // ❌ Don't use in edge runtime
713
+ import { storage } from "@repo/storage/server";
714
+
715
+ // ✅ Use edge-compatible export
716
+ import { storage } from "@repo/storage/server/edge";
717
+ ```
718
+
719
+ ### "File size exceeds maximum"
720
+
721
+ **Cause**: File larger than configured limit.
722
+
723
+ **Solution**: Increase limit or use multipart upload:
724
+
725
+ ```bash
726
+ STORAGE_MAX_FILE_SIZE=209715200 # 200MB
727
+ ```
728
+
729
+ Or use multipart for large files:
730
+
731
+ ```typescript
732
+ import { createMultipartUploadManager } from "@repo/storage/server";
733
+
734
+ const manager = createMultipartUploadManager(storage, key, largeFile);
735
+ await manager.upload();
736
+ ```
737
+
738
+ ---
739
+
740
+ ## Testing
741
+
742
+ Run tests:
743
+
744
+ ```bash
745
+ pnpm test # Run all tests
746
+ pnpm test:watch # Watch mode
747
+ pnpm test:coverage # With coverage report
748
+ ```
749
+
750
+ **Test Structure**:
751
+
752
+ ```
753
+ __tests__/
754
+ ├── unit/ # Unit tests
755
+ ├── integration/ # Integration tests
756
+ ├── shared/ # Shared test utilities
757
+ └── test-utils/ # Test factories and helpers
758
+ ```
759
+
760
+ **Coverage**: 38 test files covering:
761
+
762
+ - Provider implementations
763
+ - Server actions
764
+ - Client utilities
765
+ - Validation logic
766
+ - Error handling
767
+ - Edge cases
768
+
769
+ ---
770
+
771
+ ## Performance
772
+
773
+ ### Benchmarks
774
+
775
+ **Upload Performance** (1MB file):
776
+
777
+ - Vercel Blob: ~200ms
778
+ - Cloudflare R2: ~350ms
779
+ - Cloudflare Images: ~250ms
780
+
781
+ **List Performance** (100 files):
782
+
783
+ - Vercel Blob: ~150ms (without N+1 query fix)
784
+ - Cloudflare R2: ~100ms
785
+ - Cloudflare Images: ~120ms
786
+
787
+ **Note**: After implementing N+1 query fix (AUDIT_FINDINGS #7), Vercel Blob list performance improves to ~15ms.
788
+
789
+ ---
790
+
791
+ ## Contributing
792
+
793
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
794
+
795
+ ### Development
796
+
797
+ ```bash
798
+ # Install dependencies
799
+ pnpm install
800
+
801
+ # Run tests
802
+ pnpm test
803
+
804
+ # Type check
805
+ pnpm typecheck
806
+
807
+ # Lint
808
+ pnpm lint
809
+
810
+ # Check for circular dependencies
811
+ pnpm circular
812
+
813
+ # Find unused code
814
+ pnpm knip
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Related Documentation
820
+
821
+ - **[AUDIT_FINDINGS.md](./AUDIT_FINDINGS.md)** - Comprehensive security and code quality audit
822
+ - **[AUDIT_REPORT.md](./AUDIT_REPORT.md)** - Original audit with 47 identified issues
823
+ - **[SECURITY_FIXES.md](./SECURITY_FIXES.md)** - Security improvements tracking
824
+ - **[Examples](./examples/)** - Real-world usage examples
825
+ - **[Monorepo Docs](../../apps/docs/packages/storage.mdx)** - Full documentation site
826
+
827
+ ---
828
+
829
+ ## License
830
+
831
+ Internal package for monorepo use only.
832
+
833
+ ---
834
+
835
+ ## Support
836
+
837
+ For questions or issues:
838
+
839
+ 1. Check [AUDIT_FINDINGS.md](./AUDIT_FINDINGS.md) and [Troubleshooting](#troubleshooting)
840
+ 2. Review [examples](./examples/)
841
+ 3. Contact platform team
842
+
843
+ ---
844
+
845
+ **Version**: Internal workspace package **Maintained by**: Platform Team **Last Updated**: 2025-11-16
846
+
847
+ ## 📚 Comprehensive Documentation
848
+
849
+ For detailed documentation, see:
850
+
851
+ - **[Audit Reports](../../apps/docs/content/docs/audits/storage/)** - Comprehensive audits, fixes, and security reviews
852
+ - **[Technical Guides](../../apps/docs/content/docs/packages/storage/)** - Implementation guides and best practices
853
+
854
+ All comprehensive documentation has been centralized in the docs app.