mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-02 17:57:06 +02:00
fix: Resolve all external licenses in release SBOM (#31231)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
This commit is contained in:
parent
3d452f7cb9
commit
eff29ce1ba
8
.github/scripts/package.json
vendored
8
.github/scripts/package.json
vendored
|
|
@ -1,11 +1,14 @@
|
|||
{
|
||||
"name": "workflow-scripts",
|
||||
"scripts": {
|
||||
"test": "node --test --experimental-test-module-mocks ./*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs",
|
||||
"generate-sbom": "cdxgen -t pnpm --no-install-deps -o ../../sbom-source.cdx.json ../../compiled/"
|
||||
"test": "node --test --experimental-test-module-mocks ./*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs ../../scripts/licenses/*.test.mjs",
|
||||
"generate-sbom": "FETCH_LICENSE=true cdxgen -t pnpm --no-install-deps --profile license-compliance -o ../../sbom-source.cdx.json ../../compiled/",
|
||||
"render-licenses-md": "node ../../scripts/licenses/render-licenses-md.mjs ../../sbom-source.cdx.json ../../packages/cli/THIRD_PARTY_LICENSES.md ../../compiled/node_modules",
|
||||
"generate-licenses": "pnpm generate-sbom && pnpm render-licenses-md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/github": "9.0.0",
|
||||
"@cyclonedx/cdxgen": "12.4.0",
|
||||
"@octokit/core": "7.0.6",
|
||||
"conventional-changelog": "7.2.0",
|
||||
"debug": "4.4.3",
|
||||
|
|
@ -16,7 +19,6 @@
|
|||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cyclonedx/cdxgen": "12.4.0",
|
||||
"conventional-changelog-angular": "8.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
.github/scripts/pnpm-lock.yaml
vendored
103
.github/scripts/pnpm-lock.yaml
vendored
|
|
@ -11,6 +11,9 @@ importers:
|
|||
'@actions/github':
|
||||
specifier: 9.0.0
|
||||
version: 9.0.0
|
||||
'@cyclonedx/cdxgen':
|
||||
specifier: 12.4.0
|
||||
version: 12.4.0
|
||||
'@octokit/core':
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
|
|
@ -36,9 +39,6 @@ importers:
|
|||
specifier: ^2.8.3
|
||||
version: 2.8.3
|
||||
devDependencies:
|
||||
'@cyclonedx/cdxgen':
|
||||
specifier: 12.4.0
|
||||
version: 12.4.0
|
||||
conventional-changelog-angular:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
|
|
@ -68,24 +68,24 @@ packages:
|
|||
resolution: {integrity: sha512-mTaD3YA1pJeEom8oyKjQ7wmfR+kDBZss15+aBIZ83gYkWFlgT9rWSM1cMsAcBJLTR9vcHIQ/bCo/JZewXkHN5Q==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/generator@7.29.1':
|
||||
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
|
||||
'@babel/generator@7.29.7':
|
||||
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-globals@7.28.0':
|
||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
||||
'@babel/helper-globals@7.29.7':
|
||||
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
'@babel/helper-string-parser@7.29.7':
|
||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
'@babel/helper-validator-identifier@7.29.7':
|
||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.3':
|
||||
|
|
@ -93,16 +93,21 @@ packages:
|
|||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||
'@babel/parser@7.29.7':
|
||||
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.29.0':
|
||||
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
'@babel/types@7.29.7':
|
||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bufbuild/protobuf@2.12.0':
|
||||
|
|
@ -564,8 +569,8 @@ packages:
|
|||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
es-object-atoms@1.1.2:
|
||||
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
escalade@3.2.0:
|
||||
|
|
@ -1065,8 +1070,8 @@ packages:
|
|||
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.25.0:
|
||||
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
|
||||
undici@7.26.0:
|
||||
resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
universal-user-agent@7.0.3:
|
||||
|
|
@ -1209,52 +1214,56 @@ snapshots:
|
|||
'@bufbuild/protobuf': 2.12.0
|
||||
optional: true
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/generator@7.29.1':
|
||||
'@babel/generator@7.29.7':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.3
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-globals@7.28.0': {}
|
||||
'@babel/helper-globals@7.29.7': {}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
'@babel/helper-string-parser@7.29.7': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
'@babel/helper-validator-identifier@7.29.7': {}
|
||||
|
||||
'@babel/parser@7.29.3':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
'@babel/parser@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/parser': 7.29.3
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/traverse@7.29.0':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/helper-globals': 7.28.0
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/helper-globals': 7.29.7
|
||||
'@babel/parser': 7.29.3
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
'@babel/types@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@bufbuild/protobuf@2.12.0':
|
||||
optional: true
|
||||
|
|
@ -1611,7 +1620,7 @@ snapshots:
|
|||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 7.1.0
|
||||
parse5-parser-stream: 7.1.2
|
||||
undici: 7.25.0
|
||||
undici: 7.26.0
|
||||
whatwg-mimetype: 4.0.0
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
|
@ -1800,7 +1809,7 @@ snapshots:
|
|||
es-errors@1.3.0:
|
||||
optional: true
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
es-object-atoms@1.1.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
optional: true
|
||||
|
|
@ -1847,7 +1856,7 @@ snapshots:
|
|||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
es-object-atoms: 1.1.2
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
|
|
@ -1859,7 +1868,7 @@ snapshots:
|
|||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
es-object-atoms: 1.1.2
|
||||
optional: true
|
||||
|
||||
get-stream@9.0.1:
|
||||
|
|
@ -2307,7 +2316,7 @@ snapshots:
|
|||
|
||||
undici@6.24.1: {}
|
||||
|
||||
undici@7.25.0: {}
|
||||
undici@7.26.0: {}
|
||||
|
||||
universal-user-agent@7.0.3: {}
|
||||
|
||||
|
|
|
|||
22
.github/workflows/sbom-generation-callable.yml
vendored
22
.github/workflows/sbom-generation-callable.yml
vendored
|
|
@ -36,7 +36,10 @@ jobs:
|
|||
name: Generate and Attach SBOM to Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
continue-on-error: true
|
||||
# No continue-on-error: SBOM is now the source of truth for
|
||||
# THIRD_PARTY_LICENSES.md (served by the /third-party-licenses endpoint
|
||||
# and attached to the GitHub release). A generation failure must block
|
||||
# the release rather than silently shipping a stale or missing manifest.
|
||||
steps:
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
|
@ -47,13 +50,11 @@ jobs:
|
|||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: 'pnpm build:deploy'
|
||||
|
||||
- name: Install isolated SBOM tooling
|
||||
run: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
|
||||
|
||||
- name: Generate CycloneDX SBOM from shipped artifact
|
||||
working-directory: ./.github/scripts
|
||||
run: pnpm generate-sbom
|
||||
env:
|
||||
# Opt in to SBOM + THIRD_PARTY_LICENSES.md generation. Regular CI Docker
|
||||
# builds skip it; only this release SBOM job and explicit local opt-ins
|
||||
# produce sbom-source.cdx.json + packages/cli/THIRD_PARTY_LICENSES.md.
|
||||
N8N_GENERATE_LICENSES: 'true'
|
||||
|
||||
- name: Attest SBOM for release
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
|
|
@ -66,11 +67,10 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG_REF: ${{ inputs.release_tag_ref }}
|
||||
run: |
|
||||
cp compiled/THIRD_PARTY_LICENSES.md THIRD_PARTY_LICENSES.md
|
||||
gh release upload "$RELEASE_TAG_REF" \
|
||||
sbom-source.cdx.json \
|
||||
security/vex.openvex.json \
|
||||
THIRD_PARTY_LICENSES.md \
|
||||
packages/cli/THIRD_PARTY_LICENSES.md \
|
||||
--clobber
|
||||
|
||||
COMPONENT_COUNT=$(jq '.components | length' sbom-source.cdx.json 2>/dev/null || echo "unknown")
|
||||
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
echo " - SBOM: $COMPONENT_COUNT components from shipped artifact (no devDeps, no optional deps)"
|
||||
echo " - SBOM: $MISSING_LICENSES components missing license metadata"
|
||||
echo " - VEX: $VEX_STATEMENTS CVE statements"
|
||||
echo " - THIRD_PARTY_LICENSES.md: human-readable license breakdown"
|
||||
echo " - THIRD_PARTY_LICENSES.md: rendered from SBOM (same artefact served by /third-party-licenses endpoint)"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: failure()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:ci": "turbo run lint lint:styles",
|
||||
"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": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n",
|
||||
"test": "JEST_JUNIT_CLASSNAME={filepath} turbo run test",
|
||||
|
|
@ -77,7 +76,6 @@
|
|||
"jest-mock": "^29.6.2",
|
||||
"jest-mock-extended": "^3.0.4",
|
||||
"lefthook": "^1.7.15",
|
||||
"license-checker": "^25.0.1",
|
||||
"nock": "^14.0.14",
|
||||
"nodemon": "^3.0.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
|
|
|
|||
212
pnpm-lock.yaml
212
pnpm-lock.yaml
|
|
@ -650,9 +650,6 @@ importers:
|
|||
lefthook:
|
||||
specifier: ^1.7.15
|
||||
version: 1.7.15
|
||||
license-checker:
|
||||
specifier: ^25.0.1
|
||||
version: 25.0.1
|
||||
nock:
|
||||
specifier: ^14.0.14
|
||||
version: 14.0.14
|
||||
|
|
@ -11990,10 +11987,6 @@ 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'}
|
||||
|
|
@ -12093,10 +12086,6 @@ 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==}
|
||||
|
||||
|
|
@ -12659,10 +12648,6 @@ packages:
|
|||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
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'}
|
||||
|
|
@ -13445,10 +13430,6 @@ 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'}
|
||||
|
|
@ -15064,9 +15045,6 @@ 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==}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -16216,10 +16194,6 @@ packages:
|
|||
cpu: [x64, arm64, wasm32, arm]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
license-checker@25.0.1:
|
||||
resolution: {integrity: sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==}
|
||||
hasBin: true
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
|
|
@ -17533,10 +17507,6 @@ 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'}
|
||||
|
|
@ -17552,9 +17522,6 @@ packages:
|
|||
engines: {node: ^20.17.0 || >=22.9.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'}
|
||||
|
|
@ -17570,9 +17537,6 @@ 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}
|
||||
|
|
@ -17785,18 +17749,6 @@ 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.
|
||||
|
||||
otpauth@9.1.1:
|
||||
resolution: {integrity: sha512-XhimxmkREwf6GJvV4svS9OVMFJ/qRGz+QBEGwtW5OMf9jZlx9yw25RZMXdrO6r7DHgfIaETJb1lucZXZtn3jgw==}
|
||||
|
||||
|
|
@ -18729,18 +18681,10 @@ 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==}
|
||||
|
||||
|
|
@ -18762,10 +18706,6 @@ 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'}
|
||||
|
|
@ -19511,9 +19451,6 @@ 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'}
|
||||
|
|
@ -19581,27 +19518,6 @@ packages:
|
|||
resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
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'}
|
||||
|
|
@ -20193,10 +20109,6 @@ 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==}
|
||||
|
||||
|
|
@ -20714,9 +20626,6 @@ 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.promisify@1.1.3:
|
||||
resolution: {integrity: sha512-GIEaZ6o86fj09Wtf0VfZ5XP7tmd4t3jM5aZCgmBi231D0DB1AEBa3Aa6MP48DMsAIi96WkpWLimIWVwOjbDMOw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -20759,9 +20668,6 @@ 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.15.26:
|
||||
resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
|
@ -30539,10 +30445,6 @@ 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
|
||||
|
|
@ -30677,8 +30579,6 @@ 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
|
||||
|
|
@ -31403,12 +31303,6 @@ snapshots:
|
|||
|
||||
chai@6.2.2: {}
|
||||
|
||||
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
|
||||
|
|
@ -32250,8 +32144,6 @@ snapshots:
|
|||
optionalDependencies:
|
||||
supports-color: 8.1.1
|
||||
|
||||
debuglog@1.0.1: {}
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decamelize@4.0.0: {}
|
||||
|
|
@ -34337,8 +34229,6 @@ snapshots:
|
|||
|
||||
hookified@1.11.0: {}
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
|
@ -35792,21 +35682,6 @@ snapshots:
|
|||
'@libsql/win32-x64-msvc': 0.5.28
|
||||
optional: true
|
||||
|
||||
license-checker@25.0.1:
|
||||
dependencies:
|
||||
chalk: 2.4.2
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
mkdirp: 0.5.6
|
||||
nopt: 4.0.3
|
||||
read-installed: 4.0.3
|
||||
semver: 7.7.3
|
||||
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
|
||||
|
|
@ -37463,11 +37338,6 @@ 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
|
||||
|
|
@ -37481,13 +37351,6 @@ snapshots:
|
|||
dependencies:
|
||||
abbrev: 4.0.0
|
||||
|
||||
normalize-package-data@2.5.0:
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
resolve: 1.22.11
|
||||
semver: 7.7.3
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
|
@ -37496,8 +37359,6 @@ snapshots:
|
|||
|
||||
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:
|
||||
|
|
@ -37769,15 +37630,6 @@ 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
|
||||
|
||||
otpauth@9.1.1:
|
||||
dependencies:
|
||||
jssha: 3.3.1
|
||||
|
|
@ -38810,29 +38662,11 @@ 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.3
|
||||
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
|
||||
|
|
@ -38872,13 +38706,6 @@ snapshots:
|
|||
dependencies:
|
||||
minimatch: 5.1.8
|
||||
|
||||
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: {}
|
||||
|
|
@ -39821,8 +39648,6 @@ snapshots:
|
|||
|
||||
slick@1.12.2: {}
|
||||
|
||||
slide@1.1.6: {}
|
||||
|
||||
slugify@1.4.7: {}
|
||||
|
||||
slugify@1.6.6: {}
|
||||
|
|
@ -39932,34 +39757,6 @@ snapshots:
|
|||
signal-exit: 3.0.7
|
||||
which: 2.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: {}
|
||||
|
|
@ -40715,8 +40512,6 @@ snapshots:
|
|||
node-addon-api: 8.3.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
treeify@1.1.0: {}
|
||||
|
||||
triple-beam@1.3.0: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
|
@ -41237,8 +41032,6 @@ snapshots:
|
|||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
util-extend@1.0.3: {}
|
||||
|
||||
util.promisify@1.1.3:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
|
@ -41288,11 +41081,6 @@ 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.15.26: {}
|
||||
|
||||
varint@6.0.0: {}
|
||||
|
|
|
|||
|
|
@ -105,23 +105,6 @@ try {
|
|||
buildProcess.pipe(process.stdout);
|
||||
await buildProcess;
|
||||
|
||||
// Generate third-party licenses for production builds
|
||||
// Skip with N8N_SKIP_LICENSES=true for CI test builds
|
||||
if (process.env.N8N_SKIP_LICENSES !== 'true') {
|
||||
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}`));
|
||||
}
|
||||
} else {
|
||||
echo(chalk.gray('INFO: Skipping license generation (N8N_SKIP_LICENSES=true)'));
|
||||
}
|
||||
|
||||
echo(chalk.green('✅ pnpm install and build completed'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\n🛑 BUILD PROCESS FAILED!'));
|
||||
|
|
@ -220,6 +203,38 @@ await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --fil
|
|||
|
||||
const packageDeployTime = getElapsedTime('package_deploy');
|
||||
|
||||
// Generate SBOM + render THIRD_PARTY_LICENSES.md from the deployed runtime closure.
|
||||
// Single source of truth: the SBOM. Both the runtime endpoint (packages/cli/) and the
|
||||
// release asset (compiled/) get the same SBOM-derived attribution file.
|
||||
// Tooling (cdxgen + renderer) is installed in .github/scripts/, alongside other CI
|
||||
// scripts, so we don't carry a second isolated install.
|
||||
//
|
||||
// Default: skip. cdxgen + license rendering adds ~minutes to every build:deploy and
|
||||
// is only needed for the release SBOM job. The release-publish workflow opts in by
|
||||
// setting N8N_GENERATE_LICENSES=true; regular CI Docker prepare runs skip it.
|
||||
if (process.env.N8N_GENERATE_LICENSES === 'true') {
|
||||
echo(chalk.yellow('INFO: Generating SBOM and rendering THIRD_PARTY_LICENSES.md...'));
|
||||
try {
|
||||
const toolingDir = path.join(config.rootDir, '.github', 'scripts');
|
||||
await $`cd ${config.rootDir} && pnpm install --frozen-lockfile --dir .github/scripts --ignore-workspace`;
|
||||
const generateProcess = $`cd ${toolingDir} && pnpm generate-licenses`;
|
||||
generateProcess.pipe(process.stdout);
|
||||
await generateProcess;
|
||||
echo(chalk.green('✅ SBOM generated and THIRD_PARTY_LICENSES.md rendered'));
|
||||
} catch (error) {
|
||||
echo(chalk.red(`ERROR: SBOM/license generation failed: ${error.message}`));
|
||||
// In CI, fail loudly. A stale or missing THIRD_PARTY_LICENSES.md must never ship —
|
||||
// the release workflow uploads it unconditionally and would otherwise publish
|
||||
// an incomplete attribution file.
|
||||
if (process.env.CI === 'true') {
|
||||
throw error;
|
||||
}
|
||||
echo(chalk.yellow('⚠️ Warning: continuing local build (CI=true would have failed)'));
|
||||
}
|
||||
} else {
|
||||
echo(chalk.gray('INFO: Skipping SBOM/license generation (set N8N_GENERATE_LICENSES=true to enable)'));
|
||||
}
|
||||
|
||||
// Restore package.json files
|
||||
// This is only needed locally, not in CI
|
||||
if (process.env.CI !== 'true') {
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
#!/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();
|
||||
45
scripts/licenses/license-overrides.json
Normal file
45
scripts/licenses/license-overrides.json
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"_comment": "Hand-resolved licenses for packages cdxgen + FETCH_LICENSE cannot resolve. Keys are PURLs (pkg:npm/<name>@<version>) — exact match required. 'source' points to where the license was verified so reviewers can re-check. Optional 'skipDiskText: true' opts out of on-disk LICENSE text lookup when the file disagrees with the overridden SPDX id.",
|
||||
"overrides": {
|
||||
"pkg:npm/%40rudderstack/rudder-sdk-node@3.0.5": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/@rudderstack/rudder-sdk-node/LICENSE.md — verbatim MIT (Copyright Segment Inc.), no license field in package.json"
|
||||
},
|
||||
"pkg:npm/%40ewoudenberg/difflib@0.1.0": {
|
||||
"license": "Python-2.0",
|
||||
"source": "https://github.com/ewoudenberg/difflib.js — package.json declares legacy licenses[] array with PSF type, http://docs.python.org/license.html"
|
||||
},
|
||||
"pkg:npm/binascii@0.0.2": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/binascii/LICENSE — verbatim MIT, no license field in package.json"
|
||||
},
|
||||
"pkg:npm/busboy@1.6.0": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/busboy/LICENSE — package.json uses legacy licenses[] array"
|
||||
},
|
||||
"pkg:npm/imap@0.8.19": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/imap/LICENSE — package.json uses legacy licenses[] array"
|
||||
},
|
||||
"pkg:npm/js-nacl@1.4.0": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/js-nacl/README.md — 'is licensed under the MIT license', wraps libsodium (ISC)"
|
||||
},
|
||||
"pkg:npm/seq-queue@0.0.5": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/seq-queue/LICENSE — verbatim MIT, no license field in package.json"
|
||||
},
|
||||
"pkg:npm/ssh2@1.15.0": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/ssh2/LICENSE — package.json uses legacy licenses[] array"
|
||||
},
|
||||
"pkg:npm/streamsearch@1.1.0": {
|
||||
"license": "MIT",
|
||||
"source": "compiled/node_modules/streamsearch/LICENSE — package.json uses legacy licenses[] array"
|
||||
},
|
||||
"pkg:npm/utf7@1.0.2": {
|
||||
"license": "MIT",
|
||||
"source": "DISCREPANCY: package.json declares legacy licenses[]=BSD, but compiled/node_modules/utf7/LICENSE ships verbatim MIT text (https://github.com/chris-rock/node-utf7/blob/master/LICENSE). On-disk LICENSE file taken as authoritative — this is the file customers actually receive in the release tarball. If a future legal review concludes differently, set skipDiskText:true and switch license to BSD-3-Clause."
|
||||
}
|
||||
}
|
||||
}
|
||||
296
scripts/licenses/render-licenses-md.mjs
Normal file
296
scripts/licenses/render-licenses-md.mjs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Renders THIRD_PARTY_LICENSES.md from a CycloneDX SBOM (sbom-source.cdx.json).
|
||||
*
|
||||
* Single source of truth: the SBOM. This script is pure presentation —
|
||||
* group external components by their license expression and emit markdown.
|
||||
* License *text* is pulled from each package's on-disk LICENSE file
|
||||
* (cdxgen --include-license-content is a no-op for npm components).
|
||||
*
|
||||
* Usage: node render-licenses-md.mjs <sbom-path> <output-md-path> [node-modules-dir]
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const OVERRIDES_PATH = path.join(scriptDir, 'license-overrides.json');
|
||||
|
||||
const LICENSE_FILE_PATTERN = /^(license|licence|copying|copyright|unlicense)([.-].+)?$/i;
|
||||
const INVALID_LICENSE_FILE_PATTERN = /(readme|package\.json|changelog|history)/i;
|
||||
|
||||
const FIRST_PARTY_PATTERNS = [
|
||||
/^pkg:npm\/%40n8n\//,
|
||||
/^pkg:npm\/%40n8n_/,
|
||||
/^pkg:npm\/n8n-/,
|
||||
/^pkg:npm\/n8n@/,
|
||||
];
|
||||
|
||||
function isFirstParty(purl) {
|
||||
if (!purl) return false;
|
||||
return FIRST_PARTY_PATTERNS.some((p) => p.test(purl));
|
||||
}
|
||||
|
||||
function licenseKey(licenses) {
|
||||
if (!licenses || licenses.length === 0) return null;
|
||||
const parts = licenses.map((l) => l.expression ?? l.license?.id ?? l.license?.name).filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
if (parts.length === 1) return parts[0];
|
||||
// Per CycloneDX, multiple licenses[] entries on a single component represent
|
||||
// a licensee choice (OR), not a conjunction. AND-conjoined or otherwise
|
||||
// complex compounds must come through the `expression` field instead.
|
||||
return `(${parts.join(' OR ')})`;
|
||||
}
|
||||
|
||||
function licenseTextFor(licenses) {
|
||||
if (!licenses) return null;
|
||||
for (const entry of licenses) {
|
||||
const text = entry.license?.text?.content;
|
||||
if (text && text.trim()) return text.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// npm package name grammar — scoped (@scope/name) or unscoped name.
|
||||
// SBOM metadata can ultimately originate from upstream package.json files,
|
||||
// so we constrain the segments before joining into a filesystem path to
|
||||
// reject traversal (`..`), absolute-path, and separator-injection attempts.
|
||||
const NPM_SCOPE_PATTERN = /^@[a-z0-9][a-z0-9._-]*$/i;
|
||||
const NPM_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
||||
|
||||
async function readLicenseFromDisk(nodeModulesDir, component) {
|
||||
if (!nodeModulesDir) return null;
|
||||
if (component.group && !NPM_SCOPE_PATTERN.test(component.group)) return null;
|
||||
if (!component.name || !NPM_NAME_PATTERN.test(component.name)) return null;
|
||||
|
||||
const resolvedRoot = path.resolve(nodeModulesDir);
|
||||
const pkgDir = component.group
|
||||
? path.join(resolvedRoot, component.group, component.name)
|
||||
: path.join(resolvedRoot, component.name);
|
||||
const resolvedPkgDir = path.resolve(pkgDir);
|
||||
|
||||
// Defence in depth: even after the regex checks, confirm the resolved path
|
||||
// remains within nodeModulesDir before reading anything.
|
||||
const rootWithSep = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
||||
if (!resolvedPkgDir.startsWith(rootWithSep)) return null;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(resolvedPkgDir);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = entries.filter(
|
||||
(e) => LICENSE_FILE_PATTERN.test(e) && !INVALID_LICENSE_FILE_PATTERN.test(e),
|
||||
);
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const text = await readFile(path.join(resolvedPkgDir, candidate), 'utf-8');
|
||||
if (text && text.trim()) return text.trim();
|
||||
} catch {
|
||||
/* try next */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function copyrightFor(component) {
|
||||
return component.copyright?.trim() || null;
|
||||
}
|
||||
|
||||
function qualifiedName(component) {
|
||||
return component.group ? `${component.group}/${component.name}` : component.name;
|
||||
}
|
||||
|
||||
function applyOverride(component, overrides, matchedKeys) {
|
||||
const override = overrides[component.purl];
|
||||
if (!override) return component;
|
||||
matchedKeys?.add(component.purl);
|
||||
return {
|
||||
...component,
|
||||
licenses: [{ license: { id: override.license } }],
|
||||
// Propagate the override flag so callers (e.g. the disk-text lookup)
|
||||
// can opt out when the on-disk LICENSE is known to disagree with the
|
||||
// overridden SPDX id.
|
||||
_overrideSkipDiskText: override.skipDiskText === true,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOverrides() {
|
||||
try {
|
||||
const raw = await readFile(OVERRIDES_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed.overrides ?? {};
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return {};
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function buildMarkdown(groups, texts) {
|
||||
const sortedKeys = [...groups.keys()].sort((a, b) => a.localeCompare(b));
|
||||
|
||||
let doc = `# 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.
|
||||
|
||||
`;
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const packages = groups.get(key).slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
doc += `## ${key}\n\n`;
|
||||
for (const pkg of packages) {
|
||||
const copyright = pkg.copyright ? `, ${pkg.copyright}` : '';
|
||||
doc += `* ${pkg.name} ${pkg.version}${copyright}\n`;
|
||||
}
|
||||
doc += '\n';
|
||||
}
|
||||
|
||||
doc += `# License Texts
|
||||
|
||||
The license text below is reproduced from a single representative package per license.
|
||||
Copyright notices identify the contributor of that representative package only; each
|
||||
listed component carries its own copyright. The verbatim LICENSE file of every package
|
||||
is distributed inside its \`node_modules/\` directory in the n8n release artefact.
|
||||
|
||||
`;
|
||||
for (const key of sortedKeys) {
|
||||
const text = texts.get(key);
|
||||
doc += `## ${key} License Text\n\n`;
|
||||
if (text) {
|
||||
doc += '```\n' + text + '\n```\n\n';
|
||||
} else {
|
||||
doc += `${key} license text not available.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function renderSbom(sbom, overrides, { readDiskText } = {}) {
|
||||
const groups = new Map();
|
||||
const texts = new Map();
|
||||
const unresolved = [];
|
||||
const matchedOverrideKeys = new Set();
|
||||
let externalCount = 0;
|
||||
let skippedFirstParty = 0;
|
||||
|
||||
for (const rawComponent of sbom.components ?? []) {
|
||||
if (isFirstParty(rawComponent.purl)) {
|
||||
skippedFirstParty++;
|
||||
continue;
|
||||
}
|
||||
externalCount++;
|
||||
|
||||
const component = applyOverride(rawComponent, overrides, matchedOverrideKeys);
|
||||
const key = licenseKey(component.licenses);
|
||||
|
||||
if (!key) {
|
||||
unresolved.push(`${qualifiedName(rawComponent)}@${rawComponent.version}\t${rawComponent.purl}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push({
|
||||
name: qualifiedName(component),
|
||||
version: component.version,
|
||||
copyright: copyrightFor(component),
|
||||
});
|
||||
|
||||
if (!texts.has(key)) {
|
||||
const inlineText = licenseTextFor(component.licenses);
|
||||
if (inlineText) {
|
||||
texts.set(key, inlineText);
|
||||
} else if (readDiskText && !component._overrideSkipDiskText) {
|
||||
// Disk lookup uses rawComponent (real group/name on disk) even when
|
||||
// licenses were overridden, since the file location is independent of the SPDX expression.
|
||||
// Skip when the override explicitly opts out (on-disk LICENSE disagrees with the overridden id).
|
||||
const diskText = await readDiskText(rawComponent);
|
||||
if (diskText) texts.set(key, diskText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unusedOverrides = Object.keys(overrides).filter((purl) => !matchedOverrideKeys.has(purl));
|
||||
|
||||
return {
|
||||
markdown: buildMarkdown(groups, texts),
|
||||
summary: {
|
||||
totalComponents: sbom.components?.length ?? 0,
|
||||
skippedFirstParty,
|
||||
externalComponents: externalCount,
|
||||
uniqueLicenses: groups.size,
|
||||
unresolved: unresolved.length,
|
||||
unusedOverrides: unusedOverrides.length,
|
||||
},
|
||||
unresolved,
|
||||
unusedOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
qualifiedName,
|
||||
isFirstParty,
|
||||
licenseKey,
|
||||
licenseTextFor,
|
||||
copyrightFor,
|
||||
applyOverride,
|
||||
loadOverrides,
|
||||
readLicenseFromDisk,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const [sbomPath, outputPath, nodeModulesDir] = process.argv.slice(2);
|
||||
if (!sbomPath || !outputPath) {
|
||||
console.error('Usage: render-licenses-md.mjs <sbom-path> <output-md-path> [node-modules-dir]');
|
||||
process.exit(1);
|
||||
}
|
||||
const resolvedNodeModules = nodeModulesDir ? path.resolve(nodeModulesDir) : null;
|
||||
|
||||
const sbom = JSON.parse(await readFile(sbomPath, 'utf-8'));
|
||||
const overrides = await loadOverrides();
|
||||
|
||||
const { markdown, summary, unresolved, unusedOverrides } = await renderSbom(sbom, overrides, {
|
||||
readDiskText: resolvedNodeModules
|
||||
? (component) => readLicenseFromDisk(resolvedNodeModules, component)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
|
||||
if (unusedOverrides.length > 0) {
|
||||
// Stale override: a PURL was overridden but no component matched it (e.g. after a version bump).
|
||||
// Surface loudly so it's caught on the next bump instead of silently re-introducing unresolved licenses.
|
||||
console.error('\nUnused overrides (stale — no matching component PURL):');
|
||||
for (const purl of unusedOverrides) console.error(' ' + purl);
|
||||
}
|
||||
|
||||
if (unresolved.length > 0) {
|
||||
console.error('\nUnresolved (no license detected, no override):');
|
||||
for (const line of unresolved) console.error(' ' + line);
|
||||
// Refuse to write a partial markdown — a retried CI step or cached artifact must not
|
||||
// pick up an incomplete THIRD_PARTY_LICENSES.md as if it were the canonical file.
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (unusedOverrides.length > 0) {
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
// Only write after all integrity checks pass.
|
||||
await writeFile(outputPath, markdown);
|
||||
}
|
||||
|
||||
// Run main() only when invoked as a script, not when imported by tests.
|
||||
// pathToFileURL correctly percent-encodes the argv path so the comparison
|
||||
// holds for checkouts containing spaces, unicode, '@', etc.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
476
scripts/licenses/render-licenses-md.test.mjs
Normal file
476
scripts/licenses/render-licenses-md.test.mjs
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
renderSbom,
|
||||
qualifiedName,
|
||||
isFirstParty,
|
||||
licenseKey,
|
||||
licenseTextFor,
|
||||
copyrightFor,
|
||||
applyOverride,
|
||||
loadOverrides,
|
||||
readLicenseFromDisk,
|
||||
} from './render-licenses-md.mjs';
|
||||
|
||||
const mit = { license: { id: 'MIT' } };
|
||||
|
||||
describe('qualifiedName', () => {
|
||||
it('includes group for scoped packages', () => {
|
||||
assert.equal(qualifiedName({ group: '@smithy', name: 'core' }), '@smithy/core');
|
||||
});
|
||||
|
||||
it('returns bare name for unscoped packages', () => {
|
||||
assert.equal(qualifiedName({ name: 'busboy' }), 'busboy');
|
||||
});
|
||||
|
||||
it('treats empty/missing group as unscoped', () => {
|
||||
assert.equal(qualifiedName({ group: '', name: 'busboy' }), 'busboy');
|
||||
assert.equal(qualifiedName({ group: undefined, name: 'busboy' }), 'busboy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFirstParty', () => {
|
||||
it('matches n8n scoped packages', () => {
|
||||
assert.equal(isFirstParty('pkg:npm/%40n8n/config@2.22.0'), true);
|
||||
assert.equal(isFirstParty('pkg:npm/%40n8n_io/license-sdk@2.25.0'), true);
|
||||
});
|
||||
|
||||
it('matches unscoped n8n packages', () => {
|
||||
assert.equal(isFirstParty('pkg:npm/n8n-workflow@2.23.0'), true);
|
||||
assert.equal(isFirstParty('pkg:npm/n8n@2.23.0'), true);
|
||||
});
|
||||
|
||||
it('rejects other scoped packages', () => {
|
||||
assert.equal(isFirstParty('pkg:npm/%40smithy/core@3.23.12'), false);
|
||||
assert.equal(isFirstParty('pkg:npm/busboy@1.6.0'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('licenseKey', () => {
|
||||
it('returns SPDX id from single license', () => {
|
||||
assert.equal(licenseKey([mit]), 'MIT');
|
||||
});
|
||||
|
||||
it('returns expression when present', () => {
|
||||
assert.equal(licenseKey([{ expression: '(MIT OR Apache-2.0)' }]), '(MIT OR Apache-2.0)');
|
||||
});
|
||||
|
||||
it('returns null when no licenses', () => {
|
||||
assert.equal(licenseKey([]), null);
|
||||
assert.equal(licenseKey(null), null);
|
||||
});
|
||||
|
||||
it('joins multiple distinct licenses with OR (CycloneDX choice semantics)', () => {
|
||||
assert.equal(
|
||||
licenseKey([{ license: { id: 'MIT' } }, { license: { id: 'Apache-2.0' } }]),
|
||||
'(MIT OR Apache-2.0)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOverride', () => {
|
||||
it('replaces licenses array when override exists for purl', () => {
|
||||
const component = {
|
||||
name: 'ssh2',
|
||||
version: '1.15.0',
|
||||
purl: 'pkg:npm/ssh2@1.15.0',
|
||||
licenses: [],
|
||||
};
|
||||
const overrides = { 'pkg:npm/ssh2@1.15.0': { license: 'MIT' } };
|
||||
const result = applyOverride(component, overrides);
|
||||
assert.equal(licenseKey(result.licenses), 'MIT');
|
||||
});
|
||||
|
||||
it('leaves component untouched when no override matches', () => {
|
||||
const component = { purl: 'pkg:npm/other@1.0.0', licenses: [mit] };
|
||||
const result = applyOverride(component, {});
|
||||
assert.equal(result, component);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderSbom — scope handling (regression)', () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{ group: '@smithy', name: 'core', version: '3.23.12', purl: 'pkg:npm/%40smithy/core@3.23.12', licenses: [mit] },
|
||||
{ group: '@opentelemetry', name: 'core', version: '2.7.1', purl: 'pkg:npm/%40opentelemetry/core@2.7.1', licenses: [mit] },
|
||||
{ group: '@aws-sdk', name: 'core', version: '3.808.0', purl: 'pkg:npm/%40aws-sdk/core@3.808.0', licenses: [mit] },
|
||||
{ name: 'busboy', version: '1.6.0', purl: 'pkg:npm/busboy@1.6.0', licenses: [mit] },
|
||||
{ group: '@n8n', name: 'config', version: '2.22.0', purl: 'pkg:npm/%40n8n/config@2.22.0', licenses: [mit] },
|
||||
],
|
||||
};
|
||||
|
||||
it('preserves scope in rendered package names', async () => {
|
||||
const { markdown } = await renderSbom(sbom, {});
|
||||
assert.match(markdown, /^\* @smithy\/core 3\.23\.12$/m);
|
||||
assert.match(markdown, /^\* @opentelemetry\/core 2\.7\.1$/m);
|
||||
assert.match(markdown, /^\* @aws-sdk\/core 3\.808\.0$/m);
|
||||
});
|
||||
|
||||
it('does not emit bare "* core" lines when scoped variants exist', async () => {
|
||||
const { markdown } = await renderSbom(sbom, {});
|
||||
assert.equal(
|
||||
/^\* core\s/m.test(markdown),
|
||||
false,
|
||||
'bare "* core" line found — scope was stripped',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps unscoped packages unscoped', async () => {
|
||||
const { markdown } = await renderSbom(sbom, {});
|
||||
assert.match(markdown, /^\* busboy 1\.6\.0$/m);
|
||||
});
|
||||
|
||||
it('filters first-party @n8n/* packages', async () => {
|
||||
const { summary } = await renderSbom(sbom, {});
|
||||
assert.equal(summary.skippedFirstParty, 1);
|
||||
assert.equal(summary.externalComponents, 4);
|
||||
});
|
||||
|
||||
it('reports unresolved components with qualified name', async () => {
|
||||
const sbomMissing = {
|
||||
components: [
|
||||
{ group: '@smithy', name: 'core', version: '1.0.0', purl: 'pkg:npm/%40smithy/core@1.0.0' },
|
||||
],
|
||||
};
|
||||
const { unresolved } = await renderSbom(sbomMissing, {});
|
||||
assert.equal(unresolved.length, 1);
|
||||
assert.match(unresolved[0], /^@smithy\/core@1\.0\.0\t/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderSbom — unused override detection', () => {
|
||||
it('reports overrides whose PURL did not match any component', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{ name: 'busboy', version: '1.6.0', purl: 'pkg:npm/busboy@1.6.0', licenses: [mit] },
|
||||
],
|
||||
};
|
||||
const overrides = {
|
||||
'pkg:npm/busboy@1.6.0': { license: 'MIT' },
|
||||
'pkg:npm/ssh2@1.15.0': { license: 'MIT' },
|
||||
'pkg:npm/utf7@1.0.2': { license: 'MIT' },
|
||||
};
|
||||
|
||||
const { unusedOverrides, summary } = await renderSbom(sbom, overrides);
|
||||
assert.deepEqual(unusedOverrides.sort(), [
|
||||
'pkg:npm/ssh2@1.15.0',
|
||||
'pkg:npm/utf7@1.0.2',
|
||||
]);
|
||||
assert.equal(summary.unusedOverrides, 2);
|
||||
});
|
||||
|
||||
it('reports zero unused overrides when every override matches', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{ name: 'busboy', version: '1.6.0', purl: 'pkg:npm/busboy@1.6.0', licenses: [] },
|
||||
],
|
||||
};
|
||||
const overrides = { 'pkg:npm/busboy@1.6.0': { license: 'MIT' } };
|
||||
const { unusedOverrides, summary } = await renderSbom(sbom, overrides);
|
||||
assert.deepEqual(unusedOverrides, []);
|
||||
assert.equal(summary.unusedOverrides, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderSbom — disk text lookup', () => {
|
||||
it('calls readDiskText with rawComponent (not override-mutated)', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{
|
||||
group: '@smithy',
|
||||
name: 'core',
|
||||
version: '1.0.0',
|
||||
purl: 'pkg:npm/%40smithy/core@1.0.0',
|
||||
licenses: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const overrides = { 'pkg:npm/%40smithy/core@1.0.0': { license: 'MIT' } };
|
||||
|
||||
const seen = [];
|
||||
const readDiskText = async (component) => {
|
||||
seen.push({ group: component.group, name: component.name });
|
||||
return 'MIT TEXT';
|
||||
};
|
||||
|
||||
const { markdown } = await renderSbom(sbom, overrides, { readDiskText });
|
||||
assert.deepEqual(seen, [{ group: '@smithy', name: 'core' }]);
|
||||
assert.match(markdown, /MIT TEXT/);
|
||||
});
|
||||
|
||||
it('skips disk text lookup when override sets skipDiskText:true', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{
|
||||
name: 'utf7',
|
||||
version: '1.0.2',
|
||||
purl: 'pkg:npm/utf7@1.0.2',
|
||||
licenses: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const overrides = {
|
||||
'pkg:npm/utf7@1.0.2': { license: 'BSD-3-Clause', skipDiskText: true },
|
||||
};
|
||||
|
||||
let called = false;
|
||||
const readDiskText = async () => {
|
||||
called = true;
|
||||
return 'should not appear';
|
||||
};
|
||||
|
||||
const { markdown } = await renderSbom(sbom, overrides, { readDiskText });
|
||||
assert.equal(called, false);
|
||||
assert.doesNotMatch(markdown, /should not appear/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('licenseKey — additional branches', () => {
|
||||
it('falls back to license.name when id/expression are absent', () => {
|
||||
assert.equal(licenseKey([{ license: { name: 'Custom License' } }]), 'Custom License');
|
||||
});
|
||||
|
||||
it('returns null when every entry has no usable field', () => {
|
||||
assert.equal(licenseKey([{ license: {} }, { license: { id: null } }]), null);
|
||||
});
|
||||
|
||||
it('wraps multi-entry licenses in parens with OR (CycloneDX choice)', () => {
|
||||
assert.equal(
|
||||
licenseKey([
|
||||
{ license: { id: 'MIT' } },
|
||||
{ license: { id: 'Apache-2.0' } },
|
||||
{ license: { id: 'BSD-3-Clause' } },
|
||||
]),
|
||||
'(MIT OR Apache-2.0 OR BSD-3-Clause)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('licenseTextFor', () => {
|
||||
it('returns inline text content from first license entry that has it', () => {
|
||||
const text = licenseTextFor([
|
||||
{ license: { id: 'MIT' } },
|
||||
{ license: { id: 'Apache-2.0', text: { content: 'INLINE APACHE TEXT' } } },
|
||||
]);
|
||||
assert.equal(text, 'INLINE APACHE TEXT');
|
||||
});
|
||||
|
||||
it('returns null when no entry has text', () => {
|
||||
assert.equal(licenseTextFor([{ license: { id: 'MIT' } }]), null);
|
||||
assert.equal(licenseTextFor([]), null);
|
||||
assert.equal(licenseTextFor(null), null);
|
||||
});
|
||||
|
||||
it('treats whitespace-only content as missing', () => {
|
||||
assert.equal(licenseTextFor([{ license: { text: { content: ' \n\t' } } }]), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyrightFor', () => {
|
||||
it('returns trimmed copyright string', () => {
|
||||
assert.equal(copyrightFor({ copyright: ' Copyright (c) 2024 Foo ' }), 'Copyright (c) 2024 Foo');
|
||||
});
|
||||
|
||||
it('returns null for missing or blank copyright', () => {
|
||||
assert.equal(copyrightFor({}), null);
|
||||
assert.equal(copyrightFor({ copyright: ' ' }), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyright rendering in markdown', () => {
|
||||
it('appends trimmed copyright after version, comma-separated', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{
|
||||
name: 'busboy',
|
||||
version: '1.6.0',
|
||||
purl: 'pkg:npm/busboy@1.6.0',
|
||||
licenses: [mit],
|
||||
copyright: ' Copyright (c) 2024 Foo ',
|
||||
},
|
||||
],
|
||||
};
|
||||
const { markdown } = await renderSbom(sbom, {});
|
||||
assert.match(markdown, /^\* busboy 1\.6\.0, Copyright \(c\) 2024 Foo$/m);
|
||||
});
|
||||
|
||||
it('omits the trailing comma when copyright is missing', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{ name: 'busboy', version: '1.6.0', purl: 'pkg:npm/busboy@1.6.0', licenses: [mit] },
|
||||
],
|
||||
};
|
||||
const { markdown } = await renderSbom(sbom, {});
|
||||
assert.match(markdown, /^\* busboy 1\.6\.0$/m);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOverride — additional branches', () => {
|
||||
it('overwrites an existing (wrong) license declaration', () => {
|
||||
const component = {
|
||||
purl: 'pkg:npm/utf7@1.0.2',
|
||||
licenses: [{ license: { id: 'BSD-3-Clause' } }],
|
||||
};
|
||||
const overrides = { 'pkg:npm/utf7@1.0.2': { license: 'MIT' } };
|
||||
const result = applyOverride(component, overrides);
|
||||
assert.equal(licenseKey(result.licenses), 'MIT');
|
||||
});
|
||||
|
||||
it('records matched keys when matchedKeys set is provided', () => {
|
||||
const component = { purl: 'pkg:npm/x@1', licenses: [] };
|
||||
const overrides = { 'pkg:npm/x@1': { license: 'MIT' } };
|
||||
const matched = new Set();
|
||||
applyOverride(component, overrides, matched);
|
||||
assert.ok(matched.has('pkg:npm/x@1'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderSbom — edge cases', () => {
|
||||
it('handles SBOM with missing components key', async () => {
|
||||
const { summary, markdown } = await renderSbom({}, {});
|
||||
assert.equal(summary.totalComponents, 0);
|
||||
assert.equal(summary.externalComponents, 0);
|
||||
assert.equal(summary.unresolved, 0);
|
||||
assert.match(markdown, /^# Third-Party Licenses/);
|
||||
});
|
||||
|
||||
it('handles SBOM with empty components array', async () => {
|
||||
const { summary } = await renderSbom({ components: [] }, {});
|
||||
assert.equal(summary.totalComponents, 0);
|
||||
assert.equal(summary.externalComponents, 0);
|
||||
});
|
||||
|
||||
it('reports unresolved when license entries have no usable fields', async () => {
|
||||
const sbom = {
|
||||
components: [
|
||||
{
|
||||
name: 'mystery',
|
||||
version: '1.0.0',
|
||||
purl: 'pkg:npm/mystery@1.0.0',
|
||||
licenses: [{ license: {} }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { unresolved, summary } = await renderSbom(sbom, {});
|
||||
assert.equal(summary.unresolved, 1);
|
||||
assert.match(unresolved[0], /^mystery@1\.0\.0\t/);
|
||||
});
|
||||
|
||||
it('all documented overrides resolve to zero unresolved (end-to-end)', async () => {
|
||||
const purls = [
|
||||
'pkg:npm/%40rudderstack/rudder-sdk-node@3.0.5',
|
||||
'pkg:npm/%40ewoudenberg/difflib@0.1.0',
|
||||
'pkg:npm/binascii@0.0.2',
|
||||
'pkg:npm/busboy@1.6.0',
|
||||
'pkg:npm/imap@0.8.19',
|
||||
'pkg:npm/js-nacl@1.4.0',
|
||||
'pkg:npm/seq-queue@0.0.5',
|
||||
'pkg:npm/ssh2@1.15.0',
|
||||
'pkg:npm/streamsearch@1.1.0',
|
||||
'pkg:npm/utf7@1.0.2',
|
||||
];
|
||||
const sbom = {
|
||||
components: purls.map((purl) => {
|
||||
const m = purl.match(/^pkg:npm\/(?:%40([^/]+)\/)?([^@]+)@(.+)$/);
|
||||
return {
|
||||
group: m[1] ? `@${m[1]}` : undefined,
|
||||
name: m[2],
|
||||
version: m[3],
|
||||
purl,
|
||||
licenses: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const overridesModule = await loadOverrides();
|
||||
const { summary, unresolved, unusedOverrides } = await renderSbom(sbom, overridesModule);
|
||||
assert.equal(unresolved.length, 0, `unresolved: ${unresolved.join(', ')}`);
|
||||
assert.equal(summary.unresolved, 0);
|
||||
assert.equal(unusedOverrides.length, 0, `unused: ${unusedOverrides.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadOverrides', () => {
|
||||
it('loads and parses license-overrides.json (happy path)', async () => {
|
||||
const overrides = await loadOverrides();
|
||||
assert.ok(typeof overrides === 'object' && overrides !== null);
|
||||
assert.ok(overrides['pkg:npm/busboy@1.6.0'], 'expected busboy override entry');
|
||||
assert.equal(overrides['pkg:npm/busboy@1.6.0'].license, 'MIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readLicenseFromDisk', () => {
|
||||
let tmpRoot;
|
||||
before(async () => {
|
||||
tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'license-disk-'));
|
||||
});
|
||||
after(async () => {
|
||||
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reads LICENSE file when present', async () => {
|
||||
const pkgDir = path.join(tmpRoot, 'pkga');
|
||||
await mkdir(pkgDir, { recursive: true });
|
||||
await writeFile(path.join(pkgDir, 'LICENSE'), 'PKGA LICENSE TEXT');
|
||||
const text = await readLicenseFromDisk(tmpRoot, { name: 'pkga' });
|
||||
assert.equal(text, 'PKGA LICENSE TEXT');
|
||||
});
|
||||
|
||||
it('reads LICENSE for scoped package using group/name', async () => {
|
||||
const pkgDir = path.join(tmpRoot, '@scope', 'pkgb');
|
||||
await mkdir(pkgDir, { recursive: true });
|
||||
await writeFile(path.join(pkgDir, 'LICENSE.md'), 'SCOPED PKGB TEXT');
|
||||
const text = await readLicenseFromDisk(tmpRoot, { group: '@scope', name: 'pkgb' });
|
||||
assert.equal(text, 'SCOPED PKGB TEXT');
|
||||
});
|
||||
|
||||
it('falls through README/CHANGELOG/package.json filenames', async () => {
|
||||
const pkgDir = path.join(tmpRoot, 'pkgc');
|
||||
await mkdir(pkgDir, { recursive: true });
|
||||
await writeFile(path.join(pkgDir, 'README.md'), 'not a license');
|
||||
await writeFile(path.join(pkgDir, 'CHANGELOG'), 'not a license');
|
||||
await writeFile(path.join(pkgDir, 'package.json'), '{}');
|
||||
await writeFile(path.join(pkgDir, 'COPYING'), 'COPYING IS A LICENSE');
|
||||
const text = await readLicenseFromDisk(tmpRoot, { name: 'pkgc' });
|
||||
assert.equal(text, 'COPYING IS A LICENSE');
|
||||
});
|
||||
|
||||
it('returns null when nodeModulesDir is empty/falsy', async () => {
|
||||
assert.equal(await readLicenseFromDisk(null, { name: 'x' }), null);
|
||||
assert.equal(await readLicenseFromDisk('', { name: 'x' }), null);
|
||||
});
|
||||
|
||||
it('returns null when package dir does not exist', async () => {
|
||||
assert.equal(await readLicenseFromDisk(tmpRoot, { name: 'does-not-exist' }), null);
|
||||
});
|
||||
|
||||
it('rejects path traversal in component.name', async () => {
|
||||
assert.equal(await readLicenseFromDisk(tmpRoot, { name: '../etc' }), null);
|
||||
assert.equal(await readLicenseFromDisk(tmpRoot, { name: '..' }), null);
|
||||
});
|
||||
|
||||
it('rejects path traversal in component.group', async () => {
|
||||
assert.equal(
|
||||
await readLicenseFromDisk(tmpRoot, { group: '../..', name: 'pkga' }),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects absolute-path-looking component name', async () => {
|
||||
assert.equal(await readLicenseFromDisk(tmpRoot, { name: '/etc/passwd' }), null);
|
||||
});
|
||||
|
||||
it('rejects component.group without leading @', async () => {
|
||||
assert.equal(
|
||||
await readLicenseFromDisk(tmpRoot, { group: 'noatsign', name: 'pkga' }),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for missing component.name', async () => {
|
||||
assert.equal(await readLicenseFromDisk(tmpRoot, {}), null);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "",
|
||||
"version": "",
|
||||
"description": "",
|
||||
"licenses": "",
|
||||
"copyright": "",
|
||||
"licenseFile": "",
|
||||
"licenseText": "",
|
||||
"licenseModified": "",
|
||||
"repository": "",
|
||||
"url": ""
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user