@strapi/provider-upload-aws-s3 5.35.0 → 5.36.1

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/dist/index.mjs CHANGED
@@ -1,14 +1,110 @@
1
1
  import { getOr } from 'lodash/fp';
2
- import { S3Client, GetObjectCommand, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3';
2
+ import { S3Client, GetObjectCommand, DeleteObjectCommand, ObjectCannedACL, HeadObjectCommand, ChecksumAlgorithm, StorageClass, ServerSideEncryption } from '@aws-sdk/client-s3';
3
3
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
4
  import { Upload } from '@aws-sdk/lib-storage';
5
5
  import { isUrlFromBucket, extractCredentials } from './utils.mjs';
6
6
 
7
- const assertUrlProtocol = (url)=>{
8
- // Regex to test protocol like "http://", "https://"
9
- return /^\w*:\/\//.test(url);
7
+ /**
8
+ * Validates that a URL uses HTTP or HTTPS protocol.
9
+ * Rejects dangerous protocols like file://, javascript:, data:, etc.
10
+ */ const assertUrlProtocol = (url)=>{
11
+ return /^https?:\/\//.test(url);
10
12
  };
11
- const getConfig = ({ baseUrl, rootPath, s3Options, ...legacyS3Options })=>{
13
+ /**
14
+ * Sanitizes a path component to prevent path traversal attacks.
15
+ * Removes directory traversal sequences and normalizes the path.
16
+ */ const sanitizePathComponent = (component)=>{
17
+ if (!component) return '';
18
+ return component.replace(/\.\./g, '').replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
19
+ };
20
+ /**
21
+ * Maps the provider checksum algorithm to the AWS SDK checksum algorithm.
22
+ */ const mapChecksumAlgorithm = (algorithm)=>{
23
+ if (!algorithm) return undefined;
24
+ const mapping = {
25
+ CRC32: ChecksumAlgorithm.CRC32,
26
+ CRC32C: ChecksumAlgorithm.CRC32C,
27
+ SHA1: ChecksumAlgorithm.SHA1,
28
+ SHA256: ChecksumAlgorithm.SHA256,
29
+ CRC64NVME: ChecksumAlgorithm.CRC64NVME
30
+ };
31
+ return mapping[algorithm];
32
+ };
33
+ /**
34
+ * Maps the provider storage class to the AWS SDK storage class.
35
+ */ const mapStorageClass = (storageClass)=>{
36
+ if (!storageClass) return undefined;
37
+ const mapping = {
38
+ STANDARD: StorageClass.STANDARD,
39
+ REDUCED_REDUNDANCY: StorageClass.REDUCED_REDUNDANCY,
40
+ STANDARD_IA: StorageClass.STANDARD_IA,
41
+ ONEZONE_IA: StorageClass.ONEZONE_IA,
42
+ INTELLIGENT_TIERING: StorageClass.INTELLIGENT_TIERING,
43
+ GLACIER: StorageClass.GLACIER,
44
+ DEEP_ARCHIVE: StorageClass.DEEP_ARCHIVE,
45
+ GLACIER_IR: StorageClass.GLACIER_IR
46
+ };
47
+ return mapping[storageClass];
48
+ };
49
+ /**
50
+ * Maps the provider encryption type to the AWS SDK server-side encryption.
51
+ */ const mapServerSideEncryption = (type)=>{
52
+ if (!type) return undefined;
53
+ const mapping = {
54
+ AES256: ServerSideEncryption.AES256,
55
+ 'aws:kms': ServerSideEncryption.aws_kms,
56
+ 'aws:kms:dsse': ServerSideEncryption.aws_kms_dsse
57
+ };
58
+ return mapping[type];
59
+ };
60
+ /**
61
+ * Converts a tags object to the S3 Tagging header format.
62
+ * Format: key1=value1&key2=value2
63
+ */ const formatTagsForHeader = (tags)=>{
64
+ if (!tags || Object.keys(tags).length === 0) return undefined;
65
+ return Object.entries(tags).map(([key, value])=>`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
66
+ };
67
+ /**
68
+ * Checks if the endpoint appears to be a non-AWS S3-compatible provider.
69
+ */ const isNonAwsEndpoint = (endpoint)=>{
70
+ if (!endpoint) return false;
71
+ const awsPatterns = [
72
+ /\.amazonaws\.com$/i,
73
+ /\.amazonaws\.com\.cn$/i
74
+ ];
75
+ return !awsPatterns.some((pattern)=>pattern.test(endpoint));
76
+ };
77
+ /**
78
+ * Validates provider configuration and emits warnings for potential compatibility issues.
79
+ */ const validateProviderConfig = (providerConfig, s3Options)=>{
80
+ if (!providerConfig) return;
81
+ const endpoint = s3Options?.endpoint?.toString() || '';
82
+ const isNonAws = isNonAwsEndpoint(endpoint);
83
+ // Warn about AWS-specific features when using non-AWS endpoints
84
+ if (isNonAws) {
85
+ if (providerConfig.storageClass) {
86
+ process.emitWarning(`Storage class '${providerConfig.storageClass}' is AWS S3-specific and may be ignored by your S3-compatible provider.`);
87
+ }
88
+ if (providerConfig.encryption?.type && providerConfig.encryption.type !== 'AES256') {
89
+ process.emitWarning(`Encryption type '${providerConfig.encryption.type}' is AWS S3-specific. Consider using 'AES256' for better compatibility.`);
90
+ }
91
+ }
92
+ // Validate multipart configuration
93
+ if (providerConfig.multipart?.partSize) {
94
+ const minPartSize = 5 * 1024 * 1024; // 5MB
95
+ const maxPartSize = 5 * 1024 * 1024 * 1024; // 5GB
96
+ if (providerConfig.multipart.partSize < minPartSize) {
97
+ process.emitWarning(`Multipart partSize ${providerConfig.multipart.partSize} is below the minimum of 5MB. This may cause upload failures.`);
98
+ }
99
+ if (providerConfig.multipart.partSize > maxPartSize) {
100
+ process.emitWarning(`Multipart partSize ${providerConfig.multipart.partSize} exceeds the maximum of 5GB. This may cause upload failures.`);
101
+ }
102
+ }
103
+ if (providerConfig.multipart?.queueSize && providerConfig.multipart.queueSize > 16) {
104
+ process.emitWarning(`Multipart queueSize ${providerConfig.multipart.queueSize} is high and may cause memory issues. Consider using 4-8.`);
105
+ }
106
+ };
107
+ const getConfig = ({ s3Options, legacyS3Options })=>{
12
108
  if (Object.keys(legacyS3Options).length > 0) {
13
109
  process.emitWarning("S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property.");
14
110
  }
@@ -23,65 +119,219 @@ const getConfig = ({ baseUrl, rootPath, s3Options, ...legacyS3Options })=>{
23
119
  credentials
24
120
  } : {}
25
121
  };
26
- config.params.ACL = getOr(ObjectCannedACL.public_read, [
27
- 'params',
28
- 'ACL'
29
- ], config);
122
+ if (config.params !== undefined) {
123
+ // Only set default ACL when ACL is not explicitly present in params.
124
+ // Since April 2023, new AWS S3 buckets have ACLs disabled by default
125
+ // ("Bucket owner enforced"). Sending an ACL header to such buckets
126
+ // throws AccessControlListNotSupported. To disable ACLs, users should
127
+ // simply not include ACL in their params configuration.
128
+ if (!('ACL' in config.params)) {
129
+ config.params.ACL = ObjectCannedACL.public_read;
130
+ }
131
+ } else {
132
+ throw new Error('Upload AWS S3 provider: `params` are required in the config object');
133
+ }
30
134
  return config;
31
135
  };
32
136
  var index = {
33
- init ({ baseUrl, rootPath, s3Options, ...legacyS3Options }) {
137
+ init ({ baseUrl, rootPath, s3Options, providerConfig, ...legacyS3Options }) {
138
+ // Validate configuration and emit warnings for potential issues
139
+ validateProviderConfig(providerConfig, s3Options);
34
140
  // TODO V5 change config structure to avoid having to do this
35
141
  const config = getConfig({
36
- baseUrl,
37
- rootPath,
38
- s3Options,
39
- ...legacyS3Options
142
+ s3Options: s3Options,
143
+ legacyS3Options
40
144
  });
41
145
  const s3Client = new S3Client(config);
42
146
  const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, '')}/` : '';
43
147
  const getFileKey = (file)=>{
44
- const path = file.path ? `${file.path}/` : '';
45
- return `${filePrefix}${path}${file.hash}${file.ext}`;
148
+ const sanitizedPath = sanitizePathComponent(file.path);
149
+ const path = sanitizedPath ? `${sanitizedPath}/` : '';
150
+ const sanitizedHash = sanitizePathComponent(file.hash);
151
+ const sanitizedExt = file.ext ? file.ext.replace(/[^a-zA-Z0-9.]/g, '') : '';
152
+ return `${filePrefix}${path}${sanitizedHash}${sanitizedExt}`;
153
+ };
154
+ /**
155
+ * Builds the upload parameters including all configured features.
156
+ */ const buildUploadParams = (file, fileKey, customParams = {})=>{
157
+ const params = {
158
+ Bucket: config.params.Bucket,
159
+ Key: fileKey,
160
+ Body: file.stream || Buffer.from(file.buffer, 'binary'),
161
+ // ACL is optional to support providers like Cloudflare R2 that don't support ACLs.
162
+ // Set params.ACL to undefined or omit it entirely to disable ACL headers.
163
+ ...config.params.ACL ? {
164
+ ACL: config.params.ACL
165
+ } : {},
166
+ ContentType: file.mime
167
+ };
168
+ // Checksum validation
169
+ const checksumAlgorithm = mapChecksumAlgorithm(providerConfig?.checksumAlgorithm);
170
+ if (checksumAlgorithm) {
171
+ params.ChecksumAlgorithm = checksumAlgorithm;
172
+ }
173
+ // Conditional writes - prevent overwrite
174
+ if (providerConfig?.preventOverwrite) {
175
+ params.IfNoneMatch = '*';
176
+ }
177
+ // Storage class
178
+ const storageClass = mapStorageClass(providerConfig?.storageClass);
179
+ if (storageClass) {
180
+ params.StorageClass = storageClass;
181
+ }
182
+ // Server-side encryption
183
+ if (providerConfig?.encryption) {
184
+ const sse = mapServerSideEncryption(providerConfig.encryption.type);
185
+ if (sse) {
186
+ params.ServerSideEncryption = sse;
187
+ if (providerConfig.encryption.kmsKeyId) {
188
+ params.SSEKMSKeyId = providerConfig.encryption.kmsKeyId;
189
+ }
190
+ }
191
+ }
192
+ // Object tagging
193
+ const tagging = formatTagsForHeader(providerConfig?.tags);
194
+ if (tagging) {
195
+ params.Tagging = tagging;
196
+ }
197
+ // Merge customParams but preserve critical security parameters
198
+ // Bucket, Key, and Body must not be overridden by customParams
199
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
200
+ const { Bucket, Key, Body, ...safeCustomParams } = customParams;
201
+ return {
202
+ ...params,
203
+ ...safeCustomParams
204
+ };
205
+ };
206
+ /**
207
+ * Constructs the correct file URL.
208
+ * Handles S3-compatible providers that return incorrect Location formats.
209
+ */ const constructFileUrl = (fileKey, uploadLocation)=>{
210
+ // Priority 1: Use baseUrl if configured (CDN or custom domain)
211
+ if (baseUrl) {
212
+ const cleanBase = baseUrl.replace(/\/+$/, '');
213
+ return `${cleanBase}/${fileKey}`;
214
+ }
215
+ // Priority 2: Construct URL from endpoint if configured
216
+ // This fixes issues with S3-compatible providers (IONOS, MinIO, etc.)
217
+ // that return Location in incorrect format for multipart uploads
218
+ const endpoint = config.endpoint?.toString();
219
+ if (endpoint) {
220
+ const endpointUrl = endpoint.startsWith('http') ? endpoint : `https://${endpoint}`;
221
+ const cleanEndpoint = endpointUrl.replace(/\/+$/, '');
222
+ return `${cleanEndpoint}/${config.params.Bucket}/${fileKey}`;
223
+ }
224
+ // Priority 3: Use the Location from S3 response
225
+ if (assertUrlProtocol(uploadLocation)) {
226
+ return uploadLocation;
227
+ }
228
+ // Priority 4: Prepend https if protocol is missing
229
+ return `https://${uploadLocation}`;
46
230
  };
47
231
  const upload = async (file, customParams = {})=>{
48
232
  const fileKey = getFileKey(file);
49
- const uploadObj = new Upload({
233
+ const params = buildUploadParams(file, fileKey, customParams);
234
+ const uploadOptions = {
50
235
  client: s3Client,
51
- params: {
52
- Bucket: config.params.Bucket,
53
- Key: fileKey,
54
- Body: file.stream || Buffer.from(file.buffer, 'binary'),
55
- ACL: config.params.ACL,
56
- ContentType: file.mime,
57
- ...customParams
236
+ params
237
+ };
238
+ // Multipart configuration
239
+ if (providerConfig?.multipart) {
240
+ if (providerConfig.multipart.partSize) {
241
+ uploadOptions.partSize = providerConfig.multipart.partSize;
242
+ }
243
+ if (providerConfig.multipart.queueSize) {
244
+ uploadOptions.queueSize = providerConfig.multipart.queueSize;
245
+ }
246
+ if (providerConfig.multipart.leavePartsOnError !== undefined) {
247
+ uploadOptions.leavePartsOnError = providerConfig.multipart.leavePartsOnError;
58
248
  }
249
+ }
250
+ const uploadObj = new Upload(uploadOptions);
251
+ const result = await uploadObj.done();
252
+ // Construct the correct URL (handles S3-compatible provider quirks)
253
+ file.url = constructFileUrl(fileKey, result.Location);
254
+ // Store ETag for potential future conditional updates
255
+ if (result.ETag) {
256
+ file.etag = result.ETag.replace(/"/g, '');
257
+ }
258
+ };
259
+ /**
260
+ * Uploads a file only if the existing object matches the expected ETag.
261
+ * This implements optimistic locking to prevent lost updates.
262
+ */ const uploadIfMatch = async (file, expectedETag, customParams = {})=>{
263
+ const fileKey = getFileKey(file);
264
+ const params = buildUploadParams(file, fileKey, {
265
+ ...customParams,
266
+ IfMatch: expectedETag
267
+ });
268
+ const uploadObj = new Upload({
269
+ client: s3Client,
270
+ params
59
271
  });
60
- const upload = await uploadObj.done();
61
- if (assertUrlProtocol(upload.Location)) {
62
- file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload.Location;
63
- } else {
64
- // Default protocol to https protocol
65
- file.url = `https://${upload.Location}`;
272
+ const result = await uploadObj.done();
273
+ // Construct the correct URL (handles S3-compatible provider quirks)
274
+ file.url = constructFileUrl(fileKey, result.Location);
275
+ if (result.ETag) {
276
+ file.etag = result.ETag.replace(/"/g, '');
277
+ }
278
+ };
279
+ /**
280
+ * Retrieves metadata for an object including its ETag.
281
+ */ const getObjectMetadata = async (file)=>{
282
+ const command = new HeadObjectCommand({
283
+ Bucket: config.params.Bucket,
284
+ Key: getFileKey(file)
285
+ });
286
+ const response = await s3Client.send(command);
287
+ return {
288
+ etag: response.ETag?.replace(/"/g, ''),
289
+ contentLength: response.ContentLength,
290
+ contentType: response.ContentType,
291
+ lastModified: response.LastModified,
292
+ storageClass: response.StorageClass,
293
+ serverSideEncryption: response.ServerSideEncryption
294
+ };
295
+ };
296
+ /**
297
+ * Checks if an object exists in the bucket.
298
+ */ const objectExists = async (file)=>{
299
+ try {
300
+ await getObjectMetadata(file);
301
+ return true;
302
+ } catch (error) {
303
+ if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
304
+ return false;
305
+ }
306
+ throw error;
66
307
  }
67
308
  };
68
309
  return {
69
- isPrivate () {
310
+ /**
311
+ * Returns whether the bucket is configured with private ACL.
312
+ */ isPrivate () {
70
313
  return config.params.ACL === 'private';
71
314
  },
72
- async getSignedUrl (file, customParams) {
73
- // Do not sign the url if it does not come from the same bucket.
315
+ /**
316
+ * Returns the current provider configuration.
317
+ */ getProviderConfig () {
318
+ return providerConfig;
319
+ },
320
+ /**
321
+ * Generates a signed URL for accessing a private object.
322
+ */ async getSignedUrl (file, customParams) {
74
323
  if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {
75
324
  return {
76
325
  url: file.url
77
326
  };
78
327
  }
79
328
  const fileKey = getFileKey(file);
80
- const url = await getSignedUrl(// @ts-expect-error - TODO fix client type
81
- s3Client, new GetObjectCommand({
329
+ // Spread customParams first, then override with secure values
330
+ // This prevents malicious override of Bucket and Key
331
+ const url = await getSignedUrl(s3Client, new GetObjectCommand({
332
+ ...customParams,
82
333
  Bucket: config.params.Bucket,
83
- Key: fileKey,
84
- ...customParams
334
+ Key: fileKey
85
335
  }), {
86
336
  expiresIn: getOr(15 * 60, [
87
337
  'params',
@@ -92,17 +342,42 @@ var index = {
92
342
  url
93
343
  };
94
344
  },
95
- uploadStream (file, customParams = {}) {
345
+ /**
346
+ * Uploads a file using streaming.
347
+ */ uploadStream (file, customParams = {}) {
96
348
  return upload(file, customParams);
97
349
  },
98
- upload (file, customParams = {}) {
350
+ /**
351
+ * Uploads a file to S3.
352
+ */ upload (file, customParams = {}) {
99
353
  return upload(file, customParams);
100
354
  },
101
- delete (file, customParams = {}) {
355
+ /**
356
+ * Uploads a file only if it matches the expected ETag (optimistic locking).
357
+ * Throws PreconditionFailed error if ETag does not match.
358
+ */ uploadIfMatch (file, expectedETag, customParams = {}) {
359
+ return uploadIfMatch(file, expectedETag, customParams);
360
+ },
361
+ /**
362
+ * Retrieves object metadata including ETag.
363
+ */ getObjectMetadata (file) {
364
+ return getObjectMetadata(file);
365
+ },
366
+ /**
367
+ * Checks if an object exists in the bucket.
368
+ */ objectExists (file) {
369
+ return objectExists(file);
370
+ },
371
+ /**
372
+ * Deletes an object from S3.
373
+ */ delete (file, customParams = {}) {
374
+ // Spread customParams first, then override with secure values
375
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
376
+ const { Bucket, Key, ...safeParams } = customParams;
102
377
  const command = new DeleteObjectCommand({
378
+ ...safeParams,
103
379
  Bucket: config.params.Bucket,
104
- Key: getFileKey(file),
105
- ...customParams
380
+ Key: getFileKey(file)
106
381
  });
107
382
  return s3Client.send(command);
108
383
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport {\n S3Client,\n GetObjectCommand,\n DeleteObjectCommand,\n DeleteObjectCommandOutput,\n PutObjectCommandInput,\n CompleteMultipartUploadCommandOutput,\n AbortMultipartUploadCommandOutput,\n S3ClientConfig,\n ObjectCannedACL,\n} from '@aws-sdk/client-s3';\nimport type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { extractCredentials, isUrlFromBucket } from './utils';\n\nexport interface File {\n name: string;\n alternativeText?: string;\n caption?: string;\n width?: number;\n height?: number;\n formats?: Record<string, unknown>;\n hash: string;\n ext?: string;\n mime: string;\n size: number;\n sizeInBytes: number;\n url: string;\n previewUrl?: string;\n path?: string;\n provider?: string;\n provider_metadata?: Record<string, unknown>;\n stream?: ReadStream;\n buffer?: Buffer;\n}\n\nexport type UploadCommandOutput = (\n | CompleteMultipartUploadCommandOutput\n | AbortMultipartUploadCommandOutput\n) & {\n Location: string;\n};\n\nexport interface AWSParams {\n Bucket: string; // making it required\n ACL?: ObjectCannedACL;\n signedUrlExpires?: number;\n}\n\nexport interface DefaultOptions extends S3ClientConfig {\n // TODO Remove this in V5\n accessKeyId?: AwsCredentialIdentity['accessKeyId'];\n secretAccessKey?: AwsCredentialIdentity['secretAccessKey'];\n // Keep this for V5\n credentials?: AwsCredentialIdentity;\n params?: AWSParams;\n [k: string]: any;\n}\n\nexport type InitOptions = (DefaultOptions | { s3Options: DefaultOptions }) & {\n baseUrl?: string;\n rootPath?: string;\n [k: string]: any;\n};\n\nconst assertUrlProtocol = (url: string) => {\n // Regex to test protocol like \"http://\", \"https://\"\n return /^\\w*:\\/\\//.test(url);\n};\n\nconst getConfig = ({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) => {\n if (Object.keys(legacyS3Options).length > 0) {\n process.emitWarning(\n \"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property.\"\n );\n }\n const credentials = extractCredentials({ s3Options, ...legacyS3Options });\n const config = {\n ...s3Options,\n ...legacyS3Options,\n ...(credentials ? { credentials } : {}),\n };\n\n config.params.ACL = getOr(ObjectCannedACL.public_read, ['params', 'ACL'], config);\n\n return config;\n};\n\nexport default {\n init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) {\n // TODO V5 change config structure to avoid having to do this\n const config = getConfig({ baseUrl, rootPath, s3Options, ...legacyS3Options });\n const s3Client = new S3Client(config);\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const path = file.path ? `${file.path}/` : '';\n return `${filePrefix}${path}${file.hash}${file.ext}`;\n };\n\n const upload = async (file: File, customParams: Partial<PutObjectCommandInput> = {}) => {\n const fileKey = getFileKey(file);\n const uploadObj = new Upload({\n client: s3Client,\n params: {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Body: file.stream || Buffer.from(file.buffer as any, 'binary'),\n ACL: config.params.ACL,\n ContentType: file.mime,\n ...customParams,\n },\n });\n\n const upload = (await uploadObj.done()) as UploadCommandOutput;\n\n if (assertUrlProtocol(upload.Location)) {\n file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload.Location;\n } else {\n // Default protocol to https protocol\n file.url = `https://${upload.Location}`;\n }\n };\n\n return {\n isPrivate() {\n return config.params.ACL === 'private';\n },\n\n async getSignedUrl(file: File, customParams: any): Promise<{ url: string }> {\n // Do not sign the url if it does not come from the same bucket.\n if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {\n return { url: file.url };\n }\n const fileKey = getFileKey(file);\n\n const url = await getSignedUrl(\n // @ts-expect-error - TODO fix client type\n s3Client,\n new GetObjectCommand({\n Bucket: config.params.Bucket,\n Key: fileKey,\n ...customParams,\n }),\n {\n expiresIn: getOr(15 * 60, ['params', 'signedUrlExpires'], config),\n }\n );\n\n return { url };\n },\n uploadStream(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n upload(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n delete(file: File, customParams = {}): Promise<DeleteObjectCommandOutput> {\n const command = new DeleteObjectCommand({\n Bucket: config.params.Bucket,\n Key: getFileKey(file),\n ...customParams,\n });\n return s3Client.send(command);\n },\n };\n },\n};\n"],"names":["assertUrlProtocol","url","test","getConfig","baseUrl","rootPath","s3Options","legacyS3Options","Object","keys","length","process","emitWarning","credentials","extractCredentials","config","params","ACL","getOr","ObjectCannedACL","public_read","init","s3Client","S3Client","filePrefix","replace","getFileKey","file","path","hash","ext","upload","customParams","fileKey","uploadObj","Upload","client","Bucket","Key","Body","stream","Buffer","from","buffer","ContentType","mime","done","Location","isPrivate","getSignedUrl","isUrlFromBucket","GetObjectCommand","expiresIn","uploadStream","delete","command","DeleteObjectCommand","send"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport {\n S3Client,\n GetObjectCommand,\n DeleteObjectCommand,\n DeleteObjectCommandOutput,\n HeadObjectCommand,\n PutObjectCommandInput,\n CompleteMultipartUploadCommandOutput,\n AbortMultipartUploadCommandOutput,\n S3ClientConfig,\n ObjectCannedACL,\n ChecksumAlgorithm,\n StorageClass,\n ServerSideEncryption,\n} from '@aws-sdk/client-s3';\nimport type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { extractCredentials, isUrlFromBucket } from './utils';\n\n/**\n * Supported checksum algorithms for data integrity validation.\n * CRC64NVME is recommended for best performance on modern hardware.\n */\nexport type SupportedChecksumAlgorithm = 'CRC32' | 'CRC32C' | 'SHA1' | 'SHA256' | 'CRC64NVME';\n\n/**\n * Supported S3 storage classes for cost optimization.\n */\nexport type SupportedStorageClass =\n | 'STANDARD'\n | 'REDUCED_REDUNDANCY'\n | 'STANDARD_IA'\n | 'ONEZONE_IA'\n | 'INTELLIGENT_TIERING'\n | 'GLACIER'\n | 'DEEP_ARCHIVE'\n | 'GLACIER_IR';\n\n/**\n * Server-side encryption types.\n */\nexport type EncryptionType = 'AES256' | 'aws:kms' | 'aws:kms:dsse';\n\n/**\n * Encryption configuration for server-side encryption.\n */\nexport interface EncryptionConfig {\n type: EncryptionType;\n kmsKeyId?: string;\n}\n\n/**\n * Multipart upload configuration for large files.\n */\nexport interface MultipartConfig {\n partSize?: number;\n queueSize?: number;\n leavePartsOnError?: boolean;\n}\n\nexport interface File {\n name: string;\n alternativeText?: string;\n caption?: string;\n width?: number;\n height?: number;\n formats?: Record<string, unknown>;\n hash: string;\n ext?: string;\n mime: string;\n size: number;\n sizeInBytes: number;\n url: string;\n previewUrl?: string;\n path?: string;\n provider?: string;\n provider_metadata?: Record<string, unknown>;\n stream?: ReadStream;\n buffer?: Buffer;\n etag?: string;\n}\n\nexport type UploadCommandOutput = (\n | CompleteMultipartUploadCommandOutput\n | AbortMultipartUploadCommandOutput\n) & {\n Location: string;\n ETag?: string;\n};\n\nexport interface AWSParams {\n Bucket: string;\n ACL?: ObjectCannedACL;\n signedUrlExpires?: number;\n}\n\n/**\n * Extended configuration options for the S3 provider.\n */\nexport interface ProviderConfig {\n /**\n * Checksum algorithm for data integrity validation during upload.\n * When enabled, the SDK calculates a checksum and S3 validates it server-side.\n */\n checksumAlgorithm?: SupportedChecksumAlgorithm;\n\n /**\n * When true, uploads will fail if an object with the same key already exists.\n * This prevents accidental overwrites due to race conditions.\n */\n preventOverwrite?: boolean;\n\n /**\n * S3 storage class for uploaded objects.\n * Use lower-cost classes for infrequently accessed data.\n */\n storageClass?: SupportedStorageClass;\n\n /**\n * Server-side encryption configuration.\n */\n encryption?: EncryptionConfig;\n\n /**\n * Tags to apply to uploaded objects.\n * Useful for cost allocation and lifecycle policies.\n */\n tags?: Record<string, string>;\n\n /**\n * Multipart upload configuration for large files.\n */\n multipart?: MultipartConfig;\n}\n\nexport interface DefaultOptions extends S3ClientConfig {\n // TODO Remove this in V5\n accessKeyId?: AwsCredentialIdentity['accessKeyId'];\n secretAccessKey?: AwsCredentialIdentity['secretAccessKey'];\n // Keep this for V5\n credentials?: AwsCredentialIdentity;\n params?: AWSParams;\n [k: string]: unknown;\n}\n\nexport type InitOptions = (DefaultOptions | { s3Options: DefaultOptions }) & {\n baseUrl?: string;\n rootPath?: string;\n providerConfig?: ProviderConfig;\n [k: string]: unknown;\n};\n\n/**\n * Validates that a URL uses HTTP or HTTPS protocol.\n * Rejects dangerous protocols like file://, javascript:, data:, etc.\n */\nconst assertUrlProtocol = (url: string) => {\n return /^https?:\\/\\//.test(url);\n};\n\n/**\n * Sanitizes a path component to prevent path traversal attacks.\n * Removes directory traversal sequences and normalizes the path.\n */\nconst sanitizePathComponent = (component: string | undefined): string => {\n if (!component) return '';\n return component\n .replace(/\\.\\./g, '')\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\/+/g, '/');\n};\n\n/**\n * Maps the provider checksum algorithm to the AWS SDK checksum algorithm.\n */\nconst mapChecksumAlgorithm = (\n algorithm?: SupportedChecksumAlgorithm\n): ChecksumAlgorithm | undefined => {\n if (!algorithm) return undefined;\n const mapping: Record<SupportedChecksumAlgorithm, ChecksumAlgorithm> = {\n CRC32: ChecksumAlgorithm.CRC32,\n CRC32C: ChecksumAlgorithm.CRC32C,\n SHA1: ChecksumAlgorithm.SHA1,\n SHA256: ChecksumAlgorithm.SHA256,\n CRC64NVME: ChecksumAlgorithm.CRC64NVME,\n };\n return mapping[algorithm];\n};\n\n/**\n * Maps the provider storage class to the AWS SDK storage class.\n */\nconst mapStorageClass = (storageClass?: SupportedStorageClass): StorageClass | undefined => {\n if (!storageClass) return undefined;\n const mapping: Record<SupportedStorageClass, StorageClass> = {\n STANDARD: StorageClass.STANDARD,\n REDUCED_REDUNDANCY: StorageClass.REDUCED_REDUNDANCY,\n STANDARD_IA: StorageClass.STANDARD_IA,\n ONEZONE_IA: StorageClass.ONEZONE_IA,\n INTELLIGENT_TIERING: StorageClass.INTELLIGENT_TIERING,\n GLACIER: StorageClass.GLACIER,\n DEEP_ARCHIVE: StorageClass.DEEP_ARCHIVE,\n GLACIER_IR: StorageClass.GLACIER_IR,\n };\n return mapping[storageClass];\n};\n\n/**\n * Maps the provider encryption type to the AWS SDK server-side encryption.\n */\nconst mapServerSideEncryption = (type?: EncryptionType): ServerSideEncryption | undefined => {\n if (!type) return undefined;\n const mapping: Record<EncryptionType, ServerSideEncryption> = {\n AES256: ServerSideEncryption.AES256,\n 'aws:kms': ServerSideEncryption.aws_kms,\n 'aws:kms:dsse': ServerSideEncryption.aws_kms_dsse,\n };\n return mapping[type];\n};\n\n/**\n * Converts a tags object to the S3 Tagging header format.\n * Format: key1=value1&key2=value2\n */\nconst formatTagsForHeader = (tags?: Record<string, string>): string | undefined => {\n if (!tags || Object.keys(tags).length === 0) return undefined;\n return Object.entries(tags)\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join('&');\n};\n\n/**\n * Checks if the endpoint appears to be a non-AWS S3-compatible provider.\n */\nconst isNonAwsEndpoint = (endpoint?: string): boolean => {\n if (!endpoint) return false;\n const awsPatterns = [/\\.amazonaws\\.com$/i, /\\.amazonaws\\.com\\.cn$/i];\n return !awsPatterns.some((pattern) => pattern.test(endpoint));\n};\n\n/**\n * Validates provider configuration and emits warnings for potential compatibility issues.\n */\nconst validateProviderConfig = (\n providerConfig: ProviderConfig | undefined,\n s3Options: DefaultOptions | undefined\n): void => {\n if (!providerConfig) return;\n\n const endpoint = s3Options?.endpoint?.toString() || '';\n const isNonAws = isNonAwsEndpoint(endpoint);\n\n // Warn about AWS-specific features when using non-AWS endpoints\n if (isNonAws) {\n if (providerConfig.storageClass) {\n process.emitWarning(\n `Storage class '${providerConfig.storageClass}' is AWS S3-specific and may be ignored by your S3-compatible provider.`\n );\n }\n\n if (providerConfig.encryption?.type && providerConfig.encryption.type !== 'AES256') {\n process.emitWarning(\n `Encryption type '${providerConfig.encryption.type}' is AWS S3-specific. Consider using 'AES256' for better compatibility.`\n );\n }\n }\n\n // Validate multipart configuration\n if (providerConfig.multipart?.partSize) {\n const minPartSize = 5 * 1024 * 1024; // 5MB\n const maxPartSize = 5 * 1024 * 1024 * 1024; // 5GB\n\n if (providerConfig.multipart.partSize < minPartSize) {\n process.emitWarning(\n `Multipart partSize ${providerConfig.multipart.partSize} is below the minimum of 5MB. This may cause upload failures.`\n );\n }\n\n if (providerConfig.multipart.partSize > maxPartSize) {\n process.emitWarning(\n `Multipart partSize ${providerConfig.multipart.partSize} exceeds the maximum of 5GB. This may cause upload failures.`\n );\n }\n }\n\n if (providerConfig.multipart?.queueSize && providerConfig.multipart.queueSize > 16) {\n process.emitWarning(\n `Multipart queueSize ${providerConfig.multipart.queueSize} is high and may cause memory issues. Consider using 4-8.`\n );\n }\n};\n\nconst getConfig = ({\n s3Options,\n legacyS3Options,\n}: {\n s3Options: DefaultOptions;\n legacyS3Options: Record<string, unknown>;\n}) => {\n if (Object.keys(legacyS3Options).length > 0) {\n process.emitWarning(\n \"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property.\"\n );\n }\n const credentials = extractCredentials({ s3Options, ...legacyS3Options });\n const config = {\n ...s3Options,\n ...legacyS3Options,\n ...(credentials ? { credentials } : {}),\n };\n\n if (config.params !== undefined) {\n // Only set default ACL when ACL is not explicitly present in params.\n // Since April 2023, new AWS S3 buckets have ACLs disabled by default\n // (\"Bucket owner enforced\"). Sending an ACL header to such buckets\n // throws AccessControlListNotSupported. To disable ACLs, users should\n // simply not include ACL in their params configuration.\n if (!('ACL' in config.params)) {\n config.params.ACL = ObjectCannedACL.public_read;\n }\n } else {\n throw new Error('Upload AWS S3 provider: `params` are required in the config object');\n }\n\n return config as DefaultOptions & {\n params: AWSParams;\n };\n};\n\nexport default {\n init({ baseUrl, rootPath, s3Options, providerConfig, ...legacyS3Options }: InitOptions) {\n // Validate configuration and emit warnings for potential issues\n validateProviderConfig(providerConfig, s3Options as DefaultOptions);\n\n // TODO V5 change config structure to avoid having to do this\n const config = getConfig({ s3Options: s3Options as DefaultOptions, legacyS3Options });\n const s3Client = new S3Client(config);\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const sanitizedPath = sanitizePathComponent(file.path);\n const path = sanitizedPath ? `${sanitizedPath}/` : '';\n const sanitizedHash = sanitizePathComponent(file.hash);\n const sanitizedExt = file.ext ? file.ext.replace(/[^a-zA-Z0-9.]/g, '') : '';\n return `${filePrefix}${path}${sanitizedHash}${sanitizedExt}`;\n };\n\n /**\n * Builds the upload parameters including all configured features.\n */\n const buildUploadParams = (\n file: File,\n fileKey: string,\n customParams: Partial<PutObjectCommandInput> = {}\n ): PutObjectCommandInput => {\n const params: PutObjectCommandInput = {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Body: file.stream || Buffer.from(file.buffer as any, 'binary'),\n // ACL is optional to support providers like Cloudflare R2 that don't support ACLs.\n // Set params.ACL to undefined or omit it entirely to disable ACL headers.\n ...(config.params.ACL ? { ACL: config.params.ACL } : {}),\n ContentType: file.mime,\n };\n\n // Checksum validation\n const checksumAlgorithm = mapChecksumAlgorithm(providerConfig?.checksumAlgorithm);\n if (checksumAlgorithm) {\n params.ChecksumAlgorithm = checksumAlgorithm;\n }\n\n // Conditional writes - prevent overwrite\n if (providerConfig?.preventOverwrite) {\n params.IfNoneMatch = '*';\n }\n\n // Storage class\n const storageClass = mapStorageClass(providerConfig?.storageClass);\n if (storageClass) {\n params.StorageClass = storageClass;\n }\n\n // Server-side encryption\n if (providerConfig?.encryption) {\n const sse = mapServerSideEncryption(providerConfig.encryption.type);\n if (sse) {\n params.ServerSideEncryption = sse;\n if (providerConfig.encryption.kmsKeyId) {\n params.SSEKMSKeyId = providerConfig.encryption.kmsKeyId;\n }\n }\n }\n\n // Object tagging\n const tagging = formatTagsForHeader(providerConfig?.tags);\n if (tagging) {\n params.Tagging = tagging;\n }\n\n // Merge customParams but preserve critical security parameters\n // Bucket, Key, and Body must not be overridden by customParams\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { Bucket, Key, Body, ...safeCustomParams } = customParams as any;\n return { ...params, ...safeCustomParams };\n };\n\n /**\n * Constructs the correct file URL.\n * Handles S3-compatible providers that return incorrect Location formats.\n */\n const constructFileUrl = (fileKey: string, uploadLocation: string): string => {\n // Priority 1: Use baseUrl if configured (CDN or custom domain)\n if (baseUrl) {\n const cleanBase = baseUrl.replace(/\\/+$/, '');\n return `${cleanBase}/${fileKey}`;\n }\n\n // Priority 2: Construct URL from endpoint if configured\n // This fixes issues with S3-compatible providers (IONOS, MinIO, etc.)\n // that return Location in incorrect format for multipart uploads\n const endpoint = config.endpoint?.toString();\n if (endpoint) {\n const endpointUrl = endpoint.startsWith('http') ? endpoint : `https://${endpoint}`;\n const cleanEndpoint = endpointUrl.replace(/\\/+$/, '');\n return `${cleanEndpoint}/${config.params.Bucket}/${fileKey}`;\n }\n\n // Priority 3: Use the Location from S3 response\n if (assertUrlProtocol(uploadLocation)) {\n return uploadLocation;\n }\n\n // Priority 4: Prepend https if protocol is missing\n return `https://${uploadLocation}`;\n };\n\n const upload = async (file: File, customParams: Partial<PutObjectCommandInput> = {}) => {\n const fileKey = getFileKey(file);\n const params = buildUploadParams(file, fileKey, customParams);\n\n const uploadOptions: {\n client: S3Client;\n params: PutObjectCommandInput;\n partSize?: number;\n queueSize?: number;\n leavePartsOnError?: boolean;\n } = {\n client: s3Client,\n params,\n };\n\n // Multipart configuration\n if (providerConfig?.multipart) {\n if (providerConfig.multipart.partSize) {\n uploadOptions.partSize = providerConfig.multipart.partSize;\n }\n if (providerConfig.multipart.queueSize) {\n uploadOptions.queueSize = providerConfig.multipart.queueSize;\n }\n if (providerConfig.multipart.leavePartsOnError !== undefined) {\n uploadOptions.leavePartsOnError = providerConfig.multipart.leavePartsOnError;\n }\n }\n\n const uploadObj = new Upload(uploadOptions);\n const result = (await uploadObj.done()) as UploadCommandOutput;\n\n // Construct the correct URL (handles S3-compatible provider quirks)\n file.url = constructFileUrl(fileKey, result.Location);\n\n // Store ETag for potential future conditional updates\n if (result.ETag) {\n file.etag = result.ETag.replace(/\"/g, '');\n }\n };\n\n /**\n * Uploads a file only if the existing object matches the expected ETag.\n * This implements optimistic locking to prevent lost updates.\n */\n const uploadIfMatch = async (\n file: File,\n expectedETag: string,\n customParams: Partial<PutObjectCommandInput> = {}\n ) => {\n const fileKey = getFileKey(file);\n const params = buildUploadParams(file, fileKey, {\n ...customParams,\n IfMatch: expectedETag,\n });\n\n const uploadObj = new Upload({\n client: s3Client,\n params,\n });\n\n const result = (await uploadObj.done()) as UploadCommandOutput;\n\n // Construct the correct URL (handles S3-compatible provider quirks)\n file.url = constructFileUrl(fileKey, result.Location);\n\n if (result.ETag) {\n file.etag = result.ETag.replace(/\"/g, '');\n }\n };\n\n /**\n * Retrieves metadata for an object including its ETag.\n */\n const getObjectMetadata = async (file: File) => {\n const command = new HeadObjectCommand({\n Bucket: config.params.Bucket,\n Key: getFileKey(file),\n });\n\n const response = await s3Client.send(command);\n\n return {\n etag: response.ETag?.replace(/\"/g, ''),\n contentLength: response.ContentLength,\n contentType: response.ContentType,\n lastModified: response.LastModified,\n storageClass: response.StorageClass,\n serverSideEncryption: response.ServerSideEncryption,\n };\n };\n\n /**\n * Checks if an object exists in the bucket.\n */\n const objectExists = async (file: File): Promise<boolean> => {\n try {\n await getObjectMetadata(file);\n return true;\n } catch (error: any) {\n if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {\n return false;\n }\n throw error;\n }\n };\n\n return {\n /**\n * Returns whether the bucket is configured with private ACL.\n */\n isPrivate() {\n return config.params.ACL === 'private';\n },\n\n /**\n * Returns the current provider configuration.\n */\n getProviderConfig(): ProviderConfig | undefined {\n return providerConfig;\n },\n\n /**\n * Generates a signed URL for accessing a private object.\n */\n async getSignedUrl(file: File, customParams: any): Promise<{ url: string }> {\n if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {\n return { url: file.url };\n }\n const fileKey = getFileKey(file);\n\n // Spread customParams first, then override with secure values\n // This prevents malicious override of Bucket and Key\n const url = await getSignedUrl(\n s3Client,\n new GetObjectCommand({\n ...customParams,\n Bucket: config.params.Bucket,\n Key: fileKey,\n }),\n {\n expiresIn: getOr(15 * 60, ['params', 'signedUrlExpires'], config),\n }\n );\n\n return { url };\n },\n\n /**\n * Uploads a file using streaming.\n */\n uploadStream(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n\n /**\n * Uploads a file to S3.\n */\n upload(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n\n /**\n * Uploads a file only if it matches the expected ETag (optimistic locking).\n * Throws PreconditionFailed error if ETag does not match.\n */\n uploadIfMatch(file: File, expectedETag: string, customParams = {}) {\n return uploadIfMatch(file, expectedETag, customParams);\n },\n\n /**\n * Retrieves object metadata including ETag.\n */\n getObjectMetadata(file: File) {\n return getObjectMetadata(file);\n },\n\n /**\n * Checks if an object exists in the bucket.\n */\n objectExists(file: File) {\n return objectExists(file);\n },\n\n /**\n * Deletes an object from S3.\n */\n delete(file: File, customParams = {}): Promise<DeleteObjectCommandOutput> {\n // Spread customParams first, then override with secure values\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { Bucket, Key, ...safeParams } = customParams as any;\n const command = new DeleteObjectCommand({\n ...safeParams,\n Bucket: config.params.Bucket,\n Key: getFileKey(file),\n });\n return s3Client.send(command);\n },\n };\n },\n};\n"],"names":["assertUrlProtocol","url","test","sanitizePathComponent","component","replace","mapChecksumAlgorithm","algorithm","undefined","mapping","CRC32","ChecksumAlgorithm","CRC32C","SHA1","SHA256","CRC64NVME","mapStorageClass","storageClass","STANDARD","StorageClass","REDUCED_REDUNDANCY","STANDARD_IA","ONEZONE_IA","INTELLIGENT_TIERING","GLACIER","DEEP_ARCHIVE","GLACIER_IR","mapServerSideEncryption","type","AES256","ServerSideEncryption","aws_kms","aws_kms_dsse","formatTagsForHeader","tags","Object","keys","length","entries","map","key","value","encodeURIComponent","join","isNonAwsEndpoint","endpoint","awsPatterns","some","pattern","validateProviderConfig","providerConfig","s3Options","toString","isNonAws","process","emitWarning","encryption","multipart","partSize","minPartSize","maxPartSize","queueSize","getConfig","legacyS3Options","credentials","extractCredentials","config","params","ACL","ObjectCannedACL","public_read","Error","init","baseUrl","rootPath","s3Client","S3Client","filePrefix","getFileKey","file","sanitizedPath","path","sanitizedHash","hash","sanitizedExt","ext","buildUploadParams","fileKey","customParams","Bucket","Key","Body","stream","Buffer","from","buffer","ContentType","mime","checksumAlgorithm","preventOverwrite","IfNoneMatch","sse","kmsKeyId","SSEKMSKeyId","tagging","Tagging","safeCustomParams","constructFileUrl","uploadLocation","cleanBase","endpointUrl","startsWith","cleanEndpoint","upload","uploadOptions","client","leavePartsOnError","uploadObj","Upload","result","done","Location","ETag","etag","uploadIfMatch","expectedETag","IfMatch","getObjectMetadata","command","HeadObjectCommand","response","send","contentLength","ContentLength","contentType","lastModified","LastModified","serverSideEncryption","objectExists","error","name","$metadata","httpStatusCode","isPrivate","getProviderConfig","getSignedUrl","isUrlFromBucket","GetObjectCommand","expiresIn","getOr","uploadStream","delete","safeParams","DeleteObjectCommand"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,GAAG,CAAC;AASrC,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,CAoB1F;AA4DD,eAAO,MAAM,kBAAkB,YAAa,WAAW,KAAG,qBAAqB,GAAG,IAQjF,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,GAAG,CAAC;AASrD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,CAoB1F;AA4DD,eAAO,MAAM,kBAAkB,YAAa,WAAW,KAAG,qBAAqB,GAAG,IAcjF,CAAC"}
package/dist/utils.js CHANGED
@@ -83,10 +83,15 @@ function isUrlFromBucket(fileUrl, bucketName, baseUrl = '') {
83
83
  };
84
84
  }
85
85
  const extractCredentials = (options)=>{
86
- if (options.s3Options?.credentials) {
86
+ const s3Options = options.s3Options;
87
+ if (s3Options?.credentials) {
88
+ // Support AWS STS session tokens for temporary credentials
87
89
  return {
88
- accessKeyId: options.s3Options.credentials.accessKeyId,
89
- secretAccessKey: options.s3Options.credentials.secretAccessKey
90
+ accessKeyId: s3Options.credentials.accessKeyId,
91
+ secretAccessKey: s3Options.credentials.secretAccessKey,
92
+ ...s3Options.credentials.sessionToken ? {
93
+ sessionToken: s3Options.credentials.sessionToken
94
+ } : {}
90
95
  };
91
96
  }
92
97
  return null;
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { InitOptions } from '.';\n\nconst ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n if (options.s3Options?.credentials) {\n return {\n accessKeyId: options.s3Options.credentials.accessKeyId,\n secretAccessKey: options.s3Options.credentials.secretAccessKey,\n };\n }\n return null;\n};\n"],"names":["ENDPOINT_PATTERN","isUrlFromBucket","fileUrl","bucketName","baseUrl","url","URL","bucket","getBucketFromAwsUrl","host","startsWith","pathname","includes","protocol","err","matches","match","prefix","index","indexOf","substring","length","extractCredentials","options","s3Options","credentials","accessKeyId","secretAccessKey"],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { DefaultOptions, InitOptions } from '.';\n\nconst ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n const s3Options = (options as { s3Options?: DefaultOptions }).s3Options;\n\n if (s3Options?.credentials) {\n // Support AWS STS session tokens for temporary credentials\n return {\n accessKeyId: s3Options.credentials.accessKeyId,\n secretAccessKey: s3Options.credentials.secretAccessKey,\n ...(s3Options.credentials.sessionToken\n ? { sessionToken: s3Options.credentials.sessionToken }\n : {}),\n };\n }\n return null;\n};\n"],"names":["ENDPOINT_PATTERN","isUrlFromBucket","fileUrl","bucketName","baseUrl","url","URL","bucket","getBucketFromAwsUrl","host","startsWith","pathname","includes","protocol","err","matches","match","prefix","index","indexOf","substring","length","extractCredentials","options","s3Options","credentials","accessKeyId","secretAccessKey","sessionToken"],"mappings":"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"}
package/dist/utils.mjs CHANGED
@@ -81,10 +81,15 @@ function isUrlFromBucket(fileUrl, bucketName, baseUrl = '') {
81
81
  };
82
82
  }
83
83
  const extractCredentials = (options)=>{
84
- if (options.s3Options?.credentials) {
84
+ const s3Options = options.s3Options;
85
+ if (s3Options?.credentials) {
86
+ // Support AWS STS session tokens for temporary credentials
85
87
  return {
86
- accessKeyId: options.s3Options.credentials.accessKeyId,
87
- secretAccessKey: options.s3Options.credentials.secretAccessKey
88
+ accessKeyId: s3Options.credentials.accessKeyId,
89
+ secretAccessKey: s3Options.credentials.secretAccessKey,
90
+ ...s3Options.credentials.sessionToken ? {
91
+ sessionToken: s3Options.credentials.sessionToken
92
+ } : {}
88
93
  };
89
94
  }
90
95
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","sources":["../src/utils.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { InitOptions } from '.';\n\nconst ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n if (options.s3Options?.credentials) {\n return {\n accessKeyId: options.s3Options.credentials.accessKeyId,\n secretAccessKey: options.s3Options.credentials.secretAccessKey,\n };\n }\n return null;\n};\n"],"names":["ENDPOINT_PATTERN","isUrlFromBucket","fileUrl","bucketName","baseUrl","url","URL","bucket","getBucketFromAwsUrl","host","startsWith","pathname","includes","protocol","err","matches","match","prefix","index","indexOf","substring","length","extractCredentials","options","s3Options","credentials","accessKeyId","secretAccessKey"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","sources":["../src/utils.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { DefaultOptions, InitOptions } from '.';\n\nconst ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n const s3Options = (options as { s3Options?: DefaultOptions }).s3Options;\n\n if (s3Options?.credentials) {\n // Support AWS STS session tokens for temporary credentials\n return {\n accessKeyId: s3Options.credentials.accessKeyId,\n secretAccessKey: s3Options.credentials.secretAccessKey,\n ...(s3Options.credentials.sessionToken\n ? { sessionToken: s3Options.credentials.sessionToken }\n : {}),\n };\n }\n return null;\n};\n"],"names":["ENDPOINT_PATTERN","isUrlFromBucket","fileUrl","bucketName","baseUrl","url","URL","bucket","getBucketFromAwsUrl","host","startsWith","pathname","includes","protocol","err","matches","match","prefix","index","indexOf","substring","length","extractCredentials","options","s3Options","credentials","accessKeyId","secretAccessKey","sessionToken"],"mappings":"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"}