vintasend-aws-s3-attachments 0.4.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,93 @@
1
+ # VintaSend AWS S3 Attachments
2
+
3
+ AWS S3 attachment manager for VintaSend notifications. Provides production-ready file storage with presigned URLs and streaming support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install vintasend-aws-s3-attachments
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - ✅ Upload attachments to AWS S3
14
+ - ✅ Generate presigned URLs for secure access
15
+ - ✅ Stream large files efficiently
16
+ - ✅ Automatic content type detection
17
+ - ✅ SHA-256 checksum calculation
18
+ - ✅ Works with S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
19
+ - ✅ TypeScript support with full type safety
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { S3AttachmentManager } from 'vintasend-aws-s3-attachments';
25
+ import { PrismaNotificationBackend } from 'vintasend-prisma';
26
+ import { VintaSendFactory } from 'vintasend';
27
+
28
+ // Create S3 attachment manager
29
+ const attachmentManager = new S3AttachmentManager({
30
+ bucket: 'my-app-notifications',
31
+ region: 'us-east-1',
32
+ keyPrefix: 'attachments/',
33
+ });
34
+
35
+ // Create backend with attachment manager
36
+ const backend = new PrismaNotificationBackend(prisma, attachmentManager);
37
+
38
+ // Create VintaSend instance
39
+ const vintaSend = factory.create(
40
+ adapters,
41
+ backend,
42
+ contextGeneratorsMap,
43
+ logger,
44
+ );
45
+
46
+ // Send notification with attachment
47
+ await vintaSend.sendNotification({
48
+ userId: 'user-123',
49
+ notificationType: 'email',
50
+ title: 'Welcome!',
51
+ bodyTemplate: 'Welcome to our service',
52
+ contextName: 'welcome',
53
+ contextParameters: {},
54
+ sendAfter: null,
55
+ subjectTemplate: 'Welcome',
56
+ extraParams: null,
57
+ attachments: [
58
+ {
59
+ file: Buffer.from('PDF content here'),
60
+ filename: 'welcome.pdf',
61
+ contentType: 'application/pdf',
62
+ },
63
+ ],
64
+ });
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ ### S3AttachmentManagerConfig
70
+
71
+ | Option | Type | Required | Description |
72
+ |--------|------|----------|-------------|
73
+ | `bucket` | `string` | Yes | S3 bucket name where attachments will be stored |
74
+ | `region` | `string` | Yes | AWS region for the S3 bucket |
75
+ | `keyPrefix` | `string` | No | Optional prefix for all S3 keys (e.g., 'attachments/') |
76
+ | `credentials` | `object` | No | AWS credentials (accessKeyId, secretAccessKey) |
77
+ | `endpoint` | `string` | No | Custom endpoint for S3-compatible services |
78
+ | `s3ClientConfig` | `object` | No | Additional S3 client configuration |
79
+
80
+ ### AWS Credentials
81
+
82
+ The manager uses the standard AWS SDK credential chain:
83
+
84
+ 1. **Environment variables**: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
85
+ 2. **IAM roles**: Recommended for EC2/ECS/Lambda deployments
86
+ 3. **Constructor credentials**: Passed directly to the manager
87
+ 4. **AWS credentials file**: `~/.aws/credentials`
88
+
89
+ For more details, see the full README in the repository.
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,100 @@
1
+ import { BaseAttachmentManager } from 'vintasend/dist/services/attachment-manager/base-attachment-manager';
2
+ import type { AttachmentFileRecord, AttachmentFile, FileAttachment } from 'vintasend/dist/types/attachment';
3
+ export interface S3AttachmentManagerConfig {
4
+ /**
5
+ * S3 bucket name where attachments will be stored
6
+ */
7
+ bucket: string;
8
+ /**
9
+ * AWS region for the S3 bucket
10
+ */
11
+ region: string;
12
+ /**
13
+ * Optional prefix for all S3 keys (e.g., 'attachments/')
14
+ * Helps organize files within the bucket
15
+ */
16
+ keyPrefix?: string;
17
+ /**
18
+ * Optional AWS credentials
19
+ * If not provided, uses default credential chain (env vars, IAM roles, etc.)
20
+ */
21
+ credentials?: {
22
+ accessKeyId: string;
23
+ secretAccessKey: string;
24
+ };
25
+ /**
26
+ * Optional custom endpoint for S3-compatible services
27
+ * (e.g., MinIO, DigitalOcean Spaces)
28
+ */
29
+ endpoint?: string;
30
+ /**
31
+ * Optional S3 client configuration
32
+ */
33
+ s3ClientConfig?: Record<string, unknown>;
34
+ }
35
+ /**
36
+ * AWS S3 AttachmentManager implementation for VintaSend.
37
+ *
38
+ * Provides production-ready file storage with:
39
+ * - Secure presigned URLs for file access
40
+ * - Streaming support for large files
41
+ * - Works with S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
42
+ *
43
+ * Note: This manager handles S3 storage only. File metadata (AttachmentFileRecord)
44
+ * is stored by the Backend in its database. The Backend calls this manager's
45
+ * uploadFile() method and stores the returned metadata.
46
+ */
47
+ export declare class S3AttachmentManager extends BaseAttachmentManager {
48
+ private s3Client;
49
+ private bucket;
50
+ private keyPrefix;
51
+ constructor(config: S3AttachmentManagerConfig);
52
+ /**
53
+ * Upload a file to S3 and return metadata.
54
+ *
55
+ * @param file - The file data (Buffer, ReadableStream, or file path)
56
+ * @param filename - The filename to use for storage
57
+ * @param contentType - Optional MIME type (auto-detected if not provided)
58
+ * @returns AttachmentFileRecord with S3 storage metadata
59
+ */
60
+ uploadFile(file: FileAttachment, filename: string, contentType?: string): Promise<AttachmentFileRecord>;
61
+ /**
62
+ * Reconstruct an AttachmentFile accessor from storage metadata.
63
+ *
64
+ * This allows the system to recreate file access objects when loading
65
+ * notifications from the database.
66
+ *
67
+ * @param storageMetadata - S3 storage metadata (bucket, key, region)
68
+ * @returns An S3AttachmentFile instance for accessing the file
69
+ */
70
+ reconstructAttachmentFile(storageMetadata: Record<string, unknown>): AttachmentFile;
71
+ /**
72
+ * Delete a file from S3.
73
+ *
74
+ * Note: This implementation requires the Backend to pass full storage metadata
75
+ * through a separate deleteFileWithMetadata() method, since the fileId alone
76
+ * doesn't contain the S3 key needed for deletion.
77
+ *
78
+ * For S3, the Backend should call deleteFileWithMetadata() instead.
79
+ *
80
+ * @param fileId - The unique identifier of the file
81
+ */
82
+ deleteFile(fileId: string): Promise<void>;
83
+ /**
84
+ * Delete a file from S3 using storage metadata.
85
+ *
86
+ * The Backend should call this method instead of deleteFile() for S3.
87
+ *
88
+ * @param fileId - The unique identifier of the file
89
+ * @param storageMetadata - The storage metadata containing S3 key
90
+ */
91
+ deleteFileWithMetadata(fileId: string, storageMetadata: Record<string, unknown>): Promise<void>;
92
+ /**
93
+ * Build the S3 key for a file.
94
+ *
95
+ * @param fileId - The unique file identifier
96
+ * @param filename - The original filename
97
+ * @returns The full S3 key including prefix
98
+ */
99
+ private buildS3Key;
100
+ }
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3AttachmentManager = void 0;
4
+ const base_attachment_manager_1 = require("vintasend/dist/services/attachment-manager/base-attachment-manager");
5
+ const client_s3_1 = require("@aws-sdk/client-s3");
6
+ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
7
+ const node_stream_1 = require("node:stream");
8
+ const node_crypto_1 = require("node:crypto");
9
+ /**
10
+ * AWS S3 AttachmentManager implementation for VintaSend.
11
+ *
12
+ * Provides production-ready file storage with:
13
+ * - Secure presigned URLs for file access
14
+ * - Streaming support for large files
15
+ * - Works with S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
16
+ *
17
+ * Note: This manager handles S3 storage only. File metadata (AttachmentFileRecord)
18
+ * is stored by the Backend in its database. The Backend calls this manager's
19
+ * uploadFile() method and stores the returned metadata.
20
+ */
21
+ class S3AttachmentManager extends base_attachment_manager_1.BaseAttachmentManager {
22
+ constructor(config) {
23
+ super();
24
+ this.bucket = config.bucket;
25
+ this.keyPrefix = config.keyPrefix || '';
26
+ this.s3Client = new client_s3_1.S3Client({
27
+ region: config.region,
28
+ credentials: config.credentials,
29
+ endpoint: config.endpoint,
30
+ ...config.s3ClientConfig,
31
+ });
32
+ }
33
+ /**
34
+ * Upload a file to S3 and return metadata.
35
+ *
36
+ * @param file - The file data (Buffer, ReadableStream, or file path)
37
+ * @param filename - The filename to use for storage
38
+ * @param contentType - Optional MIME type (auto-detected if not provided)
39
+ * @returns AttachmentFileRecord with S3 storage metadata
40
+ */
41
+ async uploadFile(file, filename, contentType) {
42
+ // Convert file to Buffer
43
+ const buffer = await this.fileToBuffer(file);
44
+ // Calculate checksum
45
+ const checksum = this.calculateChecksum(buffer);
46
+ // Detect content type if not provided
47
+ const finalContentType = contentType || this.detectContentType(filename);
48
+ // Generate unique S3 key
49
+ const fileId = (0, node_crypto_1.randomUUID)();
50
+ const key = this.buildS3Key(fileId, filename);
51
+ // Upload to S3
52
+ const putCommand = new client_s3_1.PutObjectCommand({
53
+ Bucket: this.bucket,
54
+ Key: key,
55
+ Body: buffer,
56
+ ContentType: finalContentType,
57
+ Metadata: {
58
+ originalFilename: filename,
59
+ checksum: checksum,
60
+ },
61
+ });
62
+ await this.s3Client.send(putCommand);
63
+ // Return file record with S3 metadata
64
+ return {
65
+ id: fileId,
66
+ filename,
67
+ contentType: finalContentType,
68
+ size: buffer.length,
69
+ checksum,
70
+ storageMetadata: {
71
+ bucket: this.bucket,
72
+ key,
73
+ region: this.s3Client.config.region,
74
+ },
75
+ createdAt: new Date(),
76
+ updatedAt: new Date(),
77
+ };
78
+ }
79
+ /**
80
+ * Reconstruct an AttachmentFile accessor from storage metadata.
81
+ *
82
+ * This allows the system to recreate file access objects when loading
83
+ * notifications from the database.
84
+ *
85
+ * @param storageMetadata - S3 storage metadata (bucket, key, region)
86
+ * @returns An S3AttachmentFile instance for accessing the file
87
+ */
88
+ reconstructAttachmentFile(storageMetadata) {
89
+ if (!storageMetadata.key || !storageMetadata.bucket) {
90
+ throw new Error('Storage metadata must contain bucket and key for S3 files');
91
+ }
92
+ return new S3AttachmentFile(this.s3Client, storageMetadata.bucket, storageMetadata.key);
93
+ }
94
+ /**
95
+ * Delete a file from S3.
96
+ *
97
+ * Note: This implementation requires the Backend to pass full storage metadata
98
+ * through a separate deleteFileWithMetadata() method, since the fileId alone
99
+ * doesn't contain the S3 key needed for deletion.
100
+ *
101
+ * For S3, the Backend should call deleteFileWithMetadata() instead.
102
+ *
103
+ * @param fileId - The unique identifier of the file
104
+ */
105
+ async deleteFile(fileId) {
106
+ throw new Error('deleteFile() requires storage metadata for S3. Use deleteFileWithMetadata() or delete directly via AttachmentFile.delete()');
107
+ }
108
+ /**
109
+ * Delete a file from S3 using storage metadata.
110
+ *
111
+ * The Backend should call this method instead of deleteFile() for S3.
112
+ *
113
+ * @param fileId - The unique identifier of the file
114
+ * @param storageMetadata - The storage metadata containing S3 key
115
+ */
116
+ async deleteFileWithMetadata(fileId, storageMetadata) {
117
+ if (!storageMetadata.key) {
118
+ throw new Error('Storage metadata must contain key for S3 file deletion');
119
+ }
120
+ const deleteCommand = new client_s3_1.DeleteObjectCommand({
121
+ Bucket: this.bucket,
122
+ Key: storageMetadata.key,
123
+ });
124
+ await this.s3Client.send(deleteCommand);
125
+ }
126
+ /**
127
+ * Build the S3 key for a file.
128
+ *
129
+ * @param fileId - The unique file identifier
130
+ * @param filename - The original filename
131
+ * @returns The full S3 key including prefix
132
+ */
133
+ buildS3Key(fileId, filename) {
134
+ // Use fileId as the primary key component to ensure uniqueness
135
+ // Include original filename for better organization and debugging
136
+ const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
137
+ return `${this.keyPrefix}${fileId}/${sanitizedFilename}`;
138
+ }
139
+ }
140
+ exports.S3AttachmentManager = S3AttachmentManager;
141
+ /**
142
+ * S3 AttachmentFile implementation.
143
+ *
144
+ * Provides access to files stored in AWS S3 with support for:
145
+ * - Reading entire files into memory
146
+ * - Streaming large files
147
+ * - Generating presigned URLs for secure access
148
+ */
149
+ class S3AttachmentFile {
150
+ constructor(s3Client, bucket, key) {
151
+ this.s3Client = s3Client;
152
+ this.bucket = bucket;
153
+ this.key = key;
154
+ }
155
+ /**
156
+ * Read the entire file into memory as a Buffer.
157
+ *
158
+ * Use this for small files. For large files, prefer streaming.
159
+ */
160
+ async read() {
161
+ const getCommand = new client_s3_1.GetObjectCommand({
162
+ Bucket: this.bucket,
163
+ Key: this.key,
164
+ });
165
+ const response = await this.s3Client.send(getCommand);
166
+ if (!response.Body) {
167
+ throw new Error('S3 object has no body');
168
+ }
169
+ // Convert stream to buffer
170
+ return this.streamToBuffer(response.Body);
171
+ }
172
+ /**
173
+ * Get a readable stream for the file.
174
+ *
175
+ * Use this for large files to avoid loading everything into memory.
176
+ */
177
+ async stream() {
178
+ const getCommand = new client_s3_1.GetObjectCommand({
179
+ Bucket: this.bucket,
180
+ Key: this.key,
181
+ });
182
+ const response = await this.s3Client.send(getCommand);
183
+ if (!response.Body) {
184
+ throw new Error('S3 object has no body');
185
+ }
186
+ // Convert Node.js Readable to Web ReadableStream
187
+ const nodeStream = response.Body;
188
+ return node_stream_1.Readable.toWeb(nodeStream);
189
+ }
190
+ /**
191
+ * Generate a presigned URL for accessing the file.
192
+ *
193
+ * @param expiresIn - Seconds until the URL expires (default: 3600 = 1 hour)
194
+ * @returns A presigned URL for secure file access
195
+ */
196
+ async url(expiresIn = 3600) {
197
+ const getCommand = new client_s3_1.GetObjectCommand({
198
+ Bucket: this.bucket,
199
+ Key: this.key,
200
+ });
201
+ return (0, s3_request_presigner_1.getSignedUrl)(this.s3Client, getCommand, { expiresIn });
202
+ }
203
+ /**
204
+ * Delete this file from S3.
205
+ *
206
+ * Note: This is typically called by the AttachmentManager, not directly.
207
+ */
208
+ async delete() {
209
+ const deleteCommand = new client_s3_1.DeleteObjectCommand({
210
+ Bucket: this.bucket,
211
+ Key: this.key,
212
+ });
213
+ await this.s3Client.send(deleteCommand);
214
+ }
215
+ /**
216
+ * Convert a Node.js Readable stream to a Buffer.
217
+ */
218
+ async streamToBuffer(stream) {
219
+ const chunks = [];
220
+ for await (const chunk of stream) {
221
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
222
+ }
223
+ return Buffer.concat(chunks);
224
+ }
225
+ }
@@ -0,0 +1 @@
1
+ export { S3AttachmentManager, type S3AttachmentManagerConfig, } from './aws-s3-attachment-manager';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3AttachmentManager = void 0;
4
+ var aws_s3_attachment_manager_1 = require("./aws-s3-attachment-manager");
5
+ Object.defineProperty(exports, "S3AttachmentManager", { enumerable: true, get: function () { return aws_s3_attachment_manager_1.S3AttachmentManager; } });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vintasend-aws-s3-attachments",
3
+ "version": "0.4.0",
4
+ "description": "Implementation of AWS S3 Attachments for VintaSend",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "prepublishOnly": "npm run build",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch",
11
+ "test:coverage": "jest --coverage"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "author": "Hugo Bessa",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@aws-sdk/client-s3": "^3.967.0",
20
+ "@aws-sdk/s3-request-presigner": "^3.967.0",
21
+ "vintasend": "^0.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/jest": "^29.5.14",
25
+ "jest": "^29.7.0",
26
+ "ts-jest": "^29.2.6",
27
+ "typescript": "^5.8.2"
28
+ }
29
+ }