@appinventiv/aws-s3 1.0.0

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 ADDED
@@ -0,0 +1,595 @@
1
+ # @appinventiv/aws-s3
2
+
3
+ A comprehensive AWS S3 client package for Node.js applications. Provides easy-to-use methods for uploading, reading, deleting files, and generating presigned URLs for both S3 and CloudFront.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @appinventiv/aws-s3
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Upload files to S3 using presigned URLs
14
+ - Read files from S3 buckets
15
+ - Delete files from S3 buckets
16
+ - Generate S3 presigned URLs for uploads
17
+ - Generate CloudFront signed URLs for secure file access
18
+ - Generate CloudFront signed cookies for folder access
19
+ - Get file content as Base64 encoded string
20
+ - Support for private key loading from local filesystem or S3
21
+
22
+ ## Prerequisites
23
+
24
+ - AWS account with S3 access
25
+ - AWS credentials configured (via environment variables, IAM role, or AWS credentials file)
26
+ - `AWS_REGION` environment variable set
27
+ - (Optional) CloudFront distribution for signed URL generation
28
+
29
+ ## AWS Setup
30
+
31
+ 1. Create an S3 bucket in AWS
32
+ 2. Ensure your AWS credentials have permissions to access S3
33
+ 3. Set the `AWS_REGION` environment variable
34
+ 4. (Optional) Configure CloudFront distribution for signed URLs
35
+
36
+ ### Required IAM Permissions
37
+
38
+ ```json
39
+ {
40
+ "Version": "2012-10-17",
41
+ "Statement": [
42
+ {
43
+ "Effect": "Allow",
44
+ "Action": [
45
+ "s3:GetObject",
46
+ "s3:PutObject",
47
+ "s3:DeleteObject",
48
+ "s3:ListBucket"
49
+ ],
50
+ "Resource": [
51
+ "arn:aws:s3:::your-bucket-name",
52
+ "arn:aws:s3:::your-bucket-name/*"
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Basic Setup
62
+
63
+ ```typescript
64
+ import { s3Service } from '@appinventiv/aws-s3';
65
+
66
+ // Set AWS region (required)
67
+ process.env.AWS_REGION = 'us-east-1';
68
+
69
+ // Initialize S3 service
70
+ s3Service.initialiseS3Manager({
71
+ cloudfrontDomain: 'https://d1234567890.cloudfront.net', // Optional
72
+ cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE' // Optional
73
+ });
74
+ ```
75
+
76
+ ### Generate Presigned URL for Upload
77
+
78
+ Generate a presigned URL that allows clients to upload files directly to S3:
79
+
80
+ ```typescript
81
+ import { s3Service } from '@appinventiv/aws-s3';
82
+
83
+ // Initialize S3 service
84
+ s3Service.initialiseS3Manager();
85
+
86
+ // Generate presigned URL for file upload
87
+ const presignedUrl = await s3Service.getPreSignedUrl(
88
+ 'my-bucket', // Bucket name
89
+ 'uploads/images', // Base path/folder
90
+ 'photo.jpg', // File name
91
+ 3600 // Expiration in seconds (1 hour)
92
+ );
93
+
94
+ console.log('Presigned URL:', presignedUrl);
95
+
96
+ // Client can now upload file using this URL
97
+ // Example: PUT request to presignedUrl with file in body
98
+ ```
99
+
100
+ ### Read File from S3
101
+
102
+ Read a file directly from S3 bucket:
103
+
104
+ ```typescript
105
+ import { s3Service } from '@appinventiv/aws-s3';
106
+
107
+ // Initialize S3 service
108
+ s3Service.initialiseS3Manager();
109
+
110
+ // Read file content
111
+ const fileContent = await s3Service.readFile(
112
+ 'uploads/images/photo.jpg', // S3 object key
113
+ 'my-bucket', // Bucket name
114
+ 'utf-8' // Optional encoding (default: UTF-8)
115
+ );
116
+
117
+ console.log('File content:', fileContent);
118
+ ```
119
+
120
+ ### Delete File from S3
121
+
122
+ Delete a file from S3 bucket:
123
+
124
+ ```typescript
125
+ import { s3Service } from '@appinventiv/aws-s3';
126
+
127
+ // Initialize S3 service
128
+ s3Service.initialiseS3Manager();
129
+
130
+ // Delete file
131
+ await s3Service.deleteFile(
132
+ 'my-bucket', // Bucket name
133
+ 'uploads/images/photo.jpg' // S3 object key
134
+ );
135
+
136
+ console.log('File deleted successfully');
137
+ ```
138
+
139
+ ### Get File as Base64
140
+
141
+ Get file content as Base64 encoded string:
142
+
143
+ ```typescript
144
+ import { s3Service } from '@appinventiv/aws-s3';
145
+
146
+ // Initialize S3 service
147
+ s3Service.initialiseS3Manager();
148
+
149
+ // Get file as Base64
150
+ const base64Data = await s3Service.getFileBase64Data({
151
+ bucket: 'my-bucket',
152
+ path: 'uploads/images/photo.jpg'
153
+ });
154
+
155
+ console.log('Base64 data:', base64Data);
156
+ // Use this for embedding in HTML, sending via API, etc.
157
+ ```
158
+
159
+ ### CloudFront Signed URLs
160
+
161
+ Generate CloudFront signed URLs for secure file access:
162
+
163
+ #### Setup CloudFront Private Key
164
+
165
+ First, load the CloudFront private key:
166
+
167
+ ```typescript
168
+ import { s3Service } from '@appinventiv/aws-s3';
169
+
170
+ // Load private key from local filesystem
171
+ await s3Service.loadConfigForReadablePresignedUrl(
172
+ '/path/to/private-key.pem', // Local file path
173
+ false // Not stored on S3
174
+ );
175
+
176
+ // Or load private key from S3
177
+ await s3Service.loadConfigForReadablePresignedUrl(
178
+ 'keys/cloudfront-private-key.pem', // S3 key
179
+ true, // Stored on S3
180
+ 'my-config-bucket' // S3 bucket name
181
+ );
182
+ ```
183
+
184
+ #### Generate CloudFront Signed URL for File
185
+
186
+ ```typescript
187
+ import { s3Service } from '@appinventiv/aws-s3';
188
+
189
+ // Initialize with CloudFront config
190
+ s3Service.initialiseS3Manager({
191
+ cloudfrontDomain: 'https://d1234567890.cloudfront.net',
192
+ cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
193
+ });
194
+
195
+ // Load private key
196
+ await s3Service.loadConfigForReadablePresignedUrl(
197
+ '/path/to/private-key.pem',
198
+ false
199
+ );
200
+
201
+ // Generate signed URL for a file
202
+ const signedUrl = await s3Service.getPreSignedUrlToReadFile(
203
+ '/uploads/images/photo.jpg', // File path (relative to CloudFront domain)
204
+ 3600000 // Expiration in milliseconds (1 hour)
205
+ );
206
+
207
+ console.log('Signed URL:', signedUrl);
208
+ // URL is valid for the specified expiration time
209
+ ```
210
+
211
+ #### Generate CloudFront Signed Cookies for Folder
212
+
213
+ Generate signed cookies that allow access to all files in a folder:
214
+
215
+ ```typescript
216
+ import { s3Service } from '@appinventiv/aws-s3';
217
+
218
+ // Initialize with CloudFront config
219
+ s3Service.initialiseS3Manager({
220
+ cloudfrontDomain: 'https://d1234567890.cloudfront.net',
221
+ cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
222
+ });
223
+
224
+ // Load private key
225
+ await s3Service.loadConfigForReadablePresignedUrl(
226
+ '/path/to/private-key.pem',
227
+ false
228
+ );
229
+
230
+ // Generate signed cookies for folder access
231
+ const cookies = await s3Service.getPreSignedUrlToReadFolder(
232
+ '/uploads/images/', // Folder path (relative to CloudFront domain)
233
+ 3600000 // Expiration in milliseconds (1 hour)
234
+ );
235
+
236
+ console.log('Signed cookies:', cookies);
237
+ // Set these cookies in the browser to access all files in the folder
238
+ ```
239
+
240
+ ### Complete Example
241
+
242
+ ```typescript
243
+ import { s3Service } from '@appinventiv/aws-s3';
244
+
245
+ async function setupS3Service() {
246
+ // Set AWS region
247
+ process.env.AWS_REGION = 'us-east-1';
248
+
249
+ // Initialize S3 service with CloudFront config
250
+ s3Service.initialiseS3Manager({
251
+ cloudfrontDomain: 'https://d1234567890.cloudfront.net',
252
+ cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
253
+ });
254
+
255
+ // Load CloudFront private key
256
+ await s3Service.loadConfigForReadablePresignedUrl(
257
+ './keys/cloudfront-private-key.pem',
258
+ false
259
+ );
260
+
261
+ // Generate presigned URL for upload
262
+ const uploadUrl = await s3Service.getPreSignedUrl(
263
+ 'my-bucket',
264
+ 'uploads',
265
+ 'document.pdf',
266
+ 3600
267
+ );
268
+ console.log('Upload URL:', uploadUrl);
269
+
270
+ // Read file from S3
271
+ const content = await s3Service.readFile(
272
+ 'uploads/document.pdf',
273
+ 'my-bucket'
274
+ );
275
+ console.log('File content:', content);
276
+
277
+ // Get file as Base64
278
+ const base64 = await s3Service.getFileBase64Data({
279
+ bucket: 'my-bucket',
280
+ path: 'uploads/document.pdf'
281
+ });
282
+ console.log('Base64:', base64);
283
+
284
+ // Generate CloudFront signed URL
285
+ const signedUrl = await s3Service.getPreSignedUrlToReadFile(
286
+ '/uploads/document.pdf',
287
+ 3600000
288
+ );
289
+ console.log('Signed URL:', signedUrl);
290
+ }
291
+
292
+ setupS3Service().catch(console.error);
293
+ ```
294
+
295
+ ### Express.js Integration Example
296
+
297
+ ```typescript
298
+ import express from 'express';
299
+ import { s3Service } from '@appinventiv/aws-s3';
300
+
301
+ const app = express();
302
+
303
+ // Initialize S3 on startup
304
+ s3Service.initialiseS3Manager({
305
+ cloudfrontDomain: process.env.CLOUDFRONT_DOMAIN || '',
306
+ cloudfrontKeyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID || ''
307
+ });
308
+
309
+ // Load CloudFront private key
310
+ if (process.env.CLOUDFRONT_PRIVATE_KEY_PATH) {
311
+ await s3Service.loadConfigForReadablePresignedUrl(
312
+ process.env.CLOUDFRONT_PRIVATE_KEY_PATH,
313
+ false
314
+ );
315
+ }
316
+
317
+ // Generate upload URL endpoint
318
+ app.post('/api/upload-url', async (req, res) => {
319
+ try {
320
+ const { fileName, folder } = req.body;
321
+
322
+ const presignedUrl = await s3Service.getPreSignedUrl(
323
+ process.env.S3_BUCKET || 'my-bucket',
324
+ folder || 'uploads',
325
+ fileName,
326
+ 3600
327
+ );
328
+
329
+ res.json({ uploadUrl: presignedUrl });
330
+ } catch (error) {
331
+ res.status(500).json({ error: 'Failed to generate upload URL' });
332
+ }
333
+ });
334
+
335
+ // Get file endpoint
336
+ app.get('/api/file/:key', async (req, res) => {
337
+ try {
338
+ const content = await s3Service.readFile(
339
+ req.params.key,
340
+ process.env.S3_BUCKET || 'my-bucket'
341
+ );
342
+
343
+ res.send(content);
344
+ } catch (error) {
345
+ res.status(404).json({ error: 'File not found' });
346
+ }
347
+ });
348
+
349
+ // Generate signed URL endpoint
350
+ app.post('/api/signed-url', async (req, res) => {
351
+ try {
352
+ const { filePath, expiration } = req.body;
353
+
354
+ const signedUrl = await s3Service.getPreSignedUrlToReadFile(
355
+ filePath,
356
+ expiration || 3600000
357
+ );
358
+
359
+ res.json({ signedUrl });
360
+ } catch (error) {
361
+ res.status(500).json({ error: 'Failed to generate signed URL' });
362
+ }
363
+ });
364
+
365
+ app.listen(3000, () => {
366
+ console.log('Server started on port 3000');
367
+ });
368
+ ```
369
+
370
+ ## API Reference
371
+
372
+ ### `s3Service` (Singleton Instance)
373
+
374
+ Pre-configured S3 service instance ready to use.
375
+
376
+ ### `AWSS3Provider` Class
377
+
378
+ Main S3 provider class.
379
+
380
+ #### `initialiseS3Manager(config?: IS3Config)`
381
+
382
+ Initializes the S3 service with optional CloudFront configuration.
383
+
384
+ **Parameters:**
385
+ - `config.cloudfrontDomain` (string, optional): CloudFront distribution domain
386
+ - `config.cloudfrontKeyPairId` (string, optional): CloudFront key pair ID
387
+
388
+ **Example:**
389
+ ```typescript
390
+ s3Service.initialiseS3Manager({
391
+ cloudfrontDomain: 'https://d1234567890.cloudfront.net',
392
+ cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
393
+ });
394
+ ```
395
+
396
+ #### `loadConfigForReadablePresignedUrl(privateKeyPath: string, isStoredOnS3: boolean, bucket?: string)`
397
+
398
+ Loads CloudFront private key from local filesystem or S3.
399
+
400
+ **Parameters:**
401
+ - `privateKeyPath` (string): Path to private key (local path or S3 key)
402
+ - `isStoredOnS3` (boolean): Whether key is stored in S3
403
+ - `bucket` (string, optional): S3 bucket name (required if `isStoredOnS3` is true)
404
+
405
+ **Example:**
406
+ ```typescript
407
+ // From local filesystem
408
+ await s3Service.loadConfigForReadablePresignedUrl('/path/to/key.pem', false);
409
+
410
+ // From S3
411
+ await s3Service.loadConfigForReadablePresignedUrl('keys/key.pem', true, 'my-bucket');
412
+ ```
413
+
414
+ #### `getPreSignedUrl(bucketName: string, basePath: string, fileName: string, expiresIn?: number)`
415
+
416
+ Generates a presigned URL for uploading files to S3.
417
+
418
+ **Parameters:**
419
+ - `bucketName` (string): S3 bucket name
420
+ - `basePath` (string): Base path/folder in bucket
421
+ - `fileName` (string): Name of the file
422
+ - `expiresIn` (number, optional): Expiration time in seconds (default: 120)
423
+
424
+ **Returns:**
425
+ - `Promise<string>`: Presigned URL for upload
426
+
427
+ **Example:**
428
+ ```typescript
429
+ const url = await s3Service.getPreSignedUrl('my-bucket', 'uploads', 'file.jpg', 3600);
430
+ ```
431
+
432
+ #### `readFile(key: string, bucket: string, encoding?: string)`
433
+
434
+ Reads a file from S3 bucket.
435
+
436
+ **Parameters:**
437
+ - `key` (string): S3 object key (file path)
438
+ - `bucket` (string): S3 bucket name
439
+ - `encoding` (string, optional): File encoding (default: UTF-8)
440
+
441
+ **Returns:**
442
+ - `Promise<string>`: File content as string
443
+
444
+ **Example:**
445
+ ```typescript
446
+ const content = await s3Service.readFile('uploads/file.txt', 'my-bucket', 'utf-8');
447
+ ```
448
+
449
+ #### `deleteFile(bucket: string, key: string)`
450
+
451
+ Deletes a file from S3 bucket.
452
+
453
+ **Parameters:**
454
+ - `bucket` (string): S3 bucket name
455
+ - `key` (string): S3 object key (file path)
456
+
457
+ **Returns:**
458
+ - `Promise<object>`: Delete operation result
459
+
460
+ **Example:**
461
+ ```typescript
462
+ await s3Service.deleteFile('my-bucket', 'uploads/file.jpg');
463
+ ```
464
+
465
+ #### `getFileBase64Data(fileData: { bucket: string; path: string })`
466
+
467
+ Gets file content as Base64 encoded string.
468
+
469
+ **Parameters:**
470
+ - `fileData.bucket` (string): S3 bucket name
471
+ - `fileData.path` (string): S3 object key (file path)
472
+
473
+ **Returns:**
474
+ - `Promise<string>`: Base64 encoded file content
475
+
476
+ **Example:**
477
+ ```typescript
478
+ const base64 = await s3Service.getFileBase64Data({
479
+ bucket: 'my-bucket',
480
+ path: 'uploads/image.jpg'
481
+ });
482
+ ```
483
+
484
+ #### `getPreSignedUrlToReadFile(filePath: string, expiration: number)`
485
+
486
+ Generates a CloudFront signed URL for reading a file.
487
+
488
+ **Parameters:**
489
+ - `filePath` (string): File path relative to CloudFront domain
490
+ - `expiration` (number): Expiration time in milliseconds
491
+
492
+ **Returns:**
493
+ - `Promise<string>`: CloudFront signed URL
494
+
495
+ **Example:**
496
+ ```typescript
497
+ const signedUrl = await s3Service.getPreSignedUrlToReadFile('/uploads/file.jpg', 3600000);
498
+ ```
499
+
500
+ #### `getPreSignedUrlToReadFolder(folderPath: string, expiration: number)`
501
+
502
+ Generates CloudFront signed cookies for folder access.
503
+
504
+ **Parameters:**
505
+ - `folderPath` (string): Folder path relative to CloudFront domain
506
+ - `expiration` (number): Expiration time in milliseconds
507
+
508
+ **Returns:**
509
+ - `Promise<object>`: CloudFront signed cookies object
510
+
511
+ **Example:**
512
+ ```typescript
513
+ const cookies = await s3Service.getPreSignedUrlToReadFolder('/uploads/images/', 3600000);
514
+ ```
515
+
516
+ ## Environment Variables
517
+
518
+ - `AWS_REGION` (required): AWS region where your S3 bucket is located (e.g., `us-east-1`)
519
+
520
+ ## CloudFront Setup
521
+
522
+ To use CloudFront signed URLs:
523
+
524
+ 1. Create a CloudFront distribution pointing to your S3 bucket
525
+ 2. Create a CloudFront key pair in AWS
526
+ 3. Download the private key
527
+ 4. Configure the package with CloudFront domain and key pair ID
528
+ 5. Load the private key using `loadConfigForReadablePresignedUrl()`
529
+
530
+ ## Error Handling
531
+
532
+ The package includes structured error handling with `S3Exception` class. All errors are automatically categorized and returned in a consistent format:
533
+
534
+ ```typescript
535
+ import { s3Service, S3Exception } from '@appinventiv/aws-s3';
536
+
537
+ try {
538
+ await s3Service.readFile('file.jpg', 'my-bucket');
539
+ } catch (error) {
540
+ if (error instanceof S3Exception) {
541
+ const errorResponse = error.getError();
542
+ // Returns: { status: 404, data: { message, type, originalError, context, ... } }
543
+ }
544
+ }
545
+ ```
546
+
547
+ Error types include: Connection, Authentication, Not Found, Validation, Timeout, Server, and Operation errors.
548
+
549
+ ## TypeScript Support
550
+
551
+ The package includes full TypeScript definitions and is written in TypeScript.
552
+
553
+ ## Dependencies
554
+
555
+ - `@aws-sdk/client-s3`: ^3.975.0
556
+ - `@aws-sdk/cloudfront-signer`: ^3.975.0
557
+ - `@aws-sdk/s3-request-presigner`: ^3.975.0
558
+
559
+ ## Security Best Practices
560
+
561
+ 1. **Never commit AWS credentials** to version control
562
+ 2. **Use IAM roles** when running on AWS infrastructure (EC2, ECS, Lambda)
563
+ 3. **Set appropriate expiration times** for presigned URLs
564
+ 4. **Use CloudFront signed URLs** for secure file access
565
+ 5. **Store private keys securely** (use AWS Secrets Manager or environment variables)
566
+ 6. **Use least privilege IAM policies** for S3 access
567
+ 7. **Enable S3 bucket encryption** for sensitive files
568
+
569
+ ## Troubleshooting
570
+
571
+ ### Common Issues
572
+
573
+ 1. **"Unable to Connect Error"**
574
+ - Verify AWS credentials are configured
575
+ - Check `AWS_REGION` environment variable is set
576
+ - Ensure IAM permissions are correct
577
+
578
+ 2. **"File not found" errors**
579
+ - Verify bucket name is correct
580
+ - Check S3 object key (path) is correct
581
+ - Ensure file exists in the bucket
582
+
583
+ 3. **CloudFront signed URL errors**
584
+ - Verify CloudFront domain and key pair ID are correct
585
+ - Ensure private key is loaded before generating signed URLs
586
+ - Check private key format is correct (PEM format)
587
+
588
+ 4. **Presigned URL expiration**
589
+ - URLs expire after the specified time
590
+ - Generate new URLs if expired
591
+ - Consider longer expiration times for production use
592
+
593
+ ## License
594
+
595
+ ISC
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@appinventiv/aws-s3",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@types/node": "^25.0.10",
16
+ "typescript": "^5.9.3"
17
+ },
18
+ "dependencies": {
19
+ "@aws-sdk/client-s3": "^3.975.0",
20
+ "@aws-sdk/cloudfront-signer": "^3.975.0",
21
+ "@aws-sdk/s3-request-presigner": "^3.975.0"
22
+ }
23
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @description HTTP status messages enum
3
+ */
4
+ export enum HTTP_STATUS_MESSAGE {
5
+ BAD_REQUEST = 400,
6
+ UNAUTHORIZED = 401,
7
+ FORBIDDEN = 403,
8
+ NOT_FOUND = 404,
9
+ CONFLICT = 409,
10
+ REQUEST_TIMEOUT = 408,
11
+ TOO_MANY_REQUESTS = 429,
12
+ INTERNAL_SERVER_ERROR = 500,
13
+ SERVICE_UNAVAILABLE = 503
14
+ }
15
+
16
+ /**
17
+ * @description Exception message types for S3 operations
18
+ */
19
+ export enum ExceptionMessage {
20
+ S3_CONNECTION_ERROR = 'S3_CONNECTION_ERROR',
21
+ S3_OPERATION_ERROR = 'S3_OPERATION_ERROR',
22
+ S3_AUTH_ERROR = 'S3_AUTH_ERROR',
23
+ S3_NOT_FOUND = 'S3_NOT_FOUND',
24
+ S3_SERVER_ERROR = 'S3_SERVER_ERROR',
25
+ S3_TIMEOUT_ERROR = 'S3_TIMEOUT_ERROR',
26
+ S3_VALIDATION_ERROR = 'S3_VALIDATION_ERROR',
27
+ S3_UNKNOWN_ERROR = 'S3_UNKNOWN_ERROR'
28
+ }
29
+
30
+ /**
31
+ * @description S3 connection error codes
32
+ */
33
+ export const S3_CONNECTION_ERROR = {
34
+ ECONNREFUSED: 'ECONNREFUSED',
35
+ ENOTFOUND: 'ENOTFOUND',
36
+ ECONNRESET: 'ECONNRESET',
37
+ EHOSTUNREACH: 'EHOSTUNREACH',
38
+ EADDRNOTAVAIL: 'EADDRNOTAVAIL',
39
+ ETIMEDOUT: 'ETIMEDOUT',
40
+ NETWORK_ERROR: 'NetworkError',
41
+ TIMEOUT_ERROR: 'TimeoutError'
42
+ };
43
+
44
+ /**
45
+ * @description S3 operation error codes
46
+ */
47
+ export const S3_OPERATION_ERROR = {
48
+ NO_SUCH_KEY: 'NoSuchKey',
49
+ NO_SUCH_BUCKET: 'NoSuchBucket',
50
+ ACCESS_DENIED: 'AccessDenied',
51
+ INVALID_BUCKET_NAME: 'InvalidBucketName',
52
+ BUCKET_ALREADY_EXISTS: 'BucketAlreadyExists',
53
+ INVALID_OBJECT_STATE: 'InvalidObjectState',
54
+ KEY_TOO_LONG: 'KeyTooLongError',
55
+ INVALID_ARGUMENT: 'InvalidArgument'
56
+ };
57
+
58
+ /**
59
+ * @description Base exception handler class
60
+ */
61
+ export class ExceptionHandler {
62
+ protected code: number;
63
+ protected status: HTTP_STATUS_MESSAGE;
64
+ protected data: any;
65
+
66
+ /**
67
+ * @description Get error response object
68
+ * @returns {object} Error object with status and data
69
+ */
70
+ getError() {
71
+ return {
72
+ status: this.status || HTTP_STATUS_MESSAGE.BAD_REQUEST,
73
+ data: this.data
74
+ };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @description AWS S3 exception handler
80
+ * Categorizes and handles AWS S3 SDK errors
81
+ */
82
+ export class S3Exception extends ExceptionHandler {
83
+ constructor(error: any, context?: { bucket?: string; key?: string; operation?: string }) {
84
+ super();
85
+
86
+ const { type, status, message, name } = this.categorizeS3Error(error, context);
87
+
88
+ this.data = {
89
+ message: message || type,
90
+ type: type,
91
+ originalError: name || 'S3Error',
92
+ stack: error?.stack,
93
+ requestId: error?.$metadata?.requestId,
94
+ context: context || null
95
+ };
96
+ this.status = status;
97
+ }
98
+
99
+ /**
100
+ * @description Categorize S3 error based on error type and code
101
+ * @param {any} error - AWS SDK error object
102
+ * @param {object} context - Additional context (bucket, key, operation)
103
+ * @returns {object} Categorized error information
104
+ */
105
+ private categorizeS3Error(error: any, context?: { bucket?: string; key?: string; operation?: string }): {
106
+ type: ExceptionMessage;
107
+ message: string;
108
+ name: string;
109
+ status: HTTP_STATUS_MESSAGE;
110
+ } {
111
+ const message = (error?.message || '').toLowerCase();
112
+ const name = (error?.name || '').toLowerCase();
113
+ const code = error?.Code || error?.code || '';
114
+ const httpStatusCode = error?.$metadata?.httpStatusCode || 500;
115
+
116
+ // ------------------
117
+ // Connection Errors
118
+ // ------------------
119
+ const connectionMatches = [
120
+ S3_CONNECTION_ERROR.ECONNREFUSED,
121
+ S3_CONNECTION_ERROR.ENOTFOUND,
122
+ S3_CONNECTION_ERROR.ECONNRESET,
123
+ S3_CONNECTION_ERROR.EHOSTUNREACH,
124
+ S3_CONNECTION_ERROR.EADDRNOTAVAIL,
125
+ S3_CONNECTION_ERROR.ETIMEDOUT,
126
+ S3_CONNECTION_ERROR.NETWORK_ERROR,
127
+ S3_CONNECTION_ERROR.TIMEOUT_ERROR,
128
+ 'networkerror',
129
+ 'timeout',
130
+ 'connection'
131
+ ];
132
+
133
+ if (connectionMatches.some((m) => message.includes(m.toLowerCase()) || name.includes(m.toLowerCase()))) {
134
+ return {
135
+ type: ExceptionMessage.S3_CONNECTION_ERROR,
136
+ message: `S3 connection error: ${error?.message || 'Unable to connect to S3'}`,
137
+ name: name || 'S3ConnectionError',
138
+ status: HTTP_STATUS_MESSAGE.SERVICE_UNAVAILABLE
139
+ };
140
+ }
141
+
142
+ // ------------------
143
+ // Authentication Errors
144
+ // ------------------
145
+ const authMatches = [
146
+ S3_OPERATION_ERROR.ACCESS_DENIED,
147
+ 'InvalidAccessKeyId',
148
+ 'SignatureDoesNotMatch',
149
+ 'InvalidSecurity',
150
+ 'MissingSecurityHeader',
151
+ 'TokenRefreshRequired',
152
+ 'InvalidToken',
153
+ 'ExpiredToken',
154
+ 'accessdenied',
155
+ 'unauthorized',
156
+ 'forbidden'
157
+ ];
158
+
159
+ if (
160
+ authMatches.some((m) =>
161
+ code?.toLowerCase().includes(m.toLowerCase()) ||
162
+ message.includes(m.toLowerCase()) ||
163
+ name.includes(m.toLowerCase())
164
+ )
165
+ ) {
166
+ return {
167
+ type: ExceptionMessage.S3_AUTH_ERROR,
168
+ message: `S3 authentication error: ${error?.message || 'Access denied. Check your IAM permissions'}`,
169
+ name: name || 'S3AuthError',
170
+ status: HTTP_STATUS_MESSAGE.UNAUTHORIZED
171
+ };
172
+ }
173
+
174
+ // ------------------
175
+ // Not Found Errors
176
+ // ------------------
177
+ const notFoundMatches = [
178
+ S3_OPERATION_ERROR.NO_SUCH_KEY,
179
+ S3_OPERATION_ERROR.NO_SUCH_BUCKET,
180
+ 'nosuchkey',
181
+ 'nosuchbucket',
182
+ 'notfound'
183
+ ];
184
+
185
+ if (
186
+ notFoundMatches.some((m) =>
187
+ code?.toLowerCase().includes(m.toLowerCase()) ||
188
+ message.includes(m.toLowerCase()) ||
189
+ name.includes(m.toLowerCase())
190
+ )
191
+ ) {
192
+ const bucketInfo = context?.bucket ? ` in bucket '${context.bucket}'` : '';
193
+ const keyInfo = context?.key ? ` for key '${context.key}'` : '';
194
+
195
+ return {
196
+ type: ExceptionMessage.S3_NOT_FOUND,
197
+ message: `S3 resource not found${keyInfo}${bucketInfo}: ${error?.message || 'Resource does not exist'}`,
198
+ name: name || 'S3NotFoundError',
199
+ status: HTTP_STATUS_MESSAGE.NOT_FOUND
200
+ };
201
+ }
202
+
203
+ // ------------------
204
+ // Validation Errors
205
+ // ------------------
206
+ const validationMatches = [
207
+ S3_OPERATION_ERROR.INVALID_BUCKET_NAME,
208
+ S3_OPERATION_ERROR.KEY_TOO_LONG,
209
+ S3_OPERATION_ERROR.INVALID_ARGUMENT,
210
+ 'InvalidArgument',
211
+ 'MalformedXML',
212
+ 'KeyTooLongError',
213
+ 'validation',
214
+ 'invalid'
215
+ ];
216
+
217
+ if (
218
+ validationMatches.some((m) =>
219
+ code?.toLowerCase().includes(m.toLowerCase()) ||
220
+ message.includes(m.toLowerCase()) ||
221
+ name.includes(m.toLowerCase())
222
+ )
223
+ ) {
224
+ return {
225
+ type: ExceptionMessage.S3_VALIDATION_ERROR,
226
+ message: `S3 validation error: ${error?.message || 'Invalid request parameters'}`,
227
+ name: name || 'S3ValidationError',
228
+ status: HTTP_STATUS_MESSAGE.BAD_REQUEST
229
+ };
230
+ }
231
+
232
+ // ------------------
233
+ // Timeout Errors
234
+ // ------------------
235
+ if (
236
+ code === 'RequestTimeout' ||
237
+ message.includes('timeout') ||
238
+ name.includes('timeout') ||
239
+ httpStatusCode === 408
240
+ ) {
241
+ return {
242
+ type: ExceptionMessage.S3_TIMEOUT_ERROR,
243
+ message: `S3 request timeout: ${error?.message || 'Request took too long'}`,
244
+ name: name || 'S3TimeoutError',
245
+ status: HTTP_STATUS_MESSAGE.REQUEST_TIMEOUT
246
+ };
247
+ }
248
+
249
+ // ------------------
250
+ // Server Errors (5xx)
251
+ // ------------------
252
+ if (httpStatusCode >= 500 && httpStatusCode < 600) {
253
+ return {
254
+ type: ExceptionMessage.S3_SERVER_ERROR,
255
+ message: `S3 server error: ${error?.message || 'Internal server error'}`,
256
+ name: name || 'S3ServerError',
257
+ status: HTTP_STATUS_MESSAGE.INTERNAL_SERVER_ERROR
258
+ };
259
+ }
260
+
261
+ // ------------------
262
+ // Operational Errors (4xx)
263
+ // ------------------
264
+ if (httpStatusCode >= 400 && httpStatusCode < 500) {
265
+ return {
266
+ type: ExceptionMessage.S3_OPERATION_ERROR,
267
+ message: `S3 operation error: ${error?.message || 'Operation failed'}`,
268
+ name: name || 'S3OperationError',
269
+ status: <HTTP_STATUS_MESSAGE>httpStatusCode || HTTP_STATUS_MESSAGE.BAD_REQUEST
270
+ };
271
+ }
272
+
273
+ // ------------------
274
+ // Default / Unknown
275
+ // ------------------
276
+ return {
277
+ type: ExceptionMessage.S3_UNKNOWN_ERROR,
278
+ message: `S3 error: ${error?.message || 'Unknown error occurred'}`,
279
+ name: name || 'S3UnknownError',
280
+ status: HTTP_STATUS_MESSAGE.INTERNAL_SERVER_ERROR
281
+ };
282
+ }
283
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './s3';
2
+ export * from './interface';
3
+ export * from './exception-handler';
@@ -0,0 +1,4 @@
1
+ export interface IS3Config {
2
+ cloudfrontDomain: string,
3
+ cloudfrontKeyPairId: string
4
+ }
package/src/s3.ts ADDED
@@ -0,0 +1,251 @@
1
+ import { DeleteObjectCommandInput, GetObjectCommand, GetObjectCommandInput, PutObjectCommand, S3, S3ClientConfig } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
+ import { CloudfrontSignInput, getSignedUrl as CloudFrontSigner, getSignedCookies } from '@aws-sdk/cloudfront-signer';
4
+ import { IS3Config } from './interface';
5
+ import { readFileSync } from 'node:fs';
6
+ import { S3Exception } from './exception-handler';
7
+
8
+ /**
9
+ * @description AWS S3 provider class for managing S3 operations
10
+ * Provides functionality for uploading, reading, deleting files, and generating presigned URLs
11
+ * Supports both S3 presigned URLs and CloudFront signed URLs/cookies
12
+ */
13
+ class AWSS3Provider {
14
+ private client: S3;
15
+ private privateKey: string | Buffer;
16
+ private cloudfrontDomain: string;
17
+ private cloudfrontKeyPairId: string;
18
+
19
+ /**
20
+ * @description Initialize S3 Manager with optional CloudFront configuration
21
+ * @param {IS3Config} config - Optional configuration for CloudFront domain and key pair ID
22
+ */
23
+ public initialiseS3Manager(config?: IS3Config) {
24
+ try {
25
+ this.cloudfrontDomain = config?.cloudfrontDomain ?? '';
26
+ this.cloudfrontKeyPairId = config?.cloudfrontKeyPairId ?? '';
27
+ this.client = new S3(this.getConfiguration());
28
+ console.info('Connected to S3 Manager');
29
+ } catch (error: unknown) {
30
+ console.error(`Error--initializeS3Manager-Unable to Connect--msg :: `, error);
31
+ throw new S3Exception(error, { operation: 'initialiseS3Manager' }).getError();
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @description Get S3 client configuration from environment variables
37
+ * @returns {S3ClientConfig} S3 client configuration object
38
+ */
39
+ private getConfiguration(): S3ClientConfig {
40
+ const creds: S3ClientConfig = {};
41
+ creds.region = <string>process.env.AWS_REGION;
42
+ return creds;
43
+ }
44
+
45
+ /**
46
+ * @description Load private key for CloudFront signed URL generation
47
+ * @param {string} privateKeyPath - Path to the private key file (local path or S3 key)
48
+ * @param {boolean} isStoredOnS3 - Whether the private key is stored in S3 or locally
49
+ * @param {string} bucket - S3 bucket name (required if isStoredOnS3 is true)
50
+ */
51
+ public async loadConfigForReadablePresignedUrl(privateKeyPath: string, isStoredOnS3: boolean, bucket?: string ){
52
+ try {
53
+ if(isStoredOnS3 && bucket){
54
+ this.privateKey = <string>await this.readFile(privateKeyPath, bucket);
55
+ } else {
56
+ this.privateKey = readFileSync(privateKeyPath, 'utf8');
57
+ }
58
+ console.info('Private key Loaded Successfully');
59
+ } catch (error: unknown) {
60
+ console.error(`Error--loadConfigForReadablePresignedUrl--msg :: `, error);
61
+ throw new S3Exception(error, {
62
+ operation: 'loadConfigForReadablePresignedUrl',
63
+ bucket
64
+ }).getError();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @description Generate a presigned URL for uploading files to S3
70
+ * @param {string} bucketName - Name of the S3 bucket
71
+ * @param {string} basePath - Base path/folder in the bucket
72
+ * @param {string} fileName - Name of the file to upload
73
+ * @param {number} expiresIn - Expiration time in seconds (default: 120 seconds / 2 minutes)
74
+ * @returns {Promise<string>} Presigned URL for uploading the file
75
+ */
76
+ public async getPreSignedUrl(
77
+ bucketName: string,
78
+ basePath: string,
79
+ fileName: string,
80
+ expiresIn: number = 120 // Default expiry set to 2 minutes
81
+ ): Promise<string> {
82
+ try {
83
+ console.info(`Generating presigned URL for - ${basePath}/${fileName} in ${bucketName}`);
84
+ const command = new PutObjectCommand({
85
+ Bucket: bucketName,
86
+ Key: `${basePath}/${fileName}`,
87
+ ACL: 'private'
88
+ });
89
+ return await getSignedUrl(this.client, command, { expiresIn });
90
+ } catch (error: unknown) {
91
+ console.error(`Error--getPreSignedUrl--msg :: `, error);
92
+ throw new S3Exception(error, {
93
+ operation: 'getPreSignedUrl',
94
+ bucket: bucketName,
95
+ key: `${basePath}/${fileName}`
96
+ }).getError();
97
+ }
98
+ }
99
+
100
+ /**
101
+ * @description Generate a CloudFront signed URL for reading a file
102
+ * @param {string} filePath - Path to the file in CloudFront (relative to domain)
103
+ * @param {number} expiration - Expiration time in milliseconds
104
+ * @returns {Promise<string>} CloudFront signed URL for reading the file
105
+ */
106
+ public async getPreSignedUrlToReadFile(filePath: string, expiration: number) {
107
+ try {
108
+ const cmd: CloudfrontSignInput = {
109
+ url: `${this.cloudfrontDomain}${filePath}`,
110
+ keyPairId: this.cloudfrontKeyPairId,
111
+ dateLessThan: new Date(Date.now() + expiration).toISOString(),
112
+ privateKey: this.privateKey
113
+ }
114
+
115
+ return CloudFrontSigner(cmd);
116
+ } catch (error: unknown) {
117
+ console.error(`Error--getPreSignedUrlToReadFile--msg :: `, error);
118
+ throw new S3Exception(error, {
119
+ operation: 'getPreSignedUrlToReadFile',
120
+ key: filePath
121
+ }).getError();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @description Generate CloudFront signed cookies for reading files in a folder
127
+ * @param {string} folderPath - Path to the folder in CloudFront (relative to domain)
128
+ * @param {number} expiration - Expiration time in milliseconds
129
+ * @returns {Promise<object>} CloudFront signed cookies object
130
+ */
131
+ public async getPreSignedUrlToReadFolder(folderPath: string, expiration: number) {
132
+ try {
133
+ const policy = {
134
+ Statement: [
135
+ {
136
+ Resource: `${this.cloudfrontDomain}${folderPath}*`,
137
+ Condition: {
138
+ DateLessThan: {
139
+ 'AWS:EpochTime': Math.floor((Date.now() + expiration) / 1000) //new Date(Date.now() + expiration).valueOf()
140
+ }
141
+ }
142
+ }
143
+ ]
144
+ };
145
+ const cmd: CloudfrontSignInput = {
146
+ keyPairId: this.cloudfrontKeyPairId,
147
+ privateKey: this.privateKey,
148
+ policy: JSON.stringify(policy)
149
+ };
150
+
151
+ return getSignedCookies(cmd);
152
+ } catch (error: unknown) {
153
+ console.error(`Error--getPreSignedUrlToReadFolder--msg :: `, error);
154
+ throw new S3Exception(error, {
155
+ operation: 'getPreSignedUrlToReadFolder',
156
+ key: folderPath
157
+ }).getError();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * @description Read a file from S3 bucket
163
+ * @param {string} key - S3 object key (file path)
164
+ * @param {string} bucket - S3 bucket name
165
+ * @param {string} encoding - Optional encoding for the file content (default: UTF-8)
166
+ * @returns {Promise<string>} File content as string
167
+ */
168
+ async readFile(key: string, bucket: string, encoding?: string) {
169
+ try {
170
+ const params: GetObjectCommandInput = {
171
+ Bucket: bucket,
172
+ Key: key
173
+ };
174
+ const file = await this.client.getObject(params);
175
+ return await file.Body?.transformToString(encoding);
176
+ } catch (error: unknown) {
177
+ console.error(`Error--readFile--msg :: `, error);
178
+ throw new S3Exception(error, {
179
+ operation: 'readFile',
180
+ bucket,
181
+ key
182
+ }).getError();
183
+ }
184
+ }
185
+
186
+ /**
187
+ * @description Delete a file from S3 bucket
188
+ * @param {string} bucket - S3 bucket name
189
+ * @param {string} key - S3 object key (file path)
190
+ * @returns {Promise<object>} Delete operation result
191
+ */
192
+ async deleteFile(bucket: string,key: string) {
193
+ try {
194
+ const params: DeleteObjectCommandInput = {
195
+ Bucket: bucket,
196
+ Key: key
197
+ };
198
+ return await this.client.deleteObject(params);
199
+ } catch (error: unknown) {
200
+ console.error(`Error--deleteFile--msg :: `, error);
201
+ throw new S3Exception(error, {
202
+ operation: 'deleteFile',
203
+ bucket,
204
+ key
205
+ }).getError();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * @description Get file content as Base64 encoded string from S3
211
+ * @param {object} fileData - File data object
212
+ * @param {string} fileData.bucket - S3 bucket name
213
+ * @param {string} fileData.path - S3 object key (file path)
214
+ * @returns {Promise<string>} Base64 encoded file content
215
+ * @throws {Error} If file body is empty or unreadable
216
+ */
217
+ async getFileBase64Data(fileData: { bucket: string; path: string }) {
218
+ try {
219
+ const params: GetObjectCommandInput = {
220
+ Bucket: fileData.bucket,
221
+ Key: fileData.path
222
+ };
223
+ const file = await this.client.send(new GetObjectCommand(params));
224
+
225
+ const bytes = await file.Body?.transformToByteArray();
226
+
227
+ if (!bytes || bytes.length === 0) {
228
+ throw new S3Exception(
229
+ { message: `File body is empty or unreadable from S3`, Code: 'EmptyFileBody' },
230
+ {
231
+ operation: 'getFileBase64Data',
232
+ bucket: fileData.bucket,
233
+ key: fileData.path
234
+ }
235
+ ).getError();
236
+ }
237
+
238
+ // Convert to Base64 string
239
+ return Buffer.from(bytes).toString('base64');
240
+ } catch (error: unknown) {
241
+ console.error(`Error--getFileBase64Data--msg :: `, error);
242
+ throw new S3Exception(error, {
243
+ operation: 'getFileBase64Data',
244
+ bucket: fileData.bucket,
245
+ key: fileData.path
246
+ }).getError();
247
+ }
248
+ }
249
+ }
250
+
251
+ export const s3Service = new AWSS3Provider();
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5
+ "module": "commonjs",
6
+ "incremental": true,
7
+ "noEmit": false, /* Specify what module code is generated. */
8
+ "outDir": "./dist", /* Specify an output folder for all emitted files. */
9
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
10
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
11
+ "strict": true, /* Enable all strict type-checking options. */
12
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
13
+ "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */
14
+ "sourceMap": true, /* Create source map files for emitted JavaScript files. */
15
+ "resolveJsonModule": true,
16
+ "experimentalDecorators": true,
17
+ "emitDecoratorMetadata": true,
18
+ "baseUrl": ".",
19
+ }
20
+ }