@bloomneo/appkit 1.5.1 โ†’ 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/AGENTS.md +195 -0
  2. package/CHANGELOG.md +253 -0
  3. package/README.md +147 -799
  4. package/bin/commands/generate.js +7 -7
  5. package/cookbook/README.md +26 -0
  6. package/cookbook/api-key-service.ts +106 -0
  7. package/cookbook/auth-protected-crud.ts +112 -0
  8. package/cookbook/file-upload-pipeline.ts +113 -0
  9. package/cookbook/multi-tenant-saas.ts +87 -0
  10. package/cookbook/real-time-chat.ts +121 -0
  11. package/dist/auth/auth.d.ts +21 -4
  12. package/dist/auth/auth.d.ts.map +1 -1
  13. package/dist/auth/auth.js +56 -44
  14. package/dist/auth/auth.js.map +1 -1
  15. package/dist/auth/defaults.d.ts +1 -1
  16. package/dist/auth/defaults.js +35 -35
  17. package/dist/cache/cache.d.ts +29 -6
  18. package/dist/cache/cache.d.ts.map +1 -1
  19. package/dist/cache/cache.js +72 -44
  20. package/dist/cache/cache.js.map +1 -1
  21. package/dist/cache/defaults.js +25 -25
  22. package/dist/cache/index.d.ts +19 -10
  23. package/dist/cache/index.d.ts.map +1 -1
  24. package/dist/cache/index.js +21 -18
  25. package/dist/cache/index.js.map +1 -1
  26. package/dist/config/defaults.d.ts +1 -1
  27. package/dist/config/defaults.js +8 -8
  28. package/dist/config/index.d.ts +3 -3
  29. package/dist/config/index.js +4 -4
  30. package/dist/database/adapters/mongoose.js +2 -2
  31. package/dist/database/adapters/prisma.js +2 -2
  32. package/dist/database/defaults.d.ts +1 -1
  33. package/dist/database/defaults.js +4 -4
  34. package/dist/database/index.js +2 -2
  35. package/dist/database/index.js.map +1 -1
  36. package/dist/email/defaults.js +20 -20
  37. package/dist/error/defaults.d.ts +1 -1
  38. package/dist/error/defaults.js +12 -12
  39. package/dist/error/error.d.ts +12 -0
  40. package/dist/error/error.d.ts.map +1 -1
  41. package/dist/error/error.js +19 -0
  42. package/dist/error/error.js.map +1 -1
  43. package/dist/error/index.d.ts +14 -3
  44. package/dist/error/index.d.ts.map +1 -1
  45. package/dist/error/index.js +14 -3
  46. package/dist/error/index.js.map +1 -1
  47. package/dist/event/defaults.js +30 -30
  48. package/dist/logger/defaults.d.ts +1 -1
  49. package/dist/logger/defaults.js +40 -40
  50. package/dist/logger/index.d.ts +1 -0
  51. package/dist/logger/index.d.ts.map +1 -1
  52. package/dist/logger/index.js.map +1 -1
  53. package/dist/logger/logger.d.ts +8 -0
  54. package/dist/logger/logger.d.ts.map +1 -1
  55. package/dist/logger/logger.js +13 -3
  56. package/dist/logger/logger.js.map +1 -1
  57. package/dist/logger/transports/console.js +1 -1
  58. package/dist/logger/transports/http.d.ts +1 -1
  59. package/dist/logger/transports/http.js +1 -1
  60. package/dist/logger/transports/webhook.d.ts +1 -1
  61. package/dist/logger/transports/webhook.js +1 -1
  62. package/dist/queue/defaults.d.ts +2 -2
  63. package/dist/queue/defaults.js +38 -38
  64. package/dist/security/defaults.d.ts +1 -1
  65. package/dist/security/defaults.js +29 -29
  66. package/dist/security/index.d.ts +1 -1
  67. package/dist/security/index.js +3 -3
  68. package/dist/security/security.d.ts +1 -1
  69. package/dist/security/security.js +4 -4
  70. package/dist/storage/defaults.js +19 -19
  71. package/dist/util/defaults.d.ts +1 -1
  72. package/dist/util/defaults.js +34 -34
  73. package/dist/util/env.d.ts +35 -0
  74. package/dist/util/env.d.ts.map +1 -0
  75. package/dist/util/env.js +50 -0
  76. package/dist/util/env.js.map +1 -0
  77. package/dist/util/errors.d.ts +52 -0
  78. package/dist/util/errors.d.ts.map +1 -0
  79. package/dist/util/errors.js +82 -0
  80. package/dist/util/errors.js.map +1 -0
  81. package/examples/.env.example +80 -0
  82. package/examples/README.md +16 -0
  83. package/examples/auth.ts +228 -0
  84. package/examples/cache.ts +36 -0
  85. package/examples/config.ts +45 -0
  86. package/examples/database.ts +69 -0
  87. package/examples/email.ts +53 -0
  88. package/examples/error.ts +50 -0
  89. package/examples/event.ts +42 -0
  90. package/examples/logger.ts +41 -0
  91. package/examples/queue.ts +58 -0
  92. package/examples/security.ts +46 -0
  93. package/examples/storage.ts +44 -0
  94. package/examples/util.ts +47 -0
  95. package/llms.txt +591 -0
  96. package/package.json +19 -10
  97. package/src/auth/README.md +850 -0
  98. package/src/cache/README.md +756 -0
  99. package/src/config/README.md +604 -0
  100. package/src/database/README.md +818 -0
  101. package/src/email/README.md +759 -0
  102. package/src/error/README.md +660 -0
  103. package/src/event/README.md +729 -0
  104. package/src/logger/README.md +435 -0
  105. package/src/queue/README.md +851 -0
  106. package/src/security/README.md +612 -0
  107. package/src/storage/README.md +1008 -0
  108. package/src/util/README.md +955 -0
  109. package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
  110. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
  111. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
@@ -0,0 +1,1008 @@
1
+ # @bloomneo/appkit - Storage Module ๐Ÿ“
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@bloomneo/appkit.svg)](https://www.npmjs.com/package/@bloomneo/appkit)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ > Ultra-simple file storage that just works with automatic Local/S3/R2 strategy
7
+
8
+ **One function** returns a storage system with automatic strategy detection.
9
+ Zero configuration needed, production-ready cloud integration by default, with
10
+ built-in CDN support and cost optimization.
11
+
12
+ ## ๐Ÿš€ Why Choose This?
13
+
14
+ - **โšก One Function** - Just `storageClass.get()`, everything else is automatic
15
+ - **โ˜๏ธ Auto Strategy** - Cloud env vars โ†’ Distributed, No vars โ†’ Local
16
+ - **๐Ÿ”ง Zero Configuration** - Smart defaults for everything
17
+ - **๐Ÿ’ฐ Cost Optimized** - R2 prioritized for zero egress fees
18
+ - **๐ŸŒ CDN Ready** - Automatic CDN URL generation
19
+ - **๐Ÿ”’ Security Built-in** - File type validation, size limits, signed URLs
20
+ - **โš–๏ธ Scales Perfectly** - Development โ†’ Production with no code changes
21
+ - **๐Ÿค– AI-Ready** - Optimized for LLM code generation
22
+
23
+ ## ๐Ÿ“ฆ Installation
24
+
25
+ ```bash
26
+ npm install @bloomneo/appkit
27
+ ```
28
+
29
+ ## ๐Ÿƒโ€โ™‚๏ธ Quick Start (30 seconds)
30
+
31
+ ### Local Storage (Development)
32
+
33
+ ```typescript
34
+ import { storageClass } from '@bloomneo/appkit/storage';
35
+
36
+ const storage = storageClass.get();
37
+
38
+ // Upload files
39
+ await storage.put('avatars/user123.jpg', imageBuffer);
40
+
41
+ // Download files
42
+ const imageData = await storage.get('avatars/user123.jpg');
43
+
44
+ // Get public URL
45
+ const url = storage.url('avatars/user123.jpg');
46
+ // โ†’ /uploads/avatars/user123.jpg
47
+
48
+ // List files
49
+ const files = await storage.list('avatars/');
50
+ ```
51
+
52
+ ### Cloud Storage (Production)
53
+
54
+ ```bash
55
+ # Cloudflare R2 (Recommended - Zero egress fees)
56
+ CLOUDFLARE_R2_BUCKET=my-bucket
57
+ CLOUDFLARE_ACCOUNT_ID=account123
58
+ CLOUDFLARE_R2_ACCESS_KEY_ID=access_key
59
+ CLOUDFLARE_R2_SECRET_ACCESS_KEY=secret_key
60
+
61
+ # OR AWS S3 / S3-Compatible
62
+ AWS_S3_BUCKET=my-bucket
63
+ AWS_ACCESS_KEY_ID=access_key
64
+ AWS_SECRET_ACCESS_KEY=secret_key
65
+ ```
66
+
67
+ ```typescript
68
+ import { storageClass } from '@bloomneo/appkit/storage';
69
+
70
+ const storage = storageClass.get();
71
+
72
+ // Same code - now distributed across CDN!
73
+ await storage.put('products/item123.jpg', imageBuffer);
74
+ const url = storage.url('products/item123.jpg');
75
+ // โ†’ https://cdn.example.com/products/item123.jpg
76
+ ```
77
+
78
+ **That's it!** Files automatically sync across all your servers.
79
+
80
+ ## ๐Ÿง  Mental Model
81
+
82
+ ### **Strategy Auto-Detection**
83
+
84
+ Environment variables determine storage backend:
85
+
86
+ ```bash
87
+ # Development/Single Server
88
+ # (no cloud env vars)
89
+ โ†’ Local Strategy: ./uploads/ directory
90
+
91
+ # Production Cloud (Priority: R2 โ†’ S3 โ†’ Local)
92
+ CLOUDFLARE_R2_BUCKET=bucket โ†’ R2 (zero egress fees)
93
+ AWS_S3_BUCKET=bucket โ†’ S3 (AWS/Wasabi/MinIO)
94
+ # No cloud vars โ†’ Local (with warning)
95
+ ```
96
+
97
+ ### **File Organization**
98
+
99
+ ```typescript
100
+ // Organize files with folder structure
101
+ await storage.put('users/123/avatar.jpg', imageBuffer);
102
+ await storage.put('products/456/gallery/1.jpg', imageBuffer);
103
+ await storage.put('documents/contracts/legal.pdf', pdfBuffer);
104
+
105
+ // List by folder
106
+ const userFiles = await storage.list('users/123/');
107
+ const productGallery = await storage.list('products/456/gallery/');
108
+ ```
109
+
110
+ ## ๐Ÿค– LLM Quick Reference - Copy These Patterns
111
+
112
+ ### **Basic Storage Setup (Copy Exactly)**
113
+
114
+ ```typescript
115
+ // โœ… CORRECT - Complete storage setup
116
+ import { storageClass } from '@bloomneo/appkit/storage';
117
+ const storage = storageClass.get();
118
+
119
+ // Upload files
120
+ await storage.put('folder/file.jpg', buffer);
121
+ const data = await storage.get('folder/file.jpg');
122
+ await storage.delete('folder/file.jpg');
123
+
124
+ // URL generation
125
+ const publicUrl = storage.url('file.jpg');
126
+ const signedUrl = await storage.signedUrl('private.pdf', 3600);
127
+
128
+ // File organization
129
+ const files = await storage.list('images/');
130
+ const exists = await storage.exists('document.pdf');
131
+ ```
132
+
133
+ ### **Helper Methods (Copy These)**
134
+
135
+ ```typescript
136
+ // โœ… Quick upload with auto-naming
137
+ const { key, url } = await storageClass.upload(buffer, {
138
+ folder: 'uploads',
139
+ filename: 'document.pdf',
140
+ });
141
+
142
+ // โœ… Quick download with content type
143
+ const { data, contentType } = await storageClass.download('file.jpg');
144
+
145
+ // โœ… Strategy detection
146
+ const strategy = storageClass.getStrategy(); // 'local' | 's3' | 'r2'
147
+ const isCloud = storageClass.hasCloudStorage(); // true if S3/R2
148
+ ```
149
+
150
+ ### **Error Handling (Copy This Pattern)**
151
+
152
+ ```typescript
153
+ // โœ… CORRECT - Comprehensive error handling
154
+ try {
155
+ await storage.put('file.jpg', buffer);
156
+ console.log('โœ… File uploaded successfully');
157
+ } catch (error) {
158
+ if (error.message.includes('File too large')) {
159
+ return res.status(413).json({ error: 'File size limit exceeded' });
160
+ }
161
+ if (error.message.includes('File type not allowed')) {
162
+ return res.status(400).json({ error: 'Invalid file type' });
163
+ }
164
+ console.error('โŒ Upload failed:', error.message);
165
+ return res.status(500).json({ error: 'Upload failed' });
166
+ }
167
+ ```
168
+
169
+ ## โš ๏ธ Common LLM Mistakes - Avoid These
170
+
171
+ ### **Wrong Storage Usage**
172
+
173
+ ```typescript
174
+ // โŒ WRONG - Don't create StorageClass directly
175
+ import { StorageClass } from '@bloomneo/appkit/storage';
176
+ const storage = new StorageClass(config); // Wrong!
177
+
178
+ // โŒ WRONG - Missing await
179
+ storage.put('file.jpg', buffer); // Missing await!
180
+
181
+ // โŒ WRONG - Invalid keys
182
+ await storage.put('/file.jpg', buffer); // Leading slash
183
+ await storage.put('folder/../file.jpg', buffer); // Path traversal
184
+ await storage.put('folder\\file.jpg', buffer); // Backslashes
185
+
186
+ // โœ… CORRECT - Use storageClass.get()
187
+ import { storageClass } from '@bloomneo/appkit/storage';
188
+ const storage = storageClass.get();
189
+ await storage.put('folder/file.jpg', buffer);
190
+ ```
191
+
192
+ ### **Wrong Error Handling**
193
+
194
+ ```typescript
195
+ // โŒ WRONG - Ignoring errors
196
+ await storage.put('file.jpg', buffer); // No try-catch
197
+
198
+ // โŒ WRONG - Generic error handling
199
+ try {
200
+ await storage.put('file.jpg', buffer);
201
+ } catch (error) {
202
+ res.status(500).json({ error: 'Something went wrong' });
203
+ }
204
+
205
+ // โœ… CORRECT - Specific error handling
206
+ try {
207
+ await storage.put('file.jpg', buffer);
208
+ } catch (error) {
209
+ if (error.message.includes('File too large')) {
210
+ return res.status(413).json({
211
+ error: 'File too large',
212
+ maxSize: '50MB',
213
+ });
214
+ }
215
+ throw error;
216
+ }
217
+ ```
218
+
219
+ ### **Wrong Testing**
220
+
221
+ ```typescript
222
+ // โŒ WRONG - No cleanup between tests
223
+ test('should upload file', async () => {
224
+ await storage.put('test.jpg', buffer);
225
+ // Missing: await storageClass.clear();
226
+ });
227
+
228
+ // โœ… CORRECT - Proper test cleanup
229
+ afterEach(async () => {
230
+ await storageClass.clear(); // Essential for tests
231
+ });
232
+ ```
233
+
234
+ ## ๐Ÿšจ Error Handling Patterns
235
+
236
+ ### **File Upload API**
237
+
238
+ ```typescript
239
+ app.post('/upload', upload.single('file'), async (req, res) => {
240
+ try {
241
+ if (!req.file) {
242
+ return res.status(400).json({ error: 'No file uploaded' });
243
+ }
244
+
245
+ // Validate file size (optional - storage handles this)
246
+ if (req.file.size > 50 * 1024 * 1024) {
247
+ // 50MB
248
+ return res.status(413).json({
249
+ error: 'File too large',
250
+ maxSize: '50MB',
251
+ });
252
+ }
253
+
254
+ const key = `uploads/${Date.now()}-${req.file.originalname}`;
255
+
256
+ await storage.put(key, req.file.buffer, {
257
+ contentType: req.file.mimetype,
258
+ });
259
+
260
+ const url = storage.url(key);
261
+
262
+ res.json({
263
+ success: true,
264
+ file: { key, url, size: req.file.size },
265
+ });
266
+ } catch (error) {
267
+ if (error.message.includes('File type not allowed')) {
268
+ return res.status(400).json({
269
+ error: 'Invalid file type',
270
+ allowed: 'jpg, png, pdf, txt',
271
+ });
272
+ }
273
+
274
+ console.error('Upload error:', error);
275
+ res.status(500).json({ error: 'Upload failed' });
276
+ }
277
+ });
278
+ ```
279
+
280
+ ### **File Download API**
281
+
282
+ ```typescript
283
+ app.get('/files/:key(*)', async (req, res) => {
284
+ try {
285
+ const key = req.params.key;
286
+
287
+ if (!(await storage.exists(key))) {
288
+ return res.status(404).json({ error: 'File not found' });
289
+ }
290
+
291
+ const buffer = await storage.get(key);
292
+
293
+ res.setHeader('Content-Type', 'application/octet-stream');
294
+ res.setHeader(
295
+ 'Content-Disposition',
296
+ `attachment; filename="${key.split('/').pop()}"`
297
+ );
298
+
299
+ res.send(buffer);
300
+ } catch (error) {
301
+ console.error('Download error:', error);
302
+ res.status(500).json({ error: 'Download failed' });
303
+ }
304
+ });
305
+ ```
306
+
307
+ ### **Startup Validation**
308
+
309
+ ```typescript
310
+ // โœ… App startup validation
311
+ try {
312
+ storageClass.validateConfig();
313
+ console.log('โœ… Storage validation passed');
314
+ } catch (error) {
315
+ console.error('โŒ Storage validation failed:', error.message);
316
+ process.exit(1);
317
+ }
318
+ ```
319
+
320
+ ## ๐Ÿ”’ Security & Production
321
+
322
+ ### **File Type Security**
323
+
324
+ ```bash
325
+ # โœ… SECURE - Specific file types only
326
+ BLOOM_STORAGE_ALLOWED_TYPES=image/jpeg,image/png,application/pdf,text/plain
327
+
328
+ # โš ๏ธ DEVELOPMENT ONLY - All file types
329
+ BLOOM_STORAGE_ALLOWED_TYPES=*
330
+
331
+ # โœ… SECURE - File size limits
332
+ BLOOM_STORAGE_MAX_SIZE=52428800 # 50MB limit
333
+ ```
334
+
335
+ ### **Production Checklist**
336
+
337
+ - โœ… **Cloud Storage**: Set `AWS_S3_BUCKET` or `CLOUDFLARE_R2_BUCKET`
338
+ - โœ… **File Types**: Set `BLOOM_STORAGE_ALLOWED_TYPES` (never use `*`)
339
+ - โœ… **Size Limits**: Set reasonable `BLOOM_STORAGE_MAX_SIZE`
340
+ - โœ… **CDN**: Set `BLOOM_STORAGE_CDN_URL` for performance
341
+ - โœ… **Error Handling**: Implement proper error responses
342
+ - โœ… **Monitoring**: Log upload/download operations
343
+
344
+ ### **Security Validation**
345
+
346
+ ```typescript
347
+ // File type validation (automatic)
348
+ try {
349
+ await storage.put('malicious.exe', buffer);
350
+ } catch (error) {
351
+ // Error: File type not allowed: application/x-executable
352
+ }
353
+
354
+ // File size validation (automatic)
355
+ try {
356
+ await storage.put('huge.zip', massiveBuffer);
357
+ } catch (error) {
358
+ // Error: File too large: 100MB (max: 50MB)
359
+ }
360
+
361
+ // Path traversal prevention (automatic)
362
+ try {
363
+ await storage.put('../../../etc/passwd', buffer);
364
+ } catch (error) {
365
+ // Error: Storage key contains invalid path components
366
+ }
367
+ ```
368
+
369
+ ## ๐Ÿ“– Complete API Reference
370
+
371
+ ### Core Function
372
+
373
+ ```typescript
374
+ const storage = storageClass.get(); // One function, everything you need
375
+ ```
376
+
377
+ ### File Operations
378
+
379
+ ```typescript
380
+ // Upload files
381
+ await storage.put(key, data, options?);
382
+ await storage.put('file.jpg', buffer, {
383
+ contentType: 'image/jpeg',
384
+ metadata: { userId: '123' },
385
+ cacheControl: 'public, max-age=31536000'
386
+ });
387
+
388
+ // Download files
389
+ const buffer = await storage.get('file.jpg');
390
+
391
+ // Delete files
392
+ const success = await storage.delete('file.jpg');
393
+
394
+ // Check existence
395
+ const exists = await storage.exists('file.jpg');
396
+
397
+ // Copy files
398
+ await storage.copy('source.jpg', 'backup.jpg');
399
+ ```
400
+
401
+ ### URL Generation
402
+
403
+ ```typescript
404
+ // Public URLs
405
+ const url = storage.url('file.jpg');
406
+ // Local: /uploads/file.jpg
407
+ // S3: https://bucket.s3.region.amazonaws.com/file.jpg
408
+ // R2: https://cdn.example.com/file.jpg
409
+
410
+ // Signed URLs (temporary access)
411
+ const signedUrl = await storage.signedUrl('private.pdf', 3600); // 1 hour
412
+ ```
413
+
414
+ ### File Listing
415
+
416
+ ```typescript
417
+ // List all files
418
+ const allFiles = await storage.list();
419
+
420
+ // List with prefix
421
+ const images = await storage.list('images/');
422
+
423
+ // List with limit
424
+ const recent = await storage.list('logs/', 10);
425
+
426
+ // File metadata
427
+ files.forEach((file) => {
428
+ console.log(`${file.key}: ${file.size} bytes, ${file.lastModified}`);
429
+ });
430
+ ```
431
+
432
+ ### Helper Methods
433
+
434
+ ```typescript
435
+ // Quick upload with auto-naming
436
+ const { key, url } = await storageClass.upload(buffer, {
437
+ folder: 'uploads',
438
+ filename: 'document.pdf',
439
+ contentType: 'application/pdf',
440
+ });
441
+
442
+ // Quick download with content type
443
+ const { data, contentType } = await storageClass.download('file.jpg');
444
+ ```
445
+
446
+ ### Utility Methods
447
+
448
+ ```typescript
449
+ // Debug info
450
+ storageClass.getStrategy(); // 'local' | 's3' | 'r2'
451
+ storageClass.hasCloudStorage(); // true if S3/R2 configured
452
+ storageClass.isLocal(); // true if using local storage
453
+ storageClass.getConfig(); // Current configuration
454
+ storageClass.getStats(); // Usage statistics
455
+
456
+ // Cleanup
457
+ await storage.disconnect();
458
+ await storageClass.clear(); // For testing
459
+ ```
460
+
461
+ ## ๐ŸŽฏ Usage Examples
462
+
463
+ ### **Express File Upload API**
464
+
465
+ ```typescript
466
+ import express from 'express';
467
+ import multer from 'multer';
468
+ import { storageClass } from '@bloomneo/appkit/storage';
469
+
470
+ const app = express();
471
+ const storage = storageClass.get();
472
+ const upload = multer({ storage: multer.memoryStorage() });
473
+
474
+ // Single file upload
475
+ app.post('/upload', upload.single('file'), async (req, res) => {
476
+ try {
477
+ if (!req.file) {
478
+ return res.status(400).json({ error: 'No file uploaded' });
479
+ }
480
+
481
+ const timestamp = Date.now();
482
+ const key = `uploads/${timestamp}-${req.file.originalname}`;
483
+
484
+ await storage.put(key, req.file.buffer, {
485
+ contentType: req.file.mimetype,
486
+ metadata: {
487
+ originalName: req.file.originalname,
488
+ uploadedBy: req.user?.id || 'anonymous',
489
+ uploadedAt: new Date().toISOString(),
490
+ },
491
+ });
492
+
493
+ const url = storage.url(key);
494
+
495
+ res.json({
496
+ success: true,
497
+ file: {
498
+ key,
499
+ url,
500
+ size: req.file.size,
501
+ contentType: req.file.mimetype,
502
+ },
503
+ });
504
+ } catch (error) {
505
+ res.status(500).json({ error: error.message });
506
+ }
507
+ });
508
+
509
+ // File download
510
+ app.get('/files/:key(*)', async (req, res) => {
511
+ try {
512
+ const key = req.params.key;
513
+
514
+ if (!(await storage.exists(key))) {
515
+ return res.status(404).json({ error: 'File not found' });
516
+ }
517
+
518
+ const buffer = await storage.get(key);
519
+
520
+ // Set appropriate headers
521
+ res.setHeader('Content-Type', 'application/octet-stream');
522
+ res.setHeader(
523
+ 'Content-Disposition',
524
+ `attachment; filename="${key.split('/').pop()}"`
525
+ );
526
+
527
+ res.send(buffer);
528
+ } catch (error) {
529
+ res.status(500).json({ error: error.message });
530
+ }
531
+ });
532
+
533
+ // Generate signed download URL
534
+ app.get('/files/:key(*)/signed', async (req, res) => {
535
+ try {
536
+ const key = req.params.key;
537
+ const expiresIn = parseInt(req.query.expires as string) || 3600; // 1 hour default
538
+
539
+ const signedUrl = await storage.signedUrl(key, expiresIn);
540
+
541
+ res.json({
542
+ url: signedUrl,
543
+ expiresIn,
544
+ expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
545
+ });
546
+ } catch (error) {
547
+ res.status(500).json({ error: error.message });
548
+ }
549
+ });
550
+ ```
551
+
552
+ ### **Image Processing Pipeline**
553
+
554
+ ```typescript
555
+ import { storageClass } from '@bloomneo/appkit/storage';
556
+ import sharp from 'sharp';
557
+
558
+ const storage = storageClass.get();
559
+
560
+ export class ImageProcessor {
561
+ async processImage(originalKey: string) {
562
+ // Download original
563
+ const originalBuffer = await storage.get(originalKey);
564
+
565
+ // Create different sizes
566
+ const sizes = [
567
+ { name: 'thumb', width: 150, height: 150 },
568
+ { name: 'medium', width: 500, height: 500 },
569
+ { name: 'large', width: 1200, height: 1200 },
570
+ ];
571
+
572
+ const results = [];
573
+
574
+ for (const size of sizes) {
575
+ // Process with Sharp
576
+ const processedBuffer = await sharp(originalBuffer)
577
+ .resize(size.width, size.height, {
578
+ fit: 'cover',
579
+ withoutEnlargement: true,
580
+ })
581
+ .jpeg({ quality: 85 })
582
+ .toBuffer();
583
+
584
+ // Generate new key
585
+ const [name, ext] = originalKey.split('.');
586
+ const newKey = `${name}-${size.name}.${ext}`;
587
+
588
+ // Upload processed image
589
+ await storage.put(newKey, processedBuffer, {
590
+ contentType: 'image/jpeg',
591
+ cacheControl: 'public, max-age=31536000', // 1 year cache
592
+ });
593
+
594
+ results.push({
595
+ size: size.name,
596
+ key: newKey,
597
+ url: storage.url(newKey),
598
+ dimensions: `${size.width}x${size.height}`,
599
+ });
600
+ }
601
+
602
+ return results;
603
+ }
604
+
605
+ async cleanupProcessedImages(originalKey: string) {
606
+ const [name] = originalKey.split('.');
607
+ const files = await storage.list(name);
608
+
609
+ for (const file of files) {
610
+ if (
611
+ file.key.includes('-thumb.') ||
612
+ file.key.includes('-medium.') ||
613
+ file.key.includes('-large.')
614
+ ) {
615
+ await storage.delete(file.key);
616
+ }
617
+ }
618
+ }
619
+ }
620
+ ```
621
+
622
+ ### **Document Management System**
623
+
624
+ ```typescript
625
+ import { storageClass } from '@bloomneo/appkit/storage';
626
+
627
+ const storage = storageClass.get();
628
+
629
+ export class DocumentManager {
630
+ async uploadDocument(
631
+ file: Buffer,
632
+ metadata: {
633
+ userId: string;
634
+ category: string;
635
+ filename: string;
636
+ contentType: string;
637
+ }
638
+ ) {
639
+ const { userId, category, filename } = metadata;
640
+ const timestamp = Date.now();
641
+ const key = `documents/${userId}/${category}/${timestamp}-${filename}`;
642
+
643
+ await storage.put(key, file, {
644
+ contentType: metadata.contentType,
645
+ metadata: {
646
+ userId,
647
+ category,
648
+ originalName: filename,
649
+ uploadedAt: new Date().toISOString(),
650
+ },
651
+ });
652
+
653
+ return {
654
+ documentId: key,
655
+ url: storage.url(key),
656
+ category,
657
+ uploadedAt: new Date(),
658
+ };
659
+ }
660
+
661
+ async getUserDocuments(userId: string, category?: string) {
662
+ const prefix = category
663
+ ? `documents/${userId}/${category}/`
664
+ : `documents/${userId}/`;
665
+
666
+ const files = await storage.list(prefix);
667
+
668
+ return files.map((file) => ({
669
+ documentId: file.key,
670
+ filename: file.key.split('/').pop(),
671
+ category: file.key.split('/')[2],
672
+ size: file.size,
673
+ lastModified: file.lastModified,
674
+ url: storage.url(file.key),
675
+ }));
676
+ }
677
+
678
+ async generateShareLink(documentId: string, expiresInHours: number = 24) {
679
+ const expiresIn = expiresInHours * 3600; // Convert to seconds
680
+ const signedUrl = await storage.signedUrl(documentId, expiresIn);
681
+
682
+ return {
683
+ url: signedUrl,
684
+ expiresAt: new Date(Date.now() + expiresIn * 1000),
685
+ expiresInHours,
686
+ };
687
+ }
688
+
689
+ async deleteDocument(documentId: string) {
690
+ return await storage.delete(documentId);
691
+ }
692
+ }
693
+ ```
694
+
695
+ ### **Backup & Sync System**
696
+
697
+ ```typescript
698
+ import { storageClass } from '@bloomneo/appkit/storage';
699
+
700
+ const storage = storageClass.get();
701
+
702
+ export class BackupManager {
703
+ async createBackup(sourcePrefix: string) {
704
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
705
+ const backupPrefix = `backups/${timestamp}/`;
706
+
707
+ const sourceFiles = await storage.list(sourcePrefix);
708
+ const backupResults = [];
709
+
710
+ for (const file of sourceFiles) {
711
+ const relativePath = file.key.replace(sourcePrefix, '');
712
+ const backupKey = backupPrefix + relativePath;
713
+
714
+ try {
715
+ await storage.copy(file.key, backupKey);
716
+ backupResults.push({
717
+ original: file.key,
718
+ backup: backupKey,
719
+ status: 'success',
720
+ });
721
+ } catch (error) {
722
+ backupResults.push({
723
+ original: file.key,
724
+ backup: backupKey,
725
+ status: 'failed',
726
+ error: error.message,
727
+ });
728
+ }
729
+ }
730
+
731
+ return {
732
+ backupId: timestamp,
733
+ sourcePrefix,
734
+ backupPrefix,
735
+ totalFiles: sourceFiles.length,
736
+ successful: backupResults.filter((r) => r.status === 'success').length,
737
+ failed: backupResults.filter((r) => r.status === 'failed').length,
738
+ results: backupResults,
739
+ };
740
+ }
741
+
742
+ async restoreFromBackup(backupId: string, targetPrefix: string) {
743
+ const backupPrefix = `backups/${backupId}/`;
744
+ const backupFiles = await storage.list(backupPrefix);
745
+
746
+ for (const file of backupFiles) {
747
+ const relativePath = file.key.replace(backupPrefix, '');
748
+ const targetKey = targetPrefix + relativePath;
749
+
750
+ await storage.copy(file.key, targetKey);
751
+ }
752
+
753
+ return {
754
+ restored: backupFiles.length,
755
+ targetPrefix,
756
+ };
757
+ }
758
+
759
+ async cleanupOldBackups(retentionDays: number = 30) {
760
+ const cutoffDate = new Date();
761
+ cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
762
+
763
+ const backups = await storage.list('backups/');
764
+ const oldBackups = backups.filter((file) => file.lastModified < cutoffDate);
765
+
766
+ for (const backup of oldBackups) {
767
+ await storage.delete(backup.key);
768
+ }
769
+
770
+ return {
771
+ deleted: oldBackups.length,
772
+ retentionDays,
773
+ };
774
+ }
775
+ }
776
+ ```
777
+
778
+ ## ๐ŸŒ Environment Variables
779
+
780
+ ### Strategy Selection (Auto-detected)
781
+
782
+ ```bash
783
+ # Priority order: R2 โ†’ S3 โ†’ Local
784
+
785
+ # Cloudflare R2 (Highest priority - zero egress fees)
786
+ CLOUDFLARE_R2_BUCKET=my-bucket
787
+ CLOUDFLARE_ACCOUNT_ID=account_id
788
+ CLOUDFLARE_R2_ACCESS_KEY_ID=access_key
789
+ CLOUDFLARE_R2_SECRET_ACCESS_KEY=secret_key
790
+ CLOUDFLARE_R2_CDN_URL=https://cdn.example.com # Optional CDN
791
+
792
+ # AWS S3 / S3-Compatible (Second priority)
793
+ AWS_S3_BUCKET=my-bucket
794
+ AWS_ACCESS_KEY_ID=access_key
795
+ AWS_SECRET_ACCESS_KEY=secret_key
796
+ AWS_REGION=us-east-1 # Default: us-east-1
797
+
798
+ # S3-Compatible Services (Wasabi, MinIO, etc.)
799
+ S3_ENDPOINT=https://s3.wasabisys.com # Custom endpoint
800
+ S3_FORCE_PATH_STYLE=true # For MinIO
801
+
802
+ # Local Storage (Fallback - no cloud vars needed)
803
+ BLOOM_STORAGE_DIR=./uploads # Default: ./uploads
804
+ BLOOM_STORAGE_BASE_URL=/uploads # Default: /uploads
805
+ ```
806
+
807
+ ### Security & Limits
808
+
809
+ ```bash
810
+ # File validation
811
+ BLOOM_STORAGE_MAX_SIZE=52428800 # 50MB default
812
+ BLOOM_STORAGE_ALLOWED_TYPES=image/*,application/pdf,text/*
813
+
814
+ # Signed URL expiration
815
+ BLOOM_STORAGE_SIGNED_EXPIRY=3600 # 1 hour default
816
+
817
+ # CDN configuration
818
+ BLOOM_STORAGE_CDN_URL=https://cdn.example.com # For any strategy
819
+ ```
820
+
821
+ ## ๐Ÿ”„ Development vs Production
822
+
823
+ ### **Development Mode**
824
+
825
+ ```bash
826
+ # No environment variables needed
827
+ NODE_ENV=development
828
+ ```
829
+
830
+ ```typescript
831
+ const storage = storageClass.get();
832
+ // Strategy: Local filesystem (./uploads/)
833
+ // URLs: /uploads/file.jpg
834
+ // Features: File type validation, size limits
835
+ ```
836
+
837
+ ### **Production Mode**
838
+
839
+ ```bash
840
+ # Cloud storage required
841
+ NODE_ENV=production
842
+ CLOUDFLARE_R2_BUCKET=prod-assets
843
+ # ... other cloud credentials
844
+ ```
845
+
846
+ ```typescript
847
+ const storage = storageClass.get();
848
+ // Strategy: R2 or S3 (distributed)
849
+ // URLs: https://cdn.example.com/file.jpg
850
+ // Features: CDN delivery, signed URLs, zero egress (R2)
851
+ ```
852
+
853
+ ### **Scaling Pattern**
854
+
855
+ ```typescript
856
+ // Week 1: Local development
857
+ // No env vars needed - works immediately
858
+
859
+ // Month 1: Add cloud storage
860
+ // Set CLOUDFLARE_R2_BUCKET - zero code changes
861
+
862
+ // Year 1: Add CDN
863
+ // Set CLOUDFLARE_R2_CDN_URL - automatic CDN delivery
864
+ ```
865
+
866
+ ## ๐Ÿงช Testing
867
+
868
+ ### **Test Setup**
869
+
870
+ ```typescript
871
+ import { storageClass } from '@bloomneo/appkit/storage';
872
+
873
+ describe('File Storage', () => {
874
+ afterEach(async () => {
875
+ // IMPORTANT: Clear storage state between tests
876
+ await storageClass.clear();
877
+ });
878
+
879
+ test('should upload and download files', async () => {
880
+ const storage = storageClass.get();
881
+
882
+ const testData = Buffer.from('Hello, World!');
883
+ await storage.put('test.txt', testData);
884
+
885
+ const downloaded = await storage.get('test.txt');
886
+ expect(downloaded.toString()).toBe('Hello, World!');
887
+ });
888
+
889
+ test('should generate public URLs', async () => {
890
+ const storage = storageClass.get();
891
+
892
+ await storage.put('image.jpg', Buffer.from('fake image'));
893
+ const url = storage.url('image.jpg');
894
+
895
+ expect(url).toMatch(/image\.jpg$/);
896
+ });
897
+ });
898
+ ```
899
+
900
+ ### **Mock Cloud Storage for Tests**
901
+
902
+ ```typescript
903
+ // Force local strategy for testing
904
+ describe('Storage with Local Strategy', () => {
905
+ beforeEach(() => {
906
+ storageClass.reset({
907
+ strategy: 'local',
908
+ local: {
909
+ dir: './test-uploads',
910
+ baseUrl: '/test-uploads',
911
+ maxFileSize: 1048576, // 1MB for tests
912
+ allowedTypes: ['*'],
913
+ createDirs: true,
914
+ },
915
+ });
916
+ });
917
+
918
+ afterEach(async () => {
919
+ await storageClass.clear();
920
+ // Clean up test directory
921
+ await fs.rm('./test-uploads', { recursive: true, force: true });
922
+ });
923
+ });
924
+ ```
925
+
926
+ ## ๐Ÿ“ˆ Performance
927
+
928
+ - **Local Strategy**: ~1ms per operation (filesystem I/O)
929
+ - **S3 Strategy**: ~50-200ms per operation (network + AWS)
930
+ - **R2 Strategy**: ~50-200ms per operation (network + Cloudflare)
931
+ - **CDN URLs**: ~1ms generation (no network calls)
932
+ - **Memory Usage**: <5MB baseline per strategy
933
+
934
+ ## ๐Ÿ’ฐ Cost Comparison
935
+
936
+ | Provider | Storage | Egress | CDN | Best For |
937
+ | ----------------- | ---------- | -------- | ---------- | --------------------------- |
938
+ | **Local** | Free | Free | None | Development, single server |
939
+ | **Cloudflare R2** | $0.015/GB | **FREE** | Included | High-bandwidth, global apps |
940
+ | **AWS S3** | $0.023/GB | $0.09/GB | Extra cost | Enterprise, AWS ecosystem |
941
+ | **Wasabi** | $0.0059/GB | FREE | Extra cost | Archive, backup storage |
942
+
943
+ ## ๐Ÿ” TypeScript Support
944
+
945
+ Full TypeScript support with comprehensive interfaces:
946
+
947
+ ```typescript
948
+ import type {
949
+ Storage,
950
+ StorageFile,
951
+ PutOptions,
952
+ } from '@bloomneo/appkit/storage';
953
+
954
+ // Strongly typed storage operations
955
+ const storage: Storage = storageClass.get();
956
+
957
+ const files: StorageFile[] = await storage.list('images/');
958
+ const options: PutOptions = {
959
+ contentType: 'image/jpeg',
960
+ metadata: { userId: '123' },
961
+ };
962
+
963
+ await storage.put('image.jpg', buffer, options);
964
+ ```
965
+
966
+ ## ๐Ÿ†š Why Not AWS SDK/Google Cloud directly?
967
+
968
+ **Other approaches:**
969
+
970
+ ```javascript
971
+ // AWS SDK: Complex setup, provider-specific
972
+ const AWS = require('aws-sdk');
973
+ const s3 = new AWS.S3({
974
+ accessKeyId: 'key',
975
+ secretAccessKey: 'secret',
976
+ region: 'us-east-1',
977
+ });
978
+
979
+ const params = {
980
+ Bucket: 'bucket',
981
+ Key: 'file.jpg',
982
+ Body: buffer,
983
+ ContentType: 'image/jpeg',
984
+ };
985
+
986
+ s3.upload(params, callback);
987
+ ```
988
+
989
+ **This library:**
990
+
991
+ ```typescript
992
+ // 2 lines, works with any provider
993
+ import { storageClass } from '@bloomneo/appkit/storage';
994
+ await storageClass.get().put('file.jpg', buffer);
995
+ ```
996
+
997
+ **Same features, 90% less code, automatic provider detection.**
998
+
999
+ ## ๐Ÿ“„ License
1000
+
1001
+ MIT ยฉ [Bloomneo](https://github.com/bloomneo)
1002
+
1003
+ ---
1004
+
1005
+ <p align="center">
1006
+ <strong>Built with โค๏ธ by the <a href="https://github.com/bloomneo">Bloomneo Team</a></strong><br>
1007
+ Because file storage should be simple, not a vendor nightmare.
1008
+ </p>