From 3ef11d18317d1f4a8596e5ac9009a7b4447842ca Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 17 Sep 2026 21:49:26 +0530 Subject: [PATCH 1/5] SK-3149-fix-file-preserve-name-in-multi-file-input-element-in-js-sdk --- src/core/internal/frame-element-init.ts | 13 ++-- src/core/internal/iframe-form/index.ts | 5 +- src/libs/element-options.ts | 3 +- src/utils/logs.ts | 1 - .../frame-element-init.additional.test.js | 65 +++++++++++++++++-- .../core/internal/frame-element-init.test.js | 2 +- .../internal/iframe-form/iframe-form.test.js | 39 +++++++++++ tests/libs/element-options.test.js | 10 +++ 8 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index d4c3543f..00ded05c 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -515,7 +515,8 @@ export default class FrameElementInit { } if (state.value === undefined || state.value === null || state.value === '') { - rootReject({ error: 'No files selected' }); + const noFileError = new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_SELECTED, [], true); + rootReject({ errorResponse: [{ error: noFileError.error }] }); return; } @@ -642,9 +643,13 @@ export default class FrameElementInit { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); } - const isValidFileName = vaildateFileName(file.name); - if (!isValidFileName) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + // Only validate the original file name when it will actually be sent to the vault. + // With preserveFileName: false the name is replaced with a generated UUID before upload. + if (fileElement.preserveFileName) { + const isValidFileName = vaildateFileName(file.name); + if (!isValidFileName) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + } } }); return true; diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 889cf505..02084fdc 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -575,7 +575,10 @@ export default class IFrameFormElement extends EventEmitter { oversizedFileNames.push(files[i].name); } } - if (this.preserveFileName) vaildateFileNames = vaildateFileName(files[i].name); + // Once any file name fails, keep the flag false; do not let a later valid name reset it. + if (this.preserveFileName && vaildateFileNames) { + vaildateFileNames = vaildateFileName(files[i].name); + } } if (oversizedFileNames.length > 0) { if (files.length === 1) { diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index be517865..ed55442e 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -380,7 +380,8 @@ export const formatOptions = ( break; } - case ELEMENTS.FILE_INPUT.name || ELEMENTS.MULTI_FILE_INPUT.name: { + case ELEMENTS.FILE_INPUT.name: + case ELEMENTS.MULTI_FILE_INPUT.name: { if (!Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName')) { formattedOptions = { ...formattedOptions, preserveFileName: true }; } diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 57583319..0967afc3 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -102,7 +102,6 @@ const logs = { NO_ELEMENTS_IN_COLLECT: 'Validation error. No elements found in collect container', NO_ELEMENTS_IN_COMPOSABLE: 'Validation error. No elements found in composable container', NO_ELEMENTS_IN_REVEAL: 'Validation error. No elements found in reveal container', - INVALID_FILE_NAMES: 'Invalid File Name. Only alphanumeric characters and !-_.*() are allowed.', INVALID_FILE_NAME: 'Invalid File Name. Only alphanumeric characters and !-_.*() are allowed.', CLIENT_CONNECTION: 'client connection not established. client info has not reached iframes', INVALID_BEARER_TOKEN: 'Token generated from \'getBearerToken\' callback function is invalid. Make sure the implementation of \'getBearerToken\' is correct.', diff --git a/tests/core/internal/frame-element-init.additional.test.js b/tests/core/internal/frame-element-init.additional.test.js index a2b1f89a..0da5e82d 100644 --- a/tests/core/internal/frame-element-init.additional.test.js +++ b/tests/core/internal/frame-element-init.additional.test.js @@ -330,22 +330,24 @@ describe('FrameElementInit extended unit tests', () => { expect(err.errorResponse[0].error).toMatchObject({ code: 400 }); }); - test('multipleUploadFiles rejects with { error: "No files selected" } when state.value is empty', async () => { + const noFileSelectedError = { code: 400, description: 'No File Selected' }; + + test('multipleUploadFiles rejects with NO_FILE_SELECTED in errorResponse when state.value is empty', async () => { const instance = new FrameElementInit(); const fileElement = makeFileElement({ multiple: true, files: [makeFile('a.txt')] }); fileElement.state.value = ''; const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' }; await expect(instance['multipleUploadFiles'](fileElement, config, undefined)) - .rejects.toEqual({ error: 'No files selected' }); + .rejects.toEqual({ errorResponse: [{ error: noFileSelectedError }] }); }); - test('multipleUploadFiles rejects with { error: "No files selected" } when state.value is null', async () => { + test('multipleUploadFiles rejects with NO_FILE_SELECTED in errorResponse when state.value is null', async () => { const instance = new FrameElementInit(); const fileElement = makeFileElement({ multiple: true, files: [makeFile('a.txt')] }); fileElement.state.value = null; const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' }; await expect(instance['multipleUploadFiles'](fileElement, config, undefined)) - .rejects.toEqual({ error: 'No files selected' }); + .rejects.toEqual({ errorResponse: [{ error: noFileSelectedError }] }); }); test('multipleUploadFiles errorResponse contains error when SkyflowError has .errors[] (plural)', async () => { @@ -510,6 +512,46 @@ describe('FrameElementInit extended unit tests', () => { expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).toThrow(SkyflowError); }); + test('validateFiles skips filename validation when preserveFileName is false', () => { + const instance = new FrameElementInit(); + const files = [makeFile('my file.pdf')]; + const fileElement = makeFileElement({ multiple: true, files, preserveFileName: false }); + helpers.fileValidation = jest.fn(() => true); + helpers.vaildateFileName = jest.fn(() => false); // would reject if consulted + expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).not.toThrow(); + expect(helpers.vaildateFileName).not.toHaveBeenCalled(); + }); + + test('validateFiles still throws INVALID_FILE_NAME when preserveFileName is true', () => { + const instance = new FrameElementInit(); + const files = [makeFile('my file.pdf')]; + const fileElement = makeFileElement({ multiple: true, files, preserveFileName: true }); + helpers.fileValidation = jest.fn(() => true); + helpers.vaildateFileName = jest.fn(() => false); + expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).toThrow(SkyflowError); + expect(helpers.vaildateFileName).toHaveBeenCalledTimes(1); + }); + + test('multipleUploadFiles uploads a file with an invalid original name when preserveFileName is false', async () => { + const instance = new FrameElementInit(); + const files = [makeFile('my file.pdf')]; + const fileElement = makeFileElement({ multiple: true, files, preserveFileName: false }); + instance.iframeFormList = [fileElement]; + helpers.fileValidation = jest.fn(() => true); + helpers.vaildateFileName = jest.fn(() => false); // original name is invalid + const originalGenerateUploadFileName = helpers.generateUploadFileName; + helpers.generateUploadFileName = jest.fn(() => 'generated-uuid.pdf'); + mockClientRequest.mockResolvedValue({ skyflow_id: 'abc' }); + const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' }; + await expect(instance['multipleUploadFiles'](fileElement, config, undefined)) + .resolves.toEqual({ fileUploadResponse: [{ skyflow_id: 'abc' }] }); + expect(mockClientRequest).toHaveBeenCalledTimes(1); + const uploaded = mockClientRequest.mock.calls[0][0].body.get('file'); + expect(uploaded.name).toBe('generated-uuid.pdf'); + expect(helpers.vaildateFileName).not.toHaveBeenCalled(); + helpers.generateUploadFileName = originalGenerateUploadFileName; + }); + test('parallelUploadFiles resolves with aggregated responses when all succeed', async () => { const instance = new FrameElementInit(); const fileElementA = makeFileElement({ multiple: false, files: [makeFile('a.txt')] }); @@ -588,6 +630,21 @@ describe('FrameElementInit extended unit tests', () => { genSpy.mockRestore(); }); + test('uploadFiles (single FILE_INPUT) skips filename validation and uploads when preserveFileName is false', async () => { + const instance = new FrameElementInit(); + const file = makeFile('my file.pdf'); + const element = makeSingleFileElement({ file, preserveFileName: false }); + helpers.fileValidation = jest.fn(() => true); + helpers.vaildateFileName = jest.fn(() => false); // original name is invalid + const genSpy = jest.spyOn(helpers, 'generateUploadFileName').mockImplementation(() => 'gen_name.pdf'); + mockClientRequest.mockResolvedValue({ upload: 'ok' }); + const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'tokenXYZ' }; + await expect(instance.uploadFiles(element, config)).resolves.toEqual({ upload: 'ok' }); + expect(helpers.vaildateFileName).not.toHaveBeenCalled(); + expect(mockClientRequest.mock.calls[0][0].body.get('file').name).toBe('gen_name.pdf'); + genSpy.mockRestore(); + }); + test('uploadFiles rejects on invalid file name when preserveFileName true', async () => { const instance = new FrameElementInit(); const file = makeFile('bad.txt', 5); diff --git a/tests/core/internal/frame-element-init.test.js b/tests/core/internal/frame-element-init.test.js index f9a21d74..774585d9 100644 --- a/tests/core/internal/frame-element-init.test.js +++ b/tests/core/internal/frame-element-init.test.js @@ -697,7 +697,7 @@ describe('FrameElementInit Additional Test Cases', () => { expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:element:MULTI_FILE_INPUT:123`, - data: {"error": "No files selected"}, // Response data (success or error) + data: { errorResponse: [{ error: { code: 400, description: 'No File Selected' } }] }, // Response data (success or error) }), 'http://localhost.com' ); diff --git a/tests/core/internal/iframe-form/iframe-form.test.js b/tests/core/internal/iframe-form/iframe-form.test.js index ed7f65df..ceda2c52 100644 --- a/tests/core/internal/iframe-form/iframe-form.test.js +++ b/tests/core/internal/iframe-form/iframe-form.test.js @@ -1169,6 +1169,45 @@ describe('MULTI_FILE_INPUT validator - specific UI error messages', () => { return arr; }; + test('invalid file name shows the specific INVALID_FILE_NAME message in a COLLECT container for both file element types', () => { + const expected = logs.errorLogs.INVALID_FILE_NAME; + const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' }); + const cases = [ + [file_element, badName], + [multi_file_element, makeFileList(badName)], + ]; + cases.forEach(([name, value]) => { + const element = new IFrameFormElement(name, 'Passport', { containerType: ContainerType.COLLECT }, context); + element.preserveFileName = true; + element.state.value = value; + expect(element.validator(value)).toBe(false); + expect(element.errorText).toBe(expected); + }); + }); + + test('MULTI_FILE_INPUT with preserveFileName=true fails when an earlier file has an invalid name even if the last one is valid', () => { + const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context); + element.preserveFileName = true; + const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' }); // space is not allowed + const goodName = new File(['b'], 'ok.pdf', { type: 'application/pdf' }); + const fileList = makeFileList(badName, goodName); // invalid first, valid last + element.state.value = fileList; + const result = element.validator(fileList); + expect(result).toBe(false); + expect(element.errorText).toBe(parameterizedString(logs.errorLogs.INVALID_FILE_NAME)); + }); + + test('MULTI_FILE_INPUT with preserveFileName=false ignores invalid file names', () => { + const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context); + element.preserveFileName = false; + const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' }); + const goodName = new File(['b'], 'ok.pdf', { type: 'application/pdf' }); + const fileList = makeFileList(badName, goodName); + element.state.value = fileList; + const result = element.validator(fileList); + expect(result).toBe(true); + }); + test('invalid file type in MULTI_FILE_INPUT shows generic error not size-specific message', () => { const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context); element.maxFileSize = 10_000_000; // 10 MB — file below this so size is not the issue diff --git a/tests/libs/element-options.test.js b/tests/libs/element-options.test.js index f1673fd2..4fb1f69a 100644 --- a/tests/libs/element-options.test.js +++ b/tests/libs/element-options.test.js @@ -103,6 +103,16 @@ describe('test formatOptions function with format and translation', () => { expect(formattedOptions).toEqual({required:true,preserveFileName:true}); }); + test('should return preserveFileName true for MULTI_FILE_INPUT when not provided in options',()=>{ + const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT,{required:true},LogLevel.ERROR); + expect(formattedOptions).toEqual({required:true,preserveFileName:true}); + }); + + test('should keep preserveFileName false for MULTI_FILE_INPUT when provided as false',()=>{ + const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR); + expect(formattedOptions).toEqual({required:true,preserveFileName:false}); + }); + test('should return preserveFileName false when not provied as false in options',()=>{ const formattedOptions = formatOptions(ElementType.FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR); expect(formattedOptions).toEqual({required:true,preserveFileName:false}); From bbbb7b34f6a442f5a101f63ceefb2ea1f996d096 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 17 Sep 2026 21:52:37 +0530 Subject: [PATCH 2/5] SK-3149-fix-file-preserve-name-in-multi-file-input-element-in-js-sdk --- tests/core/internal/iframe-form/iframe-form.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/internal/iframe-form/iframe-form.test.js b/tests/core/internal/iframe-form/iframe-form.test.js index ceda2c52..4a607b32 100644 --- a/tests/core/internal/iframe-form/iframe-form.test.js +++ b/tests/core/internal/iframe-form/iframe-form.test.js @@ -1184,7 +1184,6 @@ describe('MULTI_FILE_INPUT validator - specific UI error messages', () => { expect(element.errorText).toBe(expected); }); }); - test('MULTI_FILE_INPUT with preserveFileName=true fails when an earlier file has an invalid name even if the last one is valid', () => { const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context); element.preserveFileName = true; From b377cd9d86cd92576c580fb86bd3c43da75c363c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 17 Sep 2026 22:00:20 +0530 Subject: [PATCH 3/5] SK-3149-fix-file-preserve-name-in-multi-file-input-element-in-js-sdk --- .github/workflows/common-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/common-release.yml b/.github/workflows/common-release.yml index a31d7fe9..2fcd216a 100644 --- a/.github/workflows/common-release.yml +++ b/.github/workflows/common-release.yml @@ -50,8 +50,13 @@ jobs: - run: npm install --ignore-scripts - name: Get Previous Tag + # Only consider this SDK's numeric semver tags (e.g. 2.7.9, 2.9.0-beta.1). + # Other packages released from this repo use prefixed tags (e.g. flowvault-v1.0.0), + # which are not valid npm versions and must not be picked up here. run: | - echo "TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV + SEMVER_GLOB='[0-9]*.[0-9]*.[0-9]*' + LAST_SEMVER_COMMIT=$(git rev-list --tags="$SEMVER_GLOB" --max-count=1) + echo "TAG=$(git describe --abbrev=0 --tags --match "$SEMVER_GLOB" "$LAST_SEMVER_COMMIT")" >> $GITHUB_ENV - name: Set RELEASE_VERSION run: | From ad5a47a2faf3ce458af51e9dae3294678b1bb34d Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 17 Sep 2026 16:30:56 +0000 Subject: [PATCH 4/5] [AUTOMATED] Release - 2.9.0-beta.1-dev.b377cd9d --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1eb59b3c..c228676c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.7.8-dev.819273e", + "version": "2.9.0-beta.1-dev.b377cd9d", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From c4024482c6967cae76f856dd3babd710b40f5b3c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 18 Sep 2026 07:34:31 +0000 Subject: [PATCH 5/5] [AUTOMATED] Release - 2.7.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c228676c..0143ec92 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.9.0-beta.1-dev.b377cd9d", + "version": "2.7.10", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js",