From 37d26277e4cbacdc8e34e8a6810b08ef7c4a8f49 Mon Sep 17 00:00:00 2001 From: Hui Zhao <10602282+HuiSF@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:26:31 -0700 Subject: [PATCH 1/4] feat(core): resolve webcrypto from node:crypto for Node18 (#13599) --- .../load-verdaccio-with-amplify-js/action.yml | 26 ++++++++++++------- .github/actions/node-and-build/action.yml | 6 +++-- .github/integ-config/integ-all.yml | 9 ++++++- .github/workflows/callable-e2e-test.yml | 7 +++++ .github/workflows/callable-e2e-tests.yml | 1 + package.json | 3 +++ packages/aws-amplify/package.json | 6 ++--- .../utils/globalHelpers/globalHelpers.test.ts | 6 ----- .../core/src/utils/globalHelpers/index.ts | 10 +++++++ 9 files changed, 52 insertions(+), 22 deletions(-) diff --git a/.github/actions/load-verdaccio-with-amplify-js/action.yml b/.github/actions/load-verdaccio-with-amplify-js/action.yml index c8cd349cb86..b0f2f5552ac 100644 --- a/.github/actions/load-verdaccio-with-amplify-js/action.yml +++ b/.github/actions/load-verdaccio-with-amplify-js/action.yml @@ -6,7 +6,8 @@ runs: steps: - name: Start verdaccio run: | - npx verdaccio@5.25.0 & + # This version supports Node.js v22 + npx verdaccio@5.31.1 & while ! nc -z localhost 4873; do echo "Verdaccio not running yet" sleep 1 @@ -18,25 +19,30 @@ runs: - name: Install and run npm-cli-login shell: bash env: - NPM_REGISTRY: http://localhost:4873/ + NPM_REGISTRY_HOST: localhost:4873 + NPM_REGISTRY: http://localhost:4873 NPM_USER: verdaccio NPM_PASS: verdaccio NPM_EMAIL: verdaccio@amplify.js run: | - npm i -g npm-cli-adduser - npm-cli-adduser - sleep 1 - - name: Configure registry and git + # Make the HTTP request that npm addUser makes to avoid the "Exit handler never called" error + TOKEN=$(curl -s \ + -H "Accept: application/json" \ + -H "Content-Type:application/json" \ + -X PUT --data "{\"name\": \"$NPM_USER\", \"password\": \"$NPM_PASS\", \"email\": \"$NPM_EMAIL\"}" \ + $NPM_REGISTRY/-/user/org.couchdb.user:$NPM_USER 2>&1 | jq -r '.token') + + # Set the Verdaccio registry and set the token for logging in + yarn config set registry $NPM_REGISTRY + npm set registry $NPM_REGISTRY + npm set //"$NPM_REGISTRY_HOST"/:_authToken $TOKEN + - name: Configure git shell: bash working-directory: ./amplify-js env: - NPM_REGISTRY: http://localhost:4873/ NPM_USER: verdaccio - NPM_PASS: verdaccio NPM_EMAIL: verdaccio@amplify.js run: | - yarn config set registry $NPM_REGISTRY - npm set registry $NPM_REGISTRY git config --global user.email $NPM_EMAIL git config --global user.name $NPM_USER git status diff --git a/.github/actions/node-and-build/action.yml b/.github/actions/node-and-build/action.yml index 0af092c4c84..becd2d49165 100644 --- a/.github/actions/node-and-build/action.yml +++ b/.github/actions/node-and-build/action.yml @@ -4,13 +4,15 @@ inputs: is-prebuild: required: false default: false + node_version: + required: false runs: using: 'composite' steps: - - name: Setup Node.js 18 + - name: Setup Node.js uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: 18.20.2 + node-version: ${{ inputs.node_version || '18.x' }} env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index d29ae41ba42..db7f9d64444 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -829,7 +829,7 @@ tests: sample_name: [guest-access] spec: storage-client-server browser: *minimal_browser_list - + # INAPPMESSAGING - test_name: integ_in_app_messaging desc: 'React InApp Messaging' @@ -856,3 +856,10 @@ tests: spec: ssr-context-isolation yarn_script: ci:ssr-context-isolation browser: [chrome] + - test_name: integ_node_envs + desc: 'Node.js environment tests' + framework: node + category: integration + sample_name: auth-gql-storage + yarn_script: ci:node-env-test + node_versions: ['18.x', '20.x', '22.x'] diff --git a/.github/workflows/callable-e2e-test.yml b/.github/workflows/callable-e2e-test.yml index 18697cf5dc5..fd9f9cb697a 100644 --- a/.github/workflows/callable-e2e-test.yml +++ b/.github/workflows/callable-e2e-test.yml @@ -37,6 +37,9 @@ on: yarn_script: required: false type: string + node_versions: + required: false + type: string env: AMPLIFY_DIR: /home/runner/work/amplify-js/amplify-js/amplify-js @@ -54,6 +57,8 @@ jobs: - ${{ fromJson(inputs.browser) }} sample_name: - ${{ fromJson(inputs.sample_name) }} + node_version: + - ${{ fromJson(inputs.node_versions) }} fail-fast: false timeout-minutes: ${{ inputs.timeout_minutes }} @@ -64,6 +69,8 @@ jobs: path: amplify-js - name: Setup node and build the repository uses: ./amplify-js/.github/actions/node-and-build + with: + node_version: ${{ matrix.node_version }} - name: Setup samples staging repository uses: ./amplify-js/.github/actions/setup-samples-staging with: diff --git a/.github/workflows/callable-e2e-tests.yml b/.github/workflows/callable-e2e-tests.yml index c27c51ce57f..15f6b576f8d 100644 --- a/.github/workflows/callable-e2e-tests.yml +++ b/.github/workflows/callable-e2e-tests.yml @@ -44,6 +44,7 @@ jobs: timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }} retry_count: ${{ matrix.integ-config.retry_count || 3 }} yarn_script: ${{ matrix.integ-config.yarn_script || '' }} + node_versions: ${{ toJSON(matrix.integ-config.node_versions) || '[""]' }} # e2e-test-runner-headless: # name: E2E test runnner_headless diff --git a/package.json b/package.json index 06a17c0fdaf..eef6052f3e7 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,9 @@ "webpack-bundle-analyzer": "^4.7.0", "webpack-cli": "^5.0.0" }, + "engines": { + "node": ">=18" + }, "resolutions": { "@types/babel__traverse": "7.20.0", "path-scurry": "1.10.0", diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 822d5b251ef..0ed2aeb9c4b 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.27 kB" + "limit": "28.30 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,13 +449,13 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.06 kB" + "limit": "30.10 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "21.47 kB" + "limit": "21.50 kB" }, { "name": "[Storage] copy (S3)", diff --git a/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts b/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts index 4d9dbd8bc17..bb7d2e39a9b 100644 --- a/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts +++ b/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts @@ -44,12 +44,6 @@ describe('getGlobal', () => { expect(getCrypto()).toEqual(mockCrypto); }); - - it('should throw error if crypto is unavailable globally', () => { - mockWindow.mockImplementation(() => undefined); - - expect(() => getCrypto()).toThrow(AmplifyError); - }); }); describe('getBtoa()', () => { diff --git a/packages/core/src/utils/globalHelpers/index.ts b/packages/core/src/utils/globalHelpers/index.ts index 622f4d3c3ef..dc35f897bf6 100644 --- a/packages/core/src/utils/globalHelpers/index.ts +++ b/packages/core/src/utils/globalHelpers/index.ts @@ -13,6 +13,16 @@ export const getCrypto = () => { return crypto; } + try { + const crypto = require('node:crypto').webcrypto; + + if (typeof crypto === 'object') { + return crypto; + } + } catch (_) { + // no-op + } + throw new AmplifyError({ name: 'MissingPolyfill', message: 'Cannot resolve the `crypto` function from the environment.', From 28434a69d6956369ebd7b2ac4b67e0ec1bb14748 Mon Sep 17 00:00:00 2001 From: yuhengshs <94558971+yuhengshs@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:51:00 -0700 Subject: [PATCH 2/4] feat(storage): add support for content disposition and content type in getUrl (#13615) * added content disposition and content type for getUrl and corresponded unit tests * Update content disposition type for getUrl and uploadData, increase bundle size for both * Update package.json to bump up bundle size * extracted content disposition as interface and constructContentDisposition to util * export Content Disposition interface * Update path to relative path. Update description for contentDisposition in options * update descriptions for contentDisposition * update LoadOrCreateMultipartUploadOptions's contentDisposition * add full coverage unit test for * Bump up getUrl package size to 15.63KB Bump up getUrl package size to 15.63KB * Bump up getUrl package size to 15.64KB the size exceeded 1B --- packages/aws-amplify/package.json | 4 +- .../providers/s3/apis/getUrl.test.ts | 130 ++++++++++++++++++ .../S3/getPresignedGetObjectUrl.test.ts | 40 ++++++ .../utils/constructContentDisposition.test.ts | 67 +++++++++ .../src/providers/s3/apis/internal/getUrl.ts | 9 ++ .../uploadData/multipart/initialUpload.ts | 7 +- .../s3/apis/uploadData/putObjectJob.ts | 3 +- .../storage/src/providers/s3/types/options.ts | 27 +++- .../providers/s3/utils/client/getObject.ts | 15 +- .../s3/utils/constructContentDisposition.ts | 16 +++ 10 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 packages/storage/__tests__/providers/s3/utils/constructContentDisposition.test.ts create mode 100644 packages/storage/src/providers/s3/utils/constructContentDisposition.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 0ed2aeb9c4b..bd2e711ad49 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -479,7 +479,7 @@ "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "15.54 kB" + "limit": "15.64 kB" }, { "name": "[Storage] list (S3)", @@ -497,7 +497,7 @@ "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "19.64 kB" + "limit": "19.66 kB" } ] } diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index 994f4a0b648..b9658653967 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -236,6 +236,136 @@ describe('getUrl test with path', () => { }, ); }); + describe('Happy cases: With path and Content Disposition, Content Type', () => { + const config = { + credentials, + region, + userAgentValue: expect.any(String), + }; + beforeEach(() => { + jest.mocked(headObject).mockResolvedValue({ + ContentLength: 100, + ContentType: 'text/plain', + ETag: 'etag', + LastModified: new Date('01-01-1980'), + Metadata: { meta: 'value' }, + $metadata: {} as any, + }); + jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each([ + { + path: 'path', + expectedKey: 'path', + contentDisposition: 'inline; filename="example.txt"', + contentType: 'text/plain', + }, + { + path: () => 'path', + expectedKey: 'path', + contentDisposition: { + type: 'attachment' as const, + filename: 'example.pdf', + }, + contentType: 'application/pdf', + }, + ])( + 'should getUrl with path $path and expectedKey $expectedKey and content disposition and content type', + async ({ path, expectedKey, contentDisposition, contentType }) => { + const headObjectOptions = { + Bucket: bucket, + Key: expectedKey, + }; + const { url, expiresAt } = await getUrlWrapper({ + path, + options: { + validateObjectExistence: true, + contentDisposition, + contentType, + }, + }); + expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledTimes(1); + await expect(headObject).toBeLastCalledWithConfigAndInput( + config, + headObjectOptions, + ); + expect({ url, expiresAt }).toEqual({ + url: mockURL, + expiresAt: expect.any(Date), + }); + }, + ); + }); + describe('Error cases: With invalid Content Disposition', () => { + const config = { + credentials, + region, + userAgentValue: expect.any(String), + }; + beforeEach(() => { + jest.mocked(headObject).mockResolvedValue({ + ContentLength: 100, + ContentType: 'text/plain', + ETag: 'etag', + LastModified: new Date('01-01-1980'), + Metadata: { meta: 'value' }, + $metadata: {} as any, + }); + jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each([ + { + path: 'path', + expectedKey: 'path', + contentDisposition: { + type: 'invalid' as 'attachment' | 'inline', + filename: '"example.txt', + }, + }, + { + path: 'path', + expectedKey: 'path', + contentDisposition: { + type: 'invalid' as 'attachment' | 'inline', + }, + }, + ])( + 'should ignore for invalid content disposition: $contentDisposition', + async ({ path, expectedKey, contentDisposition }) => { + const headObjectOptions = { + Bucket: bucket, + Key: expectedKey, + }; + const { url, expiresAt } = await getUrlWrapper({ + path, + options: { + validateObjectExistence: true, + contentDisposition, + }, + }); + expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledTimes(1); + await expect(headObject).toBeLastCalledWithConfigAndInput( + config, + headObjectOptions, + ); + expect({ url, expiresAt }).toEqual({ + url: mockURL, + expiresAt: expect.any(Date), + }); + }, + ); + }); describe('Error cases : With path', () => { afterAll(() => { jest.clearAllMocks(); diff --git a/packages/storage/__tests__/providers/s3/utils/client/S3/getPresignedGetObjectUrl.test.ts b/packages/storage/__tests__/providers/s3/utils/client/S3/getPresignedGetObjectUrl.test.ts index 93bd3963606..ab84fb03eb6 100644 --- a/packages/storage/__tests__/providers/s3/utils/client/S3/getPresignedGetObjectUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/utils/client/S3/getPresignedGetObjectUrl.test.ts @@ -70,4 +70,44 @@ describe('serializeGetObjectRequest', () => { }), ); }); + + it('should return get object API request with disposition and content type', async () => { + const actual = await getPresignedGetObjectUrl( + { + ...defaultConfigWithStaticCredentials, + signingRegion: defaultConfigWithStaticCredentials.region, + signingService: 's3', + expiration: 900, + userAgentValue: 'UA', + }, + { + Bucket: 'bucket', + Key: 'key', + ResponseContentDisposition: 'attachment; filename="filename.jpg"', + ResponseContentType: 'application/pdf', + }, + ); + + expect(actual).toEqual( + expect.objectContaining({ + hostname: `bucket.s3.${defaultConfigWithStaticCredentials.region}.amazonaws.com`, + pathname: '/key', + searchParams: expect.objectContaining({ + get: expect.any(Function), + }), + }), + ); + + expect(actual.searchParams.get('X-Amz-Expires')).toBe('900'); + expect(actual.searchParams.get('x-amz-content-sha256')).toEqual( + expect.any(String), + ); + expect(actual.searchParams.get('response-content-disposition')).toBe( + 'attachment; filename="filename.jpg"', + ); + expect(actual.searchParams.get('response-content-type')).toBe( + 'application/pdf', + ); + expect(actual.searchParams.get('x-amz-user-agent')).toBe('UA'); + }); }); diff --git a/packages/storage/__tests__/providers/s3/utils/constructContentDisposition.test.ts b/packages/storage/__tests__/providers/s3/utils/constructContentDisposition.test.ts new file mode 100644 index 00000000000..fe6f1d10523 --- /dev/null +++ b/packages/storage/__tests__/providers/s3/utils/constructContentDisposition.test.ts @@ -0,0 +1,67 @@ +import { constructContentDisposition } from '../../../../src/providers/s3/utils/constructContentDisposition'; +import { ContentDisposition } from '../../../../src/providers/s3/types/options'; + +describe('constructContentDisposition', () => { + it('should return undefined when input is undefined', () => { + expect(constructContentDisposition(undefined)).toBeUndefined(); + }); + + it('should return the input string when given a string', () => { + const input = 'inline; filename="example.txt"'; + expect(constructContentDisposition(input)).toBe(input); + }); + + it('should construct disposition string with filename when given an object with type and filename', () => { + const input: ContentDisposition = { + type: 'attachment', + filename: 'example.pdf', + }; + expect(constructContentDisposition(input)).toBe( + 'attachment; filename="example.pdf"', + ); + }); + + it('should return only the type when given an object with type but no filename', () => { + const input: ContentDisposition = { + type: 'inline', + }; + expect(constructContentDisposition(input)).toBe('inline'); + }); + + it('should handle empty string filename', () => { + const input: ContentDisposition = { + type: 'attachment', + filename: '', + }; + expect(constructContentDisposition(input)).toBe('attachment; filename=""'); + }); + + it('should handle filenames with spaces', () => { + const input: ContentDisposition = { + type: 'attachment', + filename: 'my file.txt', + }; + expect(constructContentDisposition(input)).toBe( + 'attachment; filename="my file.txt"', + ); + }); + + it('should handle filenames with special characters', () => { + const input: ContentDisposition = { + type: 'attachment', + filename: 'file"with"quotes.txt', + }; + expect(constructContentDisposition(input)).toBe( + 'attachment; filename="file"with"quotes.txt"', + ); + }); + + // Edge cases + it('should return undefined for null input', () => { + expect(constructContentDisposition(null as any)).toBeUndefined(); + }); + + it('should return undefined for number input', () => { + expect(constructContentDisposition(123 as any)).toBeUndefined(); + }); +}); diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index 4f866ef80b3..a5c319a1389 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -22,6 +22,7 @@ import { MAX_URL_EXPIRATION, STORAGE_INPUT_KEY, } from '../../utils/constants'; +import { constructContentDisposition } from '../../utils/constructContentDisposition'; import { getProperties } from './getProperties'; @@ -74,6 +75,14 @@ export const getUrl = async ( { Bucket: bucket, Key: finalKey, + ...(getUrlOptions?.contentDisposition && { + ResponseContentDisposition: constructContentDisposition( + getUrlOptions.contentDisposition, + ), + }), + ...(getUrlOptions?.contentType && { + ResponseContentType: getUrlOptions.contentType, + }), }, ), expiresAt: new Date(Date.now() + urlExpirationInSec * 1000), diff --git a/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts b/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts index 1179b89c08b..25338b2003f 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts @@ -3,10 +3,11 @@ import { StorageAccessLevel } from '@aws-amplify/core'; -import { ResolvedS3Config } from '../../../types/options'; +import { ContentDisposition, ResolvedS3Config } from '../../../types/options'; import { StorageUploadDataPayload } from '../../../../../types'; import { Part, createMultipartUpload } from '../../../utils/client'; import { logger } from '../../../../../utils'; +import { constructContentDisposition } from '../../../utils/constructContentDisposition'; import { cacheMultipartUpload, @@ -22,7 +23,7 @@ interface LoadOrCreateMultipartUploadOptions { keyPrefix?: string; key: string; contentType?: string; - contentDisposition?: string; + contentDisposition?: string | ContentDisposition; contentEncoding?: string; metadata?: Record; size?: number; @@ -102,7 +103,7 @@ export const loadOrCreateMultipartUpload = async ({ Bucket: bucket, Key: finalKey, ContentType: contentType, - ContentDisposition: contentDisposition, + ContentDisposition: constructContentDisposition(contentDisposition), ContentEncoding: contentEncoding, Metadata: metadata, }, diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index bb9b5ec4519..262a046ac71 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -14,6 +14,7 @@ import { ItemWithKey, ItemWithPath } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; +import { constructContentDisposition } from '../../utils/constructContentDisposition'; /** * Get a function the returns a promise to call putObject API to S3. @@ -57,7 +58,7 @@ export const putObjectJob = Key: finalKey, Body: data, ContentType: contentType, - ContentDisposition: contentDisposition, + ContentDisposition: constructContentDisposition(contentDisposition), ContentEncoding: contentEncoding, Metadata: metadata, ContentMD5: isObjectLockEnabled diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index c042d167263..3378c38d2fd 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -19,6 +19,15 @@ interface CommonOptions { useAccelerateEndpoint?: boolean; } +/** + * Represents the content disposition of a file. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + */ +export interface ContentDisposition { + type: 'attachment' | 'inline'; + filename?: string; +} + /** @deprecated This may be removed in the next major version. */ type ReadOptions = | { @@ -119,6 +128,19 @@ export type GetUrlOptions = CommonOptions & { * @default 900 (15 minutes) */ expiresIn?: number; + /** + * The default content-disposition header value of the file when downloading it. + * If a string is provided, it will be used as-is. + * If an object is provided, it will be used to construct the header value + * based on the ContentDisposition type definition. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + */ + contentDisposition?: ContentDisposition | string; + /** + * The content-type header value of the file when downloading it. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type + */ + contentType?: string; }; /** @deprecated Use {@link GetUrlOptionsWithPath} instead. */ @@ -140,9 +162,12 @@ export type UploadDataOptions = CommonOptions & TransferOptions & { /** * The default content-disposition header value of the file when downloading it. + * If a string is provided, it will be used as-is. + * If an object is provided, it will be used to construct the header value + * based on the ContentDisposition type definition. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition */ - contentDisposition?: string; + contentDisposition?: ContentDisposition | string; /** * The default content-encoding header value of the file when downloading it. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding diff --git a/packages/storage/src/providers/s3/utils/client/getObject.ts b/packages/storage/src/providers/s3/utils/client/getObject.ts index 4af6a32a39c..2b4153541cd 100644 --- a/packages/storage/src/providers/s3/utils/client/getObject.ts +++ b/packages/storage/src/providers/s3/utils/client/getObject.ts @@ -38,7 +38,11 @@ const USER_AGENT_HEADER = 'x-amz-user-agent'; export type GetObjectInput = Pick< GetObjectCommandInput, - 'Bucket' | 'Key' | 'Range' + | 'Bucket' + | 'Key' + | 'Range' + | 'ResponseContentDisposition' + | 'ResponseContentType' >; export type GetObjectOutput = GetObjectCommandOutput; @@ -156,6 +160,15 @@ export const getPresignedGetObjectUrl = async ( config.userAgentValue, ); } + if (input.ResponseContentType) { + url.searchParams.append('response-content-type', input.ResponseContentType); + } + if (input.ResponseContentDisposition) { + url.searchParams.append( + 'response-content-disposition', + input.ResponseContentDisposition, + ); + } for (const [headerName, value] of Object.entries(headers).sort( ([key1], [key2]) => key1.localeCompare(key2), diff --git a/packages/storage/src/providers/s3/utils/constructContentDisposition.ts b/packages/storage/src/providers/s3/utils/constructContentDisposition.ts new file mode 100644 index 00000000000..95ff01e8839 --- /dev/null +++ b/packages/storage/src/providers/s3/utils/constructContentDisposition.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ContentDisposition } from '../types/options'; + +export const constructContentDisposition = ( + contentDisposition?: string | ContentDisposition, +): string | undefined => { + if (!contentDisposition) return undefined; + + if (typeof contentDisposition === 'string') return contentDisposition; + + const { type, filename } = contentDisposition; + + return filename !== undefined ? `${type}; filename="${filename}"` : type; +}; From d7522e4f3a1c73cfeb58d075ffd33afa8466299e Mon Sep 17 00:00:00 2001 From: Hui Zhao <10602282+HuiSF@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:18:23 -0700 Subject: [PATCH 3/4] chore(core): fix JsonObject type doesn't allow array of JsonObject as field (#13669) * chore(core): fix JsonObject type doesn't allow array of JsonObject as field * chore: fix an array of types * chore: allow array of JsonArray * chore: refine the test --- .../__tests__/singleton/Auth/type.test.ts | 58 +++++++++++++++++++ packages/core/src/singleton/Auth/types.ts | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 packages/core/__tests__/singleton/Auth/type.test.ts diff --git a/packages/core/__tests__/singleton/Auth/type.test.ts b/packages/core/__tests__/singleton/Auth/type.test.ts new file mode 100644 index 00000000000..0ec3ffc4033 --- /dev/null +++ b/packages/core/__tests__/singleton/Auth/type.test.ts @@ -0,0 +1,58 @@ +import { JWT } from '../../../src/singleton/Auth/types'; + +describe('type validity', () => { + describe('JWT type', () => { + it('can contain property that has a value as array of JsonObjects', () => { + type OtherProperty1 = ( + | { key: string } + | number + | string + | ( + | { key: string } + | number + | string + | ({ key: string } | number | string)[] + )[] + )[]; + // For testing purpose, use type alias here, as TS will complain while using + // an interface which triggers structural typing check + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + type OtherProperty2 = { + key: number; + array: ( + | { key: string } + | number + | string + | ({ key: string } | number | string)[] + )[]; + }; + const expectedOtherProperty1 = [ + { key: '123' }, + 1, + 'hi', + [1, 'hi', { key: '345' }, [2, 'hi', { key: '456' }]], + ]; + const expectedOtherProperty2 = { + key: 1, + array: [1, 'hi', { key: '123' }, [2, 'hi', { key: '456' }]], + }; + const value: JWT = { + payload: { + otherProperty1: expectedOtherProperty1, + otherProperty2: expectedOtherProperty2, + }, + toString: () => 'mock', + }; + + const extractedOtherProperty1 = value.payload + .otherProperty1 as OtherProperty1; + const a: OtherProperty1 = extractedOtherProperty1; + expect(a).toEqual(expectedOtherProperty1); + + const extractedOtherProperty2 = value.payload + .otherProperty2 as OtherProperty2; + const b: OtherProperty2 = extractedOtherProperty2; + expect(b).toEqual(expectedOtherProperty2); + }); + }); +}); diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index 239810e8771..fd7bc788472 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -20,7 +20,7 @@ interface JwtPayloadStandardFields { type JsonPrimitive = null | string | number | boolean; /** JSON array type */ -type JsonArray = JsonPrimitive[]; +type JsonArray = (JsonPrimitive | JsonObject | JsonArray)[]; /** JSON Object type */ interface JsonObject { From f8dbc95964e541818fb79203fd286f445feee46d Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Wed, 7 Aug 2024 14:50:15 -0700 Subject: [PATCH 4/4] Revert "feat(core): resolve webcrypto from node:crypto for Node18 (#13599)" This reverts commit 37d26277e4cbacdc8e34e8a6810b08ef7c4a8f49. --- .../load-verdaccio-with-amplify-js/action.yml | 26 +++++++------------ .github/actions/node-and-build/action.yml | 6 ++--- .github/integ-config/integ-all.yml | 9 +------ .github/workflows/callable-e2e-test.yml | 7 ----- .github/workflows/callable-e2e-tests.yml | 1 - package.json | 3 --- packages/aws-amplify/package.json | 6 ++--- .../utils/globalHelpers/globalHelpers.test.ts | 6 +++++ .../core/src/utils/globalHelpers/index.ts | 10 ------- 9 files changed, 22 insertions(+), 52 deletions(-) diff --git a/.github/actions/load-verdaccio-with-amplify-js/action.yml b/.github/actions/load-verdaccio-with-amplify-js/action.yml index b0f2f5552ac..c8cd349cb86 100644 --- a/.github/actions/load-verdaccio-with-amplify-js/action.yml +++ b/.github/actions/load-verdaccio-with-amplify-js/action.yml @@ -6,8 +6,7 @@ runs: steps: - name: Start verdaccio run: | - # This version supports Node.js v22 - npx verdaccio@5.31.1 & + npx verdaccio@5.25.0 & while ! nc -z localhost 4873; do echo "Verdaccio not running yet" sleep 1 @@ -19,30 +18,25 @@ runs: - name: Install and run npm-cli-login shell: bash env: - NPM_REGISTRY_HOST: localhost:4873 - NPM_REGISTRY: http://localhost:4873 + NPM_REGISTRY: http://localhost:4873/ NPM_USER: verdaccio NPM_PASS: verdaccio NPM_EMAIL: verdaccio@amplify.js run: | - # Make the HTTP request that npm addUser makes to avoid the "Exit handler never called" error - TOKEN=$(curl -s \ - -H "Accept: application/json" \ - -H "Content-Type:application/json" \ - -X PUT --data "{\"name\": \"$NPM_USER\", \"password\": \"$NPM_PASS\", \"email\": \"$NPM_EMAIL\"}" \ - $NPM_REGISTRY/-/user/org.couchdb.user:$NPM_USER 2>&1 | jq -r '.token') - - # Set the Verdaccio registry and set the token for logging in - yarn config set registry $NPM_REGISTRY - npm set registry $NPM_REGISTRY - npm set //"$NPM_REGISTRY_HOST"/:_authToken $TOKEN - - name: Configure git + npm i -g npm-cli-adduser + npm-cli-adduser + sleep 1 + - name: Configure registry and git shell: bash working-directory: ./amplify-js env: + NPM_REGISTRY: http://localhost:4873/ NPM_USER: verdaccio + NPM_PASS: verdaccio NPM_EMAIL: verdaccio@amplify.js run: | + yarn config set registry $NPM_REGISTRY + npm set registry $NPM_REGISTRY git config --global user.email $NPM_EMAIL git config --global user.name $NPM_USER git status diff --git a/.github/actions/node-and-build/action.yml b/.github/actions/node-and-build/action.yml index becd2d49165..0af092c4c84 100644 --- a/.github/actions/node-and-build/action.yml +++ b/.github/actions/node-and-build/action.yml @@ -4,15 +4,13 @@ inputs: is-prebuild: required: false default: false - node_version: - required: false runs: using: 'composite' steps: - - name: Setup Node.js + - name: Setup Node.js 18 uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: ${{ inputs.node_version || '18.x' }} + node-version: 18.20.2 env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index db7f9d64444..d29ae41ba42 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -829,7 +829,7 @@ tests: sample_name: [guest-access] spec: storage-client-server browser: *minimal_browser_list - + # INAPPMESSAGING - test_name: integ_in_app_messaging desc: 'React InApp Messaging' @@ -856,10 +856,3 @@ tests: spec: ssr-context-isolation yarn_script: ci:ssr-context-isolation browser: [chrome] - - test_name: integ_node_envs - desc: 'Node.js environment tests' - framework: node - category: integration - sample_name: auth-gql-storage - yarn_script: ci:node-env-test - node_versions: ['18.x', '20.x', '22.x'] diff --git a/.github/workflows/callable-e2e-test.yml b/.github/workflows/callable-e2e-test.yml index fd9f9cb697a..18697cf5dc5 100644 --- a/.github/workflows/callable-e2e-test.yml +++ b/.github/workflows/callable-e2e-test.yml @@ -37,9 +37,6 @@ on: yarn_script: required: false type: string - node_versions: - required: false - type: string env: AMPLIFY_DIR: /home/runner/work/amplify-js/amplify-js/amplify-js @@ -57,8 +54,6 @@ jobs: - ${{ fromJson(inputs.browser) }} sample_name: - ${{ fromJson(inputs.sample_name) }} - node_version: - - ${{ fromJson(inputs.node_versions) }} fail-fast: false timeout-minutes: ${{ inputs.timeout_minutes }} @@ -69,8 +64,6 @@ jobs: path: amplify-js - name: Setup node and build the repository uses: ./amplify-js/.github/actions/node-and-build - with: - node_version: ${{ matrix.node_version }} - name: Setup samples staging repository uses: ./amplify-js/.github/actions/setup-samples-staging with: diff --git a/.github/workflows/callable-e2e-tests.yml b/.github/workflows/callable-e2e-tests.yml index 15f6b576f8d..c27c51ce57f 100644 --- a/.github/workflows/callable-e2e-tests.yml +++ b/.github/workflows/callable-e2e-tests.yml @@ -44,7 +44,6 @@ jobs: timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }} retry_count: ${{ matrix.integ-config.retry_count || 3 }} yarn_script: ${{ matrix.integ-config.yarn_script || '' }} - node_versions: ${{ toJSON(matrix.integ-config.node_versions) || '[""]' }} # e2e-test-runner-headless: # name: E2E test runnner_headless diff --git a/package.json b/package.json index eef6052f3e7..06a17c0fdaf 100644 --- a/package.json +++ b/package.json @@ -130,9 +130,6 @@ "webpack-bundle-analyzer": "^4.7.0", "webpack-cli": "^5.0.0" }, - "engines": { - "node": ">=18" - }, "resolutions": { "@types/babel__traverse": "7.20.0", "path-scurry": "1.10.0", diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index bd2e711ad49..f10ee23cf22 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.30 kB" + "limit": "28.27 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,13 +449,13 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.10 kB" + "limit": "30.06 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "21.50 kB" + "limit": "21.47 kB" }, { "name": "[Storage] copy (S3)", diff --git a/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts b/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts index bb7d2e39a9b..4d9dbd8bc17 100644 --- a/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts +++ b/packages/core/__tests__/utils/globalHelpers/globalHelpers.test.ts @@ -44,6 +44,12 @@ describe('getGlobal', () => { expect(getCrypto()).toEqual(mockCrypto); }); + + it('should throw error if crypto is unavailable globally', () => { + mockWindow.mockImplementation(() => undefined); + + expect(() => getCrypto()).toThrow(AmplifyError); + }); }); describe('getBtoa()', () => { diff --git a/packages/core/src/utils/globalHelpers/index.ts b/packages/core/src/utils/globalHelpers/index.ts index dc35f897bf6..622f4d3c3ef 100644 --- a/packages/core/src/utils/globalHelpers/index.ts +++ b/packages/core/src/utils/globalHelpers/index.ts @@ -13,16 +13,6 @@ export const getCrypto = () => { return crypto; } - try { - const crypto = require('node:crypto').webcrypto; - - if (typeof crypto === 'object') { - return crypto; - } - } catch (_) { - // no-op - } - throw new AmplifyError({ name: 'MissingPolyfill', message: 'Cannot resolve the `crypto` function from the environment.',