From 86e703272b48abf74cf21820fe7f84d355680e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Tue, 20 Jan 2026 19:45:21 +0100 Subject: [PATCH 01/11] Support the new API GetObjectAttributes Issue: CLDSRV-817 --- constants.js | 8 + lib/api/api.js | 2 + .../apiUtils/object/parseAttributesHeader.js | 32 + lib/api/objectGetAttributes.js | 146 ++++ package.json | 2 +- .../test/object/objectGetAttributes.js | 272 +++++++ .../test/versioning/objectGetAttributes.js | 145 ++++ .../apiUtils/object/parseAttributesHeader.js | 192 +++++ tests/unit/api/objectGetAttributes.js | 672 ++++++++++++++++++ yarn.lock | 6 +- 10 files changed, 1473 insertions(+), 4 deletions(-) create mode 100644 lib/api/apiUtils/object/parseAttributesHeader.js create mode 100644 lib/api/objectGetAttributes.js create mode 100644 tests/functional/aws-node-sdk/test/object/objectGetAttributes.js create mode 100644 tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js create mode 100644 tests/unit/api/apiUtils/object/parseAttributesHeader.js create mode 100644 tests/unit/api/objectGetAttributes.js diff --git a/constants.js b/constants.js index 8e713bc40a..2fa0a49238 100644 --- a/constants.js +++ b/constants.js @@ -279,6 +279,14 @@ const constants = { rateLimitDefaultConfigCacheTTL: 30000, // 30 seconds rateLimitDefaultBurstCapacity: 1, rateLimitCleanupInterval: 10000, // 10 seconds + // Metadata allowed to be returned by getObjectAttributes API + allowedObjectAttributes: new Set([ + 'StorageClass', + 'ObjectSize', + 'ObjectParts', + 'Checksum', + 'ETag', + ]), }; module.exports = constants; diff --git a/lib/api/api.js b/lib/api/api.js index 195441c3fe..058e63c2e6 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -56,6 +56,7 @@ const { objectDelete } = require('./objectDelete'); const objectDeleteTagging = require('./objectDeleteTagging'); const objectGet = require('./objectGet'); const objectGetACL = require('./objectGetACL'); +const objectGetAttributes = require('./objectGetAttributes.js'); const objectGetLegalHold = require('./objectGetLegalHold'); const objectGetRetention = require('./objectGetRetention'); const objectGetTagging = require('./objectGetTagging'); @@ -471,6 +472,7 @@ const api = { objectDeleteTagging, objectGet, objectGetACL, + objectGetAttributes, objectGetLegalHold, objectGetRetention, objectGetTagging, diff --git a/lib/api/apiUtils/object/parseAttributesHeader.js b/lib/api/apiUtils/object/parseAttributesHeader.js new file mode 100644 index 0000000000..92f7509251 --- /dev/null +++ b/lib/api/apiUtils/object/parseAttributesHeader.js @@ -0,0 +1,32 @@ +const { errorInstances } = require('arsenal'); +const { allowedObjectAttributes } = require('../../../../constants'); + +/** + * parseAttributesHeaders - Parse and validate the x-amz-object-attributes header + * @param {object} headers - request headers + * @returns {string[]} - array of valid attribute names + * @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid + */ +function parseAttributesHeaders(headers) { + const raw = headers['x-amz-object-attributes'] || ''; + + const attributes = raw + .split(',') + .map(s => s.trim()) + .filter(s => s !== ''); + + if (attributes.length === 0) { + throw errorInstances.InvalidRequest.customizeDescription( + 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', + ); + } + + const invalids = attributes.filter(s => !allowedObjectAttributes.has(s)); + if (invalids.length > 0) { + throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.'); + } + + return attributes; +} + +module.exports = parseAttributesHeaders; diff --git a/lib/api/objectGetAttributes.js b/lib/api/objectGetAttributes.js new file mode 100644 index 0000000000..8c232dd2f1 --- /dev/null +++ b/lib/api/objectGetAttributes.js @@ -0,0 +1,146 @@ +const { promisify } = require('util'); +const xml2js = require('xml2js'); +const { errors } = require('arsenal'); +const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils'); +const collectCorsHeaders = require('../utilities/collectCorsHeaders'); +const parseAttributesHeaders = require('./apiUtils/object/parseAttributesHeader'); +const { decodeVersionId, getVersionIdResHeader } = require('./apiUtils/object/versioning'); +const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner'); +const { pushMetric } = require('../utapi/utilities'); +const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo'); + +const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; + +const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner); + +/** + * validateBucketAndObjPromise - Promisified wrapper for standardMetadataValidateBucketAndObj + * @param {object} params - validation parameters + * @param {boolean} actionImplicitDenies - whether action has implicit denies + * @param {object} log - Werelogs logger + * @returns {Promise<{bucket: BucketInfo, objMD: object}>} - bucket and object metadata + * @throws {Error} - rejects with error from standardMetadataValidateBucketAndObj + */ +function validateBucketAndObjPromise(params, actionImplicitDenies, log) { + return new Promise((resolve, reject) => { + standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, (err, bucket, objMD) => { + if (err) { + return reject(err); + } + return resolve({ bucket, objMD }); + }); + }); +} + +/** + * buildXmlResponse - Build XML response for GetObjectAttributes + * @param {object} objMD - object metadata + * @param {array} attributes - requested attributes + * @returns {string} XML response + */ +function buildXmlResponse(objMD, attributes) { + const attrResp = {}; + + if (attributes.includes('ETag')) { + attrResp.ETag = objMD['content-md5']; + } + + // NOTE: Checksum is not implemented + if (attributes.includes('Checksum')) { + attrResp.Checksum = {}; + } + + if (attributes.includes('ObjectParts')) { + const partCount = getPartCountFromMd5(objMD); + if (partCount) { + attrResp.ObjectParts = { PartsCount: partCount }; + } + } + + if (attributes.includes('StorageClass')) { + attrResp.StorageClass = objMD['x-amz-storage-class']; + } + + if (attributes.includes('ObjectSize')) { + attrResp.ObjectSize = objMD['content-length']; + } + + const builder = new xml2js.Builder(); + return builder.buildObject({ GetObjectAttributesResponse: attrResp }); +} + +/** + * objectGetAttributes - Retrieves all metadata from an object without returning the object itself + * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info + * @param {object} request - http request object + * @param {object} log - Werelogs logger + * @param {function} callback - callback to server + * @return {undefined} + */ +async function objectGetAttributes(authInfo, request, log, callback) { + log.trace('processing request', { method: OBJECT_GET_ATTRIBUTES }); + const { bucketName, objectKey, headers, actionImplicitDenies } = request; + + let responseHeaders = {}; + + const versionId = decodeVersionId(request.query); + if (versionId instanceof Error) { + log.debug('invalid versionId query', { versionId: request.query.versionId, error: versionId }); + throw versionId; + } + + const metadataValParams = { + authInfo, + bucketName, + objectKey, + versionId, + getDeleteMarker: true, + requestType: request.apiMethods || OBJECT_GET_ATTRIBUTES, + request, + }; + + try { + const { bucket, objMD } = await validateBucketAndObjPromise(metadataValParams, actionImplicitDenies, log); + await checkExpectedBucketOwnerPromise(headers, bucket, log); + + responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket); + if (objMD) { + responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objMD); + responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString(); + } + + if (!objMD) { + const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; + log.debug('object not found', { bucket: bucketName, key: objectKey, versionId }); + throw err; + } + + if (objMD.isDeleteMarker) { + log.debug('attempt to get attributes of a delete marker', { bucket: bucketName, key: objectKey, versionId }); + responseHeaders['x-amz-delete-marker'] = true; + throw errors.MethodNotAllowed; + } + + const attributes = parseAttributesHeaders(headers); + + pushMetric(OBJECT_GET_ATTRIBUTES, log, { + authInfo, + bucket: bucketName, + keys: [objectKey], + versionId: objMD?.versionId, + location: objMD?.dataStoreName, + }); + + const xml = buildXmlResponse(objMD, attributes); + return callback(null, xml, responseHeaders); + } catch (err) { + log.debug('error processing request', { + error: err, + method: OBJECT_GET_ATTRIBUTES, + }); + + return callback(err, null, responseHeaders); + } +} + +module.exports = objectGetAttributes; diff --git a/package.json b/package.json index 3e5f4d3dec..c76f9d80e2 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@azure/storage-blob": "^12.28.0", "@hapi/joi": "^17.1.1", "@smithy/node-http-handler": "^3.0.0", - "arsenal": "git+https://github.com/scality/arsenal#8.3.2", + "arsenal": "git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes", "async": "2.6.4", "bucketclient": "scality/bucketclient#8.2.7", "bufferutil": "^4.0.8", diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js new file mode 100644 index 0000000000..b969e9926b --- /dev/null +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -0,0 +1,272 @@ +const assert = require('assert'); +const { S3 } = require('aws-sdk'); +const getConfig = require('../support/config'); + +const bucket = 'testbucket'; +const key = 'testobject'; +const body = 'hello world!'; +const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; + +describe('Test get object attributes', () => { + let s3; + + before(() => { + const config = getConfig('default', { signatureVersion: 'v4' }); + s3 = new S3(config); + }); + + beforeEach(async () => { + await s3.createBucket({ Bucket: bucket }).promise(); + await s3.putObject({ Bucket: bucket, Key: key, Body: body }).promise(); + }); + + afterEach(async () => { + await s3.deleteObject({ Bucket: bucket, Key: key }).promise(); + await s3.deleteBucket({ Bucket: bucket }).promise(); + }); + + it('should fail because a bad bucket owner', async () => { + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + ExpectedBucketOwner: 'wrongAccountId', + }) + .promise(); + assert.fail('Expected AccessDenied error'); + } catch (err) { + assert.strictEqual(err.code, 'AccessDenied'); + assert.strictEqual(err.message, 'Access Denied'); + } + }); + + it('should fail because attributes header is missing', async () => { + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: [], + }) + .promise(); + assert.fail('Expected InvalidRequest error'); + } catch (err) { + assert.strictEqual(err.code, 'InvalidRequest'); + assert.strictEqual( + err.message, + 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', + ); + } + }); + + it('should fail because attribute name is invalid', async () => { + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['InvalidAttribute'], + }) + .promise(); + assert.fail('Expected InvalidArgument error'); + } catch (err) { + assert.strictEqual(err.code, 'InvalidArgument'); + assert.strictEqual(err.message, 'Invalid attribute name specified.'); + } + }); + + it('should return NoSuchKey for non-existent object', async () => { + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: 'nonexistent', + ObjectAttributes: ['ETag'], + }) + .promise(); + assert.fail('Expected NoSuchKey error'); + } catch (err) { + assert.strictEqual(err.code, 'NoSuchKey'); + assert.strictEqual(err.message, 'The specified key does not exist.'); + } + }); + + it('should return all attributes', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag', 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize'], + }) + .promise(); + + assert.strictEqual(data.ETag, expectedMD5); + assert.strictEqual(data.StorageClass, 'STANDARD'); + assert.strictEqual(data.ObjectSize, body.length); + assert(data.Checksum, 'Checksum should be present'); + assert(!data.ObjectParts, "ObjectParts shouldn't be present for non-MPU object"); + assert(data.LastModified, 'LastModified should be present'); + }); + + it('should return ETag', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + }) + .promise(); + + assert.strictEqual(data.ETag, expectedMD5); + }); + + it('should return Checksum', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['Checksum'], + }) + .promise(); + + assert(data.Checksum, 'Checksum should be present'); + }); + + it("shouldn't return ObjectParts", async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ObjectParts'], + }) + .promise(); + + // ObjectParts may be empty for non-MPU objects + assert(!data.ObjectParts, "ObjectParts shouldn't be present"); + }); + + it('should return StorageClass', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['StorageClass'], + }) + .promise(); + + assert.strictEqual(data.StorageClass, 'STANDARD'); + }); + + it('should return ObjectSize', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ObjectSize'], + }) + .promise(); + + assert.strictEqual(data.ObjectSize, body.length); + }); + + it('should return LastModified', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + }) + .promise(); + + assert(data.LastModified, 'LastModified should be present'); + assert(data.LastModified instanceof Date, 'LastModified should be a Date'); + assert(!isNaN(data.LastModified.getTime()), 'LastModified should be a valid date'); + }); +}); + +describe('Test get object attributes with multipart upload', () => { + let s3; + const mpuKey = 'mpuObject'; + const partSize = 5 * 1024 * 1024; // Minimum part size is 5MB + const partCount = 3; + + before(async () => { + const config = getConfig('default', { signatureVersion: 'v4' }); + s3 = new S3(config); + + // Create bucket + await s3.createBucket({ Bucket: bucket }).promise(); + + // Create multipart upload + const createResult = await s3 + .createMultipartUpload({ + Bucket: bucket, + Key: mpuKey, + }) + .promise(); + const uploadId = createResult.UploadId; + + // Upload parts + const partData = Buffer.alloc(partSize, 'a'); + const parts = []; + for (let i = 1; i <= partCount; i++) { + const uploadResult = await s3 + .uploadPart({ + Bucket: bucket, + Key: mpuKey, + PartNumber: i, + UploadId: uploadId, + Body: partData, + }) + .promise(); + parts.push({ PartNumber: i, ETag: uploadResult.ETag }); + } + + // Complete multipart upload + await s3 + .completeMultipartUpload({ + Bucket: bucket, + Key: mpuKey, + UploadId: uploadId, + MultipartUpload: { Parts: parts }, + }) + .promise(); + }); + + after(async () => { + await s3.deleteObject({ Bucket: bucket, Key: mpuKey }).promise(); + await s3.deleteBucket({ Bucket: bucket }).promise(); + }); + + it('should return TotalPartsCount for MPU object', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: mpuKey, + ObjectAttributes: ['ObjectParts'], + }) + .promise(); + + assert(data.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); + }); + + it('should return TotalPartsCount along with other attributes for MPU object', async () => { + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: mpuKey, + ObjectAttributes: ['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass'], + }) + .promise(); + + assert(data.ETag, 'ETag should be present'); + assert(data.ETag.includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); + assert(data.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); + assert.strictEqual(data.ObjectSize, partSize * partCount); + assert.strictEqual(data.StorageClass, 'STANDARD'); + }); +}); diff --git a/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js new file mode 100644 index 0000000000..92c1308de6 --- /dev/null +++ b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js @@ -0,0 +1,145 @@ +const assert = require('assert'); +const { promisify } = require('util'); +const { S3 } = require('aws-sdk'); +const getConfig = require('../support/config'); +const { removeAllVersions, versioningEnabled } = require('../../lib/utility/versioning-util.js'); + +const removeAllVersionsPromise = promisify(removeAllVersions); + +const bucket = 'testbucket'; +const key = 'testobject'; +const body = 'hello world!'; +const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; + +describe('Test get object attributes with versioning', () => { + let s3; + + before(() => { + const config = getConfig('default', { signatureVersion: 'v4' }); + s3 = new S3(config); + }); + + beforeEach(async () => { + await s3.createBucket({ Bucket: bucket }).promise(); + await s3 + .putBucketVersioning({ + Bucket: bucket, + VersioningConfiguration: versioningEnabled, + }) + .promise(); + }); + + afterEach(async () => { + await removeAllVersionsPromise({ Bucket: bucket }); + await s3.deleteBucket({ Bucket: bucket }).promise(); + }); + + it('should return NoSuchVersion for non-existent versionId', async () => { + await s3 + .putObject({ + Bucket: bucket, + Key: key, + Body: body, + }) + .promise(); + + // Use a properly formatted but non-existent version ID + const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; + + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + VersionId: fakeVersionId, + ObjectAttributes: ['ETag'], + }) + .promise(); + assert.fail('Expected NoSuchVersion error'); + } catch (err) { + assert.strictEqual(err.code, 'NoSuchVersion'); + assert.strictEqual( + err.message, + 'Indicates that the version ID specified in the request does not match an existing version.', + ); + } + }); + + it('should return MethodNotAllowed for delete marker', async () => { + await s3 + .putObject({ + Bucket: bucket, + Key: key, + Body: body, + }) + .promise(); + + // Delete creates a delete marker + await s3 + .deleteObject({ + Bucket: bucket, + Key: key, + }) + .promise(); + + // Request without versionId targets the delete marker + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + }) + .promise(); + assert.fail('Expected MethodNotAllowed error'); + } catch (err) { + assert.strictEqual(err.code, 'MethodNotAllowed'); + assert.strictEqual(err.message, 'The specified method is not allowed against this resource.'); + } + }); + + it('should return attributes for specific version', async () => { + const putResult = await s3 + .putObject({ + Bucket: bucket, + Key: key, + Body: body, + }) + .promise(); + const versionId = putResult.VersionId; + + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + VersionId: versionId, + ObjectAttributes: ['ETag', 'ObjectSize'], + }) + .promise(); + + assert.strictEqual(data.ETag, expectedMD5); + assert.strictEqual(data.ObjectSize, body.length); + assert(data.LastModified, 'LastModified should be present'); + }); + + it('should return VersionId for versioned object', async () => { + const putResult = await s3 + .putObject({ + Bucket: bucket, + Key: key, + Body: body, + }) + .promise(); + const versionId = putResult.VersionId; + + const data = await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + }) + .promise(); + + assert.strictEqual(data.VersionId, versionId); + }); +}); diff --git a/tests/unit/api/apiUtils/object/parseAttributesHeader.js b/tests/unit/api/apiUtils/object/parseAttributesHeader.js new file mode 100644 index 0000000000..dc09292649 --- /dev/null +++ b/tests/unit/api/apiUtils/object/parseAttributesHeader.js @@ -0,0 +1,192 @@ +const assert = require('assert'); + +const parseAttributesHeaders = require('../../../../../lib/api/apiUtils/object/parseAttributesHeader'); + +describe('parseAttributesHeaders', () => { + describe('missing or empty header', () => { + it('should throw InvalidRequest error when header is missing', () => { + const headers = {}; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidRequest, true); + assert(err.description.includes('missing or empty')); + return true; + }, + ); + }); + + it('should throw InvalidRequest error when header is empty string', () => { + const headers = { 'x-amz-object-attributes': '' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidRequest, true); + assert(err.description.includes('missing or empty')); + return true; + }, + ); + }); + + it('should throw InvalidRequest error when header contains only whitespace', () => { + const headers = { 'x-amz-object-attributes': ' ' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidRequest, true); + return true; + }, + ); + }); + + it('should throw InvalidRequest error when header contains only commas', () => { + const headers = { 'x-amz-object-attributes': ',,,' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidRequest, true); + return true; + }, + ); + }); + }); + + describe('invalid attribute names', () => { + it('should throw InvalidArgument error for single invalid attribute', () => { + const headers = { 'x-amz-object-attributes': 'InvalidAttribute' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidArgument, true); + assert(err.description.includes('Invalid attribute name')); + return true; + }, + ); + }); + + it('should throw InvalidArgument error when one attribute is invalid among valid ones', () => { + const headers = { 'x-amz-object-attributes': 'ETag,InvalidAttribute,ObjectSize' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidArgument, true); + return true; + }, + ); + }); + + it('should throw InvalidArgument error for multiple invalid attributes', () => { + const headers = { 'x-amz-object-attributes': 'Invalid1,Invalid2' }; + + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidArgument, true); + return true; + }, + ); + }); + }); + + describe('valid attribute names', () => { + it('should return array with single valid attribute ETag', () => { + const headers = { 'x-amz-object-attributes': 'ETag' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ETag']); + }); + + it('should return array with single valid attribute StorageClass', () => { + const headers = { 'x-amz-object-attributes': 'StorageClass' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['StorageClass']); + }); + + it('should return array with single valid attribute ObjectSize', () => { + const headers = { 'x-amz-object-attributes': 'ObjectSize' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ObjectSize']); + }); + + it('should return array with single valid attribute ObjectParts', () => { + const headers = { 'x-amz-object-attributes': 'ObjectParts' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ObjectParts']); + }); + + it('should return array with single valid attribute Checksum', () => { + const headers = { 'x-amz-object-attributes': 'Checksum' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['Checksum']); + }); + + it('should return array with multiple valid attributes', () => { + const headers = { 'x-amz-object-attributes': 'ETag,ObjectSize,StorageClass' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ETag', 'ObjectSize', 'StorageClass']); + }); + + it('should return array with all valid attributes', () => { + const headers = { 'x-amz-object-attributes': 'StorageClass,ObjectSize,ObjectParts,Checksum,ETag' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.strictEqual(result.length, 5); + assert(result.includes('StorageClass')); + assert(result.includes('ObjectSize')); + assert(result.includes('ObjectParts')); + assert(result.includes('Checksum')); + assert(result.includes('ETag')); + }); + }); + + describe('whitespace handling', () => { + it('should trim whitespace around attribute names', () => { + const headers = { 'x-amz-object-attributes': ' ETag , ObjectSize ' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + }); + + it('should handle extra commas between attributes', () => { + const headers = { 'x-amz-object-attributes': 'ETag,,ObjectSize' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + }); + + it('should handle leading and trailing commas', () => { + const headers = { 'x-amz-object-attributes': ',ETag,ObjectSize,' }; + const result = parseAttributesHeaders(headers); + + assert(Array.isArray(result)); + assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + }); + }); +}); diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js new file mode 100644 index 0000000000..702a3863c6 --- /dev/null +++ b/tests/unit/api/objectGetAttributes.js @@ -0,0 +1,672 @@ +const assert = require('assert'); +const async = require('async'); +const crypto = require('crypto'); +const { parseString } = require('xml2js'); + +const { bucketPut } = require('../../../lib/api/bucketPut'); +const bucketPutVersioning = require('../../../lib/api/bucketPutVersioning'); +const { cleanup, DummyRequestLogger, makeAuthInfo, versioningTestUtils } = require('../helpers'); +const completeMultipartUpload = require('../../../lib/api/completeMultipartUpload'); +const DummyRequest = require('../DummyRequest'); +const initiateMultipartUpload = require('../../../lib/api/initiateMultipartUpload'); +const objectPut = require('../../../lib/api/objectPut'); +const { objectDelete } = require('../../../lib/api/objectDelete'); +const objectGetAttributes = require('../../../lib/api/objectGetAttributes'); +const objectPutPart = require('../../../lib/api/objectPutPart'); + +const log = new DummyRequestLogger(); +const authInfo = makeAuthInfo('accessKey1'); +const namespace = 'default'; +const bucketName = 'bucketname'; +const objectName = 'objectName'; +const body = 'hello world!'; +const postBody = Buffer.from(body, 'utf8'); +const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; + +describe('objectGetAttributes API', () => { + let testPutObjectRequest; + + const testPutBucketRequest = { + bucketName, + namespace, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${bucketName}`, + actionImplicitDenies: false, + }; + + beforeEach(() => { + cleanup(); + testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + }); + + const createGetAttributesRequest = (attributes, options = {}) => ({ + bucketName, + namespace, + objectKey: options.objectKey || objectName, + headers: { + 'x-amz-object-attributes': attributes.join(','), + ...options.headers, + }, + url: `/${bucketName}/${options.objectKey || objectName}`, + query: options.query || {}, + actionImplicitDenies: false, + }); + + it('should fail because attributes header is missing', done => { + const testGetRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + query: {}, + actionImplicitDenies: false, + }; + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual( + err.description, + 'The x-amz-object-attributes header specifying the attributes ' + + 'to be retrieved is either missing or empty', + ); + done(); + }); + }); + }); + }); + + it('should fail because attributes header is empty', done => { + const testGetRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'x-amz-object-attributes': '', + }, + url: `/${bucketName}/${objectName}`, + query: {}, + actionImplicitDenies: false, + }; + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual( + err.description, + 'The x-amz-object-attributes header specifying the attributes ' + + 'to be retrieved is either missing or empty', + ); + done(); + }); + }); + }); + }); + + it('should fail because attribute name is invalid', done => { + const testGetRequest = createGetAttributesRequest(['InvalidAttribute']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); + done(); + }); + }); + }); + }); + + it('should return NoSuchKey for non-existent object', done => { + const testGetRequest = createGetAttributesRequest(['ETag'], { + objectKey: 'nonexistent', + }); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.NoSuchKey, true); + assert.strictEqual(err.description, 'The specified key does not exist.'); + done(); + }); + }); + }); + + it('should fail because of bad bucket owner', done => { + const testGetRequest = createGetAttributesRequest(['ETag'], { + headers: { + 'x-amz-expected-bucket-owner': 'wrongAccountId', + }, + }); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.AccessDenied, true); + assert.strictEqual(err.description, 'Access Denied'); + done(); + }); + }); + }); + }); + + it('should return all attributes', done => { + const testGetRequest = createGetAttributesRequest([ + 'ETag', + 'Checksum', + 'ObjectParts', + 'StorageClass', + 'ObjectSize', + ]); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml, headers) => { + assert.ifError(err); + assert(xml, 'Response XML should be present'); + assert(headers['Last-Modified'], 'Last-Modified header should be present'); + + parseString(xml, (err, result) => { + const response = result.GetObjectAttributesResponse; + + assert.ifError(err); + assert.strictEqual(response.ETag[0], expectedMD5); + assert.strictEqual(response.StorageClass[0], 'STANDARD'); + assert.strictEqual(response.ObjectSize[0], String(body.length)); + assert(response.Checksum, 'Checksum should be present'); + assert(!response.ObjectParts, 'ObjectParts should not be present for non-MPU object'); + assert(headers['Last-Modified'], 'LastModified should be present'); + done(); + }); + }); + }); + }); + }); + + it('should return ETag', done => { + const testGetRequest = createGetAttributesRequest(['ETag']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + assert.ifError(err); + assert.strictEqual(result.GetObjectAttributesResponse.ETag[0], expectedMD5); + done(); + }); + }); + }); + }); + }); + + it('should return Checksum', done => { + const testGetRequest = createGetAttributesRequest(['Checksum']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + assert.ifError(err); + assert(result.GetObjectAttributesResponse.Checksum, 'Checksum should be present'); + done(); + }); + }); + }); + }); + }); + + it('should not return ObjectParts for non-MPU object', done => { + const testGetRequest = createGetAttributesRequest(['ObjectParts']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + assert.ifError(err); + assert(!result.GetObjectAttributesResponse.ObjectParts, 'ObjectParts should not be present'); + done(); + }); + }); + }); + }); + }); + + it('should return StorageClass', done => { + const testGetRequest = createGetAttributesRequest(['StorageClass']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + assert.ifError(err); + assert.strictEqual(result.GetObjectAttributesResponse.StorageClass[0], 'STANDARD'); + done(); + }); + }); + }); + }); + }); + + it('should return ObjectSize', done => { + const testGetRequest = createGetAttributesRequest(['ObjectSize']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + assert.ifError(err); + assert.strictEqual(result.GetObjectAttributesResponse.ObjectSize[0], String(body.length)); + done(); + }); + }); + }); + }); + }); + + it('should return LastModified in response headers', done => { + const testGetRequest = createGetAttributesRequest(['ETag']); + + bucketPut(authInfo, testPutBucketRequest, log, () => { + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { + assert.ifError(err); + assert(headers['Last-Modified'], 'Last-Modified should be present'); + assert(!isNaN(new Date(headers['Last-Modified']).getTime()), 'Last-Modified should be a valid date'); + done(); + }); + }); + }); + }); +}); + +describe('objectGetAttributes API with multipart upload', () => { + const mpuObjectName = 'mpuObject'; + const partCount = 2; + const partBody = Buffer.from('I am a part\n', 'utf8'); + + const testPutBucketRequest = { + bucketName, + namespace, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${bucketName}`, + actionImplicitDenies: false, + }; + + beforeEach(done => { + cleanup(); + bucketPut(authInfo, testPutBucketRequest, log, done); + }); + + const createMpuObject = callback => { + const initiateRequest = { + bucketName, + namespace, + objectKey: mpuObjectName, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${mpuObjectName}?uploads`, + actionImplicitDenies: false, + }; + + async.waterfall( + [ + next => initiateMultipartUpload(authInfo, initiateRequest, log, next), + (result, _corsHeaders, next) => parseString(result, next), + (json, next) => { + const testUploadId = json.InitiateMultipartUploadResult.UploadId[0]; + const partHash = crypto.createHash('md5').update(partBody).digest('hex'); + + // Upload first part (minimum 5MB for non-last part) + const part1Request = new DummyRequest( + { + bucketName, + namespace, + objectKey: mpuObjectName, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + 'content-length': '5242880', + }, + parsedContentLength: 5242880, + url: `/${mpuObjectName}?partNumber=1&uploadId=${testUploadId}`, + query: { + partNumber: '1', + uploadId: testUploadId, + }, + partHash, + }, + partBody, + ); + + objectPutPart(authInfo, part1Request, undefined, log, () => { + next(null, testUploadId, partHash); + }); + }, + (testUploadId, partHash, next) => { + // Upload second part + const part2Request = new DummyRequest( + { + bucketName, + namespace, + objectKey: mpuObjectName, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + 'content-length': `${partBody.length}`, + }, + parsedContentLength: partBody.length, + url: `/${mpuObjectName}?partNumber=2&uploadId=${testUploadId}`, + query: { + partNumber: '2', + uploadId: testUploadId, + }, + partHash, + }, + partBody, + ); + + objectPutPart(authInfo, part2Request, undefined, log, () => { + next(null, testUploadId, partHash); + }); + }, + (testUploadId, partHash, next) => { + // Complete the multipart upload + const completeBody = + '' + + '' + + '1' + + `"${partHash}"` + + '' + + '' + + '2' + + `"${partHash}"` + + '' + + ''; + + const completeRequest = { + bucketName, + namespace, + objectKey: mpuObjectName, + parsedHost: 's3.amazonaws.com', + url: `/${mpuObjectName}?uploadId=${testUploadId}`, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + query: { uploadId: testUploadId }, + post: completeBody, + actionImplicitDenies: false, + }; + + completeMultipartUpload(authInfo, completeRequest, log, err => { + next(err); + }); + }, + ], + callback, + ); + }; + + const createGetAttributesRequest = attributes => ({ + bucketName, + namespace, + objectKey: mpuObjectName, + headers: { + 'x-amz-object-attributes': attributes.join(','), + }, + url: `/${bucketName}/${mpuObjectName}`, + query: {}, + actionImplicitDenies: false, + }); + + it('should return TotalPartsCount for MPU object', done => { + createMpuObject(err => { + assert.ifError(err); + const testGetRequest = createGetAttributesRequest(['ObjectParts']); + + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + const response = result.GetObjectAttributesResponse; + + assert.ifError(err); + assert(response.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); + done(); + }); + }); + }); + }); + + it('should return TotalPartsCount along with other attributes for MPU object', done => { + createMpuObject(err => { + assert.ifError(err); + const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass']); + + objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { + assert.ifError(err); + parseString(xml, (err, result) => { + const response = result.GetObjectAttributesResponse; + + assert.ifError(err); + assert(response.ETag, 'ETag should be present'); + assert(response.ETag[0].includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); + assert(response.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); + assert(response.ObjectSize, 'ObjectSize should be present'); + assert.strictEqual(response.StorageClass[0], 'STANDARD'); + done(); + }); + }); + }); + }); +}); + +describe('objectGetAttributes API with versioning', () => { + const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); + + const testPutBucketRequest = { + bucketName, + namespace, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${bucketName}`, + actionImplicitDenies: false, + }; + + beforeEach(done => { + cleanup(); + async.series( + [ + next => bucketPut(authInfo, testPutBucketRequest, log, next), + next => bucketPutVersioning(authInfo, enableVersioningRequest, log, next), + ], + done, + ); + }); + + const createGetAttributesRequest = (attributes, options = {}) => ({ + bucketName, + namespace, + objectKey: options.objectKey || objectName, + headers: { + 'x-amz-object-attributes': attributes.join(','), + ...options.headers, + }, + url: `/${bucketName}/${options.objectKey || objectName}`, + query: options.query || {}, + actionImplicitDenies: false, + }); + + it('should return NoSuchVersion for non-existent versionId', done => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + // Use a properly formatted but non-existent version ID + const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; + + objectPut(authInfo, testPutObjectRequest, undefined, log, err => { + assert.ifError(err); + const testGetRequest = createGetAttributesRequest(['ETag'], { + query: { versionId: fakeVersionId }, + }); + + objectGetAttributes(authInfo, testGetRequest, log, err => { + assert.strictEqual(err.is.NoSuchVersion, true); + assert.strictEqual( + err.description, + 'Indicates that the version ID specified in the request does not match an existing version.', + ); + done(); + }); + }); + }); + + it('should return MethodNotAllowed for delete marker', done => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + const testDeleteRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + actionImplicitDenies: false, + }; + + async.series( + [ + next => objectPut(authInfo, testPutObjectRequest, undefined, log, next), + next => objectDelete(authInfo, testDeleteRequest, log, next), + ], + err => { + assert.ifError(err); + // Request without versionId targets the delete marker + const testGetRequest = createGetAttributesRequest(['ETag']); + + objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { + assert.strictEqual(err.is.MethodNotAllowed, true); + assert.strictEqual(err.description, 'The specified method is not allowed against this resource.'); + assert.strictEqual(headers['x-amz-delete-marker'], true); + done(); + }); + }, + ); + }); + + it('should return attributes for specific version', done => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + objectPut(authInfo, testPutObjectRequest, undefined, log, (err, resHeaders) => { + assert.ifError(err); + const versionId = resHeaders['x-amz-version-id']; + assert(versionId, 'Version ID should be present'); + + const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectSize'], { + query: { versionId }, + }); + + objectGetAttributes(authInfo, testGetRequest, log, (err, xml, headers) => { + assert.ifError(err); + assert(headers['Last-Modified'], 'Last-Modified should be present'); + + parseString(xml, (err, result) => { + const response = result.GetObjectAttributesResponse; + + assert.ifError(err); + assert.strictEqual(response.ETag[0], expectedMD5); + assert.strictEqual(response.ObjectSize[0], String(body.length)); + done(); + }); + }); + }); + }); + + it('should return VersionId in response headers for versioned object', done => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + objectPut(authInfo, testPutObjectRequest, undefined, log, (err, resHeaders) => { + assert.ifError(err); + const versionId = resHeaders['x-amz-version-id']; + assert(versionId, 'Version ID should be present from PUT'); + + const testGetRequest = createGetAttributesRequest(['ETag']); + + objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { + assert.ifError(err); + assert.strictEqual(headers['x-amz-version-id'], versionId); + done(); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index eff69188c9..92698d9e55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5349,9 +5349,9 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/arsenal#8.3.2": - version "8.3.2" - resolved "git+https://github.com/scality/arsenal#c448c35acb026eeaaf5336a982c3b53ee64c51fa" +"arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": + version "8.2.44" + resolved "git+https://github.com/scality/Arsenal#f0e0fea7ae19df55ce0239b8475d7594a358c253" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" From 243dbbc9559a1180a9d5cc64a1967b4848ab6bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 28 Jan 2026 15:12:28 +0100 Subject: [PATCH 02/11] fixup! Support the new API GetObjectAttributes --- lib/api/objectGetAttributes.js | 115 +++++++++--------- lib/metadata/metadataUtils.js | 1 + .../test/object/objectGetAttributes.js | 19 ++- .../test/versioning/objectGetAttributes.js | 3 - tests/unit/api/objectGetAttributes.js | 5 - yarn.lock | 10 +- 6 files changed, 68 insertions(+), 85 deletions(-) diff --git a/lib/api/objectGetAttributes.js b/lib/api/objectGetAttributes.js index 8c232dd2f1..86f09fe31f 100644 --- a/lib/api/objectGetAttributes.js +++ b/lib/api/objectGetAttributes.js @@ -12,25 +12,7 @@ const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo'); const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner); - -/** - * validateBucketAndObjPromise - Promisified wrapper for standardMetadataValidateBucketAndObj - * @param {object} params - validation parameters - * @param {boolean} actionImplicitDenies - whether action has implicit denies - * @param {object} log - Werelogs logger - * @returns {Promise<{bucket: BucketInfo, objMD: object}>} - bucket and object metadata - * @throws {Error} - rejects with error from standardMetadataValidateBucketAndObj - */ -function validateBucketAndObjPromise(params, actionImplicitDenies, log) { - return new Promise((resolve, reject) => { - standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, (err, bucket, objMD) => { - if (err) { - return reject(err); - } - return resolve({ bucket, objMD }); - }); - }); -} +const validateBucketAndObj = promisify(standardMetadataValidateBucketAndObj); /** * buildXmlResponse - Build XML response for GetObjectAttributes @@ -74,15 +56,15 @@ function buildXmlResponse(objMD, attributes) { * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info * @param {object} request - http request object * @param {object} log - Werelogs logger - * @param {function} callback - callback to server - * @return {undefined} + * @returns {Promise} - { xml, responseHeaders } + * @throws {ArsenalError} NoSuchVersion - if versionId specified but not found + * @throws {ArsenalError} NoSuchKey - if object not found + * @throws {ArsenalError} MethodNotAllowed - if object is a delete marker */ -async function objectGetAttributes(authInfo, request, log, callback) { +async function objectGetAttributes(authInfo, request, log) { log.trace('processing request', { method: OBJECT_GET_ATTRIBUTES }); const { bucketName, objectKey, headers, actionImplicitDenies } = request; - let responseHeaders = {}; - const versionId = decodeVersionId(request.query); if (versionId instanceof Error) { log.debug('invalid versionId query', { versionId: request.query.versionId, error: versionId }); @@ -99,48 +81,61 @@ async function objectGetAttributes(authInfo, request, log, callback) { request, }; - try { - const { bucket, objMD } = await validateBucketAndObjPromise(metadataValParams, actionImplicitDenies, log); - await checkExpectedBucketOwnerPromise(headers, bucket, log); + const { bucket, objMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log); + await checkExpectedBucketOwnerPromise(headers, bucket, log); - responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket); - if (objMD) { - responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objMD); - responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString(); - } + const responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket); - if (!objMD) { - const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; - log.debug('object not found', { bucket: bucketName, key: objectKey, versionId }); - throw err; - } + if (!objMD) { + const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; + log.debug('object not found', { bucket: bucketName, key: objectKey, versionId }); + err.responseHeaders = responseHeaders; + throw err; + } - if (objMD.isDeleteMarker) { - log.debug('attempt to get attributes of a delete marker', { bucket: bucketName, key: objectKey, versionId }); - responseHeaders['x-amz-delete-marker'] = true; - throw errors.MethodNotAllowed; - } + responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objMD); + responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString(); + + if (objMD.isDeleteMarker) { + log.debug('attempt to get attributes of a delete marker', { bucket: bucketName, key: objectKey, versionId }); + responseHeaders['x-amz-delete-marker'] = true; + const err = errors.MethodNotAllowed; + err.responseHeaders = responseHeaders; + throw err; + } - const attributes = parseAttributesHeaders(headers); + const attributes = parseAttributesHeaders(headers); - pushMetric(OBJECT_GET_ATTRIBUTES, log, { - authInfo, - bucket: bucketName, - keys: [objectKey], - versionId: objMD?.versionId, - location: objMD?.dataStoreName, - }); + pushMetric(OBJECT_GET_ATTRIBUTES, log, { + authInfo, + bucket: bucketName, + keys: [objectKey], + versionId: objMD?.versionId, + location: objMD?.dataStoreName, + }); - const xml = buildXmlResponse(objMD, attributes); - return callback(null, xml, responseHeaders); - } catch (err) { - log.debug('error processing request', { - error: err, - method: OBJECT_GET_ATTRIBUTES, - }); + const xml = buildXmlResponse(objMD, attributes); + return { xml, responseHeaders }; +} - return callback(err, null, responseHeaders); - } +/** + * objectGetAttributesCallback - Callback wrapper for objectGetAttributes + * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info + * @param {object} request - http request object + * @param {object} log - Werelogs logger + * @param {function} callback - callback to server (err, xml, responseHeaders) + * @return {undefined} + */ +function objectGetAttributesCallback(authInfo, request, log, callback) { + objectGetAttributes(authInfo, request, log) + .then(result => callback(null, result.xml, result.responseHeaders)) + .catch(err => { + log.debug('error processing request', { + error: err, + method: OBJECT_GET_ATTRIBUTES, + }); + return callback(err, null, err.responseHeaders || {}); + }); } -module.exports = objectGetAttributes; +module.exports = objectGetAttributesCallback; diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 2702125209..9a543474e6 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -1,5 +1,6 @@ const { promisify } = require('util'); const async = require('async'); +const { promisify } = require('util'); const { errors } = require('arsenal'); const metadata = require('./wrapper'); diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js index b969e9926b..6af868181c 100644 --- a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -7,7 +7,7 @@ const key = 'testobject'; const body = 'hello world!'; const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; -describe('Test get object attributes', () => { +describe('objectGetAttributes', () => { let s3; before(() => { @@ -25,7 +25,7 @@ describe('Test get object attributes', () => { await s3.deleteBucket({ Bucket: bucket }).promise(); }); - it('should fail because a bad bucket owner', async () => { + it('should fail with a wrong bucket owner header', async () => { try { await s3 .getObjectAttributes({ @@ -105,8 +105,8 @@ describe('Test get object attributes', () => { assert.strictEqual(data.ETag, expectedMD5); assert.strictEqual(data.StorageClass, 'STANDARD'); assert.strictEqual(data.ObjectSize, body.length); - assert(data.Checksum, 'Checksum should be present'); - assert(!data.ObjectParts, "ObjectParts shouldn't be present for non-MPU object"); + assert.deepStrictEqual(data.Checksum, {}, 'Checksum should be present'); + assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); assert(data.LastModified, 'LastModified should be present'); }); @@ -131,10 +131,10 @@ describe('Test get object attributes', () => { }) .promise(); - assert(data.Checksum, 'Checksum should be present'); + assert.deepStrictEqual(data.Checksum, {}, 'Checksum should be present'); }); - it("shouldn't return ObjectParts", async () => { + it("shouldn't return ObjectParts for non-MPU objects", async () => { const data = await s3 .getObjectAttributes({ Bucket: bucket, @@ -143,8 +143,7 @@ describe('Test get object attributes', () => { }) .promise(); - // ObjectParts may be empty for non-MPU objects - assert(!data.ObjectParts, "ObjectParts shouldn't be present"); + assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present"); }); it('should return StorageClass', async () => { @@ -196,10 +195,8 @@ describe('Test get object attributes with multipart upload', () => { const config = getConfig('default', { signatureVersion: 'v4' }); s3 = new S3(config); - // Create bucket await s3.createBucket({ Bucket: bucket }).promise(); - // Create multipart upload const createResult = await s3 .createMultipartUpload({ Bucket: bucket, @@ -208,7 +205,6 @@ describe('Test get object attributes with multipart upload', () => { .promise(); const uploadId = createResult.UploadId; - // Upload parts const partData = Buffer.alloc(partSize, 'a'); const parts = []; for (let i = 1; i <= partCount; i++) { @@ -224,7 +220,6 @@ describe('Test get object attributes with multipart upload', () => { parts.push({ PartNumber: i, ETag: uploadResult.ETag }); } - // Complete multipart upload await s3 .completeMultipartUpload({ Bucket: bucket, diff --git a/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js index 92c1308de6..8b932cdfae 100644 --- a/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js @@ -43,7 +43,6 @@ describe('Test get object attributes with versioning', () => { }) .promise(); - // Use a properly formatted but non-existent version ID const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; try { @@ -74,7 +73,6 @@ describe('Test get object attributes with versioning', () => { }) .promise(); - // Delete creates a delete marker await s3 .deleteObject({ Bucket: bucket, @@ -82,7 +80,6 @@ describe('Test get object attributes with versioning', () => { }) .promise(); - // Request without versionId targets the delete marker try { await s3 .getObjectAttributes({ diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js index 702a3863c6..ba868af59a 100644 --- a/tests/unit/api/objectGetAttributes.js +++ b/tests/unit/api/objectGetAttributes.js @@ -345,7 +345,6 @@ describe('objectGetAttributes API with multipart upload', () => { const testUploadId = json.InitiateMultipartUploadResult.UploadId[0]; const partHash = crypto.createHash('md5').update(partBody).digest('hex'); - // Upload first part (minimum 5MB for non-last part) const part1Request = new DummyRequest( { bucketName, @@ -371,7 +370,6 @@ describe('objectGetAttributes API with multipart upload', () => { }); }, (testUploadId, partHash, next) => { - // Upload second part const part2Request = new DummyRequest( { bucketName, @@ -397,7 +395,6 @@ describe('objectGetAttributes API with multipart upload', () => { }); }, (testUploadId, partHash, next) => { - // Complete the multipart upload const completeBody = '' + '' + @@ -536,7 +533,6 @@ describe('objectGetAttributes API with versioning', () => { postBody, ); - // Use a properly formatted but non-existent version ID const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; objectPut(authInfo, testPutObjectRequest, undefined, log, err => { @@ -587,7 +583,6 @@ describe('objectGetAttributes API with versioning', () => { ], err => { assert.ifError(err); - // Request without versionId targets the delete marker const testGetRequest = createGetAttributesRequest(['ETag']); objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { diff --git a/yarn.lock b/yarn.lock index 92698d9e55..6333b8feeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5350,8 +5350,8 @@ arraybuffer.prototype.slice@^1.0.4: ioctl "^2.0.2" "arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": - version "8.2.44" - resolved "git+https://github.com/scality/Arsenal#f0e0fea7ae19df55ce0239b8475d7594a358c253" + version "8.2.43" + resolved "git+https://github.com/scality/Arsenal#ec21fa885c611498584ef3c56bfd62047c640e9e" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" @@ -7672,9 +7672,9 @@ ioredis@^5.6.1: standard-as-callback "^2.1.0" ioredis@^5.8.1: - version "5.8.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.8.2.tgz#c7a228a26cf36f17a5a8011148836877780e2e14" - integrity sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q== + version "5.9.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.9.2.tgz#ffdce2a019950299716e88ee56cd5802b399b108" + integrity sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ== dependencies: "@ioredis/commands" "1.4.0" cluster-key-slot "^1.1.0" From 1e12cb85ca69bb59c6801ba9930f4d3845900363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 28 Jan 2026 18:04:38 +0100 Subject: [PATCH 03/11] fixup! Support the new API GetObjectAttributes --- tests/unit/api/objectGetAttributes.js | 969 +++++++++++--------------- 1 file changed, 397 insertions(+), 572 deletions(-) diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js index ba868af59a..33c678be4f 100644 --- a/tests/unit/api/objectGetAttributes.js +++ b/tests/unit/api/objectGetAttributes.js @@ -1,7 +1,6 @@ const assert = require('assert'); -const async = require('async'); const crypto = require('crypto'); -const { parseString } = require('xml2js'); +const { parseStringPromise } = require('xml2js'); const { bucketPut } = require('../../../lib/api/bucketPut'); const bucketPutVersioning = require('../../../lib/api/bucketPutVersioning'); @@ -23,645 +22,471 @@ const body = 'hello world!'; const postBody = Buffer.from(body, 'utf8'); const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; -describe('objectGetAttributes API', () => { - let testPutObjectRequest; +// Promisify helper for functions with non-standard callback signatures +const promisify = fn => (...args) => new Promise((resolve, reject) => { + fn(...args, (err, ...results) => { + if (err) { + reject(err); + } else { + resolve(results); + } + }); +}); - const testPutBucketRequest = { +const bucketPutAsync = promisify(bucketPut); +const bucketPutVersioningAsync = promisify(bucketPutVersioning); +const objectPutAsync = promisify(objectPut); +const objectDeleteAsync = promisify(objectDelete); +const objectGetAttributesAsync = promisify(objectGetAttributes); +const initiateMultipartUploadAsync = promisify(initiateMultipartUpload); +const objectPutPartAsync = promisify(objectPutPart); +const completeMultipartUploadAsync = promisify(completeMultipartUpload); + +const testPutBucketRequest = { bucketName, namespace, headers: { host: `${bucketName}.s3.amazonaws.com` }, url: `/${bucketName}`, actionImplicitDenies: false, - }; +}; - beforeEach(() => { - cleanup(); - testPutObjectRequest = new DummyRequest( - { +const createGetAttributesRequest = (attributes, options = {}) => { + const key = options.objectKey || objectName; + return { bucketName, namespace, - objectKey: objectName, + objectKey: key, headers: { - 'content-length': `${postBody.length}`, + 'x-amz-object-attributes': attributes.join(','), + ...options.headers, }, - parsedContentLength: postBody.length, - url: `/${bucketName}/${objectName}`, - }, - postBody, - ); - }); - - const createGetAttributesRequest = (attributes, options = {}) => ({ - bucketName, - namespace, - objectKey: options.objectKey || objectName, - headers: { - 'x-amz-object-attributes': attributes.join(','), - ...options.headers, - }, - url: `/${bucketName}/${options.objectKey || objectName}`, - query: options.query || {}, - actionImplicitDenies: false, - }); - - it('should fail because attributes header is missing', done => { - const testGetRequest = { - bucketName, - namespace, - objectKey: objectName, - headers: {}, - url: `/${bucketName}/${objectName}`, - query: {}, - actionImplicitDenies: false, + url: `/${bucketName}/${key}`, + query: options.query || {}, + actionImplicitDenies: false, }; +}; - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.InvalidRequest, true); - assert.strictEqual( - err.description, - 'The x-amz-object-attributes header specifying the attributes ' + - 'to be retrieved is either missing or empty', - ); - done(); - }); - }); +describe('objectGetAttributes API', () => { + beforeEach(async () => { + cleanup(); + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + await bucketPutAsync(authInfo, testPutBucketRequest, log); + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); }); - }); - - it('should fail because attributes header is empty', done => { - const testGetRequest = { - bucketName, - namespace, - objectKey: objectName, - headers: { - 'x-amz-object-attributes': '', - }, - url: `/${bucketName}/${objectName}`, - query: {}, - actionImplicitDenies: false, - }; - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.InvalidRequest, true); - assert.strictEqual( - err.description, - 'The x-amz-object-attributes header specifying the attributes ' + - 'to be retrieved is either missing or empty', - ); - done(); - }); - }); - }); - }); - - it('should fail because attribute name is invalid', done => { - const testGetRequest = createGetAttributesRequest(['InvalidAttribute']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.InvalidArgument, true); - assert.strictEqual(err.description, 'Invalid attribute name specified.'); - done(); - }); - }); + it('should fail because attributes header is missing', async () => { + const testGetRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + query: {}, + actionImplicitDenies: false, + }; + + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual( + err.description, + 'The x-amz-object-attributes header specifying the attributes ' + + 'to be retrieved is either missing or empty', + ); + } }); - }); - it('should return NoSuchKey for non-existent object', done => { - const testGetRequest = createGetAttributesRequest(['ETag'], { - objectKey: 'nonexistent', + it('should fail because attributes header is empty', async () => { + const testGetRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'x-amz-object-attributes': '', + }, + url: `/${bucketName}/${objectName}`, + query: {}, + actionImplicitDenies: false, + }; + + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual( + err.description, + 'The x-amz-object-attributes header specifying the attributes ' + + 'to be retrieved is either missing or empty', + ); + } }); - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.NoSuchKey, true); - assert.strictEqual(err.description, 'The specified key does not exist.'); - done(); - }); - }); - }); + it('should fail because attribute name is invalid', async () => { + const testGetRequest = createGetAttributesRequest(['InvalidAttribute']); - it('should fail because of bad bucket owner', done => { - const testGetRequest = createGetAttributesRequest(['ETag'], { - headers: { - 'x-amz-expected-bucket-owner': 'wrongAccountId', - }, + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); + } }); - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.AccessDenied, true); - assert.strictEqual(err.description, 'Access Denied'); - done(); - }); - }); - }); - }); - - it('should return all attributes', done => { - const testGetRequest = createGetAttributesRequest([ - 'ETag', - 'Checksum', - 'ObjectParts', - 'StorageClass', - 'ObjectSize', - ]); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml, headers) => { - assert.ifError(err); - assert(xml, 'Response XML should be present'); - assert(headers['Last-Modified'], 'Last-Modified header should be present'); - - parseString(xml, (err, result) => { - const response = result.GetObjectAttributesResponse; - - assert.ifError(err); - assert.strictEqual(response.ETag[0], expectedMD5); - assert.strictEqual(response.StorageClass[0], 'STANDARD'); - assert.strictEqual(response.ObjectSize[0], String(body.length)); - assert(response.Checksum, 'Checksum should be present'); - assert(!response.ObjectParts, 'ObjectParts should not be present for non-MPU object'); - assert(headers['Last-Modified'], 'LastModified should be present'); - done(); - }); + it('should return NoSuchKey for non-existent object', async () => { + const testGetRequest = createGetAttributesRequest(['ETag'], { + objectKey: 'nonexistent', }); - }); + + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.NoSuchKey, true); + assert.strictEqual(err.description, 'The specified key does not exist.'); + } }); - }); - - it('should return ETag', done => { - const testGetRequest = createGetAttributesRequest(['ETag']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - assert.ifError(err); - assert.strictEqual(result.GetObjectAttributesResponse.ETag[0], expectedMD5); - done(); - }); + + it('should fail because of bad bucket owner', async () => { + const testGetRequest = createGetAttributesRequest(['ETag'], { + headers: { + 'x-amz-expected-bucket-owner': 'wrongAccountId', + }, }); - }); + + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.AccessDenied, true); + assert.strictEqual(err.description, 'Access Denied'); + } }); - }); - - it('should return Checksum', done => { - const testGetRequest = createGetAttributesRequest(['Checksum']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - assert.ifError(err); - assert(result.GetObjectAttributesResponse.Checksum, 'Checksum should be present'); - done(); - }); - }); - }); + + it('should return all attributes', async () => { + const testGetRequest = createGetAttributesRequest([ + 'ETag', + 'Checksum', + 'ObjectParts', + 'StorageClass', + 'ObjectSize', + ]); + + const [xml, headers] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert(xml, 'Response XML should be present'); + assert(headers['Last-Modified'], 'Last-Modified header should be present'); + + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert.strictEqual(response.ETag[0], expectedMD5); + assert.strictEqual(response.StorageClass[0], 'STANDARD'); + assert.strictEqual(response.ObjectSize[0], String(body.length)); + assert.deepStrictEqual(response.Checksum[0], '', 'Checksum should be empty'); + assert.strictEqual(response.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); + assert(headers['Last-Modified'], 'LastModified should be present'); }); - }); - - it('should not return ObjectParts for non-MPU object', done => { - const testGetRequest = createGetAttributesRequest(['ObjectParts']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - assert.ifError(err); - assert(!result.GetObjectAttributesResponse.ObjectParts, 'ObjectParts should not be present'); - done(); - }); - }); - }); + + it('should return ETag', async () => { + const testGetRequest = createGetAttributesRequest(['ETag']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + assert.strictEqual(result.GetObjectAttributesResponse.ETag[0], expectedMD5); }); - }); - - it('should return StorageClass', done => { - const testGetRequest = createGetAttributesRequest(['StorageClass']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - assert.ifError(err); - assert.strictEqual(result.GetObjectAttributesResponse.StorageClass[0], 'STANDARD'); - done(); - }); - }); - }); + + it('should return Checksum', async () => { + const testGetRequest = createGetAttributesRequest(['Checksum']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + assert.deepStrictEqual(result.GetObjectAttributesResponse.Checksum[0], '', 'Checksum should be empty'); }); - }); - - it('should return ObjectSize', done => { - const testGetRequest = createGetAttributesRequest(['ObjectSize']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - assert.ifError(err); - assert.strictEqual(result.GetObjectAttributesResponse.ObjectSize[0], String(body.length)); - done(); - }); - }); - }); + + it("shouldn't return ObjectParts for non-MPU object", async () => { + const testGetRequest = createGetAttributesRequest(['ObjectParts']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + assert.strictEqual( + result.GetObjectAttributesResponse.ObjectParts, + undefined, + "ObjectParts shouldn't be present", + ); }); - }); - - it('should return LastModified in response headers', done => { - const testGetRequest = createGetAttributesRequest(['ETag']); - - bucketPut(authInfo, testPutBucketRequest, log, () => { - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { - assert.ifError(err); - assert(headers['Last-Modified'], 'Last-Modified should be present'); - assert(!isNaN(new Date(headers['Last-Modified']).getTime()), 'Last-Modified should be a valid date'); - done(); - }); - }); + + it('should return StorageClass', async () => { + const testGetRequest = createGetAttributesRequest(['StorageClass']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + assert.strictEqual(result.GetObjectAttributesResponse.StorageClass[0], 'STANDARD'); }); - }); -}); -describe('objectGetAttributes API with multipart upload', () => { - const mpuObjectName = 'mpuObject'; - const partCount = 2; - const partBody = Buffer.from('I am a part\n', 'utf8'); + it('should return ObjectSize', async () => { + const testGetRequest = createGetAttributesRequest(['ObjectSize']); - const testPutBucketRequest = { - bucketName, - namespace, - headers: { host: `${bucketName}.s3.amazonaws.com` }, - url: `/${bucketName}`, - actionImplicitDenies: false, - }; - - beforeEach(done => { - cleanup(); - bucketPut(authInfo, testPutBucketRequest, log, done); - }); - - const createMpuObject = callback => { - const initiateRequest = { - bucketName, - namespace, - objectKey: mpuObjectName, - headers: { host: `${bucketName}.s3.amazonaws.com` }, - url: `/${mpuObjectName}?uploads`, - actionImplicitDenies: false, - }; + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + assert.strictEqual(result.GetObjectAttributesResponse.ObjectSize[0], String(body.length)); + }); - async.waterfall( - [ - next => initiateMultipartUpload(authInfo, initiateRequest, log, next), - (result, _corsHeaders, next) => parseString(result, next), - (json, next) => { - const testUploadId = json.InitiateMultipartUploadResult.UploadId[0]; - const partHash = crypto.createHash('md5').update(partBody).digest('hex'); + it('should return LastModified in response headers', async () => { + const testGetRequest = createGetAttributesRequest(['ETag']); - const part1Request = new DummyRequest( - { - bucketName, - namespace, - objectKey: mpuObjectName, - headers: { - host: `${bucketName}.s3.amazonaws.com`, - 'content-length': '5242880', - }, - parsedContentLength: 5242880, - url: `/${mpuObjectName}?partNumber=1&uploadId=${testUploadId}`, - query: { - partNumber: '1', - uploadId: testUploadId, - }, - partHash, - }, - partBody, - ); + const [, headers] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert(headers['Last-Modified'], 'Last-Modified should be present'); + assert(!isNaN(new Date(headers['Last-Modified']).getTime()), 'Last-Modified should be a valid date'); + }); +}); - objectPutPart(authInfo, part1Request, undefined, log, () => { - next(null, testUploadId, partHash); - }); - }, - (testUploadId, partHash, next) => { - const part2Request = new DummyRequest( - { - bucketName, - namespace, - objectKey: mpuObjectName, - headers: { - host: `${bucketName}.s3.amazonaws.com`, - 'content-length': `${partBody.length}`, - }, - parsedContentLength: partBody.length, - url: `/${mpuObjectName}?partNumber=2&uploadId=${testUploadId}`, - query: { - partNumber: '2', - uploadId: testUploadId, - }, - partHash, - }, - partBody, - ); +describe('objectGetAttributes API with multipart upload', () => { + const partCount = 2; + const partBody = Buffer.from('I am a part\n', 'utf8'); - objectPutPart(authInfo, part2Request, undefined, log, () => { - next(null, testUploadId, partHash); - }); - }, - (testUploadId, partHash, next) => { - const completeBody = - '' + - '' + - '1' + - `"${partHash}"` + - '' + - '' + - '2' + - `"${partHash}"` + - '' + - ''; - - const completeRequest = { + const createMpuObject = async () => { + const initiateRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${objectName}?uploads`, + actionImplicitDenies: false, + }; + + const [result] = await initiateMultipartUploadAsync(authInfo, initiateRequest, log); + const json = await parseStringPromise(result); + const testUploadId = json.InitiateMultipartUploadResult.UploadId[0]; + const partHash = crypto.createHash('md5').update(partBody).digest('hex'); + + const completeParts = []; + for (let i = 1; i <= partCount; i++) { + const partRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + 'content-length': '5242880', + }, + parsedContentLength: 5242880, + url: `/${objectName}?partNumber=${i}&uploadId=${testUploadId}`, + query: { + partNumber: String(i), + uploadId: testUploadId, + }, + partHash, + }, + partBody, + ); + await objectPutPartAsync(authInfo, partRequest, undefined, log); + completeParts.push(`${i}"${partHash}"`); + } + + const completeBody = + `${completeParts.join('')}`; + + const completeRequest = { bucketName, namespace, - objectKey: mpuObjectName, + objectKey: objectName, parsedHost: 's3.amazonaws.com', - url: `/${mpuObjectName}?uploadId=${testUploadId}`, + url: `/${objectName}?uploadId=${testUploadId}`, headers: { host: `${bucketName}.s3.amazonaws.com` }, query: { uploadId: testUploadId }, post: completeBody, actionImplicitDenies: false, - }; + }; - completeMultipartUpload(authInfo, completeRequest, log, err => { - next(err); - }); - }, - ], - callback, - ); - }; + await completeMultipartUploadAsync(authInfo, completeRequest, log); + }; - const createGetAttributesRequest = attributes => ({ - bucketName, - namespace, - objectKey: mpuObjectName, - headers: { - 'x-amz-object-attributes': attributes.join(','), - }, - url: `/${bucketName}/${mpuObjectName}`, - query: {}, - actionImplicitDenies: false, - }); - - it('should return TotalPartsCount for MPU object', done => { - createMpuObject(err => { - assert.ifError(err); - const testGetRequest = createGetAttributesRequest(['ObjectParts']); - - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - const response = result.GetObjectAttributesResponse; - - assert.ifError(err); - assert(response.ObjectParts, 'ObjectParts should be present'); - assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); - done(); - }); - }); + beforeEach(async () => { + cleanup(); + await bucketPutAsync(authInfo, testPutBucketRequest, log); + await createMpuObject(); }); - }); - - it('should return TotalPartsCount along with other attributes for MPU object', done => { - createMpuObject(err => { - assert.ifError(err); - const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass']); - - objectGetAttributes(authInfo, testGetRequest, log, (err, xml) => { - assert.ifError(err); - parseString(xml, (err, result) => { - const response = result.GetObjectAttributesResponse; - - assert.ifError(err); - assert(response.ETag, 'ETag should be present'); - assert(response.ETag[0].includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); - assert(response.ObjectParts, 'ObjectParts should be present'); - assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); - assert(response.ObjectSize, 'ObjectSize should be present'); - assert.strictEqual(response.StorageClass[0], 'STANDARD'); - done(); - }); - }); + + it('should return TotalPartsCount for MPU object', async () => { + const testGetRequest = createGetAttributesRequest(['ObjectParts']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); + }); + + it('should return TotalPartsCount along with other attributes for MPU object', async () => { + const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass']); + + const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.ETag, 'ETag should be present'); + assert(response.ETag[0].includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); + assert(response.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(response.ObjectParts[0].PartsCount[0], String(partCount)); + assert(response.ObjectSize, 'ObjectSize should be present'); + assert.strictEqual(response.StorageClass[0], 'STANDARD'); }); - }); }); describe('objectGetAttributes API with versioning', () => { - const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); + const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); - const testPutBucketRequest = { - bucketName, - namespace, - headers: { host: `${bucketName}.s3.amazonaws.com` }, - url: `/${bucketName}`, - actionImplicitDenies: false, - }; - - beforeEach(done => { - cleanup(); - async.series( - [ - next => bucketPut(authInfo, testPutBucketRequest, log, next), - next => bucketPutVersioning(authInfo, enableVersioningRequest, log, next), - ], - done, - ); - }); - - const createGetAttributesRequest = (attributes, options = {}) => ({ - bucketName, - namespace, - objectKey: options.objectKey || objectName, - headers: { - 'x-amz-object-attributes': attributes.join(','), - ...options.headers, - }, - url: `/${bucketName}/${options.objectKey || objectName}`, - query: options.query || {}, - actionImplicitDenies: false, - }); + beforeEach(async () => { + cleanup(); + await bucketPutAsync(authInfo, testPutBucketRequest, log); + await bucketPutVersioningAsync(authInfo, enableVersioningRequest, log); + }); - it('should return NoSuchVersion for non-existent versionId', done => { - const testPutObjectRequest = new DummyRequest( - { - bucketName, - namespace, - objectKey: objectName, - headers: { - 'content-length': `${postBody.length}`, - }, - parsedContentLength: postBody.length, - url: `/${bucketName}/${objectName}`, - }, - postBody, - ); - - const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; - - objectPut(authInfo, testPutObjectRequest, undefined, log, err => { - assert.ifError(err); - const testGetRequest = createGetAttributesRequest(['ETag'], { - query: { versionId: fakeVersionId }, - }); - - objectGetAttributes(authInfo, testGetRequest, log, err => { - assert.strictEqual(err.is.NoSuchVersion, true); - assert.strictEqual( - err.description, - 'Indicates that the version ID specified in the request does not match an existing version.', + it('should return NoSuchVersion for non-existent versionId', async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, ); - done(); - }); + + const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; + + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + const testGetRequest = createGetAttributesRequest(['ETag'], { + query: { versionId: fakeVersionId }, + }); + + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.NoSuchVersion, true); + assert.strictEqual( + err.description, + 'Indicates that the version ID specified in the request does not match an existing version.', + ); + } }); - }); - it('should return MethodNotAllowed for delete marker', done => { - const testPutObjectRequest = new DummyRequest( - { - bucketName, - namespace, - objectKey: objectName, - headers: { - 'content-length': `${postBody.length}`, - }, - parsedContentLength: postBody.length, - url: `/${bucketName}/${objectName}`, - }, - postBody, - ); - - const testDeleteRequest = { - bucketName, - namespace, - objectKey: objectName, - headers: {}, - url: `/${bucketName}/${objectName}`, - actionImplicitDenies: false, - }; + it('should return MethodNotAllowed for delete marker', async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + const testDeleteRequest = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + actionImplicitDenies: false, + }; + + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + await objectDeleteAsync(authInfo, testDeleteRequest, log); - async.series( - [ - next => objectPut(authInfo, testPutObjectRequest, undefined, log, next), - next => objectDelete(authInfo, testDeleteRequest, log, next), - ], - err => { - assert.ifError(err); const testGetRequest = createGetAttributesRequest(['ETag']); - objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { - assert.strictEqual(err.is.MethodNotAllowed, true); - assert.strictEqual(err.description, 'The specified method is not allowed against this resource.'); - assert.strictEqual(headers['x-amz-delete-marker'], true); - done(); + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.MethodNotAllowed, true); + assert.strictEqual(err.description, 'The specified method is not allowed against this resource.'); + assert.strictEqual(err.responseHeaders['x-amz-delete-marker'], true); + } + }); + + it('should return attributes for specific version', async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + + const [resHeaders] = await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + const versionId = resHeaders['x-amz-version-id']; + assert(versionId, 'Version ID should be present'); + + const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectSize'], { + query: { versionId }, }); - }, - ); - }); - it('should return attributes for specific version', done => { - const testPutObjectRequest = new DummyRequest( - { - bucketName, - namespace, - objectKey: objectName, - headers: { - 'content-length': `${postBody.length}`, - }, - parsedContentLength: postBody.length, - url: `/${bucketName}/${objectName}`, - }, - postBody, - ); - - objectPut(authInfo, testPutObjectRequest, undefined, log, (err, resHeaders) => { - assert.ifError(err); - const versionId = resHeaders['x-amz-version-id']; - assert(versionId, 'Version ID should be present'); - - const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectSize'], { - query: { versionId }, - }); - - objectGetAttributes(authInfo, testGetRequest, log, (err, xml, headers) => { - assert.ifError(err); + const [xml, headers] = await objectGetAttributesAsync(authInfo, testGetRequest, log); assert(headers['Last-Modified'], 'Last-Modified should be present'); - parseString(xml, (err, result) => { - const response = result.GetObjectAttributesResponse; + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; - assert.ifError(err); - assert.strictEqual(response.ETag[0], expectedMD5); - assert.strictEqual(response.ObjectSize[0], String(body.length)); - done(); - }); - }); + assert.strictEqual(response.ETag[0], expectedMD5); + assert.strictEqual(response.ObjectSize[0], String(body.length)); }); - }); - it('should return VersionId in response headers for versioned object', done => { - const testPutObjectRequest = new DummyRequest( - { - bucketName, - namespace, - objectKey: objectName, - headers: { - 'content-length': `${postBody.length}`, - }, - parsedContentLength: postBody.length, - url: `/${bucketName}/${objectName}`, - }, - postBody, - ); + it('should return VersionId in response headers for versioned object', async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); - objectPut(authInfo, testPutObjectRequest, undefined, log, (err, resHeaders) => { - assert.ifError(err); - const versionId = resHeaders['x-amz-version-id']; - assert(versionId, 'Version ID should be present from PUT'); + const [resHeaders] = await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + const versionId = resHeaders['x-amz-version-id']; + assert(versionId, 'Version ID should be present from PUT'); - const testGetRequest = createGetAttributesRequest(['ETag']); + const testGetRequest = createGetAttributesRequest(['ETag']); - objectGetAttributes(authInfo, testGetRequest, log, (err, _xml, headers) => { - assert.ifError(err); + const [, headers] = await objectGetAttributesAsync(authInfo, testGetRequest, log); assert.strictEqual(headers['x-amz-version-id'], versionId); - done(); - }); }); - }); }); From 57fe702027ead7bbee3af7b485f867be9b0ac1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Fri, 30 Jan 2026 15:34:08 +0100 Subject: [PATCH 04/11] fixup! Support the new API GetObjectAttributes --- .../apiUtils/object/parseAttributesHeader.js | 11 +--- .../test/object/objectGetAttributes.js | 9 ++-- .../apiUtils/object/parseAttributesHeader.js | 53 +++++++++++++------ tests/unit/api/objectGetAttributes.js | 8 +-- 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/lib/api/apiUtils/object/parseAttributesHeader.js b/lib/api/apiUtils/object/parseAttributesHeader.js index 92f7509251..0abe19a697 100644 --- a/lib/api/apiUtils/object/parseAttributesHeader.js +++ b/lib/api/apiUtils/object/parseAttributesHeader.js @@ -8,21 +8,14 @@ const { allowedObjectAttributes } = require('../../../../constants'); * @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid */ function parseAttributesHeaders(headers) { - const raw = headers['x-amz-object-attributes'] || ''; - - const attributes = raw - .split(',') - .map(s => s.trim()) - .filter(s => s !== ''); - + const attributes = headers['x-amz-object-attributes']?.split(',').map(attr => attr.trim()) ?? []; if (attributes.length === 0) { throw errorInstances.InvalidRequest.customizeDescription( 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', ); } - const invalids = attributes.filter(s => !allowedObjectAttributes.has(s)); - if (invalids.length > 0) { + if (attributes.some(attr => !allowedObjectAttributes.has(attr))) { throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.'); } diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js index 6af868181c..0eb10e561e 100644 --- a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -51,13 +51,10 @@ describe('objectGetAttributes', () => { ObjectAttributes: [], }) .promise(); - assert.fail('Expected InvalidRequest error'); + assert.fail('Expected InvalidArgument error'); } catch (err) { - assert.strictEqual(err.code, 'InvalidRequest'); - assert.strictEqual( - err.message, - 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', - ); + assert.strictEqual(err.code, 'InvalidArgument'); + assert.strictEqual(err.message, 'Invalid attribute name specified.'); } }); diff --git a/tests/unit/api/apiUtils/object/parseAttributesHeader.js b/tests/unit/api/apiUtils/object/parseAttributesHeader.js index dc09292649..68acf14a57 100644 --- a/tests/unit/api/apiUtils/object/parseAttributesHeader.js +++ b/tests/unit/api/apiUtils/object/parseAttributesHeader.js @@ -12,47 +12,52 @@ describe('parseAttributesHeaders', () => { err => { assert(err.is); assert.strictEqual(err.is.InvalidRequest, true); - assert(err.description.includes('missing or empty')); + assert.strictEqual( + err.description, + 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', + ); return true; }, ); }); - it('should throw InvalidRequest error when header is empty string', () => { + it('should throw InvalidArgument error when header is empty string', () => { const headers = { 'x-amz-object-attributes': '' }; assert.throws( () => parseAttributesHeaders(headers), err => { assert(err.is); - assert.strictEqual(err.is.InvalidRequest, true); - assert(err.description.includes('missing or empty')); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); }); - it('should throw InvalidRequest error when header contains only whitespace', () => { + it('should throw InvalidArgument error when header contains only whitespace', () => { const headers = { 'x-amz-object-attributes': ' ' }; assert.throws( () => parseAttributesHeaders(headers), err => { assert(err.is); - assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); }); - it('should throw InvalidRequest error when header contains only commas', () => { + it('should throw InvalidArgument error when header contains only commas', () => { const headers = { 'x-amz-object-attributes': ',,,' }; assert.throws( () => parseAttributesHeaders(headers), err => { assert(err.is); - assert.strictEqual(err.is.InvalidRequest, true); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); @@ -68,7 +73,7 @@ describe('parseAttributesHeaders', () => { err => { assert(err.is); assert.strictEqual(err.is.InvalidArgument, true); - assert(err.description.includes('Invalid attribute name')); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); @@ -82,6 +87,7 @@ describe('parseAttributesHeaders', () => { err => { assert(err.is); assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); @@ -95,6 +101,7 @@ describe('parseAttributesHeaders', () => { err => { assert(err.is); assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); return true; }, ); @@ -173,20 +180,32 @@ describe('parseAttributesHeaders', () => { assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); }); - it('should handle extra commas between attributes', () => { + it('should throw InvalidArgument for extra commas between attributes', () => { const headers = { 'x-amz-object-attributes': 'ETag,,ObjectSize' }; - const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); + return true; + }, + ); }); - it('should handle leading and trailing commas', () => { + it('should throw InvalidArgument for leading and trailing commas', () => { const headers = { 'x-amz-object-attributes': ',ETag,ObjectSize,' }; - const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + assert.throws( + () => parseAttributesHeaders(headers), + err => { + assert(err.is); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); + return true; + }, + ); }); }); }); diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js index 33c678be4f..2e2064d15d 100644 --- a/tests/unit/api/objectGetAttributes.js +++ b/tests/unit/api/objectGetAttributes.js @@ -127,12 +127,8 @@ describe('objectGetAttributes API', () => { await objectGetAttributesAsync(authInfo, testGetRequest, log); assert.fail('Expected error was not thrown'); } catch (err) { - assert.strictEqual(err.is.InvalidRequest, true); - assert.strictEqual( - err.description, - 'The x-amz-object-attributes header specifying the attributes ' + - 'to be retrieved is either missing or empty', - ); + assert.strictEqual(err.is.InvalidArgument, true); + assert.strictEqual(err.description, 'Invalid attribute name specified.'); } }); From df188c85a5608e1bd3c7dd68b9c84ef2e3357506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Mon, 2 Feb 2026 18:11:01 +0100 Subject: [PATCH 05/11] fixup! Support the new API GetObjectAttributes --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6333b8feeb..f330d91718 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5350,8 +5350,8 @@ arraybuffer.prototype.slice@^1.0.4: ioctl "^2.0.2" "arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": - version "8.2.43" - resolved "git+https://github.com/scality/Arsenal#ec21fa885c611498584ef3c56bfd62047c640e9e" + version "8.2.44" + resolved "git+https://github.com/scality/Arsenal#46757d474c28d616548446331722bd858b5672bb" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" From 5285cbb552a3bf898dd1cff1e0ed6ee44c7cb94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 4 Feb 2026 15:33:47 +0100 Subject: [PATCH 06/11] fixup! Support the new API GetObjectAttributes --- lib/api/objectGetAttributes.js | 67 ++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/lib/api/objectGetAttributes.js b/lib/api/objectGetAttributes.js index 86f09fe31f..ed22ab0e45 100644 --- a/lib/api/objectGetAttributes.js +++ b/lib/api/objectGetAttributes.js @@ -56,18 +56,29 @@ function buildXmlResponse(objMD, attributes) { * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info * @param {object} request - http request object * @param {object} log - Werelogs logger + * @param {function} callback - callback optional to keep backward compatibility * @returns {Promise} - { xml, responseHeaders } * @throws {ArsenalError} NoSuchVersion - if versionId specified but not found * @throws {ArsenalError} NoSuchKey - if object not found * @throws {ArsenalError} MethodNotAllowed - if object is a delete marker */ -async function objectGetAttributes(authInfo, request, log) { +async function objectGetAttributes(authInfo, request, log, callback) { + if (callback) { + return objectGetAttributes(authInfo, request, log) + .then(result => callback(null, result.xml, result.responseHeaders)) + .catch(err => callback(err, null, err.responseHeaders ?? {})); + } + log.trace('processing request', { method: OBJECT_GET_ATTRIBUTES }); const { bucketName, objectKey, headers, actionImplicitDenies } = request; const versionId = decodeVersionId(request.query); if (versionId instanceof Error) { - log.debug('invalid versionId query', { versionId: request.query.versionId, error: versionId }); + log.debug('invalid versionId query', { + method: OBJECT_GET_ATTRIBUTES, + versionId: request.query.versionId, + error: versionId, + }); throw versionId; } @@ -81,14 +92,31 @@ async function objectGetAttributes(authInfo, request, log) { request, }; - const { bucket, objMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log); - await checkExpectedBucketOwnerPromise(headers, bucket, log); + let bucket, objMD; + try { + ({ bucket, objMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log)); + await checkExpectedBucketOwnerPromise(headers, bucket, log); + } catch (err) { + log.debug('error validating bucket and object', { + method: OBJECT_GET_ATTRIBUTES, + bucket: bucketName, + key: objectKey, + versionId, + error: err, + }); + throw err; + } const responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket); if (!objMD) { + log.debug('object not found', { + method: OBJECT_GET_ATTRIBUTES, + bucket: bucketName, + key: objectKey, + versionId, + }); const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; - log.debug('object not found', { bucket: bucketName, key: objectKey, versionId }); err.responseHeaders = responseHeaders; throw err; } @@ -97,7 +125,12 @@ async function objectGetAttributes(authInfo, request, log) { responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString(); if (objMD.isDeleteMarker) { - log.debug('attempt to get attributes of a delete marker', { bucket: bucketName, key: objectKey, versionId }); + log.debug('attempt to get attributes of a delete marker', { + method: OBJECT_GET_ATTRIBUTES, + bucket: bucketName, + key: objectKey, + versionId, + }); responseHeaders['x-amz-delete-marker'] = true; const err = errors.MethodNotAllowed; err.responseHeaders = responseHeaders; @@ -118,24 +151,4 @@ async function objectGetAttributes(authInfo, request, log) { return { xml, responseHeaders }; } -/** - * objectGetAttributesCallback - Callback wrapper for objectGetAttributes - * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info - * @param {object} request - http request object - * @param {object} log - Werelogs logger - * @param {function} callback - callback to server (err, xml, responseHeaders) - * @return {undefined} - */ -function objectGetAttributesCallback(authInfo, request, log, callback) { - objectGetAttributes(authInfo, request, log) - .then(result => callback(null, result.xml, result.responseHeaders)) - .catch(err => { - log.debug('error processing request', { - error: err, - method: OBJECT_GET_ATTRIBUTES, - }); - return callback(err, null, err.responseHeaders || {}); - }); -} - -module.exports = objectGetAttributesCallback; +module.exports = objectGetAttributes; From 5dd680a5465600712049c44b14141a3534494b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 4 Feb 2026 16:49:48 +0100 Subject: [PATCH 07/11] fixup! Support the new API GetObjectAttributes --- yarn.lock | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index f330d91718..f8bfc8daee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2836,9 +2836,9 @@ integrity sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q== "@hapi/tlds@^1.1.1": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.3.tgz#bf5fee927d213f140cd54d4650965e504a546789" - integrity sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg== + version "1.1.4" + resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.4.tgz#df4a7b59082b54ba4f3b7b38f781e2ac3cbc359a" + integrity sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA== "@hapi/topo@^5.0.0", "@hapi/topo@^5.1.0": version "5.1.0" @@ -2882,16 +2882,21 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== -"@ioredis/commands@1.4.0", "@ioredis/commands@^1.3.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.4.0.tgz#9f657d51cdd5d2fdb8889592aa4a355546151f25" - integrity sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ== +"@ioredis/commands@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.5.0.tgz#3dddcea446a4b1dc177d0743a1e07ff50691652a" + integrity sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow== "@ioredis/commands@^1.1.1": version "1.2.0" resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== +"@ioredis/commands@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.4.0.tgz#9f657d51cdd5d2fdb8889592aa4a355546151f25" + integrity sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -4909,9 +4914,9 @@ integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== "@standard-schema/spec@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" - integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== "@types/async@^3.2.24": version "3.2.24" @@ -5351,7 +5356,7 @@ arraybuffer.prototype.slice@^1.0.4: "arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": version "8.2.44" - resolved "git+https://github.com/scality/Arsenal#46757d474c28d616548446331722bd858b5672bb" + resolved "git+https://github.com/scality/Arsenal#f02179acbfb6aff963c2fc72146f091544ddf86c" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" @@ -7676,7 +7681,7 @@ ioredis@^5.8.1: resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.9.2.tgz#ffdce2a019950299716e88ee56cd5802b399b108" integrity sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ== dependencies: - "@ioredis/commands" "1.4.0" + "@ioredis/commands" "1.5.0" cluster-key-slot "^1.1.0" debug "^4.3.4" denque "^2.1.0" @@ -8204,9 +8209,9 @@ joi@^17.13.3: "@sideway/pinpoint" "^2.0.0" joi@^18.0.1: - version "18.0.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.1.tgz#1e1885d035cc6ca1624e81bf22112e7c1ee38e1b" - integrity sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA== + version "18.0.2" + resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.2.tgz#30ced6aed00a7848cc11f92859515258301dc3a4" + integrity sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA== dependencies: "@hapi/address" "^5.1.1" "@hapi/formula" "^3.0.2" @@ -9030,7 +9035,7 @@ mongodb@^6.11.0: bson "^6.10.3" mongodb-connection-string-url "^3.0.0" -mongodb@^6.17.0, mongodb@^6.20.0: +mongodb@^6.17.0: version "6.20.0" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.20.0.tgz#5212dcf512719385287aa4574265352eefb01d8e" integrity sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ== @@ -9039,6 +9044,15 @@ mongodb@^6.17.0, mongodb@^6.20.0: bson "^6.10.4" mongodb-connection-string-url "^3.0.2" +mongodb@^6.20.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.21.0.tgz#f83355905900f2e7a912593f0315d5e2e0bda576" + integrity sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A== + dependencies: + "@mongodb-js/saslprep" "^1.3.0" + bson "^6.10.4" + mongodb-connection-string-url "^3.0.2" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" From ab3063e297763fc24f9e338d3d7b68d7c77c6c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Mon, 9 Feb 2026 15:36:55 +0100 Subject: [PATCH 08/11] fixup! Support the new API GetObjectAttributes --- constants.js | 4 ++-- lib/api/apiUtils/object/parseAttributesHeader.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/constants.js b/constants.js index 2fa0a49238..1337f593e8 100644 --- a/constants.js +++ b/constants.js @@ -279,8 +279,8 @@ const constants = { rateLimitDefaultConfigCacheTTL: 30000, // 30 seconds rateLimitDefaultBurstCapacity: 1, rateLimitCleanupInterval: 10000, // 10 seconds - // Metadata allowed to be returned by getObjectAttributes API - allowedObjectAttributes: new Set([ + // Supported attributes for the GetObjectAttributes 'x-amz-optional-attributes' header. + supportedGetObjectAttributes: new Set([ 'StorageClass', 'ObjectSize', 'ObjectParts', diff --git a/lib/api/apiUtils/object/parseAttributesHeader.js b/lib/api/apiUtils/object/parseAttributesHeader.js index 0abe19a697..21dd83181a 100644 --- a/lib/api/apiUtils/object/parseAttributesHeader.js +++ b/lib/api/apiUtils/object/parseAttributesHeader.js @@ -1,5 +1,5 @@ const { errorInstances } = require('arsenal'); -const { allowedObjectAttributes } = require('../../../../constants'); +const { supportedGetObjectAttributes } = require('../../../../constants'); /** * parseAttributesHeaders - Parse and validate the x-amz-object-attributes header @@ -15,7 +15,7 @@ function parseAttributesHeaders(headers) { ); } - if (attributes.some(attr => !allowedObjectAttributes.has(attr))) { + if (attributes.some(attr => !supportedGetObjectAttributes.has(attr))) { throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.'); } From 8246269547c0c29ab980bd412915420b15b2fa75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Thu, 12 Feb 2026 10:37:38 +0100 Subject: [PATCH 09/11] fixup! Support the new API GetObjectAttributes --- .../apiUtils/object/parseAttributesHeader.js | 4 +- lib/api/objectGetAttributes.js | 76 ++++++++++--------- lib/metadata/metadataUtils.js | 1 - .../test/object/objectGetAttributes.js | 27 ++++--- .../apiUtils/object/parseAttributesHeader.js | 56 +++++++------- tests/unit/api/objectGetAttributes.js | 14 ++-- yarn.lock | 8 +- 7 files changed, 98 insertions(+), 88 deletions(-) diff --git a/lib/api/apiUtils/object/parseAttributesHeader.js b/lib/api/apiUtils/object/parseAttributesHeader.js index 21dd83181a..685ac93884 100644 --- a/lib/api/apiUtils/object/parseAttributesHeader.js +++ b/lib/api/apiUtils/object/parseAttributesHeader.js @@ -4,7 +4,7 @@ const { supportedGetObjectAttributes } = require('../../../../constants'); /** * parseAttributesHeaders - Parse and validate the x-amz-object-attributes header * @param {object} headers - request headers - * @returns {string[]} - array of valid attribute names + * @returns {Set} - set of requested attribute names * @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid */ function parseAttributesHeaders(headers) { @@ -19,7 +19,7 @@ function parseAttributesHeaders(headers) { throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.'); } - return attributes; + return new Set(attributes); } module.exports = parseAttributesHeaders; diff --git a/lib/api/objectGetAttributes.js b/lib/api/objectGetAttributes.js index ed22ab0e45..696f613baa 100644 --- a/lib/api/objectGetAttributes.js +++ b/lib/api/objectGetAttributes.js @@ -9,44 +9,38 @@ const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwn const { pushMetric } = require('../utapi/utilities'); const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo'); -const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; - const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner); const validateBucketAndObj = promisify(standardMetadataValidateBucketAndObj); +const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; +const ATTRIBUTE_HANDLERS = { + ETag: objMD => objMD['content-md5'], + ObjectParts: objMD => { + const partCount = getPartCountFromMd5(objMD); + return partCount ? { PartsCount: partCount } : undefined; + }, + StorageClass: objMD => objMD['x-amz-storage-class'], + ObjectSize: objMD => objMD['content-length'], +}; + /** * buildXmlResponse - Build XML response for GetObjectAttributes * @param {object} objMD - object metadata - * @param {array} attributes - requested attributes + * @param {Set} requestedAttrs - set of requested attribute names * @returns {string} XML response */ -function buildXmlResponse(objMD, attributes) { +function buildXmlResponse(objMD, requestedAttrs) { const attrResp = {}; - if (attributes.includes('ETag')) { - attrResp.ETag = objMD['content-md5']; - } - - // NOTE: Checksum is not implemented - if (attributes.includes('Checksum')) { - attrResp.Checksum = {}; - } - - if (attributes.includes('ObjectParts')) { - const partCount = getPartCountFromMd5(objMD); - if (partCount) { - attrResp.ObjectParts = { PartsCount: partCount }; + for (const [attr, handler] of Object.entries(ATTRIBUTE_HANDLERS)) { + if (requestedAttrs.has(attr)) { + const value = handler(objMD); + if (value !== undefined) { + attrResp[attr] = value; + } } } - if (attributes.includes('StorageClass')) { - attrResp.StorageClass = objMD['x-amz-storage-class']; - } - - if (attributes.includes('ObjectSize')) { - attrResp.ObjectSize = objMD['content-length']; - } - const builder = new xml2js.Builder(); return builder.buildObject({ GetObjectAttributesResponse: attrResp }); } @@ -92,9 +86,9 @@ async function objectGetAttributes(authInfo, request, log, callback) { request, }; - let bucket, objMD; + let bucket, objectMD; try { - ({ bucket, objMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log)); + ({ bucket, objectMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log)); await checkExpectedBucketOwnerPromise(headers, bucket, log); } catch (err) { log.debug('error validating bucket and object', { @@ -109,7 +103,7 @@ async function objectGetAttributes(authInfo, request, log, callback) { const responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket); - if (!objMD) { + if (!objectMD) { log.debug('object not found', { method: OBJECT_GET_ATTRIBUTES, bucket: bucketName, @@ -121,10 +115,10 @@ async function objectGetAttributes(authInfo, request, log, callback) { throw err; } - responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objMD); - responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString(); + responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objectMD); + responseHeaders['Last-Modified'] = objectMD['last-modified'] && new Date(objectMD['last-modified']).toUTCString(); - if (objMD.isDeleteMarker) { + if (objectMD.isDeleteMarker) { log.debug('attempt to get attributes of a delete marker', { method: OBJECT_GET_ATTRIBUTES, bucket: bucketName, @@ -137,17 +131,29 @@ async function objectGetAttributes(authInfo, request, log, callback) { throw err; } - const attributes = parseAttributesHeaders(headers); + const requestedAttrs = parseAttributesHeaders(headers); + + if (requestedAttrs.has('Checksum')) { + log.debug('Checksum attribute requested but not implemented', { + method: OBJECT_GET_ATTRIBUTES, + bucket: bucketName, + key: objectKey, + versionId, + }); + const err = errors.NotImplemented.customizeDescription('Checksum attribute is not implemented'); + err.responseHeaders = responseHeaders; + throw err; + } pushMetric(OBJECT_GET_ATTRIBUTES, log, { authInfo, bucket: bucketName, keys: [objectKey], - versionId: objMD?.versionId, - location: objMD?.dataStoreName, + versionId: objectMD?.versionId, + location: objectMD?.dataStoreName, }); - const xml = buildXmlResponse(objMD, attributes); + const xml = buildXmlResponse(objectMD, requestedAttrs); return { xml, responseHeaders }; } diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 9a543474e6..2702125209 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -1,6 +1,5 @@ const { promisify } = require('util'); const async = require('async'); -const { promisify } = require('util'); const { errors } = require('arsenal'); const metadata = require('./wrapper'); diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js index 0eb10e561e..5f99e31097 100644 --- a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -95,14 +95,13 @@ describe('objectGetAttributes', () => { .getObjectAttributes({ Bucket: bucket, Key: key, - ObjectAttributes: ['ETag', 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize'], + ObjectAttributes: ['ETag', 'ObjectParts', 'StorageClass', 'ObjectSize'], }) .promise(); assert.strictEqual(data.ETag, expectedMD5); assert.strictEqual(data.StorageClass, 'STANDARD'); assert.strictEqual(data.ObjectSize, body.length); - assert.deepStrictEqual(data.Checksum, {}, 'Checksum should be present'); assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); assert(data.LastModified, 'LastModified should be present'); }); @@ -119,16 +118,20 @@ describe('objectGetAttributes', () => { assert.strictEqual(data.ETag, expectedMD5); }); - it('should return Checksum', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['Checksum'], - }) - .promise(); - - assert.deepStrictEqual(data.Checksum, {}, 'Checksum should be present'); + it('should fail with NotImplemented when Checksum is requested', async () => { + try { + await s3 + .getObjectAttributes({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['Checksum'], + }) + .promise(); + assert.fail('Expected NotImplemented error'); + } catch (err) { + assert.strictEqual(err.code, 'NotImplemented'); + assert.strictEqual(err.message, 'Checksum attribute is not implemented'); + } }); it("shouldn't return ObjectParts for non-MPU objects", async () => { diff --git a/tests/unit/api/apiUtils/object/parseAttributesHeader.js b/tests/unit/api/apiUtils/object/parseAttributesHeader.js index 68acf14a57..b722668880 100644 --- a/tests/unit/api/apiUtils/object/parseAttributesHeader.js +++ b/tests/unit/api/apiUtils/object/parseAttributesHeader.js @@ -109,65 +109,65 @@ describe('parseAttributesHeaders', () => { }); describe('valid attribute names', () => { - it('should return array with single valid attribute ETag', () => { + it('should return set with single valid attribute ETag', () => { const headers = { 'x-amz-object-attributes': 'ETag' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ETag']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['ETag'])); }); - it('should return array with single valid attribute StorageClass', () => { + it('should return set with single valid attribute StorageClass', () => { const headers = { 'x-amz-object-attributes': 'StorageClass' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['StorageClass']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['StorageClass'])); }); - it('should return array with single valid attribute ObjectSize', () => { + it('should return set with single valid attribute ObjectSize', () => { const headers = { 'x-amz-object-attributes': 'ObjectSize' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ObjectSize']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['ObjectSize'])); }); - it('should return array with single valid attribute ObjectParts', () => { + it('should return set with single valid attribute ObjectParts', () => { const headers = { 'x-amz-object-attributes': 'ObjectParts' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ObjectParts']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['ObjectParts'])); }); - it('should return array with single valid attribute Checksum', () => { + it('should return set with single valid attribute Checksum', () => { const headers = { 'x-amz-object-attributes': 'Checksum' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['Checksum']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['Checksum'])); }); - it('should return array with multiple valid attributes', () => { + it('should return set with multiple valid attributes', () => { const headers = { 'x-amz-object-attributes': 'ETag,ObjectSize,StorageClass' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ETag', 'ObjectSize', 'StorageClass']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['ETag', 'ObjectSize', 'StorageClass'])); }); - it('should return array with all valid attributes', () => { + it('should return set with all valid attributes', () => { const headers = { 'x-amz-object-attributes': 'StorageClass,ObjectSize,ObjectParts,Checksum,ETag' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.strictEqual(result.length, 5); - assert(result.includes('StorageClass')); - assert(result.includes('ObjectSize')); - assert(result.includes('ObjectParts')); - assert(result.includes('Checksum')); - assert(result.includes('ETag')); + assert(result instanceof Set); + assert.strictEqual(result.size, 5); + assert(result.has('StorageClass')); + assert(result.has('ObjectSize')); + assert(result.has('ObjectParts')); + assert(result.has('Checksum')); + assert(result.has('ETag')); }); }); @@ -176,8 +176,8 @@ describe('parseAttributesHeaders', () => { const headers = { 'x-amz-object-attributes': ' ETag , ObjectSize ' }; const result = parseAttributesHeaders(headers); - assert(Array.isArray(result)); - assert.deepStrictEqual(result, ['ETag', 'ObjectSize']); + assert(result instanceof Set); + assert.deepStrictEqual(result, new Set(['ETag', 'ObjectSize'])); }); it('should throw InvalidArgument for extra commas between attributes', () => { diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js index 2e2064d15d..b9e756f991 100644 --- a/tests/unit/api/objectGetAttributes.js +++ b/tests/unit/api/objectGetAttributes.js @@ -177,7 +177,6 @@ describe('objectGetAttributes API', () => { it('should return all attributes', async () => { const testGetRequest = createGetAttributesRequest([ 'ETag', - 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize', @@ -193,7 +192,6 @@ describe('objectGetAttributes API', () => { assert.strictEqual(response.ETag[0], expectedMD5); assert.strictEqual(response.StorageClass[0], 'STANDARD'); assert.strictEqual(response.ObjectSize[0], String(body.length)); - assert.deepStrictEqual(response.Checksum[0], '', 'Checksum should be empty'); assert.strictEqual(response.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); assert(headers['Last-Modified'], 'LastModified should be present'); }); @@ -206,12 +204,16 @@ describe('objectGetAttributes API', () => { assert.strictEqual(result.GetObjectAttributesResponse.ETag[0], expectedMD5); }); - it('should return Checksum', async () => { + it('should fail with NotImplemented when Checksum is requested', async () => { const testGetRequest = createGetAttributesRequest(['Checksum']); - const [xml] = await objectGetAttributesAsync(authInfo, testGetRequest, log); - const result = await parseStringPromise(xml); - assert.deepStrictEqual(result.GetObjectAttributesResponse.Checksum[0], '', 'Checksum should be empty'); + try { + await objectGetAttributesAsync(authInfo, testGetRequest, log); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.is.NotImplemented, true); + assert.strictEqual(err.description, 'Checksum attribute is not implemented'); + } }); it("shouldn't return ObjectParts for non-MPU object", async () => { diff --git a/yarn.lock b/yarn.lock index f8bfc8daee..e82f0fa55d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2836,9 +2836,9 @@ integrity sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q== "@hapi/tlds@^1.1.1": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.4.tgz#df4a7b59082b54ba4f3b7b38f781e2ac3cbc359a" - integrity sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA== + version "1.1.5" + resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.5.tgz#3eff5d8a3bd20833a2d779e0f839d25177fee722" + integrity sha512-Vq/1gnIIsvFUpKlDdfrPd/ssHDpAyBP/baVukh3u2KSG2xoNjsnRNjQiPmuyPPGqsn1cqVWWhtZHfOBaLizFRQ== "@hapi/topo@^5.0.0", "@hapi/topo@^5.1.0": version "5.1.0" @@ -5356,7 +5356,7 @@ arraybuffer.prototype.slice@^1.0.4: "arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": version "8.2.44" - resolved "git+https://github.com/scality/Arsenal#f02179acbfb6aff963c2fc72146f091544ddf86c" + resolved "git+https://github.com/scality/Arsenal#475f746b6d201baaa46cc9ac2cdc0a2ed121fa43" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" From bef803455ea1ef40e654c081612ef07477f54c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Thu, 12 Feb 2026 16:00:18 +0100 Subject: [PATCH 10/11] fixup! Support the new API GetObjectAttributes --- .../test/object/objectGetAttributes.js | 497 +++++----- .../test/versioning/objectGetAttributes.js | 259 +++-- yarn.lock | 894 ++++++++---------- 3 files changed, 789 insertions(+), 861 deletions(-) diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js index 5f99e31097..8a665dc12e 100644 --- a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -1,6 +1,17 @@ const assert = require('assert'); -const { S3 } = require('aws-sdk'); -const getConfig = require('../support/config'); +const { + CreateBucketCommand, + DeleteBucketCommand, + DeleteObjectCommand, + PutObjectCommand, + GetObjectAttributesCommand, + CreateMultipartUploadCommand, + UploadPartCommand, + CompleteMultipartUploadCommand, +} = require('@aws-sdk/client-s3'); + +const withV4 = require('../support/withV4'); +const BucketUtility = require('../../lib/utility/bucket-util'); const bucket = 'testbucket'; const key = 'testobject'; @@ -8,260 +19,236 @@ const body = 'hello world!'; const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; describe('objectGetAttributes', () => { - let s3; - - before(() => { - const config = getConfig('default', { signatureVersion: 'v4' }); - s3 = new S3(config); - }); - - beforeEach(async () => { - await s3.createBucket({ Bucket: bucket }).promise(); - await s3.putObject({ Bucket: bucket, Key: key, Body: body }).promise(); - }); - - afterEach(async () => { - await s3.deleteObject({ Bucket: bucket, Key: key }).promise(); - await s3.deleteBucket({ Bucket: bucket }).promise(); - }); - - it('should fail with a wrong bucket owner header', async () => { - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag'], - ExpectedBucketOwner: 'wrongAccountId', - }) - .promise(); - assert.fail('Expected AccessDenied error'); - } catch (err) { - assert.strictEqual(err.code, 'AccessDenied'); - assert.strictEqual(err.message, 'Access Denied'); - } - }); - - it('should fail because attributes header is missing', async () => { - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: [], - }) - .promise(); - assert.fail('Expected InvalidArgument error'); - } catch (err) { - assert.strictEqual(err.code, 'InvalidArgument'); - assert.strictEqual(err.message, 'Invalid attribute name specified.'); - } - }); - - it('should fail because attribute name is invalid', async () => { - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['InvalidAttribute'], - }) - .promise(); - assert.fail('Expected InvalidArgument error'); - } catch (err) { - assert.strictEqual(err.code, 'InvalidArgument'); - assert.strictEqual(err.message, 'Invalid attribute name specified.'); - } - }); - - it('should return NoSuchKey for non-existent object', async () => { - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: 'nonexistent', - ObjectAttributes: ['ETag'], - }) - .promise(); - assert.fail('Expected NoSuchKey error'); - } catch (err) { - assert.strictEqual(err.code, 'NoSuchKey'); - assert.strictEqual(err.message, 'The specified key does not exist.'); - } - }); - - it('should return all attributes', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag', 'ObjectParts', 'StorageClass', 'ObjectSize'], - }) - .promise(); - - assert.strictEqual(data.ETag, expectedMD5); - assert.strictEqual(data.StorageClass, 'STANDARD'); - assert.strictEqual(data.ObjectSize, body.length); - assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); - assert(data.LastModified, 'LastModified should be present'); - }); - - it('should return ETag', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag'], - }) - .promise(); - - assert.strictEqual(data.ETag, expectedMD5); - }); - - it('should fail with NotImplemented when Checksum is requested', async () => { - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['Checksum'], - }) - .promise(); - assert.fail('Expected NotImplemented error'); - } catch (err) { - assert.strictEqual(err.code, 'NotImplemented'); - assert.strictEqual(err.message, 'Checksum attribute is not implemented'); - } - }); - - it("shouldn't return ObjectParts for non-MPU objects", async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ObjectParts'], - }) - .promise(); - - assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present"); - }); - - it('should return StorageClass', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['StorageClass'], - }) - .promise(); - - assert.strictEqual(data.StorageClass, 'STANDARD'); - }); - - it('should return ObjectSize', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ObjectSize'], - }) - .promise(); - - assert.strictEqual(data.ObjectSize, body.length); - }); - - it('should return LastModified', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag'], - }) - .promise(); - - assert(data.LastModified, 'LastModified should be present'); - assert(data.LastModified instanceof Date, 'LastModified should be a Date'); - assert(!isNaN(data.LastModified.getTime()), 'LastModified should be a valid date'); - }); + withV4(sigCfg => { + let bucketUtil; + let s3; + + before(() => { + bucketUtil = new BucketUtility('default', sigCfg); + s3 = bucketUtil.s3; + }); + + beforeEach(async () => { + await s3.send(new CreateBucketCommand({ Bucket: bucket })); + await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body })); + }); + + afterEach(async () => { + await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); + await s3.send(new DeleteBucketCommand({ Bucket: bucket })); + }); + + it('should fail with a wrong bucket owner header', async () => { + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + ExpectedBucketOwner: 'wrongAccountId', + })); + assert.fail('Expected AccessDenied error'); + } catch (err) { + assert.strictEqual(err.name, 'AccessDenied'); + assert.strictEqual(err.message, 'Access Denied'); + } + }); + + it('should fail because attributes header is missing', async () => { + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: [], + })); + assert.fail('Expected InvalidArgument error'); + } catch (err) { + assert.strictEqual(err.name, 'InvalidArgument'); + assert.strictEqual(err.message, 'Invalid attribute name specified.'); + } + }); + + it('should fail because attribute name is invalid', async () => { + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['InvalidAttribute'], + })); + assert.fail('Expected InvalidArgument error'); + } catch (err) { + assert.strictEqual(err.name, 'InvalidArgument'); + assert.strictEqual(err.message, 'Invalid attribute name specified.'); + } + }); + + it('should return NoSuchKey for non-existent object', async () => { + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: 'nonexistent', + ObjectAttributes: ['ETag'], + })); + assert.fail('Expected NoSuchKey error'); + } catch (err) { + assert.strictEqual(err.name, 'NoSuchKey'); + assert.strictEqual(err.message, 'The specified key does not exist.'); + } + }); + + it('should return all attributes', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag', 'ObjectParts', 'StorageClass', 'ObjectSize'], + })); + + assert.strictEqual(data.ETag, expectedMD5); + assert.strictEqual(data.StorageClass, 'STANDARD'); + assert.strictEqual(data.ObjectSize, body.length); + assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present for non-MPU object"); + assert(data.LastModified, 'LastModified should be present'); + }); + + it('should return ETag', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + })); + + assert.strictEqual(data.ETag, expectedMD5); + }); + + it('should fail with NotImplemented when Checksum is requested', async () => { + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['Checksum'], + })); + assert.fail('Expected NotImplemented error'); + } catch (err) { + assert.strictEqual(err.name, 'NotImplemented'); + assert.strictEqual(err.message, 'Checksum attribute is not implemented'); + } + }); + + it("shouldn't return ObjectParts for non-MPU objects", async () => { + // Requesting only ObjectParts for a non-MPU object break AWS SDK v3 + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ObjectParts', 'ETag'], + })); + + assert.strictEqual(data.ObjectParts, undefined, "ObjectParts shouldn't be present"); + assert.strictEqual(data.ETag, expectedMD5); + }); + + it('should return StorageClass', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['StorageClass'], + })); + + assert.strictEqual(data.StorageClass, 'STANDARD'); + }); + + it('should return ObjectSize', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ObjectSize'], + })); + + assert.strictEqual(data.ObjectSize, body.length); + }); + + it('should return LastModified', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + })); + + assert(data.LastModified, 'LastModified should be present'); + assert(data.LastModified instanceof Date, 'LastModified should be a Date'); + assert(!isNaN(data.LastModified.getTime()), 'LastModified should be a valid date'); + }); + }); }); describe('Test get object attributes with multipart upload', () => { - let s3; - const mpuKey = 'mpuObject'; - const partSize = 5 * 1024 * 1024; // Minimum part size is 5MB - const partCount = 3; - - before(async () => { - const config = getConfig('default', { signatureVersion: 'v4' }); - s3 = new S3(config); - - await s3.createBucket({ Bucket: bucket }).promise(); - - const createResult = await s3 - .createMultipartUpload({ - Bucket: bucket, - Key: mpuKey, - }) - .promise(); - const uploadId = createResult.UploadId; - - const partData = Buffer.alloc(partSize, 'a'); - const parts = []; - for (let i = 1; i <= partCount; i++) { - const uploadResult = await s3 - .uploadPart({ - Bucket: bucket, - Key: mpuKey, - PartNumber: i, - UploadId: uploadId, - Body: partData, - }) - .promise(); - parts.push({ PartNumber: i, ETag: uploadResult.ETag }); - } - - await s3 - .completeMultipartUpload({ - Bucket: bucket, - Key: mpuKey, - UploadId: uploadId, - MultipartUpload: { Parts: parts }, - }) - .promise(); - }); - - after(async () => { - await s3.deleteObject({ Bucket: bucket, Key: mpuKey }).promise(); - await s3.deleteBucket({ Bucket: bucket }).promise(); - }); - - it('should return TotalPartsCount for MPU object', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: mpuKey, - ObjectAttributes: ['ObjectParts'], - }) - .promise(); - - assert(data.ObjectParts, 'ObjectParts should be present'); - assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); - }); - - it('should return TotalPartsCount along with other attributes for MPU object', async () => { - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: mpuKey, - ObjectAttributes: ['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass'], - }) - .promise(); - - assert(data.ETag, 'ETag should be present'); - assert(data.ETag.includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); - assert(data.ObjectParts, 'ObjectParts should be present'); - assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); - assert.strictEqual(data.ObjectSize, partSize * partCount); - assert.strictEqual(data.StorageClass, 'STANDARD'); - }); + withV4(sigCfg => { + let bucketUtil; + let s3; + const mpuKey = 'mpuObject'; + const partSize = 5 * 1024 * 1024; // Minimum part size is 5MB + const partCount = 3; + + before(async () => { + bucketUtil = new BucketUtility('default', sigCfg); + s3 = bucketUtil.s3; + + await s3.send(new CreateBucketCommand({ Bucket: bucket })); + + const createResult = await s3.send(new CreateMultipartUploadCommand({ + Bucket: bucket, + Key: mpuKey, + })); + const uploadId = createResult.UploadId; + + const partData = Buffer.alloc(partSize, 'a'); + const parts = []; + for (let i = 1; i <= partCount; i++) { + const uploadResult = await s3.send(new UploadPartCommand({ + Bucket: bucket, + Key: mpuKey, + PartNumber: i, + UploadId: uploadId, + Body: partData, + })); + parts.push({ PartNumber: i, ETag: uploadResult.ETag }); + } + + await s3.send(new CompleteMultipartUploadCommand({ + Bucket: bucket, + Key: mpuKey, + UploadId: uploadId, + MultipartUpload: { Parts: parts }, + })); + }); + + after(async () => { + await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: mpuKey })); + await s3.send(new DeleteBucketCommand({ Bucket: bucket })); + }); + + it('should return TotalPartsCount for MPU object', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: mpuKey, + ObjectAttributes: ['ObjectParts'], + })); + + assert(data.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); + }); + + it('should return TotalPartsCount along with other attributes for MPU object', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: mpuKey, + ObjectAttributes: ['ETag', 'ObjectParts', 'ObjectSize', 'StorageClass'], + })); + + assert(data.ETag, 'ETag should be present'); + assert(data.ETag.includes(`-${partCount}`), `ETag should indicate MPU with ${partCount} parts`); + assert(data.ObjectParts, 'ObjectParts should be present'); + assert.strictEqual(data.ObjectParts.TotalPartsCount, partCount); + assert.strictEqual(data.ObjectSize, partSize * partCount); + assert.strictEqual(data.StorageClass, 'STANDARD'); + }); + }); }); diff --git a/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js index 8b932cdfae..bae4e16a1c 100644 --- a/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/versioning/objectGetAttributes.js @@ -1,142 +1,137 @@ const assert = require('assert'); -const { promisify } = require('util'); -const { S3 } = require('aws-sdk'); -const getConfig = require('../support/config'); +const { + CreateBucketCommand, + DeleteBucketCommand, + DeleteObjectCommand, + PutBucketVersioningCommand, + PutObjectCommand, + GetObjectAttributesCommand, +} = require('@aws-sdk/client-s3'); + +const withV4 = require('../support/withV4'); +const BucketUtility = require('../../lib/utility/bucket-util'); const { removeAllVersions, versioningEnabled } = require('../../lib/utility/versioning-util.js'); -const removeAllVersionsPromise = promisify(removeAllVersions); - const bucket = 'testbucket'; const key = 'testobject'; const body = 'hello world!'; const expectedMD5 = 'fc3ff98e8c6a0d3087d515c0473f8677'; describe('Test get object attributes with versioning', () => { - let s3; - - before(() => { - const config = getConfig('default', { signatureVersion: 'v4' }); - s3 = new S3(config); - }); - - beforeEach(async () => { - await s3.createBucket({ Bucket: bucket }).promise(); - await s3 - .putBucketVersioning({ - Bucket: bucket, - VersioningConfiguration: versioningEnabled, - }) - .promise(); - }); - - afterEach(async () => { - await removeAllVersionsPromise({ Bucket: bucket }); - await s3.deleteBucket({ Bucket: bucket }).promise(); - }); - - it('should return NoSuchVersion for non-existent versionId', async () => { - await s3 - .putObject({ - Bucket: bucket, - Key: key, - Body: body, - }) - .promise(); - - const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; - - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - VersionId: fakeVersionId, - ObjectAttributes: ['ETag'], - }) - .promise(); - assert.fail('Expected NoSuchVersion error'); - } catch (err) { - assert.strictEqual(err.code, 'NoSuchVersion'); - assert.strictEqual( - err.message, - 'Indicates that the version ID specified in the request does not match an existing version.', - ); - } - }); - - it('should return MethodNotAllowed for delete marker', async () => { - await s3 - .putObject({ - Bucket: bucket, - Key: key, - Body: body, - }) - .promise(); - - await s3 - .deleteObject({ - Bucket: bucket, - Key: key, - }) - .promise(); - - try { - await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag'], - }) - .promise(); - assert.fail('Expected MethodNotAllowed error'); - } catch (err) { - assert.strictEqual(err.code, 'MethodNotAllowed'); - assert.strictEqual(err.message, 'The specified method is not allowed against this resource.'); - } - }); - - it('should return attributes for specific version', async () => { - const putResult = await s3 - .putObject({ - Bucket: bucket, - Key: key, - Body: body, - }) - .promise(); - const versionId = putResult.VersionId; - - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - VersionId: versionId, - ObjectAttributes: ['ETag', 'ObjectSize'], - }) - .promise(); - - assert.strictEqual(data.ETag, expectedMD5); - assert.strictEqual(data.ObjectSize, body.length); - assert(data.LastModified, 'LastModified should be present'); - }); - - it('should return VersionId for versioned object', async () => { - const putResult = await s3 - .putObject({ - Bucket: bucket, - Key: key, - Body: body, - }) - .promise(); - const versionId = putResult.VersionId; - - const data = await s3 - .getObjectAttributes({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['ETag'], - }) - .promise(); - - assert.strictEqual(data.VersionId, versionId); - }); + withV4(sigCfg => { + let bucketUtil; + let s3; + + before(() => { + bucketUtil = new BucketUtility('default', sigCfg); + s3 = bucketUtil.s3; + }); + + beforeEach(async () => { + await s3.send(new CreateBucketCommand({ Bucket: bucket })); + await s3.send(new PutBucketVersioningCommand({ + Bucket: bucket, + VersioningConfiguration: versioningEnabled, + })); + }); + + afterEach(done => { + removeAllVersions({ Bucket: bucket }, err => { + if (err) { + return done(err); + } + return s3.send(new DeleteBucketCommand({ Bucket: bucket })) + .then(() => done()) + .catch(done); + }); + }); + + it('should return NoSuchVersion for non-existent versionId', async () => { + await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + })); + + const fakeVersionId = '111111111111111111111111111111111111111175636f7270'; + + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + VersionId: fakeVersionId, + ObjectAttributes: ['ETag'], + })); + assert.fail('Expected NoSuchVersion error'); + } catch (err) { + assert.strictEqual(err.name, 'NoSuchVersion'); + assert.strictEqual( + err.message, + 'Indicates that the version ID specified in the request does not match an existing version.', + ); + } + }); + + it('should return MethodNotAllowed for delete marker', async () => { + await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + })); + + await s3.send(new DeleteObjectCommand({ + Bucket: bucket, + Key: key, + })); + + try { + await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + })); + assert.fail('Expected MethodNotAllowed error'); + } catch (err) { + assert.strictEqual(err.name, 'MethodNotAllowed'); + assert.strictEqual(err.message, 'The specified method is not allowed against this resource.'); + } + }); + + it('should return attributes for specific version', async () => { + const putResult = await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + })); + const versionId = putResult.VersionId; + + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + VersionId: versionId, + ObjectAttributes: ['ETag', 'ObjectSize'], + })); + + assert.strictEqual(data.ETag, expectedMD5); + assert.strictEqual(data.ObjectSize, body.length); + assert(data.LastModified, 'LastModified should be present'); + }); + + it('should return VersionId for versioned object', async () => { + const putResult = await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + })); + const versionId = putResult.VersionId; + + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag'], + })); + + assert.strictEqual(data.VersionId, versionId); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index e82f0fa55d..34211a4e89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,90 +151,90 @@ "@smithy/util-utf8" "^4.1.0" tslib "^2.6.2" -"@aws-sdk/client-cognito-identity@3.975.0": - version "3.975.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.975.0.tgz#f6494616cb1f298f8ed195db8d55c6aa62cad0e9" - integrity sha512-HjPnxHB74FaeLSce+/B2n3H7FJ23lC5D4M2FnHhHdiuvGa8etFrsF12mxC8i635huARHJkqLLbLF2U39xX6AJA== +"@aws-sdk/client-cognito-identity@3.980.0": + version "3.980.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.980.0.tgz#e74f1a87864b729405e5504badcf0a52b90bc10b" + integrity sha512-nLgMW2drTzv+dTo3ORCcotQPcrUaTQ+xoaDTdSaUXdZO7zbbVyk7ysE5GDTnJdZWcUjHOSB8xfNQhOTTNVPhFw== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.1" - "@aws-sdk/credential-provider-node" "^3.972.1" - "@aws-sdk/middleware-host-header" "^3.972.1" - "@aws-sdk/middleware-logger" "^3.972.1" - "@aws-sdk/middleware-recursion-detection" "^3.972.1" - "@aws-sdk/middleware-user-agent" "^3.972.2" - "@aws-sdk/region-config-resolver" "^3.972.1" - "@aws-sdk/types" "^3.973.0" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.1" - "@aws-sdk/util-user-agent-node" "^3.972.1" + "@aws-sdk/core" "^3.973.5" + "@aws-sdk/credential-provider-node" "^3.972.4" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.5" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.980.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.3" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.21.1" + "@smithy/core" "^3.22.0" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.11" - "@smithy/middleware-retry" "^4.4.27" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" "@smithy/node-http-handler" "^4.4.8" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.12" + "@smithy/smithy-client" "^4.11.1" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.26" - "@smithy/util-defaults-mode-node" "^4.2.29" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-cognito-identity@3.978.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.978.0.tgz#0a00a8fa66311f6fbcc8b12516e6c53572e3372a" - integrity sha512-vMmeOu1/Rbd29CSQp2bE3my+smbq7PciZshVaPMTfEDFlAr8myYiqcqk1l8fyRpzswjw6TWmVZmld27pfp8enw== +"@aws-sdk/client-cognito-identity@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.988.0.tgz#588f707d64ecaa1c6090ea3714aa3e6fe6c6ebe8" + integrity sha512-bmxVBwI+JayNvLUyhX+c5gIObB4zio20NR6qgWtv4MD/8v43FVKQeayWZ4PYdHvDSM0ssiWQEioL/2yi85n5rw== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.4" - "@aws-sdk/credential-provider-node" "^3.972.2" - "@aws-sdk/middleware-host-header" "^3.972.2" - "@aws-sdk/middleware-logger" "^3.972.2" - "@aws-sdk/middleware-recursion-detection" "^3.972.2" - "@aws-sdk/middleware-user-agent" "^3.972.4" - "@aws-sdk/region-config-resolver" "^3.972.2" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/credential-provider-node" "^3.972.7" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" + "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.2" - "@aws-sdk/util-user-agent-node" "^3.972.2" + "@aws-sdk/util-endpoints" "3.988.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.6" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/middleware-retry" "^4.4.31" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.30" + "@smithy/util-defaults-mode-node" "^4.2.33" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -288,44 +288,44 @@ tslib "^2.6.2" "@aws-sdk/client-kms@^3.975.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-kms/-/client-kms-3.978.0.tgz#36f6df642e583e52b4bcf2f9ab49a1a95d81bc87" - integrity sha512-KTCHctiTS4RJ/MeJrI/hbm5F0AhsBayjnzzfYlGAasNIGW3ucwCc2qTZ2QzKHaR3zPyysSQiTDzNUg/RmdfExA== + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-kms/-/client-kms-3.988.0.tgz#8ee38047c65d649c6ffb15749063d26a7f57e1bf" + integrity sha512-aZFKKPe71qtBQ30kikzx3n+27uV6nMifegRWE98b+kSowqpxC0vOXMBjRXsqUsa5DF/2hx/ybohVksiydDkBwQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.4" - "@aws-sdk/credential-provider-node" "^3.972.2" - "@aws-sdk/middleware-host-header" "^3.972.2" - "@aws-sdk/middleware-logger" "^3.972.2" - "@aws-sdk/middleware-recursion-detection" "^3.972.2" - "@aws-sdk/middleware-user-agent" "^3.972.4" - "@aws-sdk/region-config-resolver" "^3.972.2" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/credential-provider-node" "^3.972.7" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" + "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.2" - "@aws-sdk/util-user-agent-node" "^3.972.2" + "@aws-sdk/util-endpoints" "3.988.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.6" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/middleware-retry" "^4.4.31" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.30" + "@smithy/util-defaults-mode-node" "^4.2.33" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -396,33 +396,33 @@ tslib "^2.6.2" "@aws-sdk/client-s3@^3.975.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.978.0.tgz#2e54076abae7dab2b0001cec4fd6a0eb046176ec" - integrity sha512-2chs05VbfgRNb5ZEYIwooeHCaL+DjwvrW3ElkslI71ltEqVNdeWvB7hbkLWPPKazV3kjY3H90pLDY8mMqsET+A== + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.988.0.tgz#f445d820ad59cbedce98e80458ab641cba023cbc" + integrity sha512-mt7AdkieJJ5hEKeCxH4sdTTd679shUjo/cUvNY0fUHgQIPZa1jRuekTXnRytRrEwdrZWJDx56n1S8ism2uX7jg== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.4" - "@aws-sdk/credential-provider-node" "^3.972.2" - "@aws-sdk/middleware-bucket-endpoint" "^3.972.2" - "@aws-sdk/middleware-expect-continue" "^3.972.2" - "@aws-sdk/middleware-flexible-checksums" "^3.972.2" - "@aws-sdk/middleware-host-header" "^3.972.2" - "@aws-sdk/middleware-location-constraint" "^3.972.2" - "@aws-sdk/middleware-logger" "^3.972.2" - "@aws-sdk/middleware-recursion-detection" "^3.972.2" - "@aws-sdk/middleware-sdk-s3" "^3.972.4" - "@aws-sdk/middleware-ssec" "^3.972.2" - "@aws-sdk/middleware-user-agent" "^3.972.4" - "@aws-sdk/region-config-resolver" "^3.972.2" - "@aws-sdk/signature-v4-multi-region" "3.972.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/credential-provider-node" "^3.972.7" + "@aws-sdk/middleware-bucket-endpoint" "^3.972.3" + "@aws-sdk/middleware-expect-continue" "^3.972.3" + "@aws-sdk/middleware-flexible-checksums" "^3.972.6" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-location-constraint" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-sdk-s3" "^3.972.8" + "@aws-sdk/middleware-ssec" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/signature-v4-multi-region" "3.988.0" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.2" - "@aws-sdk/util-user-agent-node" "^3.972.2" + "@aws-sdk/util-endpoints" "3.988.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.6" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/eventstream-serde-browser" "^4.2.8" "@smithy/eventstream-serde-config-resolver" "^4.3.8" "@smithy/eventstream-serde-node" "^4.2.8" @@ -433,25 +433,25 @@ "@smithy/invalid-dependency" "^4.2.8" "@smithy/md5-js" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/middleware-retry" "^4.4.31" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.30" + "@smithy/util-defaults-mode-node" "^4.2.33" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" "@smithy/util-utf8" "^4.2.0" "@smithy/util-waiter" "^4.2.8" tslib "^2.6.2" @@ -588,44 +588,44 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-sso@3.975.0": - version "3.975.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.975.0.tgz#f22aca83b566fb5ced2c974020c42be60b350782" - integrity sha512-HpgJuleH7P6uILxzJKQOmlHdwaCY+xYC6VgRDzlwVEqU/HXjo4m2gOAyjUbpXlBOCWfGgMUzfBlNJ9z3MboqEQ== +"@aws-sdk/client-sso@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.988.0.tgz#0b6d5673e1b69d6cce03c78c11c4f705ea8fb303" + integrity sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.1" - "@aws-sdk/middleware-host-header" "^3.972.1" - "@aws-sdk/middleware-logger" "^3.972.1" - "@aws-sdk/middleware-recursion-detection" "^3.972.1" - "@aws-sdk/middleware-user-agent" "^3.972.2" - "@aws-sdk/region-config-resolver" "^3.972.1" - "@aws-sdk/types" "^3.973.0" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.1" - "@aws-sdk/util-user-agent-node" "^3.972.1" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.988.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.6" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.21.1" + "@smithy/core" "^3.23.0" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.11" - "@smithy/middleware-retry" "^4.4.27" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/middleware-retry" "^4.4.31" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.12" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.26" - "@smithy/util-defaults-mode-node" "^4.2.29" + "@smithy/util-defaults-mode-browser" "^4.3.30" + "@smithy/util-defaults-mode-node" "^4.2.33" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -735,38 +735,19 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/core@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.972.0.tgz#20d9c47fc3ad1bed4c1866eb6f6488bcc5d31754" - integrity sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A== - dependencies: - "@aws-sdk/types" "3.972.0" - "@aws-sdk/xml-builder" "3.972.0" - "@smithy/core" "^3.20.6" - "@smithy/node-config-provider" "^4.3.8" - "@smithy/property-provider" "^4.2.8" - "@smithy/protocol-http" "^5.3.8" - "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" - "@smithy/types" "^4.12.0" - "@smithy/util-base64" "^4.3.0" - "@smithy/util-middleware" "^4.2.8" - "@smithy/util-utf8" "^4.2.0" - tslib "^2.6.2" - -"@aws-sdk/core@^3.973.1", "@aws-sdk/core@^3.973.2", "@aws-sdk/core@^3.973.4": - version "3.973.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.4.tgz#a3c16c4cb40a7a816475839dbdd8f938cc8bb2f0" - integrity sha512-8Rk+kPP74YiR47x54bxYlKZswsaSh0a4XvvRUMLvyS/koNawhsGu/+qSZxREqUeTO+GkKpFvSQIsAZR+deUP+g== +"@aws-sdk/core@^3.973.5", "@aws-sdk/core@^3.973.8": + version "3.973.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.8.tgz#88f4cd0a00f9d8a2de68df810f771619e12f63ab" + integrity sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw== dependencies: "@aws-sdk/types" "^3.973.1" - "@aws-sdk/xml-builder" "^3.972.2" - "@smithy/core" "^3.22.0" + "@aws-sdk/xml-builder" "^3.972.4" + "@smithy/core" "^3.23.0" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" "@smithy/util-middleware" "^4.2.8" @@ -792,12 +773,12 @@ "@smithy/types" "^4.5.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-cognito-identity@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.2.tgz#5dbdac14772bbaa28e319c7518d6cd32a42f1f52" - integrity sha512-+tX6yfkw3+CpXLl/IoFqGSatbtZMzBcXr9HXPjbaYqCIu80SzfwjgQl8ddFeloTrA1xVPX2+eWOU04nVuACW7w== +"@aws-sdk/credential-provider-cognito-identity@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.3.tgz#699ccf8450be66848e972568e59e37b95bd62c2b" + integrity sha512-dW/DqTk90XW7hIngqntAVtJJyrkS51wcLhGz39lOMe0TlSmZl+5R/UGnAZqNbXmWuJHLzxe+MLgagxH41aTsAQ== dependencies: - "@aws-sdk/client-cognito-identity" "3.975.0" + "@aws-sdk/client-cognito-identity" "3.980.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/types" "^4.12.0" @@ -836,12 +817,12 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.2.tgz#ae9dab80f2de70b8573eb5cc73693136f54d9eb0" - integrity sha512-wzH1EdrZsytG1xN9UHaK12J9+kfrnd2+c8y0LVoS4O4laEjPoie1qVK3k8/rZe7KOtvULzyMnO3FT4Krr9Z0Dg== +"@aws-sdk/credential-provider-env@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.6.tgz#f43f8a3d3795bf275e04928e187b1319e277a0a6" + integrity sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw== dependencies: - "@aws-sdk/core" "^3.973.2" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/types" "^4.12.0" @@ -895,20 +876,20 @@ "@smithy/util-stream" "^4.5.6" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@^3.972.3", "@aws-sdk/credential-provider-http@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.4.tgz#bab20c5fbbe19eed1445d3b76a0cd608d1937767" - integrity sha512-OC7F3ipXV12QfDEWybQGHLzoeHBlAdx/nLzPfHP0Wsabu3JBffu5nlzSaJNf7to9HGtOW8Bpu8NX0ugmDrCbtw== +"@aws-sdk/credential-provider-http@^3.972.8": + version "3.972.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.8.tgz#61d353d438e7331e04b906b3fde89e8acef49f41" + integrity sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg== dependencies: - "@aws-sdk/core" "^3.973.4" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/types" "^3.973.1" "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" tslib "^2.6.2" "@aws-sdk/credential-provider-ini@3.895.0": @@ -968,19 +949,19 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.2.tgz#4a26aed6af446880cbba8ede95b38a0678697840" - integrity sha512-Jrb8sLm6k8+L7520irBrvCtdLxNtrG7arIxe9TCeMJt/HxqMGJdbIjw8wILzkEHLMIi4MecF2FbXCln7OT1Tag== - dependencies: - "@aws-sdk/core" "^3.973.2" - "@aws-sdk/credential-provider-env" "^3.972.2" - "@aws-sdk/credential-provider-http" "^3.972.3" - "@aws-sdk/credential-provider-login" "^3.972.2" - "@aws-sdk/credential-provider-process" "^3.972.2" - "@aws-sdk/credential-provider-sso" "^3.972.2" - "@aws-sdk/credential-provider-web-identity" "^3.972.2" - "@aws-sdk/nested-clients" "3.975.0" +"@aws-sdk/credential-provider-ini@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.6.tgz#4280520b3b9ed40b9219391d6130a9e7502297fc" + integrity sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow== + dependencies: + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/credential-provider-env" "^3.972.6" + "@aws-sdk/credential-provider-http" "^3.972.8" + "@aws-sdk/credential-provider-login" "^3.972.6" + "@aws-sdk/credential-provider-process" "^3.972.6" + "@aws-sdk/credential-provider-sso" "^3.972.6" + "@aws-sdk/credential-provider-web-identity" "^3.972.6" + "@aws-sdk/nested-clients" "3.988.0" "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" @@ -988,13 +969,13 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-login@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.2.tgz#2a022a80950041db4520983568290e57d06399fb" - integrity sha512-mlaw2aiI3DrimW85ZMn3g7qrtHueidS58IGytZ+mbFpsYLK5wMjCAKZQtt7VatLMtSBG/dn/EY4njbnYXIDKeQ== +"@aws-sdk/credential-provider-login@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.6.tgz#f852ed0479ca3b03c1cf3cceccdcf5990f6e2404" + integrity sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA== dependencies: - "@aws-sdk/core" "^3.973.2" - "@aws-sdk/nested-clients" "3.975.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/nested-clients" "3.988.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" @@ -1056,17 +1037,17 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@^3.972.1", "@aws-sdk/credential-provider-node@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.2.tgz#7f308337fc0674c340878da6c7061caffe03501d" - integrity sha512-Lz1J5IZdTjLYTVIcDP5DVDgi1xlgsF3p1cnvmbfKbjCRhQpftN2e2J4NFfRRvPD54W9+bZ8l5VipPXtTYK7aEg== - dependencies: - "@aws-sdk/credential-provider-env" "^3.972.2" - "@aws-sdk/credential-provider-http" "^3.972.3" - "@aws-sdk/credential-provider-ini" "^3.972.2" - "@aws-sdk/credential-provider-process" "^3.972.2" - "@aws-sdk/credential-provider-sso" "^3.972.2" - "@aws-sdk/credential-provider-web-identity" "^3.972.2" +"@aws-sdk/credential-provider-node@^3.972.4", "@aws-sdk/credential-provider-node@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.7.tgz#e08940d14d2c1a46f91728d32dcee5114c3f1277" + integrity sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.6" + "@aws-sdk/credential-provider-http" "^3.972.8" + "@aws-sdk/credential-provider-ini" "^3.972.6" + "@aws-sdk/credential-provider-process" "^3.972.6" + "@aws-sdk/credential-provider-sso" "^3.972.6" + "@aws-sdk/credential-provider-web-identity" "^3.972.6" "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" @@ -1110,12 +1091,12 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.2.tgz#15a841b2ed063342878f224cb7bc9b10228a0804" - integrity sha512-NLKLTT7jnUe9GpQAVkPTJO+cs2FjlQDt5fArIYS7h/Iw/CvamzgGYGFRVD2SE05nOHCMwafUSi42If8esGFV+g== +"@aws-sdk/credential-provider-process@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.6.tgz#e86c3a5d422b82d72b897e26f015c1e209bc85e7" + integrity sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ== dependencies: - "@aws-sdk/core" "^3.973.2" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -1164,14 +1145,14 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.2.tgz#530df0dd80f9be2bdf2a2de8c38578ab71c859e2" - integrity sha512-YpwDn8g3gCGUl61cCV0sRxP2pFIwg+ZsMfWQ/GalSyjXtRkctCMFA+u0yPb/Q4uTfNEiya1Y4nm0C5rIHyPW5Q== +"@aws-sdk/credential-provider-sso@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.6.tgz#832ef7b5928f3b35eb48dfdd2e100fb12141024f" + integrity sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg== dependencies: - "@aws-sdk/client-sso" "3.975.0" - "@aws-sdk/core" "^3.973.2" - "@aws-sdk/token-providers" "3.975.0" + "@aws-sdk/client-sso" "3.988.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/token-providers" "3.988.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -1217,13 +1198,13 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.2.tgz#30d253baee3e4e35dcc15aa9219e18befd3d17bf" - integrity sha512-x9DAiN9Qz+NjJ99ltDiVQ8d511M/tuF/9MFbe2jUgo7HZhD6+x4S3iT1YcP07ndwDUjmzKGmeOEgE24k4qvfdg== +"@aws-sdk/credential-provider-web-identity@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.6.tgz#b6bceb56771fb4281d7a9bb4fd3ce9bf41573b84" + integrity sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA== dependencies: - "@aws-sdk/core" "^3.973.2" - "@aws-sdk/nested-clients" "3.975.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/nested-clients" "3.988.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -1256,25 +1237,25 @@ tslib "^2.6.2" "@aws-sdk/credential-providers@^3.975.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.978.0.tgz#49fb888b338c8440f1680d6e1f4d849d279ad856" - integrity sha512-6SrS7Lpkllq19tpuBkSvUdeMLk4RwgU6OklcJ3NmCkIORhf/tFXTKRsSnm3XpZb7FgofCx69eOQUN/7r4xw8UQ== - dependencies: - "@aws-sdk/client-cognito-identity" "3.978.0" - "@aws-sdk/core" "^3.973.4" - "@aws-sdk/credential-provider-cognito-identity" "^3.972.2" - "@aws-sdk/credential-provider-env" "^3.972.2" - "@aws-sdk/credential-provider-http" "^3.972.4" - "@aws-sdk/credential-provider-ini" "^3.972.2" - "@aws-sdk/credential-provider-login" "^3.972.2" - "@aws-sdk/credential-provider-node" "^3.972.2" - "@aws-sdk/credential-provider-process" "^3.972.2" - "@aws-sdk/credential-provider-sso" "^3.972.2" - "@aws-sdk/credential-provider-web-identity" "^3.972.2" - "@aws-sdk/nested-clients" "3.978.0" + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.988.0.tgz#7b0a7fde3da3531cc51271c9eb86b0aefc0215fd" + integrity sha512-35gd/y5RFa2vtkdTt94NQgmHPnp7mgyCbcCP/G1NrscEIuZeTH3IpzzZvVinkC9RNJR4RriZL5xBdpETcDFfgw== + dependencies: + "@aws-sdk/client-cognito-identity" "3.988.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/credential-provider-cognito-identity" "^3.972.3" + "@aws-sdk/credential-provider-env" "^3.972.6" + "@aws-sdk/credential-provider-http" "^3.972.8" + "@aws-sdk/credential-provider-ini" "^3.972.6" + "@aws-sdk/credential-provider-login" "^3.972.6" + "@aws-sdk/credential-provider-node" "^3.972.7" + "@aws-sdk/credential-provider-process" "^3.972.6" + "@aws-sdk/credential-provider-sso" "^3.972.6" + "@aws-sdk/credential-provider-web-identity" "^3.972.6" + "@aws-sdk/nested-clients" "3.988.0" "@aws-sdk/types" "^3.973.1" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" @@ -1290,13 +1271,13 @@ tslib "^2.5.0" "@aws-sdk/lib-storage@^3.975.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.978.0.tgz#298cf18c7d3ac56f7dcca7d57dfc322a976c00a8" - integrity sha512-SvKpOdmhwI2mk4So73o2CDFWepcLO66p1YnngPPe9lrRl+apePsvyC++kwG2yYOTBVsRtsrvJwNBgPoDTO5itg== + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.988.0.tgz#16ba1c15f01868ad0d3b8e00f6740d874c45041a" + integrity sha512-EZ6US/oV1n7WtvVZ5FR80g8pA7xU92lBsvjibANVm/uHr8FOqPmSqQV9KvwzRmSdp0O6J1kXxi9LqmwgJVlN/A== dependencies: "@smithy/abort-controller" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/smithy-client" "^4.11.1" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/smithy-client" "^4.11.3" buffer "5.6.0" events "3.3.0" stream-browserify "3.0.0" @@ -1315,10 +1296,10 @@ "@smithy/util-config-provider" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-bucket-endpoint@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.2.tgz#04ab6d109a8ead757a711ee8cb18a1bd65b404da" - integrity sha512-ofuXBnitp9j8t05O4NQVrpMZDECPtUhRIWdLzR35baR5njOIPY7YqNtJE+yELVpSn2m4jt2sV1ezYMBY4/Lo+w== +"@aws-sdk/middleware-bucket-endpoint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz#158507d55505e5e7b5b8cdac9f037f6aa326f202" + integrity sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg== dependencies: "@aws-sdk/types" "^3.973.1" "@aws-sdk/util-arn-parser" "^3.972.2" @@ -1338,10 +1319,10 @@ "@smithy/types" "^4.8.0" tslib "^2.6.2" -"@aws-sdk/middleware-expect-continue@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.2.tgz#3bf62c912de18d9179dac9f192ab8b86f09a6f58" - integrity sha512-d9bBQlGk1T5j5rWfof20M2tErddOSoSLDauP2/yyuXfeOfQRCSBUZNrApSxjJ9Hw+/RDGR/XL+LEOqmXxSlV3A== +"@aws-sdk/middleware-expect-continue@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz#c60bd81e81dde215b9f3f67e3c5448b608afd530" + integrity sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" @@ -1367,15 +1348,15 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.2.tgz#038be2a6acb817cae1b12cd9084f1e5b7a0b38c0" - integrity sha512-GgWVZJdzXzqhXxzNAYB3TnZCj7d5rZNdovqSIV91e97nowHVaExRoyaZ3H/Ydqot7veHGPTl8nBp464zZeLDTQ== +"@aws-sdk/middleware-flexible-checksums@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.6.tgz#2bad5a67931d6601d7819c18f76a008c870c8b62" + integrity sha512-g5DadWO58IgQKuq+uLL3pLohOwLiA67gB49xj8694BW+LpHLNu/tjCqwLfIaWvZyABbv0LXeNiiTuTnjdgkZWw== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "^3.973.2" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/crc64-nvme" "3.972.0" "@aws-sdk/types" "^3.973.1" "@smithy/is-array-buffer" "^4.2.0" @@ -1383,7 +1364,7 @@ "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" @@ -1417,10 +1398,10 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/middleware-host-header@^3.972.1", "@aws-sdk/middleware-host-header@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.2.tgz#0d0a7fb4a5c71b4697c8f00d7de975be8146ffcf" - integrity sha512-42hZ8jEXT2uR6YybCzNq9OomqHPw43YIfRfz17biZjMQA4jKSQUaHIl6VvqO2Ddl5904pXg2Yd/ku78S0Ikgog== +"@aws-sdk/middleware-host-header@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz#47c161dec62d89c66c89f4d17ff4434021e04af5" + integrity sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" @@ -1436,10 +1417,10 @@ "@smithy/types" "^4.8.0" tslib "^2.6.2" -"@aws-sdk/middleware-location-constraint@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.2.tgz#8080605dd95e1103e5e4f42ea2ad2469973de6da" - integrity sha512-pyayzpq+VQiG1o9pEUyr6BXEJ2g2t4JIPdNxDkIHp2AhR63Gy/10WQkXTBOgRnfQ7/aLPLOnjRIWwOPp0CfUlA== +"@aws-sdk/middleware-location-constraint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz#b4f504f75baa19064b7457e5c6e3c8cecb4c32eb" + integrity sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" @@ -1472,10 +1453,10 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/middleware-logger@^3.972.1", "@aws-sdk/middleware-logger@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.2.tgz#132cf8945f27c7f93ba4f1742cefda04d18aff21" - integrity sha512-iUzdXKOgi4JVDDEG/VvoNw50FryRCEm0qAudw12DcZoiNJWl0rN6SYVLcL1xwugMfQncCXieK5UBlG6mhH7iYA== +"@aws-sdk/middleware-logger@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz#ef1afd4a0b70fe72cf5f7c817f82da9f35c7e836" + integrity sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" @@ -1514,10 +1495,10 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/middleware-recursion-detection@^3.972.1", "@aws-sdk/middleware-recursion-detection@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.2.tgz#12de11a9c4327418cf6edc20907813b2ecf4c179" - integrity sha512-/mzlyzJDtngNFd/rAYvqx29a2d0VuiYKN84Y/Mu9mGw7cfMOCyRK+896tb9wV6MoPRHUX7IXuKCIL8nzz2Pz5A== +"@aws-sdk/middleware-recursion-detection@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz#5b95dcecff76a0d2963bd954bdef87700d1b1c8c" + integrity sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q== dependencies: "@aws-sdk/types" "^3.973.1" "@aws/lambda-invoke-store" "^0.2.2" @@ -1554,43 +1535,23 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.0.tgz#2b0d89ecad90a90c7f4a3acd75966b9321ed2a66" - integrity sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w== - dependencies: - "@aws-sdk/core" "3.972.0" - "@aws-sdk/types" "3.972.0" - "@aws-sdk/util-arn-parser" "3.972.0" - "@smithy/core" "^3.20.6" - "@smithy/node-config-provider" "^4.3.8" - "@smithy/protocol-http" "^5.3.8" - "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" - "@smithy/types" "^4.12.0" - "@smithy/util-config-provider" "^4.2.0" - "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" - "@smithy/util-utf8" "^4.2.0" - tslib "^2.6.2" - -"@aws-sdk/middleware-sdk-s3@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.4.tgz#f40f2c4296009e927659def5a1f8d0b1b3ef600b" - integrity sha512-lradfn72Td7lswhZKi86VKRNkDtmQR7bq9shX1kaPK1itjThxfcx7ogXSvMm/0cuqoYGic8UUXQOaK4kpU933g== +"@aws-sdk/middleware-sdk-s3@^3.972.8": + version "3.972.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.8.tgz#3bfa20acc6c860eb585f4a7b60d6d01c749f0b4c" + integrity sha512-/yJdahpN/q3Dc88qXBTQVZfnXryLnxfCoP4hGClbKjuF0VCMxrz3il7sj0GhIkEQt5OM5+lA88XrvbjjuwSxIg== dependencies: - "@aws-sdk/core" "^3.973.4" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/types" "^3.973.1" "@aws-sdk/util-arn-parser" "^3.972.2" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" @@ -1603,10 +1564,10 @@ "@smithy/types" "^4.8.0" tslib "^2.6.2" -"@aws-sdk/middleware-ssec@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.2.tgz#73620c2401b6a64de25fd9bae1a9028ff3ca28ba" - integrity sha512-HJ3OmQnlQ1es6esrDWnx3nVPhBAN89WaFCzsDcb6oT7TMjBPUfZ5+1BpI7B0Hnme8cc6kp7qc4cgo2plrlROJA== +"@aws-sdk/middleware-ssec@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz#4f81d310fd91164e6e18ba3adab6bcf906920333" + integrity sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" @@ -1651,15 +1612,15 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/middleware-user-agent@^3.972.2", "@aws-sdk/middleware-user-agent@^3.972.3", "@aws-sdk/middleware-user-agent@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.4.tgz#ead729690b467c6c7105e220b71c5c59b0b4edaf" - integrity sha512-6sU8jrSJvY/lqSnU6IYsa8SrCKwOZ4Enl6O4xVJo8RCq9Bdr5Giuw2eUaJAk9GPcpr4OFcmSFv3JOLhpKGeRZA== +"@aws-sdk/middleware-user-agent@^3.972.5", "@aws-sdk/middleware-user-agent@^3.972.8": + version "3.972.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.8.tgz#04502a9d94fc67ee3726373ace61bd7eed12b5c0" + integrity sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg== dependencies: - "@aws-sdk/core" "^3.973.4" + "@aws-sdk/core" "^3.973.8" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.972.0" - "@smithy/core" "^3.22.0" + "@aws-sdk/util-endpoints" "3.988.0" + "@smithy/core" "^3.23.0" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" @@ -1796,88 +1757,44 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/nested-clients@3.975.0": - version "3.975.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.975.0.tgz#566d4508dfd2b83e86a829670d9219d307f6f1a5" - integrity sha512-OkeFHPlQj2c/Y5bQGkX14pxhDWUGUFt3LRHhjcDKsSCw6lrxKcxN3WFZN0qbJwKNydP+knL5nxvfgKiCLpTLRA== +"@aws-sdk/nested-clients@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.988.0.tgz#d45dd6dc370bf90b121917ebbabcfe19d472a74f" + integrity sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.1" - "@aws-sdk/middleware-host-header" "^3.972.1" - "@aws-sdk/middleware-logger" "^3.972.1" - "@aws-sdk/middleware-recursion-detection" "^3.972.1" - "@aws-sdk/middleware-user-agent" "^3.972.2" - "@aws-sdk/region-config-resolver" "^3.972.1" - "@aws-sdk/types" "^3.973.0" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.1" - "@aws-sdk/util-user-agent-node" "^3.972.1" - "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.21.1" - "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/hash-node" "^4.2.8" - "@smithy/invalid-dependency" "^4.2.8" - "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.11" - "@smithy/middleware-retry" "^4.4.27" - "@smithy/middleware-serde" "^4.2.9" - "@smithy/middleware-stack" "^4.2.8" - "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" - "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.12" - "@smithy/types" "^4.12.0" - "@smithy/url-parser" "^4.2.8" - "@smithy/util-base64" "^4.3.0" - "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.26" - "@smithy/util-defaults-mode-node" "^4.2.29" - "@smithy/util-endpoints" "^3.2.8" - "@smithy/util-middleware" "^4.2.8" - "@smithy/util-retry" "^4.2.8" - "@smithy/util-utf8" "^4.2.0" - tslib "^2.6.2" - -"@aws-sdk/nested-clients@3.978.0": - version "3.978.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.978.0.tgz#f149fc9a1812dd33d659688469b2587f11efa389" - integrity sha512-FyFiPp1SPt2JnspHlPO0LJyRwfYLBA27ToAoJAsbJIcd/Ytt9mFkRQ4kqUJQwnSl5JpdYzotNRqoRFCmx3uUfA== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.4" - "@aws-sdk/middleware-host-header" "^3.972.2" - "@aws-sdk/middleware-logger" "^3.972.2" - "@aws-sdk/middleware-recursion-detection" "^3.972.2" - "@aws-sdk/middleware-user-agent" "^3.972.4" - "@aws-sdk/region-config-resolver" "^3.972.2" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" + "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.972.0" - "@aws-sdk/util-user-agent-browser" "^3.972.2" - "@aws-sdk/util-user-agent-node" "^3.972.2" + "@aws-sdk/util-endpoints" "3.988.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.6" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.14" + "@smithy/middleware-retry" "^4.4.31" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.30" + "@smithy/util-defaults-mode-node" "^4.2.33" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -1925,10 +1842,10 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/region-config-resolver@^3.972.1", "@aws-sdk/region-config-resolver@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.2.tgz#962dc4099a3c43248724746dc2faa0d43499c374" - integrity sha512-/7vRBsfmiOlg2X67EdKrzzQGw5/SbkXb7ALHQmlQLkZh8qNgvS2G2dDC6NtF3hzFlpP3j2k+KIEtql/6VrI6JA== +"@aws-sdk/region-config-resolver@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz#25af64235ca6f4b6b21f85d4b3c0b432efc4ae04" + integrity sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/config-resolver" "^4.4.6" @@ -1962,13 +1879,13 @@ "@smithy/types" "^4.8.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.972.0.tgz#159662f1f0b26fc178ef1c8c92cbd37e79ddbdc0" - integrity sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ== +"@aws-sdk/signature-v4-multi-region@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.988.0.tgz#693c4606f272ea865b48dd0726ff711f27d349e1" + integrity sha512-SXwhbe2v0Jno7QLIBmZWAL2eVzGmXkfLLy0WkM6ZJVhE0SFUcnymDwMUA1oMDUvyArzvKBiU8khQ2ImheCKOHQ== dependencies: - "@aws-sdk/middleware-sdk-s3" "3.972.0" - "@aws-sdk/types" "3.972.0" + "@aws-sdk/middleware-sdk-s3" "^3.972.8" + "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" "@smithy/types" "^4.12.0" @@ -2021,14 +1938,14 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.975.0": - version "3.975.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.975.0.tgz#57d41c54fe8cf296816f2142de0c32f4c79b2bff" - integrity sha512-AWQt64hkVbDQ+CmM09wnvSk2mVyH4iRROkmYkr3/lmUtFNbE2L/fnw26sckZnUcFCsHPqbkQrcsZAnTcBLbH4w== +"@aws-sdk/token-providers@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.988.0.tgz#2ba93a808662fe8a13c7c04a2a578daf5e5bfe49" + integrity sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw== dependencies: - "@aws-sdk/core" "^3.973.1" - "@aws-sdk/nested-clients" "3.975.0" - "@aws-sdk/types" "^3.973.0" + "@aws-sdk/core" "^3.973.8" + "@aws-sdk/nested-clients" "3.988.0" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" @@ -2058,14 +1975,6 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/types@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.972.0.tgz#2c8ddf7fa63038da2e27d888c1bd5817b62e30fa" - integrity sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug== - dependencies: - "@smithy/types" "^4.12.0" - tslib "^2.6.2" - "@aws-sdk/types@^3.222.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.734.0.tgz#af5e620b0e761918282aa1c8e53cac6091d169a2" @@ -2074,7 +1983,7 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" -"@aws-sdk/types@^3.973.0", "@aws-sdk/types@^3.973.1": +"@aws-sdk/types@^3.973.1": version "3.973.1" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.1.tgz#1b2992ec6c8380c3e74c9bd2c74703e9a807d6e0" integrity sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg== @@ -2089,13 +1998,6 @@ dependencies: tslib "^2.6.2" -"@aws-sdk/util-arn-parser@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.0.tgz#24a29435e1d8ad6a3c2282f62521fe9961346c54" - integrity sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g== - dependencies: - tslib "^2.6.2" - "@aws-sdk/util-arn-parser@^3.972.2": version "3.972.2" resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz#ef18ba889e8ef35f083f1e962018bc0ce70acef3" @@ -2136,12 +2038,23 @@ "@smithy/util-endpoints" "^3.2.5" tslib "^2.6.2" -"@aws-sdk/util-endpoints@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz#0c483aa4853ea3858024bc502454ba395e533cb0" - integrity sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg== +"@aws-sdk/util-endpoints@3.980.0": + version "3.980.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz#0d2665ad75f92f3f208541f6fa88451d2a2e96ce" + integrity sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw== dependencies: - "@aws-sdk/types" "3.972.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-endpoints" "^3.2.8" + tslib "^2.6.2" + +"@aws-sdk/util-endpoints@3.988.0": + version "3.988.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz#99ca2322589bb67fd1c1c9cfc3c275d112b38acd" + integrity sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA== + dependencies: + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-endpoints" "^3.2.8" @@ -2194,10 +2107,10 @@ bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-browser@^3.972.1", "@aws-sdk/util-user-agent-browser@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.2.tgz#f5af782d42aed4c87059406b6f2ddf258fbb9586" - integrity sha512-gz76bUyebPZRxIsBHJUd/v+yiyFzm9adHbr8NykP2nm+z/rFyvQneOHajrUejtmnc5tTBeaDPL4X25TnagRk4A== +"@aws-sdk/util-user-agent-browser@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz#1363b388cb3af86c5322ef752c0cf8d7d25efa8a" + integrity sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" @@ -2237,12 +2150,12 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@^3.972.1", "@aws-sdk/util-user-agent-node@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.2.tgz#cbdb14a2e412b39d7751636133de604ce4727506" - integrity sha512-vnxOc4C6AR7hVbwyFo1YuH0GB6dgJlWt8nIOOJpnzJAWJPkUMPJ9Zv2lnKsSU7TTZbhP2hEO8OZ4PYH59XFv8Q== +"@aws-sdk/util-user-agent-node@^3.972.3", "@aws-sdk/util-user-agent-node@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.6.tgz#ebd0619524bf150d9da4ab8ab8b457d5cd75abc7" + integrity sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA== dependencies: - "@aws-sdk/middleware-user-agent" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.8" "@aws-sdk/types" "^3.973.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/types" "^4.12.0" @@ -2282,22 +2195,13 @@ fast-xml-parser "5.2.5" tslib "^2.6.2" -"@aws-sdk/xml-builder@3.972.0": - version "3.972.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz#8ae8f6f6e0a63d518c8c8ce9f40f3c94d9b67884" - integrity sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg== - dependencies: - "@smithy/types" "^4.12.0" - fast-xml-parser "5.2.5" - tslib "^2.6.2" - -"@aws-sdk/xml-builder@^3.972.2": - version "3.972.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.2.tgz#c63a6a788ff398491748908af528c8294c562f65" - integrity sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA== +"@aws-sdk/xml-builder@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz#8115c8cf90c71cf484a52c82eac5344cd3a5e921" + integrity sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q== dependencies: "@smithy/types" "^4.12.0" - fast-xml-parser "5.2.5" + fast-xml-parser "5.3.4" tslib "^2.6.2" "@aws/lambda-invoke-store@^0.0.1": @@ -2551,6 +2455,26 @@ events "^3.0.0" tslib "^2.8.1" +"@azure/storage-blob@^12.31.0": + version "12.31.0" + resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.31.0.tgz#97b09be2bf6ab59739b862edd8124798362ce720" + integrity sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg== + dependencies: + "@azure/abort-controller" "^2.1.2" + "@azure/core-auth" "^1.9.0" + "@azure/core-client" "^1.9.3" + "@azure/core-http-compat" "^2.2.0" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.6.2" + "@azure/core-rest-pipeline" "^1.19.1" + "@azure/core-tracing" "^1.2.0" + "@azure/core-util" "^1.11.0" + "@azure/core-xml" "^1.4.5" + "@azure/logger" "^1.1.4" + "@azure/storage-common" "^12.3.0" + events "^3.0.0" + tslib "^2.8.1" + "@azure/storage-common@^12.0.0-beta.2": version "12.0.0" resolved "https://registry.yarnpkg.com/@azure/storage-common/-/storage-common-12.0.0.tgz#a652d7daeb252b7827362b4e818f52fee15a1264" @@ -2566,6 +2490,21 @@ events "^3.3.0" tslib "^2.8.1" +"@azure/storage-common@^12.3.0": + version "12.3.0" + resolved "https://registry.yarnpkg.com/@azure/storage-common/-/storage-common-12.3.0.tgz#5bf257383836e67a426c91d7e9678479afe802a9" + integrity sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ== + dependencies: + "@azure/abort-controller" "^2.1.2" + "@azure/core-auth" "^1.9.0" + "@azure/core-http-compat" "^2.2.0" + "@azure/core-rest-pipeline" "^1.19.1" + "@azure/core-tracing" "^1.2.0" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.1.4" + events "^3.3.0" + tslib "^2.8.1" + "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" @@ -3247,10 +3186,10 @@ "@smithy/uuid" "^1.1.0" tslib "^2.6.2" -"@smithy/core@^3.20.6", "@smithy/core@^3.21.1", "@smithy/core@^3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.22.0.tgz#bf5ea9233e2be1d8681d06c8ed3c31966711dc83" - integrity sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA== +"@smithy/core@^3.22.0", "@smithy/core@^3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.23.0.tgz#64dca2825753316ace7b8342cb96c9dfc5de4e2a" + integrity sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg== dependencies: "@smithy/middleware-serde" "^4.2.9" "@smithy/protocol-http" "^5.3.8" @@ -3258,7 +3197,7 @@ "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" "@smithy/util-utf8" "^4.2.0" "@smithy/uuid" "^1.1.0" tslib "^2.6.2" @@ -3702,12 +3641,12 @@ "@smithy/util-middleware" "^4.2.3" tslib "^2.6.2" -"@smithy/middleware-endpoint@^4.4.11", "@smithy/middleware-endpoint@^4.4.12": - version "4.4.12" - resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.12.tgz#910c9f1cc738d104225d69f702bd28cd2a976eb0" - integrity sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q== +"@smithy/middleware-endpoint@^4.4.12", "@smithy/middleware-endpoint@^4.4.14": + version "4.4.14" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz#df8aca71af70366f39305eeaf18ffd650f764219" + integrity sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag== dependencies: - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.23.0" "@smithy/middleware-serde" "^4.2.9" "@smithy/node-config-provider" "^4.3.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -3744,15 +3683,15 @@ "@smithy/uuid" "^1.0.0" tslib "^2.6.2" -"@smithy/middleware-retry@^4.4.27", "@smithy/middleware-retry@^4.4.29": - version "4.4.29" - resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.29.tgz#b8f3d6e46924e7496cd107e14b238df5ef9e80b4" - integrity sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg== +"@smithy/middleware-retry@^4.4.29", "@smithy/middleware-retry@^4.4.31": + version "4.4.31" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz#1dbdbaedbd62f4900e3520f65599810123c0c461" + integrity sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg== dependencies: "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/service-error-classification" "^4.2.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -3919,7 +3858,18 @@ "@smithy/types" "^4.5.0" tslib "^2.6.2" -"@smithy/node-http-handler@^4.3.0", "@smithy/node-http-handler@^4.4.3": +"@smithy/node-http-handler@^4.3.0", "@smithy/node-http-handler@^4.4.10", "@smithy/node-http-handler@^4.4.8": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz#4945e2c2e61174ec1471337e3ddd50b8e4921204" + integrity sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA== + dependencies: + "@smithy/abort-controller" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/querystring-builder" "^4.2.8" + "@smithy/types" "^4.12.0" + tslib "^2.6.2" + +"@smithy/node-http-handler@^4.4.3": version "4.4.3" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz#fb2d16719cb4e8df0c189e8bde60e837df5c0c5b" integrity sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ== @@ -3941,17 +3891,6 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@smithy/node-http-handler@^4.4.8": - version "4.4.8" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz#298cc148c812b9a79f0ebd75e82bdab9e6d0bbcd" - integrity sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg== - dependencies: - "@smithy/abort-controller" "^4.2.8" - "@smithy/protocol-http" "^5.3.8" - "@smithy/querystring-builder" "^4.2.8" - "@smithy/types" "^4.12.0" - tslib "^2.6.2" - "@smithy/property-provider@^4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.1.1.tgz#6e11ae6729840314afed05fd6ab48f62c654116b" @@ -4258,17 +4197,17 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/smithy-client@^4.10.12", "@smithy/smithy-client@^4.10.8", "@smithy/smithy-client@^4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.11.1.tgz#8203e620da22e7f7218597a60193ef5a53325c41" - integrity sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ== +"@smithy/smithy-client@^4.11.1", "@smithy/smithy-client@^4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.11.3.tgz#94d1083d5bc3b09e510f680ad7f82395765badf3" + integrity sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg== dependencies: - "@smithy/core" "^3.22.0" - "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/core" "^3.23.0" + "@smithy/middleware-endpoint" "^4.4.14" "@smithy/middleware-stack" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.12" tslib "^2.6.2" "@smithy/smithy-client@^4.6.3", "@smithy/smithy-client@^4.6.4": @@ -4506,13 +4445,13 @@ bowser "^2.11.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^4.3.26", "@smithy/util-defaults-mode-browser@^4.3.28": - version "4.3.28" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.28.tgz#36903b2c5ae11d41b1bb3e1f899a062389feb8ff" - integrity sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w== +"@smithy/util-defaults-mode-browser@^4.3.28", "@smithy/util-defaults-mode-browser@^4.3.30": + version "4.3.30" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz#0494c467897ddf5b09b6f87712992b7b0ebe1cc1" + integrity sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng== dependencies: "@smithy/property-provider" "^4.2.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" @@ -4562,16 +4501,16 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^4.2.29", "@smithy/util-defaults-mode-node@^4.2.31": - version "4.2.31" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.31.tgz#edb1297274e334af44783f4a3c78d2e890b328bb" - integrity sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA== +"@smithy/util-defaults-mode-node@^4.2.31", "@smithy/util-defaults-mode-node@^4.2.33": + version "4.2.33" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz#b5d8b88d398d4556fe3e6299d7a14eac2b892750" + integrity sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA== dependencies: "@smithy/config-resolver" "^4.4.6" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" @@ -4757,13 +4696,13 @@ "@smithy/util-utf8" "^4.1.0" tslib "^2.6.2" -"@smithy/util-stream@^4.5.10": - version "4.5.10" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.10.tgz#3a7b56f0bdc3833205f80fea67d8e76756ea055b" - integrity sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g== +"@smithy/util-stream@^4.5.12": + version "4.5.12" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.12.tgz#f8734a01dce2e51530231e6afc8910397d3e300a" + integrity sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg== dependencies: "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.10" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" "@smithy/util-buffer-from" "^4.2.0" @@ -5355,15 +5294,15 @@ arraybuffer.prototype.slice@^1.0.4: ioctl "^2.0.2" "arsenal@git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes": - version "8.2.44" - resolved "git+https://github.com/scality/Arsenal#475f746b6d201baaa46cc9ac2cdc0a2ed121fa43" + version "8.3.2" + resolved "git+https://github.com/scality/Arsenal#f6f41714ec37b792083f8ac2416456102f882ca8" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" "@aws-sdk/credential-providers" "^3.975.0" "@aws-sdk/lib-storage" "^3.975.0" "@azure/identity" "^4.13.0" - "@azure/storage-blob" "^12.28.0" + "@azure/storage-blob" "^12.31.0" "@js-sdsl/ordered-set" "^4.4.2" "@scality/hdclient" "^1.3.1" "@smithy/node-http-handler" "^4.3.0" @@ -6911,6 +6850,13 @@ fast-xml-parser@5.2.5: dependencies: strnum "^2.1.0" +fast-xml-parser@5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz#06f39aafffdbc97bef0321e626c7ddd06a043ecf" + integrity sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA== + dependencies: + strnum "^2.1.0" + fast-xml-parser@^5.0.7: version "5.0.9" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz#5b64c810e70941a9c07b07ead8299841fbb8dd76" @@ -7677,9 +7623,9 @@ ioredis@^5.6.1: standard-as-callback "^2.1.0" ioredis@^5.8.1: - version "5.9.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.9.2.tgz#ffdce2a019950299716e88ee56cd5802b399b108" - integrity sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ== + version "5.9.3" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.9.3.tgz#e897af9f87ee4b7bc61d8bd6373f466aca43d4e0" + integrity sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA== dependencies: "@ioredis/commands" "1.5.0" cluster-key-slot "^1.1.0" From 3ebd2ab78e0db8cdf23c8429bbf44f0afcbb96d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Thu, 12 Feb 2026 16:56:26 +0100 Subject: [PATCH 11/11] Upgrade scubaclient to fix dependencies vulnerabilities Issue: CLDSRV-817 --- yarn.lock | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/yarn.lock b/yarn.lock index 34211a4e89..d70b36a0a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,15 +1975,7 @@ "@smithy/types" "^4.9.0" tslib "^2.6.2" -"@aws-sdk/types@^3.222.0": - version "3.734.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.734.0.tgz#af5e620b0e761918282aa1c8e53cac6091d169a2" - integrity sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg== - dependencies: - "@smithy/types" "^4.1.0" - tslib "^2.6.2" - -"@aws-sdk/types@^3.973.1": +"@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.1": version "3.973.1" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.1.tgz#1b2992ec6c8380c3e74c9bd2c74703e9a807d6e0" integrity sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg== @@ -4263,13 +4255,6 @@ dependencies: tslib "^2.6.2" -"@smithy/types@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.1.0.tgz#19de0b6087bccdd4182a334eb5d3d2629699370f" - integrity sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw== - dependencies: - tslib "^2.6.2" - "@smithy/types@^4.12.0": version "4.12.0" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.12.0.tgz#55d2479080922bda516092dbf31916991d9c6fee" @@ -5423,12 +5408,12 @@ axios@^0.18.0: is-buffer "^2.0.2" axios@^1.13.2: - version "1.13.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.4.tgz#15d109a4817fb82f73aea910d41a2c85606076bc" - integrity sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg== + version "1.13.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" + integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" + follow-redirects "^1.15.11" + form-data "^4.0.5" proxy-from-env "^1.1.0" backo@^1.1.0: @@ -6972,12 +6957,7 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" -follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -follow-redirects@^1.15.9: +follow-redirects@^1.15.11, follow-redirects@^1.15.9: version "1.15.11" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== @@ -7010,7 +6990,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== -form-data@^4.0.4: +form-data@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==