@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
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
|
+
[](./tsconfig.json)
|
|
6
|
+
[](#testing)
|
|
7
|
+
[](#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.
|