From fe782c8f6a43974a807921ab4a01b286ee7b388f 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: Tue, 21 Feb 2023 14:04:35 +0100 Subject: [PATCH 1/2] ci: Setup a semi-automated release process (no-changelog) (#5504) * ci: Setup a semi-automated release process (no-changelog) * create tag/release before deleting the temporary branch --- .github/scripts/bump-versions.mjs | 52 +++++++++++++++++++ .github/scripts/package.json | 6 +++ .github/workflows/check-pr-title.yml | 2 + .github/workflows/release-create-pr.yml | 69 +++++++++++++++++++++++++ .github/workflows/release-publish.yml | 57 ++++++++++++++++++++ CONTRIBUTING.md | 23 +++++++-- package.json | 4 +- packages/cli/package.json | 8 +-- packages/core/package.json | 2 +- packages/editor-ui/package.json | 4 +- packages/editor-ui/vite.config.ts | 24 +++------ packages/node-dev/package.json | 4 +- packages/nodes-base/package.json | 4 +- packages/workflow/package.json | 1 - pnpm-lock.yaml | 28 +++++----- 15 files changed, 240 insertions(+), 48 deletions(-) create mode 100644 .github/scripts/bump-versions.mjs create mode 100644 .github/scripts/package.json create mode 100644 .github/workflows/release-create-pr.yml create mode 100644 .github/workflows/release-publish.yml diff --git a/.github/scripts/bump-versions.mjs b/.github/scripts/bump-versions.mjs new file mode 100644 index 0000000000..da45c92a80 --- /dev/null +++ b/.github/scripts/bump-versions.mjs @@ -0,0 +1,52 @@ +import semver from 'semver'; +import { writeFile, readFile } from 'fs/promises'; +import { resolve } from 'path'; +import child_process from 'child_process'; +import { promisify } from 'util'; +import assert from 'assert'; + +const exec = promisify(child_process.exec); + +const rootDir = process.cwd(); +const releaseType = process.env.RELEASE_TYPE; +assert.match(releaseType, /^(patch|minor|major)$/, 'Invalid RELEASE_TYPE'); + +// TODO: if releaseType is `auto` determine release type based on the changelog + +const lastTag = (await exec('git describe --tags --match "n8n@*" --abbrev=0')).stdout.trim(); +const packages = JSON.parse((await exec('pnpm ls -r --only-projects --json')).stdout); + +const packageMap = {}; +for (let { name, path, version, private: isPrivate, dependencies } of packages) { + if (isPrivate && path !== rootDir) continue; + if (path === rootDir) name = 'monorepo-root'; + + const isDirty = await exec(`git diff --quiet HEAD ${lastTag} -- ${path}`) + .then(() => false) + .catch((error) => true); + + packageMap[name] = { path, isDirty, version }; +} + +assert.ok(packageMap['n8n'].isDirty, 'No changes found since the last release'); + +// Keep the monorepo version up to date with the released version +packageMap['monorepo-root'].version = packageMap['n8n'].version; + +for (const packageName in packageMap) { + const { path, version, isDirty } = packageMap[packageName]; + const packageFile = resolve(path, 'package.json'); + const packageJson = JSON.parse(await readFile(packageFile, 'utf-8')); + + packageJson.version = packageMap[packageName].nextVersion = + isDirty || + Object.keys(packageJson.dependencies).some( + (dependencyName) => packageMap[dependencyName]?.isDirty, + ) + ? semver.inc(version, releaseType) + : version; + + await writeFile(packageFile, JSON.stringify(packageJson, null, 2) + '\n'); +} + +console.log(packageMap['n8n'].nextVersion); diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 0000000000..60f945f3f5 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "semver": "^7.3.8", + "conventional-changelog-cli": "^2.2.2" + } +} diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 85011b7045..9ac4d71da5 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -6,6 +6,8 @@ on: - opened - edited - synchronize + branches: + - '!release/*' jobs: check-pr-title: diff --git a/.github/workflows/release-create-pr.yml b/.github/workflows/release-create-pr.yml new file mode 100644 index 0000000000..1a4e5668a1 --- /dev/null +++ b/.github/workflows/release-create-pr.yml @@ -0,0 +1,69 @@ +name: 'Release: Create Pull Request' + +on: + workflow_dispatch: + inputs: + base-branch: + description: 'The branch to create this release PR from.' + required: true + default: 'master' + + release-type: + description: 'A SemVer release type.' + required: true + type: choice + default: 'minor' + options: + - patch + - minor + +jobs: + create-release-pr: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + timeout-minutes: 5 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.base-branch }} + + - name: Push the base branch + run: | + git push -f origin ${{ github.event.inputs.base-branch }}:"release/${{ github.event.inputs.release-type }}" + + - uses: pnpm/action-setup@v2.2.4 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - run: npm install --prefix=.github/scripts --no-package-lock + + - name: Bump package versions + run: | + echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> $GITHUB_ENV + pnpm i --lockfile-only + env: + RELEASE_TYPE: ${{ github.event.inputs.release-type }} + + - name: Generate Changelog + run: npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -t n8n@ + + - name: Push the release branch, and Create the PR + uses: peter-evans/create-pull-request@v4 + with: + base: 'release/${{ github.event.inputs.release-type }}' + branch: 'release/${{ env.NEXT_RELEASE }}' + commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}' + delete-branch: true + labels: 'release' + title: ':rocket: Release ${{ env.NEXT_RELEASE }}' + # 'TODO: add generated changelog to the body. create a script to generate custom changelog' + body: '' + + # TODO: post PR link to slack diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000000..466ce2eacf --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,57 @@ +name: 'Release: Publish' + +on: + pull_request: + types: + - closed + branches: + - 'release/patch' + - 'release/minor' + +jobs: + publish-release: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + permissions: + contents: write + + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v2.2.4 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Publish to NPM + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public + echo "RELEASE=$(node -e 'console.log(require("./package.json").version)')" >> $GITHUB_ENV + + - name: Create Release + uses: ncipollo/release-action@v1 + with: + commit: ${{github.event.pull_request.base.ref}} + tag: 'n8n@${{env.RELEASE}}' + + - name: Merge Release into 'master' + run: | + git fetch origin + git checkout --track origin/master + git config user.name github-actions + git config user.email github-actions@github.com + git merge --ff origin/${{github.event.pull_request.base.ref}} + git push origin master + git push origin :${{github.event.pull_request.base.ref}} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c3408bff2..6960d5ce28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,15 +11,16 @@ Great that you are here and you want to contribute to n8n - [Development setup](#development-setup) - [Requirements](#requirements) - [Node.js](#nodejs) + - [pnpm](#pnpm) + - [pnpm workspaces](#pnpm-workspaces) + - [corepack](#corepack) - [Build tools](#build-tools) - - [pnpm workspaces](#pnpm-workspaces) - [Actual n8n setup](#actual-n8n-setup) - [Start](#start) - [Development cycle](#development-cycle) - [Test suite](#test-suite) + - [Releasing](#releasing) - [Create custom nodes](#create-custom-nodes) - - [Create a new node to contribute to n8n](#create-a-new-node-to-contribute-to-n8n) - - [Checklist before submitting a new node](#checklist-before-submitting-a-new-node) - [Extend documentation](#extend-documentation) - [Contributor License Agreement](#contributor-license-agreement) @@ -195,6 +196,22 @@ If that gets executed in one of the package folders it will only run the tests of this package. If it gets executed in the n8n-root folder it will run all tests of all packages. +## Releasing + +To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then + +1. Bump versions of packages that have changed or have dependencies that have changed +2. Update the Changelog +3. Create a new branch called `release/${VERSION}`, and +4. Create a new pull-request to track any further changes that need to be included in this release + +Once ready to release, simply merge the pull-request. +This triggers [another workflow](https://github.com/n8n-io/n8n/actions/workflows/release-publish.yml), that will + +1. Build and publish the packages that have a new version in this release +2. Create a new tag, and GitHub release from squashed release commit +3. Merge the squashed release commit back into `master` + ## Create custom nodes Learn about [building nodes](https://docs.n8n.io/integrations/creating-nodes/) to create custom nodes for n8n. You can create community nodes and make them available using [npm](https://www.npmjs.com/). diff --git a/package.json b/package.json index 530c323e4c..5a55493af9 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "test:e2e:all": "cross-env E2E_TESTS=true start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'" }, "dependencies": { - "n8n": "*" + "n8n": "workspace:*" }, "devDependencies": { - "@n8n_io/eslint-config": "*", + "@n8n_io/eslint-config": "workspace:*", "@ngneat/falso": "^6.1.0", "@types/jest": "^29.2.2", "@types/supertest": "^2.0.12", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6b1f9227b3..4e17130ca5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -167,10 +167,10 @@ "lodash.unset": "^4.5.2", "luxon": "^3.1.0", "mysql2": "~2.3.3", - "n8n-core": "~0.155.0", - "n8n-editor-ui": "~0.182.0", - "n8n-nodes-base": "~0.214.0", - "n8n-workflow": "~0.137.0", + "n8n-core": "workspace:*", + "n8n-editor-ui": "workspace:*", + "n8n-nodes-base": "workspace:*", + "n8n-workflow": "workspace:*", "nodemailer": "^6.7.1", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 53932ea78d..96585c2491 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -55,7 +55,7 @@ "form-data": "^4.0.0", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.137.0", + "n8n-workflow": "workspace:*", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "pretty-bytes": "^5.6.0", diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index eb4ead029a..56c76aaa1c 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -63,8 +63,8 @@ "lodash.set": "^4.3.2", "luxon": "^3.1.0", "monaco-editor": "^0.33.0", - "n8n-design-system": "~0.54.0", - "n8n-workflow": "~0.137.0", + "n8n-design-system": "workspace:*", + "n8n-workflow": "workspace:*", "normalize-wheel": "^1.0.1", "pinia": "^2.0.22", "prettier": "^2.8.3", diff --git a/packages/editor-ui/vite.config.ts b/packages/editor-ui/vite.config.ts index 82d3be9ee7..1277051887 100644 --- a/packages/editor-ui/vite.config.ts +++ b/packages/editor-ui/vite.config.ts @@ -7,8 +7,6 @@ import { defineConfig as defineVitestConfig } from 'vitest/config'; import packageJSON from './package.json'; -const isCI = process.env.CI === 'true'; - const vendorChunks = ['vue', 'vue-router']; const n8nChunks = ['n8n-workflow', 'n8n-design-system']; const ignoreChunks = [ @@ -63,18 +61,14 @@ export default mergeConfig( }, plugins: [ vue(), - ...(!isCI - ? [ - legacy({ - targets: ['defaults', 'not IE 11'], - }), - monacoEditorPlugin({ - publicPath: 'assets/monaco-editor', - customDistPath: (root: string, buildOutDir: string, base: string) => - `${root}/${buildOutDir}/assets/monaco-editor`, - }), - ] - : []), + legacy({ + targets: ['defaults', 'not IE 11'], + }), + monacoEditorPlugin({ + publicPath: 'assets/monaco-editor', + customDistPath: (root: string, buildOutDir: string, base: string) => + `${root}/${buildOutDir}/assets/monaco-editor`, + }), ], resolve: { alias: [ @@ -113,11 +107,9 @@ export default mergeConfig( }, }, build: { - minify: !isCI, assetsInlineLimit: 0, sourcemap: false, rollupOptions: { - treeshake: !isCI, output: { manualChunks: { vendor: vendorChunks, diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 94b8dc3b2f..b873b7c64f 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -59,8 +59,8 @@ "change-case": "^4.1.1", "fast-glob": "^3.2.5", "inquirer": "^7.0.1", - "n8n-core": "~0.155.0", - "n8n-workflow": "~0.137.0", + "n8n-core": "workspace:*", + "n8n-workflow": "workspace:*", "oauth-1.0a": "^2.2.6", "replace-in-file": "^6.0.0", "request": "^2.88.2", diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 99b6cbbd51..73ef88e91f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -758,7 +758,7 @@ "@types/xml2js": "^0.4.3", "eslint-plugin-n8n-nodes-base": "^1.12.0", "gulp": "^4.0.0", - "n8n-workflow": "~0.137.0" + "n8n-workflow": "workspace:*" }, "dependencies": { "@kafkajs/confluent-schema-registry": "1.0.6", @@ -797,7 +797,7 @@ "mqtt": "4.2.6", "mssql": "^8.1.2", "mysql2": "~2.3.0", - "n8n-core": "~0.155.0", + "n8n-core": "workspace:*", "node-html-markdown": "^1.1.3", "node-ssh": "^12.0.0", "nodemailer": "^6.7.1", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 5814aa2169..0a89d973c0 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -39,7 +39,6 @@ "dist/**/*" ], "devDependencies": { - "@n8n_io/eslint-config": "", "@types/crypto-js": "^4.1.1", "@types/deep-equal": "^1.0.1", "@types/express": "^4.17.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 055fb2d89c..d5ef413748 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ importers: .: specifiers: - '@n8n_io/eslint-config': '*' + '@n8n_io/eslint-config': workspace:* '@ngneat/falso': ^6.1.0 '@types/jest': ^29.2.2 '@types/supertest': ^2.0.12 @@ -37,7 +37,7 @@ importers: jest-environment-jsdom: ^29.4.2 jest-mock: ^29.4.2 jest-mock-extended: ^3.0.1 - n8n: '*' + n8n: workspace:* nock: ^13.2.9 node-fetch: ^2.6.7 p-limit: ^3.1.0 @@ -207,10 +207,10 @@ importers: luxon: ^3.1.0 mock-jwks: ^1.0.9 mysql2: ~2.3.3 - n8n-core: ~0.155.0 - n8n-editor-ui: ~0.182.0 - n8n-nodes-base: ~0.214.0 - n8n-workflow: ~0.137.0 + n8n-core: workspace:* + n8n-editor-ui: workspace:* + n8n-nodes-base: workspace:* + n8n-workflow: workspace:* nodemailer: ^6.7.1 nodemon: ^2.0.2 oauth-1.0a: ^2.2.6 @@ -403,7 +403,7 @@ importers: form-data: ^4.0.0 lodash.get: ^4.4.2 mime-types: ^2.1.27 - n8n-workflow: ~0.137.0 + n8n-workflow: workspace:* oauth-1.0a: ^2.2.6 p-cancelable: ^2.0.0 pretty-bytes: ^5.6.0 @@ -590,8 +590,8 @@ importers: lodash.set: ^4.3.2 luxon: ^3.1.0 monaco-editor: ^0.33.0 - n8n-design-system: ~0.54.0 - n8n-workflow: ~0.137.0 + n8n-design-system: workspace:* + n8n-workflow: workspace:* normalize-wheel: ^1.0.1 pinia: ^2.0.22 prettier: ^2.8.3 @@ -721,8 +721,8 @@ importers: change-case: ^4.1.1 fast-glob: ^3.2.5 inquirer: ^7.0.1 - n8n-core: ~0.155.0 - n8n-workflow: ~0.137.0 + n8n-core: workspace:* + n8n-workflow: workspace:* oauth-1.0a: ^2.2.6 replace-in-file: ^6.0.0 request: ^2.88.2 @@ -814,8 +814,8 @@ importers: mqtt: 4.2.6 mssql: ^8.1.2 mysql2: ~2.3.0 - n8n-core: ~0.155.0 - n8n-workflow: ~0.137.0 + n8n-core: workspace:* + n8n-workflow: workspace:* node-html-markdown: ^1.1.3 node-ssh: ^12.0.0 nodemailer: ^6.7.1 @@ -931,7 +931,6 @@ importers: packages/workflow: specifiers: - '@n8n_io/eslint-config': '' '@n8n_io/riot-tmpl': ^2.0.0 '@types/crypto-js': ^4.1.1 '@types/deep-equal': ^1.0.1 @@ -976,7 +975,6 @@ importers: transliteration: 2.3.5 xml2js: 0.4.23 devDependencies: - '@n8n_io/eslint-config': link:../@n8n_io/eslint-config '@types/crypto-js': 4.1.1 '@types/deep-equal': 1.0.1 '@types/express': 4.17.14 From 7400c35a48e60c784c36e4946ef6a7d91d73321e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 14:24:02 +0100 Subject: [PATCH 2/2] :rocket: Release 0.216.1 (#5531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :rocket: Release 0.216.1 * fix(core): Do not allow arbitrary path traversal in the credential-translation endpoint (#5522) * fix(core): Do not allow arbitrary path traversal in BinaryDataManager (#5523) * fix(core): User update endpoint should only allow updating email, firstName, and lastName (#5526) * fix(core): Do not explicitly bypass auth on urls containing `.svg` (#5525) * :books: Update CHANGELOG.md --------- Co-authored-by: janober Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ Co-authored-by: Jan Oberhauser --- CHANGELOG.md | 12 +++ package.json | 2 +- packages/cli/package.json | 3 +- packages/cli/src/GenericHelpers.ts | 3 +- packages/cli/src/Server.ts | 79 ++++++------------- packages/cli/src/TranslationHelpers.ts | 16 ---- packages/cli/src/controllers/index.ts | 1 + packages/cli/src/controllers/me.controller.ts | 35 ++++---- .../src/controllers/translation.controller.ts | 58 ++++++++++++++ packages/cli/src/databases/entities/User.ts | 5 +- packages/cli/src/middlewares/auth.ts | 14 ++-- packages/cli/src/requests.ts | 23 ++++-- packages/cli/test/setup-mocks.ts | 2 + .../unit/controllers/me.controller.test.ts | 48 +++++++++-- .../translation.controller.test.ts | 40 ++++++++++ packages/core/package.json | 2 +- .../core/src/BinaryDataManager/FileSystem.ts | 27 ++++--- packages/core/src/errors.ts | 5 ++ packages/core/src/index.ts | 1 + packages/editor-ui/package.json | 2 +- packages/node-dev/package.json | 2 +- packages/nodes-base/package.json | 2 +- packages/workflow/package.json | 2 +- pnpm-lock.yaml | 26 +++--- 24 files changed, 272 insertions(+), 138 deletions(-) create mode 100644 packages/cli/src/controllers/translation.controller.ts create mode 100644 packages/cli/test/unit/controllers/translation.controller.test.ts create mode 100644 packages/core/src/errors.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ea8b39bff..1ba0cebe54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [0.216.1](https://github.com/n8n-io/n8n/compare/n8n@0.216.0...n8n@0.216.1) (2023-02-21) + + +### Bug Fixes + +* **core:** Do not allow arbitrary path traversal in BinaryDataManager ([#5523](https://github.com/n8n-io/n8n/issues/5523)) ([40b9784](https://github.com/n8n-io/n8n/commit/40b97846483fe7c58229c156acb66f43a5a79dc3)) +* **core:** Do not allow arbitrary path traversal in the credential-translation endpoint ([#5522](https://github.com/n8n-io/n8n/issues/5522)) ([fb07d77](https://github.com/n8n-io/n8n/commit/fb07d77106bb4933758c63bbfb87f591bf4a27dd)) +* **core:** Do not explicitly bypass auth on urls containing `.svg` ([#5525](https://github.com/n8n-io/n8n/issues/5525)) ([27adea7](https://github.com/n8n-io/n8n/commit/27adea70459329fc0dddabee69e10c9d1453835f)) +* **core:** User update endpoint should only allow updating email, firstName, and lastName ([#5526](https://github.com/n8n-io/n8n/issues/5526)) ([5599221](https://github.com/n8n-io/n8n/commit/5599221007cb09cb81f0623874fafc6cd481384c)) + + + # [0.216.0](https://github.com/n8n-io/n8n/compare/n8n@0.215.2...n8n@0.216.0) (2023-02-16) diff --git a/package.json b/package.json index 5a55493af9..73d50b1db7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.216.0", + "version": "0.216.1", "private": true, "homepage": "https://n8n.io", "engines": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 4e17130ca5..30a1e3905e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.216.0", + "version": "0.216.1", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -127,6 +127,7 @@ "callsites": "^3.1.0", "change-case": "^4.1.1", "class-validator": "^0.14.0", + "class-transformer": "^0.5.1", "client-oauth2": "^4.2.5", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index e3940a620b..613e450e3b 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -22,6 +22,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; +import type { UserUpdatePayload } from '@/requests'; /** * Returns the base URL n8n is reachable from @@ -99,7 +100,7 @@ export async function generateUniqueName( } export async function validateEntity( - entity: WorkflowEntity | CredentialsEntity | TagEntity | User, + entity: WorkflowEntity | CredentialsEntity | TagEntity | User | UserUpdatePayload, ): Promise { const errors = await validate(entity); diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index c653001316..166b75af2b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -33,6 +33,7 @@ import { LoadNodeParameterOptions, LoadNodeListSearch, UserSettings, + FileNotFoundError, } from 'n8n-core'; import type { @@ -55,7 +56,6 @@ import history from 'connect-history-api-fallback'; import config from '@/config'; import * as Queue from '@/Queue'; import { InternalHooksManager } from '@/InternalHooksManager'; -import { getCredentialTranslationPath } from '@/TranslationHelpers'; import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { nodesController } from '@/api/nodes.api'; @@ -86,6 +86,7 @@ import { MeController, OwnerController, PasswordResetController, + TranslationController, UsersController, } from '@/controllers'; @@ -347,6 +348,7 @@ class Server extends AbstractServer { new OwnerController({ config, internalHooks, repositories, logger }), new MeController({ externalHooks, internalHooks, repositories, logger }), new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }), + new TranslationController(config, this.credentialTypes), new UsersController({ config, mailer, @@ -585,48 +587,6 @@ class Server extends AbstractServer { ), ); - this.app.get( - `/${this.restEndpoint}/credential-translation`, - ResponseHelper.send( - async ( - req: express.Request & { query: { credentialType: string } }, - res: express.Response, - ): Promise => { - const translationPath = getCredentialTranslationPath({ - locale: this.frontendSettings.defaultLocale, - credentialType: req.query.credentialType, - }); - - try { - return require(translationPath); - } catch (error) { - return null; - } - }, - ), - ); - - // Returns node information based on node names and versions - const headersPath = pathJoin(NODES_BASE_DIR, 'dist', 'nodes', 'headers'); - this.app.get( - `/${this.restEndpoint}/node-translation-headers`, - ResponseHelper.send( - async (req: express.Request, res: express.Response): Promise => { - try { - await fsAccess(`${headersPath}.js`); - } catch (_) { - return; // no headers available - } - - try { - return require(headersPath); - } catch (error) { - res.status(500).send('Failed to load headers file'); - } - }, - ), - ); - // ---------------------------------------- // Node-Types // ---------------------------------------- @@ -1160,21 +1120,26 @@ class Server extends AbstractServer { // TODO UM: check if this needs permission check for UM const identifier = req.params.path; const binaryDataManager = BinaryDataManager.getInstance(); - const binaryPath = binaryDataManager.getBinaryPath(identifier); - let { mode, fileName, mimeType } = req.query; - if (!fileName || !mimeType) { - try { - const metadata = await binaryDataManager.getBinaryMetadata(identifier); - fileName = metadata.fileName; - mimeType = metadata.mimeType; - res.setHeader('Content-Length', metadata.fileSize); - } catch {} + try { + const binaryPath = binaryDataManager.getBinaryPath(identifier); + let { mode, fileName, mimeType } = req.query; + if (!fileName || !mimeType) { + try { + const metadata = await binaryDataManager.getBinaryMetadata(identifier); + fileName = metadata.fileName; + mimeType = metadata.mimeType; + res.setHeader('Content-Length', metadata.fileSize); + } catch {} + } + if (mimeType) res.setHeader('Content-Type', mimeType); + if (mode === 'download') { + res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`); + } + res.sendFile(binaryPath); + } catch (error) { + if (error instanceof FileNotFoundError) res.writeHead(404).end(); + else throw error; } - if (mimeType) res.setHeader('Content-Type', mimeType); - if (mode === 'download') { - res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`); - } - res.sendFile(binaryPath); }, ); diff --git a/packages/cli/src/TranslationHelpers.ts b/packages/cli/src/TranslationHelpers.ts index cc4319b04f..dd829534a6 100644 --- a/packages/cli/src/TranslationHelpers.ts +++ b/packages/cli/src/TranslationHelpers.ts @@ -1,7 +1,6 @@ import { join, dirname } from 'path'; import { readdir } from 'fs/promises'; import type { Dirent } from 'fs'; -import { NODES_BASE_DIR } from '@/constants'; const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // e.g. v1, v10 @@ -47,18 +46,3 @@ export async function getNodeTranslationPath({ ? join(nodeDir, `v${maxVersion}`, 'translations', locale, `${nodeType}.json`) : join(nodeDir, 'translations', locale, `${nodeType}.json`); } - -/** - * Get the full path to a credential translation file in `/dist`. - */ -export function getCredentialTranslationPath({ - locale, - credentialType, -}: { - locale: string; - credentialType: string; -}): string { - const credsPath = join(NODES_BASE_DIR, 'dist', 'credentials'); - - return join(credsPath, 'translations', locale, `${credentialType}.json`); -} diff --git a/packages/cli/src/controllers/index.ts b/packages/cli/src/controllers/index.ts index 2ee7c7fd06..37ce548a54 100644 --- a/packages/cli/src/controllers/index.ts +++ b/packages/cli/src/controllers/index.ts @@ -2,4 +2,5 @@ export { AuthController } from './auth.controller'; export { MeController } from './me.controller'; export { OwnerController } from './owner.controller'; export { PasswordResetController } from './passwordReset.controller'; +export { TranslationController } from './translation.controller'; export { UsersController } from './users.controller'; diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 603140715b..be2f5f32c2 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -1,4 +1,5 @@ import validator from 'validator'; +import { plainToInstance } from 'class-transformer'; import { Delete, Get, Patch, Post, RestController } from '@/decorators'; import { compareHash, @@ -7,13 +8,13 @@ import { validatePassword, } from '@/UserManagement/UserManagementHelper'; import { BadRequestError } from '@/ResponseHelper'; -import { User } from '@db/entities/User'; +import type { User } from '@db/entities/User'; import { validateEntity } from '@/GenericHelpers'; import { issueCookie } from '@/auth/jwt'; import { Response } from 'express'; import type { Repository } from 'typeorm'; import type { ILogger } from 'n8n-workflow'; -import { AuthenticatedRequest, MeRequest } from '@/requests'; +import { AuthenticatedRequest, MeRequest, UserUpdatePayload } from '@/requests'; import type { PublicUser, IDatabaseCollections, @@ -61,38 +62,40 @@ export class MeController { * Update the logged-in user's settings, except password. */ @Patch('/') - async updateCurrentUser(req: MeRequest.Settings, res: Response): Promise { - const { email } = req.body; + async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise { + const { id: userId, email: currentEmail } = req.user; + const payload = plainToInstance(UserUpdatePayload, req.body); + + const { email } = payload; if (!email) { this.logger.debug('Request to update user email failed because of missing email in payload', { - userId: req.user.id, - payload: req.body, + userId, + payload, }); throw new BadRequestError('Email is mandatory'); } if (!validator.isEmail(email)) { this.logger.debug('Request to update user email failed because of invalid email in payload', { - userId: req.user.id, + userId, invalidEmail: email, }); throw new BadRequestError('Invalid email address'); } - const { email: currentEmail } = req.user; - const newUser = new User(); + await validateEntity(payload); - Object.assign(newUser, req.user, req.body); + await this.userRepository.update(userId, payload); + const user = await this.userRepository.findOneOrFail({ + where: { id: userId }, + relations: { globalRole: true }, + }); - await validateEntity(newUser); - - const user = await this.userRepository.save(newUser); - - this.logger.info('User updated successfully', { userId: user.id }); + this.logger.info('User updated successfully', { userId }); await issueCookie(res, user); - const updatedKeys = Object.keys(req.body); + const updatedKeys = Object.keys(payload); void this.internalHooks.onUserUpdate({ user, fields_changed: updatedKeys, diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts new file mode 100644 index 0000000000..8d24f9e113 --- /dev/null +++ b/packages/cli/src/controllers/translation.controller.ts @@ -0,0 +1,58 @@ +import type { Request } from 'express'; +import { ICredentialTypes } from 'n8n-workflow'; +import { join } from 'path'; +import { access } from 'fs/promises'; +import { Get, RestController } from '@/decorators'; +import { BadRequestError, InternalServerError } from '@/ResponseHelper'; +import { Config } from '@/config'; +import { NODES_BASE_DIR } from '@/constants'; + +export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations'; +export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers'); + +export declare namespace TranslationRequest { + export type Credential = Request<{}, {}, {}, { credentialType: string }>; +} + +@RestController('/') +export class TranslationController { + constructor(private config: Config, private credentialTypes: ICredentialTypes) {} + + @Get('/credential-translation') + async getCredentialTranslation(req: TranslationRequest.Credential) { + const { credentialType } = req.query; + + if (!this.credentialTypes.recognizes(credentialType)) + throw new BadRequestError(`Invalid Credential type: "${credentialType}"`); + + const defaultLocale = this.config.getEnv('defaultLocale'); + const translationPath = join( + CREDENTIAL_TRANSLATIONS_DIR, + defaultLocale, + `${credentialType}.json`, + ); + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return require(translationPath); + } catch (error) { + return null; + } + } + + @Get('/node-translation-headers') + async getNodeTranslationHeaders() { + try { + await access(`${NODE_HEADERS_PATH}.js`); + } catch (_) { + return; // no headers available + } + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return require(NODE_HEADERS_PATH); + } catch (error) { + throw new InternalServerError('Failed to load headers file'); + } + } +} diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 6cba438f79..d62bf8482f 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -111,6 +111,9 @@ export class User extends AbstractEntity implements IUser { @AfterLoad() @AfterUpdate() computeIsPending(): void { - this.isPending = this.password === null; + this.isPending = + this.globalRole?.name === 'owner' && this.globalRole.scope === 'global' + ? false + : this.password === null; } } diff --git a/packages/cli/src/middlewares/auth.ts b/packages/cli/src/middlewares/auth.ts index a0c13d4f21..070b86dd09 100644 --- a/packages/cli/src/middlewares/auth.ts +++ b/packages/cli/src/middlewares/auth.ts @@ -3,11 +3,12 @@ import jwt from 'jsonwebtoken'; import cookieParser from 'cookie-parser'; import passport from 'passport'; import { Strategy } from 'passport-jwt'; +import { sync as globSync } from 'fast-glob'; import { LoggerProxy as Logger } from 'n8n-workflow'; import type { JwtPayload } from '@/Interfaces'; import type { AuthenticatedRequest } from '@/requests'; import config from '@/config'; -import { AUTH_COOKIE_NAME } from '@/constants'; +import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants'; import { issueCookie, resolveJwtContent } from '@/auth/jwt'; import { isAuthenticatedRequest, @@ -61,6 +62,10 @@ const refreshExpiringCookie: RequestHandler = async (req: AuthenticatedRequest, const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler; +const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'], { + cwd: EDITOR_UI_DIST_DIR, +}); + /** * This sets up the auth middlewares in the correct order */ @@ -79,12 +84,7 @@ export const setupAuthMiddlewares = ( // TODO: refactor me!!! // skip authentication for preflight requests req.method === 'OPTIONS' || - req.url === '/index.html' || - req.url === '/favicon.ico' || - req.url.startsWith('/css/') || - req.url.startsWith('/js/') || - req.url.startsWith('/fonts/') || - req.url.includes('.svg') || + staticAssets.includes(req.url.slice(1)) || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/login`) || req.url.startsWith(`/${restEndpoint}/logout`) || diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index e0636d7611..9171d2f9c3 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -10,11 +10,28 @@ import type { IWorkflowSettings, } from 'n8n-workflow'; +import { IsEmail, IsString, Length } from 'class-validator'; +import { NoXss } from '@db/utils/customValidators'; import type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces'; import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import type * as UserManagementMailer from '@/UserManagement/email/UserManagementMailer'; +export class UserUpdatePayload implements Pick { + @IsEmail() + email: string; + + @NoXss() + @IsString({ message: 'First name must be of type string.' }) + @Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' }) + firstName: string; + + @NoXss() + @IsString({ message: 'Last name must be of type string.' }) + @Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' }) + lastName: string; +} + export type AuthlessRequest< RouteParams = {}, ResponseBody = {}, @@ -144,11 +161,7 @@ export declare namespace ExecutionRequest { // ---------------------------------- export declare namespace MeRequest { - export type Settings = AuthenticatedRequest< - {}, - {}, - Pick - >; + export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>; export type Password = AuthenticatedRequest< {}, {}, diff --git a/packages/cli/test/setup-mocks.ts b/packages/cli/test/setup-mocks.ts index 1a9bb4e9c0..c6db2d147c 100644 --- a/packages/cli/test/setup-mocks.ts +++ b/packages/cli/test/setup-mocks.ts @@ -1,3 +1,5 @@ +import 'reflect-metadata'; + jest.mock('@sentry/node'); jest.mock('@n8n_io/license-sdk'); jest.mock('@/telemetry'); diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index 1606915398..de57d031a9 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -28,40 +28,74 @@ describe('MeController', () => { describe('updateCurrentUser', () => { it('should throw BadRequestError if email is missing in the payload', async () => { - const req = mock({}); + const req = mock({}); expect(controller.updateCurrentUser(req, mock())).rejects.toThrowError( new BadRequestError('Email is mandatory'), ); }); it('should throw BadRequestError if email is invalid', async () => { - const req = mock({ body: { email: 'invalid-email' } }); + const req = mock({ body: { email: 'invalid-email' } }); expect(controller.updateCurrentUser(req, mock())).rejects.toThrowError( new BadRequestError('Invalid email address'), ); }); it('should update the user in the DB, and issue a new cookie', async () => { - const req = mock({ - user: mock({ id: '123', password: 'password', authIdentities: [] }), - body: { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }, + const user = mock({ + id: '123', + password: 'password', + authIdentities: [], + globalRoleId: '1', }); + const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; + const req = mock({ user, body: reqBody }); const res = mock(); - userRepository.save.calledWith(anyObject()).mockResolvedValue(req.user); + userRepository.findOneOrFail.mockResolvedValue(user); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); await controller.updateCurrentUser(req, res); + expect(userRepository.update).toHaveBeenCalled(); + const cookieOptions = captor(); expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions); expect(cookieOptions.value.httpOnly).toBe(true); expect(cookieOptions.value.sameSite).toBe('lax'); expect(externalHooks.run).toHaveBeenCalledWith('user.profile.update', [ - req.user.email, + user.email, anyObject(), ]); }); + + it('should not allow updating any other fields on a user besides email and name', async () => { + const user = mock({ + id: '123', + password: 'password', + authIdentities: [], + globalRoleId: '1', + }); + const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; + const req = mock({ user, body: reqBody }); + const res = mock(); + userRepository.findOneOrFail.mockResolvedValue(user); + jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); + + // Add invalid data to the request payload + Object.assign(reqBody, { id: '0', globalRoleId: '42' }); + + await controller.updateCurrentUser(req, res); + + expect(userRepository.update).toHaveBeenCalled(); + + const updatedUser = userRepository.update.mock.calls[0][1]; + expect(updatedUser.email).toBe(reqBody.email); + expect(updatedUser.firstName).toBe(reqBody.firstName); + expect(updatedUser.lastName).toBe(reqBody.lastName); + expect(updatedUser.id).not.toBe('0'); + expect(updatedUser.globalRoleId).not.toBe('42'); + }); }); describe('updatePassword', () => { diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/test/unit/controllers/translation.controller.test.ts new file mode 100644 index 0000000000..a24d462f81 --- /dev/null +++ b/packages/cli/test/unit/controllers/translation.controller.test.ts @@ -0,0 +1,40 @@ +import { mock } from 'jest-mock-extended'; +import type { ICredentialTypes } from 'n8n-workflow'; +import type { Config } from '@/config'; +import { + TranslationController, + TranslationRequest, + CREDENTIAL_TRANSLATIONS_DIR, +} from '@/controllers/translation.controller'; +import { BadRequestError } from '@/ResponseHelper'; + +describe('TranslationController', () => { + const config = mock(); + const credentialTypes = mock(); + const controller = new TranslationController(config, credentialTypes); + + describe('getCredentialTranslation', () => { + it('should throw 400 on invalid credential types', async () => { + const credentialType = 'not-a-valid-credential-type'; + const req = mock({ query: { credentialType } }); + credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(false); + + expect(controller.getCredentialTranslation(req)).rejects.toThrowError( + new BadRequestError(`Invalid Credential type: "${credentialType}"`), + ); + }); + + it('should return translation json on valid credential types', async () => { + const credentialType = 'credential-type'; + const req = mock({ query: { credentialType } }); + config.getEnv.calledWith('defaultLocale').mockReturnValue('de'); + credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(true); + const response = { translation: 'string' }; + jest.mock(`${CREDENTIAL_TRANSLATIONS_DIR}/de/credential-type.json`, () => response, { + virtual: true, + }); + + expect(await controller.getCredentialTranslation(req)).toEqual(response); + }); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 96585c2491..60ec9df01b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.155.0", + "version": "0.155.1", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/core/src/BinaryDataManager/FileSystem.ts b/packages/core/src/BinaryDataManager/FileSystem.ts index b66093b3d2..df03356975 100644 --- a/packages/core/src/BinaryDataManager/FileSystem.ts +++ b/packages/core/src/BinaryDataManager/FileSystem.ts @@ -7,6 +7,7 @@ import type { BinaryMetadata } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow'; import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces'; +import { FileNotFoundError } from '../errors'; const PREFIX_METAFILE = 'binarymeta'; const PREFIX_PERSISTED_METAFILE = 'persistedmeta'; @@ -85,17 +86,17 @@ export class BinaryDataFileSystem implements IBinaryDataManager { } getBinaryPath(identifier: string): string { - return path.join(this.storagePath, identifier); + return this.resolveStoragePath(identifier); } getMetadataPath(identifier: string): string { - return path.join(this.storagePath, `${identifier}.metadata`); + return this.resolveStoragePath(`${identifier}.metadata`); } async markDataForDeletionByExecutionId(executionId: string): Promise { const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000); return fs.writeFile( - path.join(this.getBinaryDataMetaPath(), `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`), + this.resolveStoragePath('meta', `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`), '', ); } @@ -116,8 +117,8 @@ export class BinaryDataFileSystem implements IBinaryDataManager { const timeAtNextHour = currentTime + 3600000 - (currentTime % 3600000); const timeoutTime = timeAtNextHour + this.persistedBinaryDataTTL * 60000; - const filePath = path.join( - this.getBinaryDataPersistMetaPath(), + const filePath = this.resolveStoragePath( + 'persistMeta', `${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`, ); @@ -170,21 +171,18 @@ export class BinaryDataFileSystem implements IBinaryDataManager { const newBinaryDataId = this.generateFileName(prefix); return fs - .copyFile( - path.join(this.storagePath, binaryDataId), - path.join(this.storagePath, newBinaryDataId), - ) + .copyFile(this.resolveStoragePath(binaryDataId), this.resolveStoragePath(newBinaryDataId)) .then(() => newBinaryDataId); } async deleteBinaryDataByExecutionId(executionId: string): Promise { const regex = new RegExp(`${executionId}_*`); - const filenames = await fs.readdir(path.join(this.storagePath)); + const filenames = await fs.readdir(this.storagePath); const proms = filenames.reduce( (allProms, filename) => { if (regex.test(filename)) { - allProms.push(fs.rm(path.join(this.storagePath, filename))); + allProms.push(fs.rm(this.resolveStoragePath(filename))); } return allProms; @@ -253,4 +251,11 @@ export class BinaryDataFileSystem implements IBinaryDataManager { throw new Error(`Error finding file: ${filePath}`); } } + + private resolveStoragePath(...args: string[]) { + const returnPath = path.join(this.storagePath, ...args); + if (path.relative(this.storagePath, returnPath).startsWith('..')) + throw new FileNotFoundError('Invalid path detected'); + return returnPath; + } } diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts new file mode 100644 index 0000000000..c425675c89 --- /dev/null +++ b/packages/core/src/errors.ts @@ -0,0 +1,5 @@ +export class FileNotFoundError extends Error { + constructor(readonly filePath: string) { + super(`File not found: ${filePath}`); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cef794cfa5..7a77667f59 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,7 @@ export * from './LoadNodeListSearch'; export * from './NodeExecuteFunctions'; export * from './WorkflowExecute'; export { eventEmitter, NodeExecuteFunctions, UserSettings }; +export * from './errors'; declare module 'http' { export interface IncomingMessage { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 56c76aaa1c..7863b7193a 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.182.0", + "version": "0.182.1", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index b873b7c64f..0447533dbf 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.94.0", + "version": "0.94.1", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 73ef88e91f..f83852c5b2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.214.0", + "version": "0.214.1", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 0a89d973c0..ee1bc2cb27 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.137.0", + "version": "0.137.1", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5ef413748..137ae4a86e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,7 @@ importers: callsites: ^3.1.0 change-case: ^4.1.1 chokidar: 3.5.2 + class-transformer: ^0.5.1 class-validator: ^0.14.0 client-oauth2: ^4.2.5 compression: ^1.7.4 @@ -259,6 +260,7 @@ importers: bull: 4.10.2 callsites: 3.1.0 change-case: 4.1.2 + class-transformer: 0.5.1 class-validator: 0.14.0 client-oauth2: 4.3.3 compression: 1.7.4 @@ -4136,7 +4138,7 @@ packages: dependencies: '@storybook/client-logger': 7.0.0-beta.46 '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/global': 5.0.0 '@storybook/manager-api': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe '@storybook/preview-api': 7.0.0-beta.46 @@ -4344,7 +4346,7 @@ packages: '@storybook/client-logger': 7.0.0-beta.46 '@storybook/components': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/docs-tools': 7.0.0-beta.46 '@storybook/global': 5.0.0 '@storybook/manager-api': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe @@ -4564,7 +4566,7 @@ packages: '@babel/core': 7.20.12 '@babel/preset-env': 7.20.2_@babel+core@7.20.12 '@babel/types': 7.20.7 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/csf-tools': 7.0.0-beta.46 '@storybook/node-logger': 7.0.0-beta.46 '@storybook/types': 7.0.0-beta.46 @@ -4604,7 +4606,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/global': 5.0.0 '@storybook/theming': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe '@storybook/types': 7.0.0-beta.46 @@ -4675,7 +4677,7 @@ packages: '@storybook/builder-manager': 7.0.0-beta.46 '@storybook/core-common': 7.0.0-beta.46 '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/csf-tools': 7.0.0-beta.46 '@storybook/docs-mdx': 0.0.1-next.6 '@storybook/global': 5.0.0 @@ -4745,7 +4747,7 @@ packages: resolution: {integrity: sha512-H7zXfL1wf/1jWi5MaFISt/taxE41fgpV/uLfi5CHcHLX9ZgeQs2B/2utpUgwvBsxiL+E/jKAt5cLeuZCIvglMg==} dependencies: '@babel/types': 7.20.7 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/types': 7.0.0-beta.46 fs-extra: 11.1.0 recast: 0.23.1 @@ -4760,8 +4762,8 @@ packages: lodash: 4.17.21 dev: true - /@storybook/csf/0.0.2-next.9: - resolution: {integrity: sha512-ECOLMK425s+z8oA0aVAhBhhquuwTsZrM4oha/5De44JG8uYGXhqVrv/l27oxZEkwytuiQu+9f65HxYli+DY+3w==} + /@storybook/csf/0.0.2-next.10: + resolution: {integrity: sha512-m2PFgBP/xRIF85VrDhvesn9ktaD2pN3VUjvMqkAL/cINp/3qXsCyI81uw7N5VEOkQAbWrY2FcydnvEPDEdE8fA==} dependencies: type-fest: 2.19.0 dev: true @@ -4797,7 +4799,7 @@ packages: '@storybook/channels': 7.0.0-beta.46 '@storybook/client-logger': 7.0.0-beta.46 '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/global': 5.0.0 '@storybook/router': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe '@storybook/theming': 7.0.0-beta.46_6l5554ty5ajsajah6yazvrjhoe @@ -4887,7 +4889,7 @@ packages: '@storybook/channels': 7.0.0-beta.46 '@storybook/client-logger': 7.0.0-beta.46 '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.9 + '@storybook/csf': 0.0.2-next.10 '@storybook/global': 5.0.0 '@storybook/types': 7.0.0-beta.46 '@types/qs': 6.9.7 @@ -8088,6 +8090,10 @@ packages: resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} dev: false + /class-transformer/0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + dev: false + /class-utils/0.3.6: resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} engines: {node: '>=0.10.0'}