From 65c5609ab51881c223dcbf5ee567dbc83e6dd4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 19 Jun 2024 13:33:57 +0200 Subject: [PATCH] feat(core): Use WebCrypto to generate all random numbers and strings (#9786) --- cypress/e2e/6-code-node.cy.ts | 9 +-- cypress/utils/executions.ts | 3 +- packages/cli/jest.config.js | 1 + packages/cli/src/Ldap/helpers.ts | 13 ++--- packages/cli/src/commands/start.ts | 9 +-- .../cli/src/databases/utils/generators.ts | 3 +- .../execution-recovery.service.test.ts | 10 ++-- packages/cli/src/sso/saml/samlHelpers.ts | 44 +++++---------- packages/cli/src/utils.ts | 3 +- .../integration/PermissionChecker.test.ts | 10 +--- .../credentials/credentials.api.test.ts | 2 +- packages/cli/test/integration/me.api.test.ts | 9 +-- .../cli/test/integration/mfa/mfa.api.test.ts | 20 ++----- .../cli/test/integration/owner.api.test.ts | 5 +- .../integration/passwordReset.api.test.ts | 4 +- .../integration/publicApi/credentials.test.ts | 3 +- .../cli/test/integration/shared/random.ts | 36 +++--------- .../cli/test/integration/shared/testDb.ts | 5 +- packages/cli/test/unit/WebhookHelpers.test.ts | 4 +- packages/cli/test/unit/shared/mockObjects.ts | 6 +- .../components/CodeNodeEditor/AskAI/AskAI.vue | 3 +- .../global/GlobalExecutionsList.test.ts | 4 +- .../WorkflowExecutionsPreview.test.ts | 4 +- packages/editor-ui/src/stores/root.store.ts | 4 +- .../nodes/HelpScout/HelpScoutTrigger.node.ts | 3 +- .../nodes/ItemLists/V1/ItemListsV1.node.ts | 4 +- .../nodes/ItemLists/V2/ItemListsV2.node.ts | 4 +- .../nodes/ItemLists/V3/helpers/utils.ts | 4 +- packages/nodes-base/nodes/MQTT/Mqtt.node.ts | 6 +- .../nodes-base/nodes/MQTT/MqttTrigger.node.ts | 5 +- .../nodes-base/nodes/Odoo/GenericFunctions.ts | 34 ++++++------ packages/nodes-base/nodes/Odoo/Odoo.node.ts | 26 ++++----- .../nodes-base/nodes/Transform/Sort/utils.ts | 5 +- .../nodes/Typeform/TypeformTrigger.node.ts | 4 +- packages/workflow/jest.config.js | 5 +- packages/workflow/src/Constants.ts | 5 ++ packages/workflow/src/Cron.ts | 4 +- .../src/Extensions/ArrayExtensions.ts | 8 ++- .../src/errors/abstract/node.error.ts | 2 +- .../src/errors/node-operation.error.ts | 2 +- .../src/errors/workflow-operation.error.ts | 2 +- packages/workflow/src/index.ts | 2 + packages/workflow/src/utils.ts | 38 ++++++++++++- packages/workflow/test/NodeHelpers.test.ts | 2 +- .../workflow/test/TelemetryHelpers.test.ts | 24 +++----- .../errors/workflow-activation.error.test.ts | 2 +- packages/workflow/test/setup.ts | 7 +++ packages/workflow/test/utils.test.ts | 55 ++++++++++++++++++- packages/workflow/tsconfig.json | 1 + 49 files changed, 254 insertions(+), 214 deletions(-) create mode 100644 packages/workflow/test/setup.ts diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index 4c6379cfd6..74e775453b 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -1,3 +1,4 @@ +import { nanoid } from 'nanoid'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { NDV } from '../pages/ndv'; import { successToast } from '../pages/notifications'; @@ -85,7 +86,7 @@ describe('Code node', () => { cy.getByTestId('ask-ai-cta-tooltip-no-prompt').should('exist'); cy.getByTestId('ask-ai-prompt-input') // Type random 14 character string - .type([...Array(14)].map(() => ((Math.random() * 36) | 0).toString(36)).join('')); + .type(nanoid(14)); cy.getByTestId('ask-ai-cta').realHover(); cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist'); @@ -93,14 +94,14 @@ describe('Code node', () => { cy.getByTestId('ask-ai-prompt-input') .clear() // Type random 15 character string - .type([...Array(15)].map(() => ((Math.random() * 36) | 0).toString(36)).join('')); + .type(nanoid(15)); cy.getByTestId('ask-ai-cta').should('be.enabled'); cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600'); }); it('should send correct schema and replace code', () => { - const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''); + const prompt = nanoid(20); cy.get('#tab-ask-ai').click(); ndv.actions.executePrevious(); @@ -130,7 +131,7 @@ describe('Code node', () => { }); it('should show error based on status code', () => { - const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''); + const prompt = nanoid(20); cy.get('#tab-ask-ai').click(); ndv.actions.executePrevious(); diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 4a40eff8fe..e42e2152d6 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -1,3 +1,4 @@ +import { nanoid } from 'nanoid'; import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow'; import { clickExecuteWorkflowButton } from '../composables/workflow'; @@ -85,7 +86,7 @@ export function runMockWorkflowExecution({ runData: Array>; workflowExecutionData?: ReturnType; }) { - const executionId = Math.random().toString(36).substring(4); + const executionId = nanoid(8); cy.intercept('POST', '/rest/workflows/**/run', { statusCode: 201, diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 9c1d248a00..72a653e07d 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -7,6 +7,7 @@ module.exports = { globalSetup: '/test/setup.ts', globalTeardown: '/test/teardown.ts', setupFilesAfterEnv: [ + 'n8n-workflow/test/setup.ts', '/test/setup-test-folder.ts', '/test/setup-mocks.ts', '/test/extend-expect.ts', diff --git a/packages/cli/src/Ldap/helpers.ts b/packages/cli/src/Ldap/helpers.ts index 567031d01d..6040544bad 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -3,6 +3,8 @@ import type { Entry as LdapUser } from 'ldapts'; import { Filter } from 'ldapts/filters/Filter'; import { Container } from 'typedi'; import { validate } from 'jsonschema'; +import { randomString } from 'n8n-workflow'; + import * as Db from '@/Db'; import config from '@/config'; import { User } from '@db/entities/User'; @@ -38,13 +40,6 @@ export const getLdapLoginLabel = (): string => config.getEnv(LDAP_LOGIN_LABEL); */ export const isLdapLoginEnabled = (): boolean => config.getEnv(LDAP_LOGIN_ENABLED); -/** - * Return a random password to be assigned to the LDAP users - */ -export const randomPassword = (): string => { - return Math.random().toString(36).slice(-8); -}; - /** * Validate the structure of the LDAP configuration schema */ @@ -161,7 +156,7 @@ export const mapLdapUserToDbUser = ( Object.assign(user, data); if (toCreate) { user.role = 'global:member'; - user.password = randomPassword(); + user.password = randomString(8); user.disabled = false; } else { user.disabled = true; @@ -278,7 +273,7 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => { export const createLdapUserOnLocalDb = async (data: Partial, ldapId: string) => { const { user } = await Container.get(UserRepository).createUserWithProject({ - password: randomPassword(), + password: randomString(8), role: 'global:member', ...data, }); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 8b58549c0d..bc4c4bfe59 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -8,7 +8,7 @@ import { createReadStream, createWriteStream, existsSync } from 'fs'; import { pipeline } from 'stream/promises'; import replaceStream from 'replacestream'; import glob from 'fast-glob'; -import { jsonParse } from 'n8n-workflow'; +import { jsonParse, randomString } from 'n8n-workflow'; import config from '@/config'; import { ActiveExecutions } from '@/ActiveExecutions'; @@ -265,12 +265,7 @@ export class Start extends BaseCommand { if (tunnelSubdomain === '') { // When no tunnel subdomain did exist yet create a new random one - const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; - tunnelSubdomain = Array.from({ length: 24 }) - .map(() => - availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)), - ) - .join(''); + tunnelSubdomain = randomString(24).toLowerCase(); this.instanceSettings.update({ tunnelSubdomain }); } diff --git a/packages/cli/src/databases/utils/generators.ts b/packages/cli/src/databases/utils/generators.ts index 2074805240..7a228d96ab 100644 --- a/packages/cli/src/databases/utils/generators.ts +++ b/packages/cli/src/databases/utils/generators.ts @@ -1,7 +1,8 @@ import { customAlphabet } from 'nanoid'; +import { ALPHABET } from 'n8n-workflow'; import type { N8nInstanceType } from '@/Interfaces'; -const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 16); +const nanoid = customAlphabet(ALPHABET, 16); export function generateNanoId() { return nanoid(); diff --git a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts index 6c18d12788..b20f74edbe 100644 --- a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts @@ -1,18 +1,18 @@ import Container from 'typedi'; import { stringify } from 'flatted'; +import { NodeConnectionType, randomInt } from 'n8n-workflow'; import { mockInstance } from '@test/mocking'; -import { randomInteger } from '@test-integration/random'; import { createWorkflow } from '@test-integration/db/workflows'; import { createExecution } from '@test-integration/db/executions'; import * as testDb from '@test-integration/testDb'; -import { NodeConnectionType } from 'n8n-workflow'; import { mock } from 'jest-mock-extended'; import { OrchestrationService } from '@/services/orchestration.service'; import config from '@/config'; import { ExecutionRecoveryService } from '@/executions/execution-recovery.service'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; +import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { InternalHooks } from '@/InternalHooks'; import { Push } from '@/push'; import { ARTIFICIAL_TASK_DATA } from '@/constants'; @@ -20,9 +20,7 @@ import { NodeCrashedError } from '@/errors/node-crashed.error'; import { WorkflowCrashedError } from '@/errors/workflow-crashed.error'; import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode'; import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow'; - import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses'; -import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import type { Logger } from '@/Logger'; /** @@ -301,7 +299,7 @@ describe('ExecutionRecoveryService', () => { /** * Arrange */ - const inexistentExecutionId = randomInteger(100).toString(); + const inexistentExecutionId = randomInt(100).toString(); const noMessages: EventMessage[] = []; /** @@ -373,7 +371,7 @@ describe('ExecutionRecoveryService', () => { /** * Arrange */ - const inexistentExecutionId = randomInteger(100).toString(); + const inexistentExecutionId = randomInt(100).toString(); const messages = setupMessages(inexistentExecutionId, 'Some workflow'); /** diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index 0334e01b4c..d7f53900e5 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -1,12 +1,19 @@ import { Container } from 'typedi'; +import type { FlowResult } from 'samlify/types/src/flow'; +import { randomString } from 'n8n-workflow'; + import config from '@/config'; import { AuthIdentity } from '@db/entities/AuthIdentity'; import type { User } from '@db/entities/User'; +import { UserRepository } from '@db/repositories/user.repository'; +import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { AuthError } from '@/errors/response-errors/auth.error'; import { License } from '@/License'; import { PasswordUtility } from '@/services/password.utility'; + import type { SamlPreferences } from './types/samlPreferences'; import type { SamlUserAttributes } from './types/samlUserAttributes'; -import type { FlowResult } from 'samlify/types/src/flow'; import type { SamlAttributeMapping } from './types/samlAttributeMapping'; import { SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; import { @@ -17,10 +24,6 @@ import { } from '../ssoHelpers'; import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee'; import type { SamlConfiguration } from './types/requests'; -import { UserRepository } from '@db/repositories/user.repository'; -import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; -import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { AuthError } from '@/errors/response-errors/auth.error'; /** * Check whether the SAML feature is licensed and enabled in the instance @@ -73,39 +76,18 @@ export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferen ); }; -export function generatePassword(): string { - const length = 18; - const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - const charsetNoNumbers = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const randomNumber = Math.floor(Math.random() * 10); - const randomUpper = charset.charAt(Math.floor(Math.random() * charsetNoNumbers.length)); - const randomNumberPosition = Math.floor(Math.random() * length); - const randomUpperPosition = Math.floor(Math.random() * length); - let password = ''; - for (let i = 0, n = charset.length; i < length; ++i) { - password += charset.charAt(Math.floor(Math.random() * n)); - } - password = - password.substring(0, randomNumberPosition) + - randomNumber.toString() + - password.substring(randomNumberPosition); - password = - password.substring(0, randomUpperPosition) + - randomUpper + - password.substring(randomUpperPosition); - return password; -} - export async function createUserFromSamlAttributes(attributes: SamlUserAttributes): Promise { - return await Container.get(UserRepository).manager.transaction(async (trx) => { - const { user } = await Container.get(UserRepository).createUserWithProject( + const randomPassword = randomString(18); + const userRepository = Container.get(UserRepository); + return await userRepository.manager.transaction(async (trx) => { + const { user } = await userRepository.createUserWithProject( { email: attributes.email.toLowerCase(), firstName: attributes.firstName, lastName: attributes.lastName, role: 'global:member', // generates a password that is not used or known to the user - password: await Container.get(PasswordUtility).hash(generatePassword()), + password: await Container.get(PasswordUtility).hash(randomPassword), }, trx, ); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 059dfec86b..407aa49559 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { CliWorkflowOperationError, SubworkflowOperationError } from 'n8n-workflow'; import type { INode } from 'n8n-workflow'; -import { STARTING_NODES } from './constants'; +import { STARTING_NODES } from '@/constants'; /** * Returns if the given id is a valid workflow id diff --git a/packages/cli/test/integration/PermissionChecker.test.ts b/packages/cli/test/integration/PermissionChecker.test.ts index 6c176cb5dd..5cdf74fdb8 100644 --- a/packages/cli/test/integration/PermissionChecker.test.ts +++ b/packages/cli/test/integration/PermissionChecker.test.ts @@ -1,7 +1,7 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; import type { INode, WorkflowSettings } from 'n8n-workflow'; -import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; +import { SubworkflowOperationError, Workflow, randomInt } from 'n8n-workflow'; import config from '@/config'; import type { User } from '@db/entities/User'; @@ -15,11 +15,7 @@ import { OwnershipService } from '@/services/ownership.service'; import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { mockInstance } from '../shared/mocking'; -import { - randomCredentialPayload as randomCred, - randomName, - randomPositiveDigit, -} from '../integration/shared/random'; +import { randomCredentialPayload as randomCred, randomName } from '../integration/shared/random'; import { LicenseMocker } from '../integration/shared/license'; import * as testDb from '../integration/shared/testDb'; import type { SaveCredentialFunction } from '../integration/shared/types'; @@ -77,7 +73,7 @@ const ownershipService = mockInstance(OwnershipService); const createWorkflow = async (nodes: INode[], workflowOwner?: User): Promise => { const workflowDetails = { - id: randomPositiveDigit().toString(), + id: randomInt(1, 10).toString(), name: 'test', active: false, connections: {}, diff --git a/packages/cli/test/integration/credentials/credentials.api.test.ts b/packages/cli/test/integration/credentials/credentials.api.test.ts index d11eac4cd7..4de1817027 100644 --- a/packages/cli/test/integration/credentials/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.test.ts @@ -1,6 +1,7 @@ import { Container } from 'typedi'; import type { Scope } from '@sentry/node'; import { Credentials } from 'n8n-core'; +import { randomString } from 'n8n-workflow'; import type { ListQuery } from '@/requests'; import type { User } from '@db/entities/User'; @@ -16,7 +17,6 @@ import { randomCredentialPayload as payload, randomCredentialPayload, randomName, - randomString, } from '../shared/random'; import { saveCredential, diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 218c305c15..d23cd53132 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -1,6 +1,7 @@ import { Container } from 'typedi'; import { IsNull } from '@n8n/typeorm'; import validator from 'validator'; +import { randomString } from 'n8n-workflow'; import config from '@/config'; import type { User } from '@db/entities/User'; @@ -8,13 +9,7 @@ import { UserRepository } from '@db/repositories/user.repository'; import { ProjectRepository } from '@db/repositories/project.repository'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; -import { - randomApiKey, - randomEmail, - randomName, - randomString, - randomValidPassword, -} from './shared/random'; +import { randomApiKey, randomEmail, randomName, randomValidPassword } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; import { addApiKey, createOwner, createUser, createUserShell } from './shared/db/users'; diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index de4f80abde..7755ad4e3c 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -1,15 +1,15 @@ import Container from 'typedi'; +import { randomInt, randomString } from 'n8n-workflow'; import { AuthService } from '@/auth/auth.service'; import config from '@/config'; import type { User } from '@db/entities/User'; import { AuthUserRepository } from '@db/repositories/authUser.repository'; -import { randomPassword } from '@/Ldap/helpers'; import { TOTPService } from '@/Mfa/totp.service'; import * as testDb from '../shared/testDb'; import * as utils from '../shared/utils'; -import { randomDigit, randomString, randomValidPassword, uniqueId } from '../shared/random'; +import { randomValidPassword, uniqueId } from '../shared/random'; import { createUser, createUserWithMfaEnabled } from '../shared/db/users'; jest.mock('@/telemetry'); @@ -150,18 +150,6 @@ describe('Disable MFA setup', () => { }); describe('Change password with MFA enabled', () => { - test('PATCH /me/password should fail due to missing MFA token', async () => { - const { user, rawPassword } = await createUserWithMfaEnabled(); - - const newPassword = randomPassword(); - - await testServer - .authAgentFor(user) - .patch('/me/password') - .send({ currentPassword: rawPassword, newPassword }) - .expect(400); - }); - test('POST /change-password should fail due to missing MFA token', async () => { await createUserWithMfaEnabled(); @@ -185,7 +173,7 @@ describe('Change password with MFA enabled', () => { .send({ password: newPassword, token: resetPasswordToken, - mfaToken: randomDigit(), + mfaToken: randomInt(10), }) .expect(404); }); @@ -226,7 +214,7 @@ describe('Change password with MFA enabled', () => { describe('Login', () => { test('POST /login with email/password should succeed when mfa is disabled', async () => { - const password = randomPassword(); + const password = randomString(8); const user = await createUser({ password }); diff --git a/packages/cli/test/integration/owner.api.test.ts b/packages/cli/test/integration/owner.api.test.ts index ff5dffe854..d917eeab4f 100644 --- a/packages/cli/test/integration/owner.api.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -1,7 +1,10 @@ +import { Container } from 'typedi'; import validator from 'validator'; import config from '@/config'; import type { User } from '@db/entities/User'; +import { UserRepository } from '@db/repositories/user.repository'; + import { randomEmail, randomInvalidPassword, @@ -11,8 +14,6 @@ import { import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; import { createUserShell } from './shared/db/users'; -import { UserRepository } from '@db/repositories/user.repository'; -import Container from 'typedi'; const testServer = utils.setupTestServer({ endpointGroups: ['owner'] }); diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 6aca848e84..3b46fd476f 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -2,6 +2,7 @@ import { v4 as uuid } from 'uuid'; import { compare } from 'bcryptjs'; import { Container } from 'typedi'; import { mock } from 'jest-mock-extended'; +import { randomString } from 'n8n-workflow'; import { AuthService } from '@/auth/auth.service'; import { License } from '@/License'; @@ -12,6 +13,7 @@ import { ExternalHooks } from '@/ExternalHooks'; import { JwtService } from '@/services/jwt.service'; import { UserManagementMailer } from '@/UserManagement/email'; import { UserRepository } from '@db/repositories/user.repository'; +import { PasswordUtility } from '@/services/password.utility'; import { mockInstance } from '../shared/mocking'; import { getAuthToken, setupTestServer } from './shared/utils/'; @@ -19,12 +21,10 @@ import { randomEmail, randomInvalidPassword, randomName, - randomString, randomValidPassword, } from './shared/random'; import * as testDb from './shared/testDb'; import { createUser } from './shared/db/users'; -import { PasswordUtility } from '@/services/password.utility'; config.set('userManagement.jwtSecret', randomString(5, 10)); diff --git a/packages/cli/test/integration/publicApi/credentials.test.ts b/packages/cli/test/integration/publicApi/credentials.test.ts index 71406a0fd4..dfa6c44dd4 100644 --- a/packages/cli/test/integration/publicApi/credentials.test.ts +++ b/packages/cli/test/integration/publicApi/credentials.test.ts @@ -1,10 +1,11 @@ import { Container } from 'typedi'; +import { randomString } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { randomApiKey, randomName, randomString } from '../shared/random'; +import { randomApiKey, randomName } from '../shared/random'; import * as utils from '../shared/utils/'; import type { CredentialPayload, SaveCredentialFunction } from '../shared/types'; import * as testDb from '../shared/testDb'; diff --git a/packages/cli/test/integration/shared/random.ts b/packages/cli/test/integration/shared/random.ts index 62b5c73ee0..c6c62f7124 100644 --- a/packages/cli/test/integration/shared/random.ts +++ b/packages/cli/test/integration/shared/random.ts @@ -1,39 +1,19 @@ -import { randomBytes } from 'crypto'; +import { v4 as uuid } from 'uuid'; +import { randomInt, randomString, UPPERCASE_LETTERS } from 'n8n-workflow'; + import { MIN_PASSWORD_CHAR_LENGTH, MAX_PASSWORD_CHAR_LENGTH } from '@/constants'; import type { CredentialPayload } from './types'; -import { v4 as uuid } from 'uuid'; -/** - * Create a random alphanumeric string of random length between two limits, both inclusive. - * Limits should be even numbers (round down otherwise). - */ -export function randomString(min: number, max: number) { - const randomInteger = Math.floor(Math.random() * (max - min) + min) + 1; - return randomBytes(randomInteger / 2).toString('hex'); -} +export const randomApiKey = () => `n8n_api_${randomString(40)}`; -export function randomApiKey() { - return `n8n_api_${randomBytes(20).toString('hex')}`; -} +export const chooseRandomly = (array: T[]) => array[randomInt(array.length)]; -export const chooseRandomly = (array: T[]) => array[Math.floor(Math.random() * array.length)]; - -export const randomInteger = (max = 1000) => Math.floor(Math.random() * max); - -export const randomDigit = () => Math.floor(Math.random() * 10); - -export const randomPositiveDigit = (): number => { - const digit = randomDigit(); - - return digit === 0 ? randomPositiveDigit() : digit; -}; - -const randomUppercaseLetter = () => chooseRandomly('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')); +const randomUppercaseLetter = () => chooseRandomly(UPPERCASE_LETTERS.split('')); export const randomValidPassword = () => randomString(MIN_PASSWORD_CHAR_LENGTH, MAX_PASSWORD_CHAR_LENGTH - 2) + randomUppercaseLetter() + - randomDigit(); + randomInt(10); export const randomInvalidPassword = () => chooseRandomly([ @@ -54,7 +34,7 @@ const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu']; const randomTopLevelDomain = () => chooseRandomly(POPULAR_TOP_LEVEL_DOMAINS); -export const randomName = () => randomString(4, 8); +export const randomName = () => randomString(4, 8).toLowerCase(); export const randomCredentialPayload = (): CredentialPayload => ({ name: randomName(), diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 514b04a6b3..15701f86a7 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -2,13 +2,12 @@ import type { DataSourceOptions, Repository } from '@n8n/typeorm'; import { DataSource as Connection } from '@n8n/typeorm'; import { Container } from 'typedi'; import type { Class } from 'n8n-core'; +import { randomString } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; import { getOptionOverrides } from '@db/config'; -import { randomString } from './random'; - export const testDbPrefix = 'n8n_test_'; /** @@ -16,7 +15,7 @@ export const testDbPrefix = 'n8n_test_'; */ export async function init() { const dbType = config.getEnv('database.type'); - const testDbName = `${testDbPrefix}${randomString(6, 10)}_${Date.now()}`; + const testDbName = `${testDbPrefix}${randomString(6, 10).toLowerCase()}_${Date.now()}`; if (dbType === 'postgresdb') { const bootstrapPostgres = await new Connection( diff --git a/packages/cli/test/unit/WebhookHelpers.test.ts b/packages/cli/test/unit/WebhookHelpers.test.ts index fb455b098a..391d01b6fc 100644 --- a/packages/cli/test/unit/WebhookHelpers.test.ts +++ b/packages/cli/test/unit/WebhookHelpers.test.ts @@ -1,6 +1,8 @@ import { type Response } from 'express'; import { mock } from 'jest-mock-extended'; +import { randomString } from 'n8n-workflow'; import type { IHttpRequestMethods } from 'n8n-workflow'; + import type { IWebhookManager, WebhookCORSRequest, WebhookRequest } from '@/Interfaces'; import { webhookRequestHandler } from '@/WebhookHelpers'; @@ -82,7 +84,7 @@ describe('WebhookHelpers', () => { }); it('should handle wildcard origin', async () => { - const randomOrigin = (Math.random() * 10e6).toString(16); + const randomOrigin = randomString(10); const req = mock({ method: 'OPTIONS', headers: { diff --git a/packages/cli/test/unit/shared/mockObjects.ts b/packages/cli/test/unit/shared/mockObjects.ts index ccc85eb72d..e7a1659773 100644 --- a/packages/cli/test/unit/shared/mockObjects.ts +++ b/packages/cli/test/unit/shared/mockObjects.ts @@ -1,21 +1,21 @@ +import { randomInt } from 'n8n-workflow'; import { User } from '@db/entities/User'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; +import { Project } from '@db/entities/Project'; import { randomCredentialPayload, randomEmail, - randomInteger, randomName, uniqueId, } from '../../integration/shared/random'; -import { Project } from '@/databases/entities/Project'; export const mockCredential = (): CredentialsEntity => Object.assign(new CredentialsEntity(), randomCredentialPayload()); export const mockUser = (): User => Object.assign(new User(), { - id: randomInteger(), + id: randomInt(1000), email: randomEmail(), firstName: randomName(), lastName: randomName(), diff --git a/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue b/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue index 9607277660..dfda2eccd6 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue @@ -4,6 +4,7 @@ import { snakeCase } from 'lodash-es'; import { useSessionStorage } from '@vueuse/core'; import { N8nButton, N8nInput, N8nTooltip } from 'n8n-design-system/components'; +import { randomInt } from 'n8n-workflow'; import type { CodeExecutionMode, INodeExecutionData } from 'n8n-workflow'; import type { BaseTextKey } from '@/plugins/i18n'; @@ -208,7 +209,7 @@ function triggerLoadingChange() { // Loading phrase change if (!lastPhraseChange || timestamp - lastPhraseChange >= loadingPhraseUpdateMs) { - loadingPhraseIndex.value = Math.floor(Math.random() * loadingPhrasesCount); + loadingPhraseIndex.value = randomInt(loadingPhrasesCount); lastPhraseChange = timestamp; } diff --git a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.test.ts b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.test.ts index a2fb907e91..c0c37e7b10 100644 --- a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.test.ts +++ b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.test.ts @@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker'; import { STORES, VIEWS } from '@/constants'; import ExecutionsList from '@/components/executions/global/GlobalExecutionsList.vue'; import type { IWorkflowDb } from '@/Interface'; -import type { ExecutionSummary } from 'n8n-workflow'; +import { randomInt, type ExecutionSummary } from 'n8n-workflow'; import { retry, SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils'; import { createComponentRenderer } from '@/__tests__/render'; import { waitFor } from '@testing-library/vue'; @@ -22,7 +22,7 @@ vi.mock('vue-router', () => ({ let pinia: ReturnType; const generateUndefinedNullOrString = () => { - switch (Math.floor(Math.random() * 4)) { + switch (randomInt(4)) { case 0: return undefined; case 1: diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts index 687a2674d5..eb09c96974 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { faker } from '@faker-js/faker'; import { createRouter, createWebHistory } from 'vue-router'; import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia'; -import type { ExecutionSummary } from 'n8n-workflow'; +import { randomInt, type ExecutionSummary } from 'n8n-workflow'; import { useSettingsStore } from '@/stores/settings.store'; import WorkflowExecutionsPreview from '@/components/executions/workflow/WorkflowExecutionsPreview.vue'; import { EnterpriseEditionFeature, VIEWS } from '@/constants'; @@ -33,7 +33,7 @@ const $route = { }; const generateUndefinedNullOrString = () => { - switch (Math.floor(Math.random() * 4)) { + switch (randomInt(4)) { case 0: return undefined; case 1: diff --git a/packages/editor-ui/src/stores/root.store.ts b/packages/editor-ui/src/stores/root.store.ts index ece720e0bb..339b40dd41 100644 --- a/packages/editor-ui/src/stores/root.store.ts +++ b/packages/editor-ui/src/stores/root.store.ts @@ -1,6 +1,6 @@ import { CLOUD_BASE_URL_PRODUCTION, CLOUD_BASE_URL_STAGING, STORES } from '@/constants'; import type { RootState } from '@/Interface'; -import { setGlobalState } from 'n8n-workflow'; +import { randomString, setGlobalState } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; @@ -26,7 +26,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => { versionCli: '0.0.0', oauthCallbackUrls: {}, n8nMetadata: {}, - pushRef: Math.random().toString(36).substring(2, 15), + pushRef: randomString(10).toLowerCase(), urlBaseWebhook: 'http://localhost:5678/', urlBaseEditor: 'http://localhost:5678', isNpmAvailable: false, diff --git a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts index 5fb6e92793..e64ca604af 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -7,6 +7,7 @@ import type { INodeTypeDescription, IWebhookResponseData, } from 'n8n-workflow'; +import { randomString } from 'n8n-workflow'; import { helpscoutApiRequest, helpscoutApiRequestAllItems } from './GenericFunctions'; @@ -140,7 +141,7 @@ export class HelpScoutTrigger implements INodeType { const body = { url: webhookUrl, events, - secret: Math.random().toString(36).substring(2, 15), + secret: randomString(10).toLowerCase(), }; const responseData = await helpscoutApiRequest.call( diff --git a/packages/nodes-base/nodes/ItemLists/V1/ItemListsV1.node.ts b/packages/nodes-base/nodes/ItemLists/V1/ItemListsV1.node.ts index 674a9eb42d..c2f17001d9 100644 --- a/packages/nodes-base/nodes/ItemLists/V1/ItemListsV1.node.ts +++ b/packages/nodes-base/nodes/ItemLists/V1/ItemListsV1.node.ts @@ -7,7 +7,7 @@ import type { INodeTypeBaseDescription, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { NodeOperationError, randomInt } from 'n8n-workflow'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; @@ -52,7 +52,7 @@ const flattenKeys = (obj: IDataObject, path: string[] = []): IDataObject => { const shuffleArray = (array: any[]) => { for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [array[i], array[j]] = [array[j], array[i]]; } }; diff --git a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts index 7b1738dc07..5a41622055 100644 --- a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts +++ b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts @@ -8,7 +8,7 @@ import type { INodeTypeDescription, IPairedItemData, } from 'n8n-workflow'; -import { NodeOperationError, deepCopy } from 'n8n-workflow'; +import { NodeOperationError, deepCopy, randomInt } from 'n8n-workflow'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; @@ -53,7 +53,7 @@ const flattenKeys = (obj: IDataObject, path: string[] = []): IDataObject => { const shuffleArray = (array: any[]) => { for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [array[i], array[j]] = [array[j], array[i]]; } }; diff --git a/packages/nodes-base/nodes/ItemLists/V3/helpers/utils.ts b/packages/nodes-base/nodes/ItemLists/V3/helpers/utils.ts index 49b648527d..e98bd773ae 100644 --- a/packages/nodes-base/nodes/ItemLists/V3/helpers/utils.ts +++ b/packages/nodes-base/nodes/ItemLists/V3/helpers/utils.ts @@ -7,7 +7,7 @@ import type { INodeExecutionData, GenericValue, } from 'n8n-workflow'; -import { ApplicationError, NodeOperationError } from 'n8n-workflow'; +import { ApplicationError, NodeOperationError, randomInt } from 'n8n-workflow'; import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; @@ -47,7 +47,7 @@ export const flattenKeys = (obj: IDataObject, path: string[] = []): IDataObject export const shuffleArray = (array: any[]) => { for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [array[i], array[j]] = [array[j], array[i]]; } }; diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts index 3822fa1037..2af6354b2f 100644 --- a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts @@ -8,6 +8,7 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; +import { randomString } from 'n8n-workflow'; import * as mqtt from 'mqtt'; import { formatPrivateKey } from '@utils/utilities'; @@ -116,7 +117,7 @@ export class Mqtt implements INodeType { const brokerUrl = `${protocol}://${host}`; const port = (credentials.port as number) || 1883; const clientId = - (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; const clean = credentials.clean as boolean; const ssl = credentials.ssl as boolean; const ca = formatPrivateKey(credentials.ca as string); @@ -189,8 +190,7 @@ export class Mqtt implements INodeType { const host = credentials.host as string; const brokerUrl = `${protocol}://${host}`; const port = (credentials.port as number) || 1883; - const clientId = - (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; const clean = credentials.clean as boolean; const ssl = credentials.ssl as boolean; const ca = credentials.ca as string; diff --git a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts index f4209c0822..23a6ddfd1c 100644 --- a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts +++ b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts @@ -7,7 +7,7 @@ import type { IDeferredPromise, IRun, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { NodeOperationError, randomString } from 'n8n-workflow'; import * as mqtt from 'mqtt'; import { formatPrivateKey } from '@utils/utilities'; @@ -109,8 +109,7 @@ export class MqttTrigger implements INodeType { const host = credentials.host as string; const brokerUrl = `${protocol}://${host}`; const port = (credentials.port as number) || 1883; - const clientId = - (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; const clean = credentials.clean as boolean; const ssl = credentials.ssl as boolean; const ca = formatPrivateKey(credentials.ca as string); diff --git a/packages/nodes-base/nodes/Odoo/GenericFunctions.ts b/packages/nodes-base/nodes/Odoo/GenericFunctions.ts index 53634b6438..5039fb8941 100644 --- a/packages/nodes-base/nodes/Odoo/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Odoo/GenericFunctions.ts @@ -6,7 +6,7 @@ import type { JsonObject, IRequestOptions, } from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; +import { NodeApiError, randomInt } from 'n8n-workflow'; const serviceJSONRPC = 'object'; const methodJSONRPC = 'execute'; @@ -65,7 +65,7 @@ export interface IOdooNameValueFields { }>; } -export interface IOdooResponceFields { +export interface IOdooResponseFields { fields: Array<{ field: string; fromList?: boolean; @@ -97,8 +97,8 @@ export function processNameValueFields(value: IDataObject) { }, {}); } -// function processResponceFields(value: IDataObject) { -// const data = value as unknown as IOdooResponceFields; +// function processResponseFields(value: IDataObject) { +// const data = value as unknown as IOdooResponseFields; // return data?.fields?.map((entry) => entry.field); // } @@ -121,13 +121,13 @@ export async function odooJSONRPCRequest( json: true, }; - const responce = await this.helpers.request(options); - if (responce.error) { - throw new NodeApiError(this.getNode(), responce.error.data as JsonObject, { - message: responce.error.data.message, + const response = await this.helpers.request(options); + if (response.error) { + throw new NodeApiError(this.getNode(), response.error.data as JsonObject, { + message: response.error.data.message, }); } - return responce.result; + return response.result; } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -158,7 +158,7 @@ export async function odooGetModelFields( ['string', 'type', 'help', 'required', 'name'], ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const result = await odooJSONRPCRequest.call(this, body, url); @@ -194,7 +194,7 @@ export async function odooCreate( newItem || {}, ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const result = await odooJSONRPCRequest.call(this, body, url); @@ -238,7 +238,7 @@ export async function odooGet( fieldsToReturn || [], ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const result = await odooJSONRPCRequest.call(this, body, url); @@ -279,7 +279,7 @@ export async function odooGetAll( limit, ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const result = await odooJSONRPCRequest.call(this, body, url); @@ -329,7 +329,7 @@ export async function odooUpdate( fieldsToUpdate, ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; await odooJSONRPCRequest.call(this, body, url); @@ -371,7 +371,7 @@ export async function odooDelete( itemsID ? [+itemsID] : [], ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; await odooJSONRPCRequest.call(this, body, url); @@ -397,7 +397,7 @@ export async function odooGetUserID( method: 'login', args: [db, username, password], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const loginResult = await odooJSONRPCRequest.call(this, body, url); return loginResult as unknown as number; @@ -419,7 +419,7 @@ export async function odooGetServerVersion( method: 'version', args: [], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const result = await odooJSONRPCRequest.call(this, body, url); return result; diff --git a/packages/nodes-base/nodes/Odoo/Odoo.node.ts b/packages/nodes-base/nodes/Odoo/Odoo.node.ts index 2df531dc54..5bd254dcd4 100644 --- a/packages/nodes-base/nodes/Odoo/Odoo.node.ts +++ b/packages/nodes-base/nodes/Odoo/Odoo.node.ts @@ -11,7 +11,7 @@ import type { INodeTypeDescription, IRequestOptions, } from 'n8n-workflow'; -import { deepCopy } from 'n8n-workflow'; +import { deepCopy, randomInt } from 'n8n-workflow'; import { capitalCase } from 'change-case'; import { @@ -115,8 +115,8 @@ export class Odoo implements INodeType { const db = odooGetDBName(credentials.db as string, url); const userID = await odooGetUserID.call(this, db, username, password, url); - const responce = await odooGetModelFields.call(this, db, userID, password, resource, url); - const options = Object.values(responce).map((field) => { + const response = await odooGetModelFields.call(this, db, userID, password, resource, url); + const options = Object.values(response).map((field) => { const optionField = field as { [key: string]: string }; let name = ''; try { @@ -158,12 +158,12 @@ export class Odoo implements INodeType { ['name', 'model', 'modules'], ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; - const responce = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; + const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; - const options = responce.map((model) => { + const options = response.map((model) => { return { name: model.name, value: model.model, @@ -188,12 +188,12 @@ export class Odoo implements INodeType { method: 'execute', args: [db, userID, password, 'res.country.state', 'search_read', [], ['id', 'name']], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; - const responce = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; + const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; - const options = responce.map((state) => { + const options = response.map((state) => { return { name: state.name as string, value: state.id, @@ -217,12 +217,12 @@ export class Odoo implements INodeType { method: 'execute', args: [db, userID, password, 'res.country', 'search_read', [], ['id', 'name']], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; - const responce = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; + const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; - const options = responce.map((country) => { + const options = response.map((country) => { return { name: country.name as string, value: country.id, @@ -252,7 +252,7 @@ export class Odoo implements INodeType { credentials?.password, ], }, - id: Math.floor(Math.random() * 100), + id: randomInt(100), }; const options: IRequestOptions = { diff --git a/packages/nodes-base/nodes/Transform/Sort/utils.ts b/packages/nodes-base/nodes/Transform/Sort/utils.ts index 3b156e206b..00b7b6798b 100644 --- a/packages/nodes-base/nodes/Transform/Sort/utils.ts +++ b/packages/nodes-base/nodes/Transform/Sort/utils.ts @@ -1,9 +1,10 @@ import { NodeVM } from '@n8n/vm2'; -import { type IExecuteFunctions, type INodeExecutionData, NodeOperationError } from 'n8n-workflow'; +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError, randomInt } from 'n8n-workflow'; export const shuffleArray = (array: any[]) => { for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [array[i], array[j]] = [array[j], array[i]]; } }; diff --git a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts index 52aa072dae..8993fb6774 100644 --- a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts +++ b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts @@ -10,7 +10,7 @@ import type { IWebhookResponseData, JsonObject, } from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; +import { NodeApiError, randomString } from 'n8n-workflow'; import type { ITypeformAnswer, @@ -177,7 +177,7 @@ export class TypeformTrigger implements INodeType { const webhookUrl = this.getNodeWebhookUrl('default'); const formId = this.getNodeParameter('formId') as string; - const webhookId = 'n8n-' + Math.random().toString(36).substring(2, 15); + const webhookId = 'n8n-' + randomString(10).toLowerCase(); const endpoint = `forms/${formId}/webhooks/${webhookId}`; diff --git a/packages/workflow/jest.config.js b/packages/workflow/jest.config.js index 6d9cddf364..3c831cd739 100644 --- a/packages/workflow/jest.config.js +++ b/packages/workflow/jest.config.js @@ -1,2 +1,5 @@ /** @type {import('jest').Config} */ -module.exports = require('../../jest.config'); +module.exports = { + ...require('../../jest.config'), + setupFilesAfterEnv: ['/test/setup.ts'], +}; diff --git a/packages/workflow/src/Constants.ts b/packages/workflow/src/Constants.ts index be9a323e6b..368ca1598b 100644 --- a/packages/workflow/src/Constants.ts +++ b/packages/workflow/src/Constants.ts @@ -1,5 +1,10 @@ import type { NodeParameterValue } from './Interfaces'; +export const DIGITS = '0123456789'; +export const UPPERCASE_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +export const LOWERCASE_LETTERS = UPPERCASE_LETTERS.toLowerCase(); +export const ALPHABET = [DIGITS, UPPERCASE_LETTERS, LOWERCASE_LETTERS].join(''); + export const BINARY_ENCODING = 'base64'; export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z'; diff --git a/packages/workflow/src/Cron.ts b/packages/workflow/src/Cron.ts index fb1446fa04..7f41e9ccd5 100644 --- a/packages/workflow/src/Cron.ts +++ b/packages/workflow/src/Cron.ts @@ -1,3 +1,5 @@ +import { randomInt } from './utils'; + interface BaseTriggerTime { mode: T; } @@ -47,7 +49,7 @@ export type TriggerTime = | EveryWeek | EveryMonth; -const randomSecond = () => Math.floor(Math.random() * 60).toString(); +const randomSecond = () => randomInt(60).toString(); export const toCronExpression = (item: TriggerTime): CronExpression => { if (item.mode === 'everyMinute') return `${randomSecond()} * * * * *`; diff --git a/packages/workflow/src/Extensions/ArrayExtensions.ts b/packages/workflow/src/Extensions/ArrayExtensions.ts index d78ec715de..a97049ece3 100644 --- a/packages/workflow/src/Extensions/ArrayExtensions.ts +++ b/packages/workflow/src/Extensions/ArrayExtensions.ts @@ -1,9 +1,11 @@ +import deepEqual from 'deep-equal'; +import uniqWith from 'lodash/uniqWith'; + import { ExpressionError } from '../errors/expression.error'; import { ExpressionExtensionError } from '../errors/expression-extension.error'; import type { Extension, ExtensionMap } from './Extensions'; import { compact as oCompact } from './ObjectExtensions'; -import deepEqual from 'deep-equal'; -import uniqWith from 'lodash/uniqWith'; +import { randomInt } from '../utils'; function first(value: unknown[]): unknown { return value[0]; @@ -49,7 +51,7 @@ function pluck(value: unknown[], extraArgs: unknown[]): unknown[] { function randomItem(value: unknown[]): unknown { const len = value === undefined ? 0 : value.length; - return len ? value[Math.floor(Math.random() * len)] : undefined; + return len ? value[randomInt(len)] : undefined; } function unique(value: unknown[], extraArgs: string[]): unknown[] { diff --git a/packages/workflow/src/errors/abstract/node.error.ts b/packages/workflow/src/errors/abstract/node.error.ts index dde40284f8..b9be7d8e95 100644 --- a/packages/workflow/src/errors/abstract/node.error.ts +++ b/packages/workflow/src/errors/abstract/node.error.ts @@ -1,5 +1,5 @@ import { isTraversableObject } from '../../utils'; -import type { IDataObject, INode, JsonObject } from '../..'; +import type { IDataObject, INode, JsonObject } from '@/Interfaces'; import { ExecutionBaseError } from './execution-base.error'; /** diff --git a/packages/workflow/src/errors/node-operation.error.ts b/packages/workflow/src/errors/node-operation.error.ts index c4c8c780b1..9fda163ba0 100644 --- a/packages/workflow/src/errors/node-operation.error.ts +++ b/packages/workflow/src/errors/node-operation.error.ts @@ -1,4 +1,4 @@ -import type { INode, JsonObject } from '..'; +import type { INode, JsonObject } from '@/Interfaces'; import type { NodeOperationErrorOptions } from './node-api.error'; import { NodeError } from './abstract/node.error'; diff --git a/packages/workflow/src/errors/workflow-operation.error.ts b/packages/workflow/src/errors/workflow-operation.error.ts index 302bc310b7..bbf57c06a0 100644 --- a/packages/workflow/src/errors/workflow-operation.error.ts +++ b/packages/workflow/src/errors/workflow-operation.error.ts @@ -1,4 +1,4 @@ -import type { INode } from '..'; +import type { INode } from '@/Interfaces'; import { ExecutionBaseError } from './abstract/execution-base.error'; /** diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 05f9f935b2..55805993ef 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -34,6 +34,8 @@ export { assert, removeCircularRefs, updateDisplayOptions, + randomInt, + randomString, } from './utils'; export { isINodeProperties, diff --git a/packages/workflow/src/utils.ts b/packages/workflow/src/utils.ts index e7e5182443..e043b18ad0 100644 --- a/packages/workflow/src/utils.ts +++ b/packages/workflow/src/utils.ts @@ -1,9 +1,10 @@ import FormData from 'form-data'; +import { merge } from 'lodash'; + +import { ALPHABET } from './Constants'; import type { BinaryFileType, IDisplayOptions, INodeProperties, JsonObject } from './Interfaces'; import { ApplicationError } from './errors/application.error'; -import { merge } from 'lodash'; - const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']); // NOTE: BigInt.prototype.toJSON is not available, which causes JSON.stringify to throw an error @@ -179,3 +180,36 @@ export function updateDisplayOptions( }; }); } + +export function randomInt(max: number): number; +export function randomInt(min: number, max: number): number; +/** + * Generates a random integer within a specified range. + * + * @param {number} min - The lower bound of the range. If `max` is not provided, this value is used as the upper bound and the lower bound is set to 0. + * @param {number} [max] - The upper bound of the range, not inclusive. + * @returns {number} A random integer within the specified range. + */ +export function randomInt(min: number, max?: number): number { + if (max === undefined) { + max = min; + min = 0; + } + return min + (crypto.getRandomValues(new Uint32Array(1))[0] % (max - min)); +} + +export function randomString(length: number): string; +export function randomString(minLength: number, maxLength: number): string; +/** + * Generates a random alphanumeric string of a specified length, or within a range of lengths. + * + * @param {number} minLength - If `maxLength` is not provided, this is the length of the string to generate. Otherwise, this is the lower bound of the range of possible lengths. + * @param {number} [maxLength] - The upper bound of the range of possible lengths. If provided, the actual length of the string will be a random number between `minLength` and `maxLength`, inclusive. + * @returns {string} A random alphanumeric string of the specified length or within the specified range of lengths. + */ +export function randomString(minLength: number, maxLength?: number): string { + const length = maxLength === undefined ? minLength : randomInt(minLength, maxLength + 1); + return [...crypto.getRandomValues(new Uint32Array(length))] + .map((byte) => ALPHABET[byte % ALPHABET.length]) + .join(''); +} diff --git a/packages/workflow/test/NodeHelpers.test.ts b/packages/workflow/test/NodeHelpers.test.ts index 820a7321bc..74508cdce9 100644 --- a/packages/workflow/test/NodeHelpers.test.ts +++ b/packages/workflow/test/NodeHelpers.test.ts @@ -1,5 +1,5 @@ import type { INode, INodeParameters, INodeProperties, INodeTypeDescription } from '@/Interfaces'; -import type { Workflow } from '../src'; +import type { Workflow } from '@/Workflow'; import { getNodeParameters, getNodeHints, isSingleExecution } from '@/NodeHelpers'; diff --git a/packages/workflow/test/TelemetryHelpers.test.ts b/packages/workflow/test/TelemetryHelpers.test.ts index 01f6c252ec..90bddbdcdd 100644 --- a/packages/workflow/test/TelemetryHelpers.test.ts +++ b/packages/workflow/test/TelemetryHelpers.test.ts @@ -1,14 +1,18 @@ import { v5 as uuidv5, v3 as uuidv3, v4 as uuidv4, v1 as uuidv1 } from 'uuid'; +import { mock } from 'jest-mock-extended'; + import { ANONYMIZATION_CHARACTER as CHAR, generateNodesGraph, getDomainBase, getDomainPath, } from '@/TelemetryHelpers'; -import { ApplicationError, STICKY_NODE_TYPE, type IWorkflowBase } from '@/index'; import { nodeTypes } from './ExpressionExtensions/Helpers'; -import { mock } from 'jest-mock-extended'; import * as nodeHelpers from '@/NodeHelpers'; +import type { IWorkflowBase } from '@/Interfaces'; +import { STICKY_NODE_TYPE } from '@/Constants'; +import { ApplicationError } from '@/errors'; +import { randomInt } from '@/utils'; describe('getDomainBase should return protocol plus domain', () => { test('in valid URLs', () => { @@ -872,22 +876,12 @@ function uuidUrls( ]; } -function digit() { - return Math.floor(Math.random() * 10); -} - -function positiveDigit(): number { - const d = digit(); - - return d === 0 ? positiveDigit() : d; -} - -function numericId(length = positiveDigit()) { - return Array.from({ length }, digit).join(''); +function numericId(length = randomInt(1, 10)) { + return Array.from({ length }, () => randomInt(10)).join(''); } function alphanumericId() { return chooseRandomly([`john${numericId()}`, `title${numericId(1)}`, numericId()]); } -const chooseRandomly = (array: T[]) => array[Math.floor(Math.random() * array.length)]; +const chooseRandomly = (array: T[]) => array[randomInt(array.length)]; diff --git a/packages/workflow/test/errors/workflow-activation.error.test.ts b/packages/workflow/test/errors/workflow-activation.error.test.ts index c7be134071..b542747cfc 100644 --- a/packages/workflow/test/errors/workflow-activation.error.test.ts +++ b/packages/workflow/test/errors/workflow-activation.error.test.ts @@ -1,4 +1,4 @@ -import { WorkflowActivationError } from '@/index'; +import { WorkflowActivationError } from '@/errors'; describe('WorkflowActivationError', () => { it('should default to `error` level', () => { diff --git a/packages/workflow/test/setup.ts b/packages/workflow/test/setup.ts new file mode 100644 index 0000000000..783203e76a --- /dev/null +++ b/packages/workflow/test/setup.ts @@ -0,0 +1,7 @@ +import { randomFillSync } from 'crypto'; + +Object.defineProperty(globalThis, 'crypto', { + value: { + getRandomValues: (buffer: NodeJS.ArrayBufferView) => randomFillSync(buffer), + }, +}); diff --git a/packages/workflow/test/utils.test.ts b/packages/workflow/test/utils.test.ts index a0867f5517..fe6bccd201 100644 --- a/packages/workflow/test/utils.test.ts +++ b/packages/workflow/test/utils.test.ts @@ -1,5 +1,14 @@ +import { ALPHABET } from '@/Constants'; import { ApplicationError } from '@/errors/application.error'; -import { jsonParse, jsonStringify, deepCopy, isObjectEmpty, fileTypeFromMimeType } from '@/utils'; +import { + jsonParse, + jsonStringify, + deepCopy, + isObjectEmpty, + fileTypeFromMimeType, + randomInt, + randomString, +} from '@/utils'; describe('isObjectEmpty', () => { it('should handle null and undefined', () => { @@ -237,3 +246,47 @@ describe('fileTypeFromMimeType', () => { expect(fileTypeFromMimeType('application/pdf')).toEqual('pdf'); }); }); + +const repeat = (fn: () => void, times = 10) => Array(times).fill(0).forEach(fn); + +describe('randomInt', () => { + it('should generate random integers', () => { + repeat(() => { + const result = randomInt(10); + expect(result).toBeLessThanOrEqual(10); + expect(result).toBeGreaterThanOrEqual(0); + }); + }); + + it('should generate random in range', () => { + repeat(() => { + const result = randomInt(10, 100); + expect(result).toBeLessThanOrEqual(100); + expect(result).toBeGreaterThanOrEqual(10); + }); + }); +}); + +describe('randomString', () => { + it('should return a random string of the specified length', () => { + repeat(() => { + const result = randomString(42); + expect(result).toHaveLength(42); + }); + }); + + it('should return a random string of the in the length range', () => { + repeat(() => { + const result = randomString(10, 100); + expect(result.length).toBeGreaterThanOrEqual(10); + expect(result.length).toBeLessThanOrEqual(100); + }); + }); + + it('should only contain characters from the specified character set', () => { + repeat(() => { + const result = randomString(1000); + result.split('').every((char) => ALPHABET.includes(char)); + }); + }); +}); diff --git a/packages/workflow/tsconfig.json b/packages/workflow/tsconfig.json index 2f0507b565..0b486eab47 100644 --- a/packages/workflow/tsconfig.json +++ b/packages/workflow/tsconfig.json @@ -6,6 +6,7 @@ "paths": { "@/*": ["./*"] }, + "lib": ["es2020", "es2022.error", "dom"], "tsBuildInfoFile": "dist/typecheck.tsbuildinfo" }, "include": ["src/**/*.ts", "test/**/*.ts"]