ci: Include THIRD_PARTY_LICENSES.md file with release (#18739)

This commit is contained in:
Artem Sorokin
2025-09-01 12:41:42 +02:00
committed by GitHub
parent daac88b3ec
commit 168ac0e9f2
16 changed files with 729 additions and 8 deletions

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ packages/testing/**/.cursor/rules/
.venv
.ruff_cache
__pycache__
packages/cli/THIRD_PARTY_LICENSES.md

View File

@@ -35,6 +35,7 @@
"lint:affected": "turbo run lint --affected",
"lint:fix": "turbo run lint:fix",
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"generate:third-party-licenses": "node scripts/generate-third-party-licenses.mjs",
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
"start": "run-script-os",
"start:default": "cd packages/cli/bin && ./n8n",
@@ -66,6 +67,7 @@
"jest-mock": "^29.6.2",
"jest-mock-extended": "^3.0.4",
"lefthook": "^1.7.15",
"license-checker": "^25.0.1",
"nock": "^14.0.1",
"nodemon": "^3.0.1",
"npm-run-all2": "^7.0.2",

View File

@@ -0,0 +1,5 @@
# Third-Party Licenses
**Note**: This is a placeholder file used during local development. The complete third-party licenses file is generated during production builds and contains detailed licensing information for all dependencies.
For the complete list of third-party licenses, please refer to the production build.

View File

@@ -0,0 +1,25 @@
import { CLI_DIR } from '@/constants';
import { Get, RestController } from '@n8n/decorators';
import { Request, Response } from 'express';
import { readFile } from 'fs/promises';
import { resolve } from 'path';
@RestController('/third-party-licenses')
export class ThirdPartyLicensesController {
/**
* Get third-party licenses content
* Requires authentication to access
*/
@Get('/')
async getThirdPartyLicenses(_: Request, res: Response) {
const licenseFile = resolve(CLI_DIR, 'THIRD_PARTY_LICENSES.md');
try {
const content = await readFile(licenseFile, 'utf-8');
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
res.send(content);
} catch {
res.status(404).send('Third-party licenses file not found');
}
}
}

View File

@@ -91,6 +91,7 @@ export class Server extends AbstractServer {
const { FrontendService } = await import('@/services/frontend.service');
this.frontendService = Container.get(FrontendService);
await import('@/controllers/module-settings.controller');
await import('@/controllers/third-party-licenses.controller');
}
this.presetCredentialsLoaded = false;

View File

@@ -0,0 +1,96 @@
import { createMember, createOwner } from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types';
import { setupTestServer } from '../shared/utils';
jest.mock('fs/promises', () => ({
readFile: jest.fn(),
}));
import { readFile } from 'fs/promises';
const mockReadFile = readFile as jest.MockedFunction<typeof readFile>;
describe('ThirdPartyLicensesController', () => {
const testServer = setupTestServer({ endpointGroups: ['third-party-licenses'] });
let ownerAgent: SuperAgentTest;
let memberAgent: SuperAgentTest;
beforeAll(async () => {
const owner = await createOwner();
const member = await createMember();
ownerAgent = testServer.authAgentFor(owner);
memberAgent = testServer.authAgentFor(member);
});
describe('GET /third-party-licenses', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should require authentication', async () => {
await testServer.authlessAgent.get('/third-party-licenses').expect(401);
});
describe('when license file exists', () => {
beforeEach(() => {
mockReadFile.mockResolvedValue('# Third Party Licenses\n\nSome license content...');
});
it('should allow authenticated owner to get third-party licenses', async () => {
const response = await ownerAgent.get('/third-party-licenses');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toMatch(/text\/markdown/);
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
});
it('should allow authenticated member to get third-party licenses', async () => {
const response = await memberAgent.get('/third-party-licenses');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toMatch(/text\/markdown/);
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
});
});
describe('when license file does not exist', () => {
beforeEach(() => {
mockReadFile.mockRejectedValue(new Error('ENOENT: no such file or directory'));
});
it('should return 404 for authenticated owner', async () => {
const response = await ownerAgent.get('/third-party-licenses');
expect(response.status).toBe(404);
expect(response.text).toBe('Third-party licenses file not found');
});
it('should return 404 for authenticated member', async () => {
const response = await memberAgent.get('/third-party-licenses');
expect(response.status).toBe(404);
expect(response.text).toBe('Third-party licenses file not found');
});
});
describe('when file read fails with other errors', () => {
beforeEach(() => {
mockReadFile.mockRejectedValue(new Error('EACCES: permission denied'));
});
it('should return 404 for permission errors', async () => {
const response = await ownerAgent.get('/third-party-licenses');
expect(response.status).toBe(404);
expect(response.text).toBe('Third-party licenses file not found');
});
});
describe('file path resolution', () => {
it('should request the correct file path', async () => {
mockReadFile.mockResolvedValue('test content');
await ownerAgent.get('/third-party-licenses');
expect(mockReadFile).toHaveBeenCalledWith(
expect.stringMatching(/THIRD_PARTY_LICENSES\.md$/),
'utf-8',
);
});
});
});
});

View File

@@ -43,8 +43,10 @@ type EndpointGroup =
| 'ai'
| 'folder'
| 'insights'
| 'data-store'
| 'module-settings'
| 'data-table'
| 'module-settings';
| 'third-party-licenses';
type ModuleName = 'insights' | 'external-secrets' | 'community-packages' | 'data-table';

View File

@@ -313,6 +313,10 @@ export const setupTestServer = ({
case 'module-settings':
await import('@/controllers/module-settings.controller');
break;
case 'third-party-licenses':
await import('@/controllers/third-party-licenses.controller');
break;
}
}

View File

@@ -123,6 +123,9 @@
"about.debug.message": "Copy debug information",
"about.debug.toast.title": "Debug info",
"about.debug.toast.message": "Copied debug info to clipboard",
"about.thirdPartyLicenses": "Third-Party Licenses",
"about.thirdPartyLicensesLink": "View all third-party licenses",
"about.thirdPartyLicenses.downloadError": "Failed to download third-party licenses file",
"askAi.dialog.title": "'Ask AI' is almost ready",
"askAi.dialog.body": "Were still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.",
"askAi.dialog.signup": "Join Waitlist",

View File

@@ -16,6 +16,7 @@ export * from './module-settings';
export * from './sso';
export type * from './tags';
export * from './templates';
export * from './third-party-licenses';
export * from './ui';
export * from './users';
export * from './versions';

View File

@@ -0,0 +1,10 @@
import type { IRestApiContext } from '../types';
import { request } from '../utils';
export async function getThirdPartyLicenses(context: IRestApiContext): Promise<string> {
return await request({
method: 'GET',
baseURL: context.baseUrl,
endpoint: '/third-party-licenses',
});
}

View File

@@ -7,6 +7,7 @@ import { useToast } from '@/composables/useToast';
import { useClipboard } from '@/composables/useClipboard';
import { useDebugInfo } from '@/composables/useDebugInfo';
import { useI18n } from '@n8n/i18n';
import { getThirdPartyLicenses } from '@n8n/rest-api-client';
const modalBus = createEventBus();
const toast = useToast();
@@ -19,6 +20,23 @@ const closeDialog = () => {
modalBus.emit('close');
};
const downloadThirdPartyLicenses = async () => {
try {
const thirdPartyLicenses = await getThirdPartyLicenses(rootStore.restApiContext);
const blob = new File([thirdPartyLicenses], 'THIRD_PARTY_LICENSES.md', {
type: 'text/markdown',
});
window.open(URL.createObjectURL(blob));
} catch (error) {
toast.showToast({
title: i18n.baseText('about.thirdPartyLicenses.downloadError'),
message: error.message,
type: 'error',
});
}
};
const copyDebugInfoToClipboard = async () => {
toast.showToast({
title: i18n.baseText('about.debug.toast.title'),
@@ -66,6 +84,16 @@ const copyDebugInfoToClipboard = async () => {
</n8n-link>
</el-col>
</el-row>
<el-row>
<el-col :span="8" class="info-name">
<n8n-text>{{ i18n.baseText('about.thirdPartyLicenses') }}</n8n-text>
</el-col>
<el-col :span="16">
<n8n-link @click="downloadThirdPartyLicenses">
{{ i18n.baseText('about.thirdPartyLicensesLink') }}
</n8n-link>
</el-col>
</el-row>
<el-row>
<el-col :span="8" class="info-name">
<n8n-text>{{ i18n.baseText('about.instanceID') }}</n8n-text>

226
pnpm-lock.yaml generated
View File

@@ -299,6 +299,9 @@ importers:
lefthook:
specifier: ^1.7.15
version: 1.7.15
license-checker:
specifier: ^25.0.1
version: 25.0.1
nock:
specifier: ^14.0.1
version: 14.0.1
@@ -8366,6 +8369,10 @@ packages:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -8452,6 +8459,10 @@ packages:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'}
array-find-index@1.0.2:
resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==}
engines: {node: '>=0.10.0'}
array-hyper-unique@2.1.4:
resolution: {integrity: sha512-RVsGx2YpFGhGpgdkK7A0VjFQecVUCowpkQerGCsyXVRXHxccAlPPTDt9ueF/X7Zq/6z6duZ49i9WzTCzcnQygQ==}
@@ -8998,6 +9009,10 @@ packages:
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
engines: {node: '>=12'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
chalk@3.0.0:
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
engines: {node: '>=8'}
@@ -9743,6 +9758,10 @@ packages:
supports-color:
optional: true
debuglog@1.0.1:
resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
@@ -11214,6 +11233,9 @@ packages:
hookified@1.11.0:
resolution: {integrity: sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==}
hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
html-encoding-sniffer@3.0.0:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'}
@@ -12386,6 +12408,10 @@ packages:
libqp@2.0.1:
resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==}
license-checker@25.0.1:
resolution: {integrity: sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==}
hasBin: true
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
@@ -13319,6 +13345,10 @@ packages:
resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
hasBin: true
nopt@4.0.3:
resolution: {integrity: sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==}
hasBin: true
nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
@@ -13329,6 +13359,9 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
hasBin: true
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -13340,6 +13373,9 @@ packages:
normalize-wheel-es@1.2.0:
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
npm-normalize-package-bin@1.0.1:
resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==}
npm-normalize-package-bin@4.0.0:
resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -13535,6 +13571,18 @@ packages:
os-browserify@0.3.0:
resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==}
os-homedir@1.0.2:
resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==}
engines: {node: '>=0.10.0'}
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
osenv@0.1.5:
resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==}
deprecated: This package is no longer supported.
ospath@1.2.2:
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
@@ -14368,10 +14416,18 @@ packages:
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
read-installed@4.0.3:
resolution: {integrity: sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==}
deprecated: This package is no longer supported.
read-package-json-fast@4.0.0:
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
engines: {node: ^18.17.0 || >=20.5.0}
read-package-json@2.1.2:
resolution: {integrity: sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==}
deprecated: This package is no longer supported. Please use @npmcli/package-json instead.
readable-stream@1.1.14:
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
@@ -14400,6 +14456,10 @@ packages:
readdir-glob@1.1.3:
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
readdir-scoped-modules@1.1.0:
resolution: {integrity: sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==}
deprecated: This functionality has been moved to @npmcli/fs
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@@ -14955,6 +15015,9 @@ packages:
slick@1.12.2:
resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
slide@1.1.6:
resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==}
slugify@1.4.7:
resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==}
engines: {node: '>=8.0.0'}
@@ -15010,6 +15073,27 @@ packages:
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
spdx-compare@1.0.0:
resolution: {integrity: sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==}
spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
spdx-exceptions@2.5.0:
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
spdx-expression-parse@3.0.1:
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
spdx-license-ids@3.0.22:
resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
spdx-ranges@2.1.1:
resolution: {integrity: sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==}
spdx-satisfies@4.0.1:
resolution: {integrity: sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==}
spex@3.3.0:
resolution: {integrity: sha512-VNiXjFp6R4ldPbVRYbpxlD35yRHceecVXlct1J4/X80KuuPnW2AXMq3sGwhnJOhKkUsOxAT6nRGfGE5pocVw5w==}
engines: {node: '>=10.0.0'}
@@ -15602,6 +15686,10 @@ packages:
tree-sitter@0.21.1:
resolution: {integrity: sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==}
treeify@1.1.0:
resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==}
engines: {node: '>=0.6'}
triple-beam@1.3.0:
resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==}
@@ -16085,6 +16173,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
util-extend@1.0.3:
resolution: {integrity: sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==}
util@0.12.5:
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
@@ -16134,6 +16225,9 @@ packages:
resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
engines: {node: '>=10'}
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
validator@13.7.0:
resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==}
engines: {node: '>= 0.10'}
@@ -23400,6 +23494,10 @@ snapshots:
ansi-regex@6.0.1: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -23489,6 +23587,8 @@ snapshots:
call-bound: 1.0.4
is-array-buffer: 3.0.5
array-find-index@1.0.2: {}
array-hyper-unique@2.1.4:
dependencies:
deep-eql: 4.0.0
@@ -24196,6 +24296,12 @@ snapshots:
loupe: 3.1.4
pathval: 2.0.0
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
chalk@3.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -25047,6 +25153,8 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
debuglog@1.0.1: {}
decamelize@1.2.0: {}
decamelize@4.0.0: {}
@@ -25738,7 +25846,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
is-core-module: 2.16.1
resolve: 1.22.10
transitivePeerDependencies:
@@ -25762,7 +25870,7 @@ snapshots:
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@1.21.7))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.29.0(jiti@1.21.7)):
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
'@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@1.21.7))(typescript@5.9.2)
eslint: 9.29.0(jiti@1.21.7)
@@ -25801,7 +25909,7 @@ snapshots:
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
doctrine: 2.1.0
eslint: 9.29.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
@@ -26760,7 +26868,7 @@ snapshots:
array-parallel: 0.1.3
array-series: 0.1.5
cross-spawn: 7.0.6
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -26942,6 +27050,8 @@ snapshots:
hookified@1.11.0: {}
hosted-git-info@2.8.9: {}
html-encoding-sniffer@3.0.0:
dependencies:
whatwg-encoding: 2.0.0
@@ -27111,7 +27221,7 @@ snapshots:
'@types/debug': 4.1.12
'@types/node': 20.19.11
'@types/tough-cookie': 4.0.5
axios: 1.11.0(debug@4.4.1)
axios: 1.11.0(debug@4.3.6)
camelcase: 6.3.0
debug: 4.4.1(supports-color@8.1.1)
dotenv: 16.6.1
@@ -28530,6 +28640,21 @@ snapshots:
libqp@2.0.1: {}
license-checker@25.0.1:
dependencies:
chalk: 2.4.2
debug: 3.2.7(supports-color@8.1.1)
mkdirp: 0.5.6
nopt: 4.0.3
read-installed: 4.0.3
semver: 7.7.2
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
spdx-satisfies: 4.0.1
treeify: 1.1.0
transitivePeerDependencies:
- supports-color
lie@3.3.0:
dependencies:
immediate: 3.0.6
@@ -29714,6 +29839,11 @@ snapshots:
dependencies:
abbrev: 1.1.1
nopt@4.0.3:
dependencies:
abbrev: 1.1.1
osenv: 0.1.5
nopt@5.0.0:
dependencies:
abbrev: 1.1.1
@@ -29723,12 +29853,21 @@ snapshots:
dependencies:
abbrev: 1.1.1
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
resolve: 1.22.10
semver: 7.7.2
validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
normalize-wheel-es@1.2.0: {}
npm-normalize-package-bin@1.0.1: {}
npm-normalize-package-bin@4.0.0: {}
npm-run-all2@7.0.2:
@@ -29973,6 +30112,15 @@ snapshots:
os-browserify@0.3.0: {}
os-homedir@1.0.2: {}
os-tmpdir@1.0.2: {}
osenv@0.1.5:
dependencies:
os-homedir: 1.0.2
os-tmpdir: 1.0.2
ospath@1.2.2: {}
otpauth@9.1.1:
@@ -30165,7 +30313,7 @@ snapshots:
pdf-parse@1.1.1:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
node-ensure: 0.0.0
transitivePeerDependencies:
- supports-color
@@ -30828,11 +30976,29 @@ snapshots:
dependencies:
pify: 2.3.0
read-installed@4.0.3:
dependencies:
debuglog: 1.0.1
read-package-json: 2.1.2
readdir-scoped-modules: 1.1.0
semver: 7.7.2
slide: 1.1.6
util-extend: 1.0.3
optionalDependencies:
graceful-fs: 4.2.11
read-package-json-fast@4.0.0:
dependencies:
json-parse-even-better-errors: 4.0.0
npm-normalize-package-bin: 4.0.0
read-package-json@2.1.2:
dependencies:
glob: 7.2.3
json-parse-even-better-errors: 2.3.1
normalize-package-data: 2.5.0
npm-normalize-package-bin: 1.0.1
readable-stream@1.1.14:
dependencies:
core-util-is: 1.0.3
@@ -30890,6 +31056,13 @@ snapshots:
dependencies:
minimatch: 5.1.6
readdir-scoped-modules@1.1.0:
dependencies:
debuglog: 1.0.1
dezalgo: 1.0.4
graceful-fs: 4.2.11
once: 1.4.0
readdirp@4.1.2: {}
readline-sync@1.4.10: {}
@@ -31159,7 +31332,7 @@ snapshots:
rhea@1.0.24:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -31646,6 +31819,8 @@ snapshots:
slick@1.12.2: {}
slide@1.1.6: {}
slugify@1.4.7: {}
smart-buffer@4.2.0:
@@ -31742,6 +31917,34 @@ snapshots:
spawn-command@0.0.2: {}
spdx-compare@1.0.0:
dependencies:
array-find-index: 1.0.2
spdx-expression-parse: 3.0.1
spdx-ranges: 2.1.1
spdx-correct@3.2.0:
dependencies:
spdx-expression-parse: 3.0.1
spdx-license-ids: 3.0.22
spdx-exceptions@2.5.0: {}
spdx-expression-parse@3.0.1:
dependencies:
spdx-exceptions: 2.5.0
spdx-license-ids: 3.0.22
spdx-license-ids@3.0.22: {}
spdx-ranges@2.1.1: {}
spdx-satisfies@4.0.1:
dependencies:
spdx-compare: 1.0.0
spdx-expression-parse: 3.0.1
spdx-ranges: 2.1.1
spex@3.3.0: {}
split-ca@1.0.1: {}
@@ -32501,6 +32704,8 @@ snapshots:
node-addon-api: 8.3.0
node-gyp-build: 4.8.4
treeify@1.1.0: {}
triple-beam@1.3.0: {}
ts-api-utils@1.4.3(typescript@5.9.2):
@@ -33023,6 +33228,8 @@ snapshots:
util-deprecate@1.0.2: {}
util-extend@1.0.3: {}
util@0.12.5:
dependencies:
inherits: 2.0.4
@@ -33064,6 +33271,11 @@ snapshots:
valid-data-url@3.0.1: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
validator@13.7.0: {}
vary@1.1.2: {}

View File

@@ -97,7 +97,20 @@ try {
buildProcess.pipe(process.stdout);
await buildProcess;
// Generate third-party licenses for production build
echo(chalk.yellow('INFO: Generating third-party licenses...'));
try {
const licenseProcess = $`cd ${config.rootDir} && node scripts/generate-third-party-licenses.mjs`;
licenseProcess.pipe(process.stdout);
await licenseProcess;
echo(chalk.green('✅ Third-party licenses generated successfully'));
} catch (error) {
echo(chalk.yellow('⚠️ Warning: Third-party license generation failed, continuing build...'));
echo(chalk.red(`ERROR: License generation failed: ${error.message}`));
}
echo(chalk.green('✅ pnpm install and build completed'));
} catch (error) {
console.error(chalk.red('\n🛑 BUILD PROCESS FAILED!'));
console.error(chalk.red('An error occurred during the build process:'));

View File

@@ -0,0 +1,306 @@
#!/usr/bin/env node
/**
* Third-Party License Generator for n8n
*
* Generates THIRD_PARTY_LICENSES.md by scanning all dependencies using license-checker,
* extracting license information, and formatting it into a markdown report.
*
* Usage: node scripts/generate-third-party-licenses.mjs
*/
import { $, echo, fs, chalk } from 'zx';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
// Disable verbose zx output
$.verbose = false;
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(scriptDir, '..');
const config = {
tempLicenseFile: 'licenses.json',
outputFile: 'THIRD_PARTY_LICENSES.md',
invalidLicenseFiles: ['readme.md', 'readme.txt', 'readme', 'package.json', 'changelog.md', 'history.md'],
validLicenseFiles: ['license', 'licence', 'copying', 'copyright', 'unlicense'],
paths: {
root: rootDir,
cliRoot: path.join(rootDir, 'packages', 'cli'),
formatConfig: path.join(scriptDir, 'third-party-license-format.json'),
tempLicenses: path.join(os.tmpdir(), 'licenses.json'),
output: path.join(rootDir, 'packages', 'cli', 'THIRD_PARTY_LICENSES.md'),
},
};
// #region ===== Helper Functions =====
async function generateLicenseData() {
echo(chalk.yellow('📊 Running license-checker...'));
try {
$.cwd = config.paths.root;
await $`pnpm exec license-checker --json --customPath ${config.paths.formatConfig}`.pipe(
fs.createWriteStream(config.paths.tempLicenses),
);
echo(chalk.green('✅ License data collected'));
return config.paths.tempLicenses;
} catch (error) {
echo(chalk.red('❌ Failed to run license-checker'));
throw error;
}
}
async function readLicenseData(filePath) {
try {
const data = await fs.readFile(filePath, 'utf-8');
const parsed = JSON.parse(data);
echo(chalk.green(`✅ Parsed ${Object.keys(parsed).length} packages`));
return parsed;
} catch (error) {
echo(chalk.red('❌ Failed to parse license data'));
throw error;
}
}
function parsePackageKey(packageKey) {
const lastAtIndex = packageKey.lastIndexOf('@');
return {
packageName: packageKey.substring(0, lastAtIndex),
version: packageKey.substring(lastAtIndex + 1),
};
}
function shouldExcludePackage(packageName) {
const n8nPatterns = [
/^@n8n\//, // @n8n/package
/^@n8n_/, // @n8n_io/package
/^n8n-/, // n8n-package
/-n8n/ // package-n8n
];
return n8nPatterns.some(pattern => pattern.test(packageName));
}
function isValidLicenseFile(filePath) {
if (!filePath) return false;
const fileName = path.basename(filePath).toLowerCase();
// Exclude non-license files
const isInvalidFile = config.invalidLicenseFiles.some((invalid) =>
fileName === invalid || fileName.endsWith(invalid)
);
if (isInvalidFile) return false;
// Must contain license-related keywords
return config.validLicenseFiles.some((valid) => fileName.includes(valid));
}
function getFallbackLicenseText(licenseType, packages = []) {
const fallbacks = {
'CC-BY-3.0': 'Creative Commons Attribution 3.0 Unported License\n\nFull license text available at: https://creativecommons.org/licenses/by/3.0/legalcode',
'LGPL-3.0-or-later': 'GNU Lesser General Public License v3.0 or later\n\nFull license text available at: https://www.gnu.org/licenses/lgpl-3.0.html',
'PSF': 'Python Software Foundation License\n\nFull license text available at: https://docs.python.org/3/license.html',
'(MIT OR CC0-1.0)': 'Licensed under MIT OR CC0-1.0\n\nMIT License full text available at: https://opensource.org/licenses/MIT\nCC0 1.0 Universal full text available at: https://creativecommons.org/publicdomain/zero/1.0/legalcode',
'UNKNOWN': `License information not available for the following packages:\n${packages.map(pkg => `- ${pkg.name} ${pkg.version}`).join('\n')}\n\nPlease check individual package repositories for license details.`,
};
// Check for custom licenses that start with "Custom:"
if (licenseType.startsWith('Custom:')) {
return `Custom license. See: ${licenseType.replace('Custom: ', '')}`;
}
return fallbacks[licenseType] || null;
}
function cleanLicenseText(text) {
return text
.replaceAll('\\n', '\n')
.replaceAll('\\"', '"')
.replaceAll('\r\n', '\n')
.trim();
}
function addPackageToGroup(licenseGroups, licenseType, packageInfo) {
if (!licenseGroups.has(licenseType)) {
licenseGroups.set(licenseType, []);
}
licenseGroups.get(licenseType).push(packageInfo);
}
function processLicenseText(licenseTexts, licenseType, pkg) {
if (!licenseTexts.has(licenseType)) {
licenseTexts.set(licenseType, null);
}
if (!licenseTexts.get(licenseType) && pkg.licenseText?.trim() && isValidLicenseFile(pkg.licenseFile)) {
licenseTexts.set(licenseType, cleanLicenseText(pkg.licenseText));
}
}
function applyFallbackLicenseTexts(licenseTexts, licenseGroups) {
const missingTexts = [];
const fallbacksUsed = [];
for (const [licenseType, text] of licenseTexts.entries()) {
if (!text || !text.trim()) {
const packagesForLicense = licenseGroups.get(licenseType) || [];
const fallback = getFallbackLicenseText(licenseType, packagesForLicense);
if (fallback) {
licenseTexts.set(licenseType, fallback);
fallbacksUsed.push(licenseType);
} else {
missingTexts.push(licenseType);
}
}
}
return { missingTexts, fallbacksUsed };
}
function logProcessingResults(processedCount, licenseGroupCount, fallbacksUsed, missingTexts) {
echo(chalk.cyan(`📦 Processed ${processedCount} packages in ${licenseGroupCount} license groups`));
if (fallbacksUsed.length > 0) {
echo(chalk.blue(` Used fallback texts for: ${fallbacksUsed.join(', ')}`));
}
if (missingTexts.length > 0) {
echo(chalk.yellow(`⚠️ Still missing license texts for: ${missingTexts.join(', ')}`));
} else {
echo(chalk.green(`✅ All license types have texts`));
}
}
function processPackages(packages) {
const licenseGroups = new Map();
const licenseTexts = new Map();
let processedCount = 0;
for (const [packageKey, pkg] of Object.entries(packages)) {
const { packageName, version } = parsePackageKey(packageKey);
if (shouldExcludePackage(packageName)) {
continue;
}
const licenseType = pkg.licenses || 'Unknown';
processedCount++;
// Group packages by license
addPackageToGroup(licenseGroups, licenseType, {
name: packageName,
version,
repository: pkg.repository,
copyright: pkg.copyright,
});
// Store license text (use first non-empty occurrence)
processLicenseText(licenseTexts, licenseType, pkg);
}
// Apply fallback license texts for missing ones
const { missingTexts, fallbacksUsed } = applyFallbackLicenseTexts(licenseTexts, licenseGroups);
logProcessingResults(processedCount, licenseGroups.size, fallbacksUsed, missingTexts);
return { licenseGroups, licenseTexts, processedCount };
}
// #endregion ===== Helper Functions =====
// #region ===== Document Generation =====
function createPackageSection(licenseType, packages) {
const sortedPackages = [...packages].sort((a, b) => a.name.localeCompare(b.name));
let section = `## ${licenseType}\n\n`;
for (const pkg of sortedPackages) {
section += `* ${pkg.name} ${pkg.version}`;
if (pkg.copyright) {
section += `, ${pkg.copyright}`;
}
section += '\n';
}
section += '\n';
return section;
}
function createLicenseTextSection(licenseType, licenseText) {
let section = `## ${licenseType} License Text\n\n`;
if (licenseText && licenseText.trim()) {
section += `\`\`\`\n${licenseText}\n\`\`\`\n\n`;
} else {
section += `${licenseType} license text not available.\n\n`;
}
return section;
}
function createDocumentHeader() {
return `# Third-Party Licenses
This file lists third-party software components included in n8n and their respective license terms.
The n8n software includes open source packages, libraries, and modules, each of which is subject to its own license. The following sections list those dependencies and provide required attributions and license texts.
`;
}
function buildMarkdownDocument(packages) {
const { licenseGroups, licenseTexts, processedCount } = processPackages(packages);
let document = createDocumentHeader();
const sortedLicenseTypes = [...licenseGroups.keys()].sort();
// First: Add all package sections
for (const licenseType of sortedLicenseTypes) {
const packages = licenseGroups.get(licenseType);
document += createPackageSection(licenseType, packages);
}
// Second: Add license texts section
document += '# License Texts\n\n';
for (const licenseType of sortedLicenseTypes) {
const licenseText = licenseTexts.get(licenseType);
document += createLicenseTextSection(licenseType, licenseText);
}
return { content: document, processedCount };
}
// #endregion ===== Document Generation =====
async function generateThirdPartyLicenses() {
echo(chalk.blue('🚀 Generating third-party licenses for n8n...'));
try {
const licensesJsonPath = await generateLicenseData();
const packages = await readLicenseData(licensesJsonPath);
echo(chalk.yellow('📝 Building markdown document...'));
const { content, processedCount } = buildMarkdownDocument(packages);
await fs.ensureDir(config.paths.cliRoot);
await fs.writeFile(config.paths.output, content);
// Clean up temporary file
await fs.remove(licensesJsonPath);
echo(chalk.green('\n🎉 License generation completed successfully!'));
echo(chalk.green(`📄 Output: ${config.paths.output}`));
echo(chalk.green(`📦 Packages: ${processedCount}`));
} catch (error) {
echo(chalk.red(`\n❌ Generation failed: ${error.message}`));
process.exit(1);
}
}
generateThirdPartyLicenses();

View File

@@ -0,0 +1,12 @@
{
"name": "",
"version": "",
"description": "",
"licenses": "",
"copyright": "",
"licenseFile": "",
"licenseText": "",
"licenseModified": "",
"repository": "",
"url": ""
}