From dd9677012a2fde2ae847570816fdad90460d77ce Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 20 Aug 2026 21:39:18 -0700 Subject: [PATCH] fix(release): scan publication-equivalent plugin artifacts --- .github/workflows/plugin-prerelease.yml | 10 +- CHANGELOG.md | 1 + package.json | 3 - pnpm-lock.yaml | 649 ------------------ scripts/lib/plugin-npm-pack-files.mjs | 11 - scripts/lib/plugin-npm-security-scan.mts | 468 ++++++------- scripts/plugin-npm-security-prepare.mts | 86 +-- scripts/plugin-npm-security-scan-runner.mjs | 62 +- test/scripts/plugin-npm-security-scan.test.ts | 382 ++++++----- .../plugin-prerelease-test-plan.test.ts | 21 +- 10 files changed, 556 insertions(+), 1137 deletions(-) delete mode 100644 scripts/lib/plugin-npm-pack-files.mjs diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index d0ce013e0190..ae5565411174 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -359,13 +359,13 @@ jobs: persist-credentials: false submodules: false - - name: Setup trusted inert pack environment + - name: Setup candidate package build environment uses: ./.github/actions/setup-node-env with: node-version: "24.x" install-bun: "false" - - name: Pack inert plugin package input + - name: Build publication-equivalent plugin artifact env: CANDIDATE_SHA: ${{ needs.resolve-candidate.outputs.checkout_revision }} EXTENSION_ID: ${{ matrix.extension_id }} @@ -385,7 +385,7 @@ jobs: --package-name "$PACKAGE_NAME" \ --tooling-sha "$TOOLING_SHA" - - name: Upload inert plugin package input + - name: Upload publication-equivalent plugin artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: plugin-npm-security-package-${{ needs.resolve-candidate.outputs.checkout_revision }}-${{ matrix.extension_id }} @@ -426,14 +426,14 @@ jobs: - name: Install trusted scanner dependencies run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts - - name: Download inert plugin package inputs + - name: Download publication-equivalent plugin artifacts continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: plugin-npm-security-package-${{ needs.resolve-candidate.outputs.checkout_revision }}-* path: ${{ runner.temp }}/plugin-npm-security-packages - - name: Scan inert plugin package inputs + - name: Scan publication-equivalent plugin artifacts env: CANDIDATE_SHA: ${{ needs.resolve-candidate.outputs.checkout_revision }} EXPECTED_PACKAGES_JSON: ${{ needs.plugin-npm-security-plan.outputs.packages_json }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e70c654434..5e4de06d27db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - **Secret egress host binding:** bind each shared-store secret to exact HTTPS destination hosts across CLI, Gateway RPC, and Control UI so unbound sentinel substitution fails closed before plaintext egress. +- **Plugin release security scan:** scan immutable publication-equivalent npm plugin tarballs in trusted tooling, while isolating candidate builds in secretless jobs and bounding malformed artifacts, archive bytes, findings, memory, and reports. - **Release validation:** defer beta candidate Parallels smoke to postpublish `release:beta-smoke` by default, keep stable/full prepublish coverage, and bound nested release workflow monitors with explicit job timeouts. - **macOS app profiles:** isolate named app instances across state, preferences, Keychain, Gateway services, and duplicate-instance ownership while keeping host-global login and node services untouched. - **Developer workflow:** remove the obsolete scoped-commit helper and use standard Git commands in isolated worktrees. diff --git a/package.json b/package.json index a05f3e936e42..0187123a4831 100644 --- a/package.json +++ b/package.json @@ -2094,7 +2094,6 @@ "@lit/context": "1.1.6", "@lit/task": "1.0.3", "@mdx-js/mdx": "3.1.1", - "@npmcli/arborist": "9.8.0", "@openclaw/session-url-contract": "workspace:*", "@opentelemetry/sdk-node": "0.221.0", "@shikijs/core": "4.3.1", @@ -2119,7 +2118,6 @@ "lit-analyzer": "2.0.3", "markdown-it": "14.3.0", "marked": "18.0.7", - "npm-packlist": "10.0.4", "oxfmt": "0.60.0", "oxlint": "1.75.0", "oxlint-tsgolint": "7.0.2001", @@ -2135,7 +2133,6 @@ "tsx": "4.23.1", "unrun": "0.3.1", "vite": "8.1.5", - "validate-npm-package-name": "7.0.2", "vitest": "4.1.10" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efc7e0b22190..cefd1fdf77f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -259,9 +259,6 @@ importers: '@mdx-js/mdx': specifier: 3.1.1 version: 3.1.1(supports-color@10.2.2) - '@npmcli/arborist': - specifier: 9.8.0 - version: 9.8.0(supports-color@10.2.2) '@openclaw/session-url-contract': specifier: workspace:* version: link:packages/session-url-contract @@ -334,12 +331,6 @@ importers: marked: specifier: 18.0.7 version: 18.0.7 - npm-packlist: - specifier: 10.0.4 - version: 10.0.4 - validate-npm-package-name: - specifier: 7.0.2 - version: 7.0.2 oxfmt: specifier: 0.60.0 version: 0.60.0 @@ -3531,9 +3522,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@isaacs/string-locale-compare@1.1.0': - resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -3954,76 +3942,18 @@ packages: '@novnc/novnc@1.7.0': resolution: {integrity: sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==} - '@npmcli/agent@4.0.2': - resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} - engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/agent@5.0.2': resolution: {integrity: sha512-EkzGmEsgbQ1rqWkRJe2P0oQHx/ylZozDUNPMXCklLuSFL3GY+QyEfBUjhjCsgGXzh4OGpnHvkboSQgczjP/jJg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@npmcli/arborist@9.8.0': - resolution: {integrity: sha512-bqjei/1+uait6wA30G7IElMs5VCyGpuVPFQYsvzqhTEEAVmDMsHBCFstcT2lLfeQDvi8MeUQHStfHSArvdTQuw==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - - '@npmcli/fs@5.0.0': - resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} - engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/fs@6.0.0': resolution: {integrity: sha512-AheOs4swKka/XLtht6xxJDPezlQ7K2IYQ9Y8lST4JLDjnralnWuMM9AE2CdVcgQJ5omrXhsRzM7F7aYmeZBvKQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@npmcli/git@7.0.2': - resolution: {integrity: sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/installed-package-contents@4.0.0': - resolution: {integrity: sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - - '@npmcli/map-workspaces@5.0.3': - resolution: {integrity: sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/metavuln-calculator@9.0.3': - resolution: {integrity: sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/name-from-folder@4.0.0': - resolution: {integrity: sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/node-gyp@5.0.0': - resolution: {integrity: sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/package-json@7.0.5': - resolution: {integrity: sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/promise-spawn@9.0.1': - resolution: {integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/query@5.0.0': - resolution: {integrity: sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - '@npmcli/redact@4.0.0': - resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} - engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/redact@5.0.0': resolution: {integrity: sha512-3zcN5Q3yEmeyxXBzqB6fXPQFzYa2ROsGFSr69W0ArXIAGJqxl/aFECOVPD2kbkYPm0U/EHxFKgclK3UA9WQg5A==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@npmcli/run-script@10.0.4': - resolution: {integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==} - engines: {node: ^20.17.0 || >=22.9.0} - '@openai/codex@0.148.0': resolution: {integrity: sha512-bh5kH9+BMrFaHGmLeoSansPdfRksvr4UXzjQInns/KRO7r8VJ+6AAW+SqUsE8XcG3+OW/mI4EEy8Gpo9UDXGvQ==} engines: {node: '>=16'} @@ -4885,18 +4815,10 @@ packages: '@shoelace-style/localize@3.2.3': resolution: {integrity: sha512-pWoZywizMilenLfQcpE9bjaqi7WKiQXJB42cxvmSPwYS76U776wJ7B5QQ0P28yVit1vpHuFZzq1CtfVFv32jaQ==} - '@sigstore/bundle@4.0.0': - resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} - engines: {node: ^20.17.0 || >=22.9.0} - '@sigstore/bundle@5.0.0': resolution: {integrity: sha512-wefjygudENbzbQMks1t5u34EP0fFoD0XvaEP7DOUP/sXKvogzEJYFw5E6pegGyp3onGWzVEYKVa3bNZWyTYX+A==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@sigstore/core@3.2.1': - resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} - engines: {node: ^20.17.0 || >=22.9.0} - '@sigstore/core@4.0.1': resolution: {integrity: sha512-9v5hRjujn5NXq8o7XFEUgLyAtdr5Iisb4pzM05u3K61IS5q3hP3luWAndk0RkPPLTUFoTbg7Vb84UQ1ZQeajWQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -4905,26 +4827,14 @@ packages: resolution: {integrity: sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==} engines: {node: ^18.17.0 || >=20.5.0} - '@sigstore/sign@4.1.1': - resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==} - engines: {node: ^20.17.0 || >=22.9.0} - '@sigstore/sign@5.0.0': resolution: {integrity: sha512-DSFivqz9/i5AkwZ5fq0YdjaJlc4o1WeS2Zffon0kqtChx0vy4W9NOjkEet9bF2vkzOufX72eVH8kZBIGtcBp1w==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@sigstore/tuf@4.0.2': - resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==} - engines: {node: ^20.17.0 || >=22.9.0} - '@sigstore/tuf@5.0.0': resolution: {integrity: sha512-Zyqg9tcHps3uRAlKHLNmsW4ohsUZAjb9G+31r7lg0ICh/JOcadzmJsIRdjKljlRHpaR0K4aJ2kXXIdywdcdMlA==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@sigstore/verify@3.1.1': - resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} - engines: {node: ^20.17.0 || >=22.9.0} - '@sigstore/verify@4.1.0': resolution: {integrity: sha512-p/s720RiWxLG8XtmfdPfEJOlATA6H/2knFqmtQbFkHKN3IrhWGUwPfpQAf1UnQIEES9IaH6zzhfjkrhTfeSdZw==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -5202,10 +5112,6 @@ packages: resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} - '@tufjs/models@4.1.0': - resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} - engines: {node: ^20.17.0 || >=22.9.0} - '@tufjs/models@5.0.0': resolution: {integrity: sha512-U4mVcdFGOi6pt8n38LdWZp67Svn7ppnU1Pj8SGOVaBi1X4gm+G4ztQlLfkoJbKSHfjA6WeaiJp2A4V83AJF6nQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -5657,10 +5563,6 @@ packages: '@wasm-audio-decoders/opus-ml@0.0.2': resolution: {integrity: sha512-58rWEqDGg+CKCyEeKm2KoxxSwTWtHh/NLTW9ObR4K8CGF6VwuuGudEI1CtniS/oSRmL1nJq/eh8MKARiluw4DQ==} - abbrev@4.0.0: - resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} - engines: {node: ^20.17.0 || >=22.9.0} - abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -5891,10 +5793,6 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - bin-links@6.0.2: - resolution: {integrity: sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==} - engines: {node: ^20.17.0 || >=22.9.0} - birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} @@ -5951,10 +5849,6 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} - cacache@20.0.4: - resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} - engines: {node: ^20.17.0 || >=22.9.0} - cacache@21.0.1: resolution: {integrity: sha512-pTwz/uj3Jyp6WXdJ6fWhR+7LVxVs6RyroQSn7KJwHsSxXuyGSp0pcMVcwSwTpCFq1X2YG8QBe0W+vN+cr0SwzA==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -6039,10 +5933,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - cmd-shim@8.0.0: - resolution: {integrity: sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==} - engines: {node: ^20.17.0 || >=22.9.0} - codec-parser@2.5.0: resolution: {integrity: sha512-Ru9t80fV8B0ZiixQl8xhMTLru+dzuis/KQld32/x5T/+3LwZb0/YvQdSKytX9JqCnRdiupvAvyYJINKrXieziQ==} @@ -6084,10 +5974,6 @@ packages: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} - common-ancestor-path@2.0.0: - resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} - engines: {node: '>= 18'} - composed-offset-position@0.0.6: resolution: {integrity: sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==} peerDependencies: @@ -6473,9 +6359,6 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - exponential-backoff@3.1.3: - resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express-rate-limit@8.6.2: resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} engines: {node: '>= 16'} @@ -6774,10 +6657,6 @@ packages: resolution: {integrity: sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - hosted-git-info@9.0.3: - resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} - engines: {node: ^20.17.0 || >=22.9.0} - html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -6843,10 +6722,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore-walk@8.0.0: - resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} - engines: {node: ^20.17.0 || >=22.9.0} - ignore@7.0.6: resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} @@ -6875,10 +6750,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@6.0.0: - resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} - engines: {node: ^20.17.0 || >=22.9.0} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -6991,10 +6862,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@4.0.0: - resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} - engines: {node: '>=20'} - isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -7093,10 +6960,6 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-parse-even-better-errors@5.0.0: - resolution: {integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==} - engines: {node: ^20.17.0 || >=22.9.0} - json-schema-to-ts@3.1.1: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} @@ -7107,9 +6970,6 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-stringify-nice@1.1.4: - resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -7118,10 +6978,6 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -7129,12 +6985,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - just-diff-apply@5.5.0: - resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} - - just-diff@6.0.2: - resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -7379,10 +7229,6 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-fetch-happen@15.0.6: - resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} - engines: {node: ^20.17.0 || >=22.9.0} - make-fetch-happen@16.0.1: resolution: {integrity: sha512-uUv1yxHzaKVVEPfcFeGSNov/Cehjv08ovlY8ImTljgL7Q+SiA0dAYLQ6SYVa2kkKqNj4Y3aZEI7xv2teadie0A==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -7626,10 +7472,6 @@ packages: resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} engines: {node: '>=16 || 14 >=14.17'} - minipass-fetch@5.0.2: - resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} - engines: {node: ^20.17.0 || >=22.9.0} - minipass-fetch@6.0.0: resolution: {integrity: sha512-AWI8bKapGmgx/J0E6IGYSKj8TiHebZkmKWSs8raPSw8KXwgEAJ+Bw3+LSdXHR6T/RHKAWCOYk2MiLrYluaUU6w==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -7732,11 +7574,6 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@12.4.0: - resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - node-pty@1.2.0-beta.14: resolution: {integrity: sha512-XORU9BQgpxVgqr7WivjJ17mLenOHUgKKWzuZZNaw3NDYgHc/wPJQMSaoLDrpEgqV6aU1nNwil1o/OqYj6lWmUA==} @@ -7744,11 +7581,6 @@ packages: resolution: {integrity: sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA==} engines: {node: '>=4.4.0'} - nopt@9.0.0: - resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -7764,34 +7596,6 @@ packages: nostr-wasm@0.1.0: resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} - npm-bundled@5.0.0: - resolution: {integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-install-checks@8.0.0: - resolution: {integrity: sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-normalize-package-bin@5.0.0: - resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-package-arg@13.0.2: - resolution: {integrity: sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-packlist@10.0.4: - resolution: {integrity: sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-pick-manifest@11.0.3: - resolution: {integrity: sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - npm-registry-fetch@19.1.1: - resolution: {integrity: sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==} - engines: {node: ^20.17.0 || >=22.9.0} - npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -7966,11 +7770,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pacote@21.5.1: - resolution: {integrity: sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -7984,10 +7783,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-conflict-json@5.0.1: - resolution: {integrity: sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==} - engines: {node: ^20.17.0 || >=22.9.0} - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -8148,10 +7943,6 @@ packages: opusscript: optional: true - proc-log@6.1.0: - resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} - engines: {node: ^20.17.0 || >=22.9.0} - proc-log@7.0.0: resolution: {integrity: sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -8162,16 +7953,6 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - proggy@4.0.0: - resolution: {integrity: sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - promise-all-reject-late@1.0.1: - resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} - - promise-call-limit@3.0.2: - resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} - proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -8267,10 +8048,6 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} - read-cmd-shim@6.0.0: - resolution: {integrity: sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==} - engines: {node: ^20.17.0 || >=22.9.0} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -8501,10 +8278,6 @@ packages: peerDependencies: signal-polyfill: ^0.2.0 - sigstore@4.1.1: - resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} - engines: {node: ^20.17.0 || >=22.9.0} - sigstore@5.0.0: resolution: {integrity: sha512-hJqJfoG/e4qFQaauQL00c6J6FrHLBGKtkFvW3JbTSIEFOhLrSjdSM/gWd/yUOfYo/gsERehTXGC1VZWX+9X4Dg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -8544,10 +8317,6 @@ packages: resolution: {integrity: sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==} engines: {node: '>= 20'} - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - socks@2.8.9: resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} @@ -8576,15 +8345,6 @@ packages: spark-md5@3.0.2: resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@4.0.0: - resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -8617,10 +8377,6 @@ packages: sqlite-vec@0.1.9: resolution: {integrity: sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==} - ssri@13.0.1: - resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} - engines: {node: ^20.17.0 || >=22.9.0} - ssri@14.0.0: resolution: {integrity: sha512-jQxKI0yx0ZnTKrqjKkLDV2DXkBQn3k49JVmVqDGcDwKDtGDbImD/GXsq04KD0VVzCQQ9wZJYal3RwR1GzWTSow==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -8814,10 +8570,6 @@ packages: tree-sitter: optional: true - treeverse@3.0.0: - resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -8887,10 +8639,6 @@ packages: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} - tuf-js@4.1.0: - resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} - engines: {node: ^20.17.0 || >=22.9.0} - tuf-js@6.0.0: resolution: {integrity: sha512-zlJVOIO68hmgo1//X4ENEcTGfuOTAtDPi8PsTsG+FyxD85E/ww1ZnwBbWo/yCEExGpI+Kilg7Z3qCdHX2BoJTQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -8953,10 +8701,6 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@6.28.0: - resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} - engines: {node: '>=18.17'} - undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} @@ -9018,10 +8762,6 @@ packages: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true - validate-npm-package-name@7.0.2: - resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} - engines: {node: ^20.17.0 || >=22.9.0} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -9149,10 +8889,6 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - walk-up-path@4.0.0: - resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} - engines: {node: 20 || >=22} - web-component-analyzer@2.0.0: resolution: {integrity: sha512-UEvwfpD+XQw99sLKiH5B1T4QwpwNyWJxp59cnlRwFfhUW6JsQpw5jMeMwi7580sNou8YL3kYoS7BWLm+yJ/jVQ==} hasBin: true @@ -9234,11 +8970,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@6.0.1: - resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -10691,8 +10422,6 @@ snapshots: dependencies: minipass: 7.1.3 - '@isaacs/string-locale-compare@1.1.0': {} - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -11178,16 +10907,6 @@ snapshots: '@novnc/novnc@1.7.0': {} - '@npmcli/agent@4.0.2(supports-color@10.2.2)': - dependencies: - agent-base: 7.1.4 - http-proxy-agent: 7.0.2(supports-color@10.2.2) - https-proxy-agent: 7.0.6(supports-color@10.2.2) - lru-cache: 11.5.2 - socks-proxy-agent: 8.0.5(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - '@npmcli/agent@5.0.2(supports-color@10.2.2)': dependencies: agent-base: 9.0.0 @@ -11199,120 +10918,12 @@ snapshots: - kerberos - supports-color - '@npmcli/arborist@9.8.0(supports-color@10.2.2)': - dependencies: - '@gar/promise-retry': 1.0.3 - '@isaacs/string-locale-compare': 1.1.0 - '@npmcli/fs': 5.0.0 - '@npmcli/installed-package-contents': 4.0.0 - '@npmcli/map-workspaces': 5.0.3 - '@npmcli/metavuln-calculator': 9.0.3(supports-color@10.2.2) - '@npmcli/name-from-folder': 4.0.0 - '@npmcli/node-gyp': 5.0.0 - '@npmcli/package-json': 7.0.5 - '@npmcli/query': 5.0.0 - '@npmcli/redact': 4.0.0 - '@npmcli/run-script': 10.0.4 - bin-links: 6.0.2 - cacache: 20.0.4 - common-ancestor-path: 2.0.0 - hosted-git-info: 9.0.3 - json-stringify-nice: 1.1.4 - lru-cache: 11.5.2 - minimatch: 10.2.5 - nopt: 9.0.0 - npm-install-checks: 8.0.0 - npm-package-arg: 13.0.2 - npm-pick-manifest: 11.0.3 - npm-registry-fetch: 19.1.1(supports-color@10.2.2) - pacote: 21.5.1(supports-color@10.2.2) - parse-conflict-json: 5.0.1 - proc-log: 6.1.0 - proggy: 4.0.0 - promise-all-reject-late: 1.0.1 - promise-call-limit: 3.0.2 - semver: 7.8.5 - ssri: 13.0.1 - treeverse: 3.0.0 - walk-up-path: 4.0.0 - transitivePeerDependencies: - - supports-color - - '@npmcli/fs@5.0.0': - dependencies: - semver: 7.8.5 - '@npmcli/fs@6.0.0': dependencies: semver: 7.8.5 - '@npmcli/git@7.0.2': - dependencies: - '@gar/promise-retry': 1.0.3 - '@npmcli/promise-spawn': 9.0.1 - ini: 6.0.0 - lru-cache: 11.5.2 - npm-pick-manifest: 11.0.3 - proc-log: 6.1.0 - semver: 7.8.5 - which: 6.0.1 - - '@npmcli/installed-package-contents@4.0.0': - dependencies: - npm-bundled: 5.0.0 - npm-normalize-package-bin: 5.0.0 - - '@npmcli/map-workspaces@5.0.3': - dependencies: - '@npmcli/name-from-folder': 4.0.0 - '@npmcli/package-json': 7.0.5 - glob: 13.0.6 - minimatch: 10.2.5 - - '@npmcli/metavuln-calculator@9.0.3(supports-color@10.2.2)': - dependencies: - cacache: 20.0.4 - json-parse-even-better-errors: 5.0.0 - pacote: 21.5.1(supports-color@10.2.2) - proc-log: 6.1.0 - semver: 7.8.5 - transitivePeerDependencies: - - supports-color - - '@npmcli/name-from-folder@4.0.0': {} - - '@npmcli/node-gyp@5.0.0': {} - - '@npmcli/package-json@7.0.5': - dependencies: - '@npmcli/git': 7.0.2 - glob: 13.0.6 - hosted-git-info: 9.0.3 - json-parse-even-better-errors: 5.0.0 - proc-log: 6.1.0 - semver: 7.8.5 - spdx-expression-parse: 4.0.0 - - '@npmcli/promise-spawn@9.0.1': - dependencies: - which: 6.0.1 - - '@npmcli/query@5.0.0': - dependencies: - postcss-selector-parser: 7.1.4 - - '@npmcli/redact@4.0.0': {} - '@npmcli/redact@5.0.0': {} - '@npmcli/run-script@10.0.4': - dependencies: - '@npmcli/node-gyp': 5.0.0 - '@npmcli/package-json': 7.0.5 - '@npmcli/promise-spawn': 9.0.1 - node-gyp: 12.4.0 - proc-log: 6.1.0 - '@openai/codex@0.148.0': optionalDependencies: '@openai/codex-darwin-arm64': '@openai/codex@0.148.0-darwin-arm64' @@ -12036,31 +11647,14 @@ snapshots: '@shoelace-style/localize@3.2.3': {} - '@sigstore/bundle@4.0.0': - dependencies: - '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/bundle@5.0.0': dependencies: '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/core@3.2.1': {} - '@sigstore/core@4.0.1': {} '@sigstore/protobuf-specs@0.5.1': {} - '@sigstore/sign@4.1.1(supports-color@10.2.2)': - dependencies: - '@gar/promise-retry': 1.0.3 - '@sigstore/bundle': 4.0.0 - '@sigstore/core': 3.2.1 - '@sigstore/protobuf-specs': 0.5.1 - make-fetch-happen: 15.0.6(supports-color@10.2.2) - proc-log: 6.1.0 - transitivePeerDependencies: - - supports-color - '@sigstore/sign@5.0.0(supports-color@10.2.2)': dependencies: '@gar/promise-retry': 1.0.3 @@ -12073,13 +11667,6 @@ snapshots: - kerberos - supports-color - '@sigstore/tuf@4.0.2(supports-color@10.2.2)': - dependencies: - '@sigstore/protobuf-specs': 0.5.1 - tuf-js: 4.1.0(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - '@sigstore/tuf@5.0.0(supports-color@10.2.2)': dependencies: '@sigstore/protobuf-specs': 0.5.1 @@ -12087,12 +11674,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sigstore/verify@3.1.1': - dependencies: - '@sigstore/bundle': 4.0.0 - '@sigstore/core': 3.2.1 - '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/verify@4.1.0': dependencies: '@sigstore/bundle': 5.0.0 @@ -12363,11 +11944,6 @@ snapshots: '@tufjs/canonical-json@2.0.0': {} - '@tufjs/models@4.1.0': - dependencies: - '@tufjs/canonical-json': 2.0.0 - minimatch: 10.2.5 - '@tufjs/models@5.0.0': dependencies: '@tufjs/canonical-json': 2.0.0 @@ -12856,8 +12432,6 @@ snapshots: dependencies: '@wasm-audio-decoders/common': 9.0.7 - abbrev@4.0.0: {} - abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -13082,14 +12656,6 @@ snapshots: bignumber.js@9.3.1: {} - bin-links@6.0.2: - dependencies: - cmd-shim: 8.0.0 - npm-normalize-package-bin: 5.0.0 - proc-log: 6.1.0 - read-cmd-shim: 6.0.0 - write-file-atomic: 7.0.1 - birpc@4.0.0: {} bn.js@4.12.5: {} @@ -13145,19 +12711,6 @@ snapshots: cac@7.0.0: {} - cacache@20.0.4: - dependencies: - '@npmcli/fs': 5.0.0 - fs-minipass: 3.0.3 - glob: 13.0.6 - lru-cache: 11.5.2 - minipass: 7.1.3 - minipass-collect: 2.0.1 - minipass-flush: 1.0.7 - minipass-pipeline: 1.2.4 - p-map: 7.0.6 - ssri: 13.0.1 - cacache@21.0.1: dependencies: '@npmcli/fs': 6.0.0 @@ -13247,8 +12800,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - cmd-shim@8.0.0: {} - codec-parser@2.5.0: {} collapse-white-space@2.1.0: {} @@ -13289,8 +12840,6 @@ snapshots: commander@15.0.0: {} - common-ancestor-path@2.0.0: {} - composed-offset-position@0.0.6(@floating-ui/utils@0.2.12): dependencies: '@floating-ui/utils': 0.2.12 @@ -13665,8 +13214,6 @@ snapshots: expect-type@1.4.0: {} - exponential-backoff@3.1.3: {} - express-rate-limit@8.6.2(express@5.2.1(supports-color@10.2.2))(supports-color@10.2.2): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -14057,10 +13604,6 @@ snapshots: dependencies: lru-cache: 11.5.2 - hosted-git-info@9.0.3: - dependencies: - lru-cache: 11.5.2 - html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) @@ -14146,10 +13689,6 @@ snapshots: ieee754@1.2.1: {} - ignore-walk@8.0.0: - dependencies: - minimatch: 10.2.5 - ignore@7.0.6: {} immediate@3.0.6: {} @@ -14173,8 +13712,6 @@ snapshots: ini@1.3.8: {} - ini@6.0.0: {} - inline-style-parser@0.2.7: {} int64-buffer@1.1.1: {} @@ -14260,8 +13797,6 @@ snapshots: isexe@2.0.0: {} - isexe@4.0.0: {} - isobject@3.0.1: {} istanbul-lib-coverage@3.2.2: {} @@ -14354,8 +13889,6 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - json-parse-even-better-errors@5.0.0: {} - json-schema-to-ts@3.1.1: dependencies: '@babel/runtime': 7.29.7 @@ -14365,14 +13898,10 @@ snapshots: json-schema-typed@8.0.2: {} - json-stringify-nice@1.1.4: {} - json5@2.2.3: {} jsonc-parser@3.3.1: {} - jsonparse@1.3.1: {} - jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -14393,10 +13922,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - just-diff-apply@5.5.0: {} - - just-diff@6.0.2: {} - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -14627,23 +14152,6 @@ snapshots: dependencies: semver: 7.8.5 - make-fetch-happen@15.0.6(supports-color@10.2.2): - dependencies: - '@gar/promise-retry': 1.0.3 - '@npmcli/agent': 4.0.2(supports-color@10.2.2) - '@npmcli/redact': 4.0.0 - cacache: 20.0.4 - http-cache-semantics: 4.2.0 - minipass: 7.1.3 - minipass-fetch: 5.0.2 - minipass-flush: 1.0.7 - minipass-pipeline: 1.2.4 - negotiator: 1.0.0 - proc-log: 6.1.0 - ssri: 13.0.1 - transitivePeerDependencies: - - supports-color - make-fetch-happen@16.0.1(supports-color@10.2.2): dependencies: '@gar/promise-retry': 1.0.3 @@ -15095,14 +14603,6 @@ snapshots: dependencies: minipass: 7.1.3 - minipass-fetch@5.0.2: - dependencies: - minipass: 7.1.3 - minipass-sized: 2.0.0 - minizlib: 3.1.0 - optionalDependencies: - iconv-lite: 0.7.3 - minipass-fetch@6.0.0: dependencies: minipass: 7.1.3 @@ -15201,29 +14701,12 @@ snapshots: node-gyp-build@4.8.4: {} - node-gyp@12.4.0: - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.3 - graceful-fs: 4.2.11 - nopt: 9.0.0 - proc-log: 6.1.0 - semver: 7.8.5 - tar: 7.5.22 - tinyglobby: 0.2.17 - undici: 6.28.0 - which: 6.0.1 - node-pty@1.2.0-beta.14: dependencies: node-addon-api: 7.1.1 node-wav@0.0.2: {} - nopt@9.0.0: - dependencies: - abbrev: 4.0.0 - normalize-path@3.0.0: {} nostr-tools@2.24.1(typescript@7.0.2): @@ -15240,48 +14723,6 @@ snapshots: nostr-wasm@0.1.0: {} - npm-bundled@5.0.0: - dependencies: - npm-normalize-package-bin: 5.0.0 - - npm-install-checks@8.0.0: - dependencies: - semver: 7.8.5 - - npm-normalize-package-bin@5.0.0: {} - - npm-package-arg@13.0.2: - dependencies: - hosted-git-info: 9.0.3 - proc-log: 6.1.0 - semver: 7.8.5 - validate-npm-package-name: 7.0.2 - - npm-packlist@10.0.4: - dependencies: - ignore-walk: 8.0.0 - proc-log: 6.1.0 - - npm-pick-manifest@11.0.3: - dependencies: - npm-install-checks: 8.0.0 - npm-normalize-package-bin: 5.0.0 - npm-package-arg: 13.0.2 - semver: 7.8.5 - - npm-registry-fetch@19.1.1(supports-color@10.2.2): - dependencies: - '@npmcli/redact': 4.0.0 - jsonparse: 1.3.1 - make-fetch-happen: 15.0.6(supports-color@10.2.2) - minipass: 7.1.3 - minipass-fetch: 5.0.2 - minizlib: 3.1.0 - npm-package-arg: 13.0.2 - proc-log: 6.1.0 - transitivePeerDependencies: - - supports-color - npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -15492,28 +14933,6 @@ snapshots: p-try@2.2.0: {} - pacote@21.5.1(supports-color@10.2.2): - dependencies: - '@gar/promise-retry': 1.0.3 - '@npmcli/git': 7.0.2 - '@npmcli/installed-package-contents': 4.0.0 - '@npmcli/package-json': 7.0.5 - '@npmcli/promise-spawn': 9.0.1 - '@npmcli/run-script': 10.0.4 - cacache: 20.0.4 - fs-minipass: 3.0.3 - minipass: 7.1.3 - npm-package-arg: 13.0.2 - npm-packlist: 10.0.4 - npm-pick-manifest: 11.0.3 - npm-registry-fetch: 19.1.1(supports-color@10.2.2) - proc-log: 6.1.0 - sigstore: 4.1.1(supports-color@10.2.2) - ssri: 13.0.1 - tar: 7.5.22 - transitivePeerDependencies: - - supports-color - pako@1.0.11: {} pako@2.2.0: {} @@ -15524,12 +14943,6 @@ snapshots: dependencies: callsites: 3.1.0 - parse-conflict-json@5.0.1: - dependencies: - json-parse-even-better-errors: 5.0.0 - just-diff: 6.0.2 - just-diff-apply: 5.5.0 - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -15669,20 +15082,12 @@ snapshots: prism-media@1.3.5: {} - proc-log@6.1.0: {} - proc-log@7.0.0: {} process-nextick-args@2.0.1: {} process-warning@5.0.0: {} - proggy@4.0.0: {} - - promise-all-reject-late@1.0.1: {} - - promise-call-limit@3.0.2: {} - proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -15763,8 +15168,6 @@ snapshots: react@19.2.4: {} - read-cmd-shim@6.0.0: {} - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -16094,17 +15497,6 @@ snapshots: dependencies: signal-polyfill: 0.2.2 - sigstore@4.1.1(supports-color@10.2.2): - dependencies: - '@sigstore/bundle': 4.0.0 - '@sigstore/core': 3.2.1 - '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/sign': 4.1.1(supports-color@10.2.2) - '@sigstore/tuf': 4.0.2(supports-color@10.2.2) - '@sigstore/verify': 3.1.1 - transitivePeerDependencies: - - supports-color - sigstore@5.0.0(supports-color@10.2.2): dependencies: '@sigstore/bundle': 5.0.0 @@ -16156,14 +15548,6 @@ snapshots: transitivePeerDependencies: - supports-color - socks-proxy-agent@8.0.5(supports-color@10.2.2): - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.2.2) - socks: 2.8.9 - transitivePeerDependencies: - - supports-color - socks@2.8.9: dependencies: ip-address: 10.3.1 @@ -16188,15 +15572,6 @@ snapshots: spark-md5@3.0.2: {} - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@4.0.0: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-license-ids@3.0.23: {} - split2@4.2.0: {} sqlite-vec-darwin-arm64@0.1.9: @@ -16223,10 +15598,6 @@ snapshots: sqlite-vec-windows-x64: 0.1.9 optional: true - ssri@13.0.1: - dependencies: - minipass: 7.1.3 - ssri@14.0.0: dependencies: minipass: 7.1.3 @@ -16464,8 +15835,6 @@ snapshots: node-addon-api: 8.9.0 node-gyp-build: 4.8.4 - treeverse@3.0.0: {} - trim-lines@3.0.1: {} trough@2.2.0: {} @@ -16519,14 +15888,6 @@ snapshots: dependencies: tslib: 1.14.1 - tuf-js@4.1.0(supports-color@10.2.2): - dependencies: - '@tufjs/models': 4.1.0 - debug: 4.4.3(supports-color@10.2.2) - make-fetch-happen: 15.0.6(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - tuf-js@6.0.0(supports-color@10.2.2): dependencies: '@gar/promise-retry': 1.0.3 @@ -16597,8 +15958,6 @@ snapshots: undici-types@8.3.0: {} - undici@6.28.0: {} - undici@7.29.0: {} undici@8.9.0: {} @@ -16656,8 +16015,6 @@ snapshots: uuid@14.0.1: {} - validate-npm-package-name@7.0.2: {} - vary@1.1.2: {} vfile-message@4.0.3: @@ -16794,8 +16151,6 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - walk-up-path@4.0.0: {} - web-component-analyzer@2.0.0: dependencies: fast-glob: 3.3.3 @@ -16916,10 +16271,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@6.0.1: - dependencies: - isexe: 4.0.0 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/scripts/lib/plugin-npm-pack-files.mjs b/scripts/lib/plugin-npm-pack-files.mjs deleted file mode 100644 index 2a98ea87d5c1..000000000000 --- a/scripts/lib/plugin-npm-pack-files.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import Arborist from "@npmcli/arborist"; -import packlist from "npm-packlist"; - -const packageDir = process.argv[2]; -if (!packageDir) { - throw new Error("package directory is required"); -} - -const tree = await new Arborist({ path: packageDir }).loadActual(); -const files = await packlist(tree, { path: packageDir }); -process.stdout.write(JSON.stringify(files)); diff --git a/scripts/lib/plugin-npm-security-scan.mts b/scripts/lib/plugin-npm-security-scan.mts index 9c64cb4cc6e7..763370e087c7 100644 --- a/scripts/lib/plugin-npm-security-scan.mts +++ b/scripts/lib/plugin-npm-security-scan.mts @@ -1,6 +1,5 @@ import { execFile } from "node:child_process"; import { - copyFileSync, lstatSync, mkdirSync, mkdtempSync, @@ -10,10 +9,8 @@ import { rmSync, writeFileSync, } from "node:fs"; -import { createRequire } from "node:module"; import { tmpdir } from "node:os"; -import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { fileURLToPath } from "node:url"; +import { basename, dirname, join, relative, resolve, sep } from "node:path"; import { promisify } from "node:util"; import { isScannable, @@ -51,9 +48,12 @@ export type ScanPackageResult = { }; type PluginNpmSecurityArtifact = PublishablePluginPackage & { - artifactKind: "inert-package-input"; + artifactKind: "publication-equivalent-plugin-tarball"; artifactDir: string; candidateSha: string; + compressedBytes: number; + expandedBytes: number; + packOwner: "scripts/plugin-npm-publish.sh"; tarballPath: string; tarballSha256: string; toolingSha: string; @@ -76,10 +76,6 @@ export type PluginNpmSecurityScanReport = { }; const execFileAsync = promisify(execFile); -const require = createRequire(import.meta.url); -const validateNpmPackageName = require("validate-npm-package-name") as (name: unknown) => { - validForNewPackages: boolean; -}; export const MAX_PUBLISHABLE_PLUGIN_PACKAGES = 256; export const MAX_PLUGIN_PACKAGE_MANIFEST_BYTES = 256 * 1024; export const MAX_PLUGIN_SCAN_FINDINGS_PER_PACKAGE = 10_000; @@ -87,26 +83,16 @@ export const MAX_PLUGIN_SCAN_TOTAL_FINDINGS = 50_000; export const MAX_PLUGIN_SCAN_REPORT_BYTES = 1024 * 1024; const MAX_PLUGIN_SECURITY_ARTIFACT_METADATA_BYTES = 64 * 1024; const MAX_PLUGIN_TARBALL_BYTES = 128 * 1024 * 1024; +const MAX_PLUGIN_TARBALL_TOTAL_BYTES = 512 * 1024 * 1024; +const MAX_PLUGIN_EXPANDED_TOTAL_BYTES = 2 * 1024 * 1024 * 1024; const MAX_PACKED_FILES_PER_PACKAGE = 20_000; const MAX_PACKED_FILE_BYTES = 64 * 1024 * 1024; const MAX_PACKED_TOTAL_BYTES_PER_PACKAGE = 256 * 1024 * 1024; const MAX_SCANNABLE_FILES_PER_PACKAGE = 10_000; const MAX_SCANNABLE_FILE_BYTES = 1024 * 1024; const MAX_SCANNABLE_TOTAL_BYTES_PER_PACKAGE = 64 * 1024 * 1024; -const MAX_PACKED_PATH_BYTES = 4096; -const PACKLIST_HELPER_MAX_BUFFER_BYTES = 8 * 1024 * 1024; -const PACKLIST_HELPER_MAX_OLD_SPACE_MB = 256; -const PACKLIST_HELPER_TIMEOUT_MS = 60_000; const PACKAGE_SCAN_CONCURRENCY = 4; -const PACKLIST_HELPER_PATH = fileURLToPath(new URL("./plugin-npm-pack-files.mjs", import.meta.url)); -const DEFAULT_SCANNER_INPUT_LIMITS = { - maxPackedFileBytes: MAX_PACKED_FILE_BYTES, - maxPackedFiles: MAX_PACKED_FILES_PER_PACKAGE, - maxPackedTotalBytes: MAX_PACKED_TOTAL_BYTES_PER_PACKAGE, - maxFileBytes: MAX_SCANNABLE_FILE_BYTES, - maxFiles: MAX_SCANNABLE_FILES_PER_PACKAGE, - maxTotalBytes: MAX_SCANNABLE_TOTAL_BYTES_PER_PACKAGE, -}; +const CANONICAL_NPM_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u; const COMMON_REVIEWED_CRITICAL_FINDING_COUNTS = new Map([ ["@openclaw/acpx:dangerous-exec:src/codex-auth-bridge.ts", 1], @@ -197,89 +183,6 @@ export function resolveReviewedSourceLayout( ); } -export function parsePacklistFiles(raw: string, packageName: string): string[] { - const parsed = JSON.parse(raw) as unknown; - if (!Array.isArray(parsed)) { - throw new Error(`${packageName}: packlist helper did not return a files list.`); - } - - if (parsed.length > MAX_PACKED_FILES_PER_PACKAGE) { - throw new Error(`${packageName}: packlist exceeds the file-count limit.`); - } - - const packedPaths: string[] = []; - const seenPaths = new Set(); - for (const [index, packedPath] of parsed.entries()) { - if ( - typeof packedPath !== "string" || - !isSafePackedPath(packedPath) || - Buffer.byteLength(packedPath, "utf8") > MAX_PACKED_PATH_BYTES - ) { - throw new Error(`${packageName}: packlist entry ${index} has an invalid path.`); - } - if (seenPaths.has(packedPath)) { - throw new Error(`${packageName}: packlist returned a duplicate path: ${packedPath}`); - } - seenPaths.add(packedPath); - packedPaths.push(packedPath); - } - return packedPaths.toSorted(); -} - -type PacklistHelperLimits = { - helperPath?: string; - maxBufferBytes?: number; - maxOldSpaceMb?: number; - timeoutMs?: number; -}; - -export async function collectNpmPackedFiles( - packageDir: string, - packageName: string, - limits: PacklistHelperLimits = {}, -): Promise { - const helperPath = limits.helperPath ?? PACKLIST_HELPER_PATH; - const maxOldSpaceMb = limits.maxOldSpaceMb ?? PACKLIST_HELPER_MAX_OLD_SPACE_MB; - const timeoutMs = limits.timeoutMs ?? PACKLIST_HELPER_TIMEOUT_MS; - try { - const { stdout } = await execFileAsync( - process.execPath, - [`--max-old-space-size=${maxOldSpaceMb}`, helperPath, packageDir], - { - cwd: dirname(PACKLIST_HELPER_PATH), - encoding: "utf8", - env: { - CI: "1", - HOME: tmpdir(), - PATH: process.env.PATH, - }, - killSignal: "SIGKILL", - maxBuffer: limits.maxBufferBytes ?? PACKLIST_HELPER_MAX_BUFFER_BYTES, - signal: AbortSignal.timeout(timeoutMs), - }, - ); - return parsePacklistFiles(stdout, packageName); - } catch (error) { - const failure = - error && typeof error === "object" - ? (error as { code?: unknown; killed?: unknown; signal?: unknown }) - : {}; - if (failure.code === "ABORT_ERR" || failure.code === "ETIMEDOUT") { - throw new Error(`${packageName}: trusted packlist helper timed out.`); - } - if (failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") { - throw new Error(`${packageName}: trusted packlist helper exceeded its output limit.`); - } - if (failure.killed === true || typeof failure.signal === "string") { - throw new Error(`${packageName}: trusted packlist helper exceeded its process limit.`); - } - if (typeof failure.code === "number") { - throw new Error(`${packageName}: trusted packlist helper failed.`); - } - throw new Error(`${packageName}: trusted packlist helper could not start.`); - } -} - export function normalizePackedFindingPath(packedPath: string): string { for (const prefix of [ "dynamic-tools", @@ -319,96 +222,6 @@ function isReviewedCriticalFinding(key: string): boolean { ); } -function isSafePackedPath(packedPath: string): boolean { - if ( - !packedPath || - isAbsolute(packedPath) || - packedPath.includes("\\") || - /[\u0000-\u001f\u007f]/u.test(packedPath) || - packedPath.split("/").some((segment) => !segment || segment === "." || segment === "..") - ) { - return false; - } - return !packedPath.split("/").some((segment) => { - return segment === "node_modules" || segment.startsWith("."); - }); -} - -function assertPathInside(parentPath: string, childPath: string): void { - const relativePath = relative(parentPath, childPath); - if (relativePath === "" || (!relativePath.startsWith(`..${sep}`) && relativePath !== "..")) { - return; - } - throw new Error(`Packed file escaped its plugin package: ${relativePath}`); -} - -export function stageScannerRelevantPackedFiles( - packageDir: string, - packedFiles: readonly string[], - limits = DEFAULT_SCANNER_INPUT_LIMITS, -): { - fileCount: number; - packedFileCount: number; - packedTotalBytes: number; - stageDir: string; - totalBytes: number; -} { - const stageDir = mkdtempSync(join(tmpdir(), "openclaw-plugin-npm-scan-")); - const realPackageDir = realpathSync(packageDir); - let fileCount = 0; - let packedFileCount = 0; - let packedTotalBytes = 0; - let totalBytes = 0; - - try { - for (const packedPath of packedFiles) { - if (!isSafePackedPath(packedPath)) { - throw new Error(`npm pack returned an unsafe path: ${packedPath}`); - } - const source = resolve(realPackageDir, packedPath); - const sourceStat = lstatSync(source); - if (!sourceStat.isFile()) { - throw new Error(`Packed scanner input is not a regular file: ${packedPath}`); - } - packedFileCount += 1; - packedTotalBytes += sourceStat.size; - if (sourceStat.size > limits.maxPackedFileBytes) { - throw new Error(`Packed input exceeds the per-file byte limit: ${packedPath}`); - } - if (packedFileCount > limits.maxPackedFiles) { - throw new Error("Packed input exceeds the file-count limit."); - } - if (packedTotalBytes > limits.maxPackedTotalBytes) { - throw new Error("Packed input exceeds the total-byte limit."); - } - - const realSource = realpathSync(source); - assertPathInside(realPackageDir, realSource); - if (!isScannable(packedPath)) { - continue; - } - if (sourceStat.size > limits.maxFileBytes) { - throw new Error(`Packed scanner input exceeds the per-file byte limit: ${packedPath}`); - } - fileCount += 1; - totalBytes += sourceStat.size; - if (fileCount > limits.maxFiles) { - throw new Error("Packed scanner input exceeds the file-count limit."); - } - if (totalBytes > limits.maxTotalBytes) { - throw new Error("Packed scanner input exceeds the total-byte limit."); - } - const target = join(stageDir, ...packedPath.split("/")); - mkdirSync(dirname(target), { recursive: true }); - copyFileSync(realSource, target); - } - return { fileCount, packedFileCount, packedTotalBytes, stageDir, totalBytes }; - } catch (error) { - rmSync(stageDir, { recursive: true, force: true }); - throw error; - } -} - async function gitOutput(rootDir: string, args: string[]): Promise { const { stdout } = await execFileAsync("git", ["-C", rootDir, ...args], { encoding: "utf8", @@ -421,13 +234,39 @@ export function assertCanonicalNpmPackageName(packageName: unknown, label: strin if ( typeof packageName !== "string" || packageName.trim() !== packageName || - !validateNpmPackageName(packageName).validForNewPackages + packageName.length > 214 || + !CANONICAL_NPM_PACKAGE_NAME.test(packageName) ) { throw new Error(`${label}: publishable plugin has an invalid npm package name.`); } return packageName; } +export function resolveCandidatePluginPackageDir( + candidateDir: string, + extensionId: string, +): string { + const candidateRoot = realpathSync(candidateDir); + const packageDir = resolve(candidateRoot, "extensions", extensionId); + const relativePackageDir = relative(candidateRoot, packageDir); + if (relativePackageDir !== `extensions${sep}${extensionId}`) { + throw new Error(`extensions/${extensionId}: package directory escaped the candidate checkout.`); + } + const packageStat = lstatSync(packageDir); + if (!packageStat.isDirectory() || packageStat.isSymbolicLink()) { + throw new Error(`extensions/${extensionId}: package directory is not a real directory.`); + } + if (realpathSync(packageDir) !== packageDir) { + throw new Error(`extensions/${extensionId}: package directory resolves outside its path.`); + } + const packageJsonPath = join(packageDir, "package.json"); + const packageJsonStat = lstatSync(packageJsonPath); + if (!packageJsonStat.isFile() || packageJsonStat.isSymbolicLink()) { + throw new Error(`extensions/${extensionId}/package.json: manifest is not a regular file.`); + } + return packageDir; +} + export async function listPublishablePluginPackages( candidateDir: string, limits: { @@ -454,12 +293,9 @@ export async function listPublishablePluginPackages( if (!match?.[1]) { return []; } - const packageDir = resolve(candidateDir, "extensions", match[1]); + const packageDir = resolveCandidatePluginPackageDir(candidateDir, match[1]); const packageJsonPath = join(packageDir, "package.json"); const packageStat = lstatSync(packageJsonPath); - if (!packageStat.isFile()) { - throw new Error(`${packageFile}: package manifest is not a regular file.`); - } if ( packageStat.size === 0 || packageStat.size > (limits.maxManifestBytes ?? MAX_PLUGIN_PACKAGE_MANIFEST_BYTES) @@ -504,6 +340,19 @@ export async function listPublishablePluginPackages( } const PLUGIN_SECURITY_ARTIFACT_METADATA = "plugin-npm-security-artifact.json"; +const PLUGIN_SECURITY_ARTIFACT_PREFIX = "plugin-npm-security-package-"; + +type PluginNpmSecurityArtifactLimits = { + maxCompressedBytes?: number; + maxExpandedBytes?: number; +}; + +export type PluginNpmSecurityArtifactLoadResult = { + artifacts: PluginNpmSecurityArtifact[]; + compressedBytes: number; + expandedBytes: number; + ingestionErrors: string[]; +}; function parseExpectedPackages(value: unknown): PublishablePluginPackage[] { if (!Array.isArray(value) || value.length > MAX_PUBLISHABLE_PLUGIN_PACKAGES) { @@ -548,6 +397,7 @@ function parseExpectedPackages(value: unknown): PublishablePluginPackage[] { function readPluginSecurityArtifact( artifactDir: string, + expectedPackage: PublishablePluginPackage, expectedCandidateSha: string, expectedToolingSha: string, ): PluginNpmSecurityArtifact { @@ -555,16 +405,23 @@ function readPluginSecurityArtifact( const metadataStat = lstatSync(metadataPath); if ( !metadataStat.isFile() || + metadataStat.isSymbolicLink() || metadataStat.size === 0 || metadataStat.size > MAX_PLUGIN_SECURITY_ARTIFACT_METADATA_BYTES ) { throw new Error("Plugin security artifact metadata is outside the byte limit."); } - const metadata = JSON.parse(readFileSync(metadataPath, "utf8")) as Record; + let metadata: Record; + try { + metadata = JSON.parse(readFileSync(metadataPath, "utf8")) as Record; + } catch { + throw new Error("Plugin security artifact metadata is not valid JSON."); + } const expectedKeys = [ "artifactKind", "candidateSha", "extensionId", + "packOwner", "packageDir", "packageName", "packageVersion", @@ -574,7 +431,8 @@ function readPluginSecurityArtifact( "toolingSha", ]; if ( - metadata.artifactKind !== "inert-package-input" || + metadata.artifactKind !== "publication-equivalent-plugin-tarball" || + metadata.packOwner !== "scripts/plugin-npm-publish.sh" || metadata.schemaVersion !== 1 || JSON.stringify(Object.keys(metadata).toSorted()) !== JSON.stringify(expectedKeys) ) { @@ -592,12 +450,10 @@ function readPluginSecurityArtifact( if ( metadata.candidateSha !== expectedCandidateSha || metadata.toolingSha !== expectedToolingSha || - typeof extensionId !== "string" || - !/^[a-z0-9][a-z0-9._-]*$/u.test(extensionId) || - packageDir !== `extensions/${extensionId}` || - typeof packageVersion !== "string" || - !packageVersion || - packageVersion.trim() !== packageVersion || + extensionId !== expectedPackage.extensionId || + packageDir !== expectedPackage.packageDir || + packageName !== expectedPackage.packageName || + packageVersion !== expectedPackage.packageVersion || typeof tarballName !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/u.test(tarballName) || basename(tarballName) !== tarballName || @@ -612,6 +468,7 @@ function readPluginSecurityArtifact( artifactEntries.some( (entry) => !entry.isFile() || + entry.isSymbolicLink() || (entry.name !== PLUGIN_SECURITY_ARTIFACT_METADATA && entry.name !== tarballName), ) ) { @@ -621,16 +478,44 @@ function readPluginSecurityArtifact( const tarballStat = lstatSync(tarballPath); if ( !tarballStat.isFile() || + tarballStat.isSymbolicLink() || tarballStat.size === 0 || tarballStat.size > MAX_PLUGIN_TARBALL_BYTES ) { throw new Error(`${packageName}: plugin tarball is outside the byte limit.`); } + const tarballBytes = readBoundedRegularFile(tarballPath, { + label: "Plugin security tarball", + maxBytes: MAX_PLUGIN_TARBALL_BYTES, + }); + let inspection: ReturnType; + try { + inspection = inspectPackageTarballBytes(tarballBytes, { + maxArchiveBytes: MAX_PLUGIN_TARBALL_BYTES, + maxEntries: MAX_PACKED_FILES_PER_PACKAGE, + maxEntryBytes: MAX_PACKED_FILE_BYTES, + maxExpandedBytes: MAX_PACKED_TOTAL_BYTES_PER_PACKAGE, + maxPathBytes: 4 * 1024 * 1024, + maxTotalFileBytes: MAX_PACKED_TOTAL_BYTES_PER_PACKAGE, + }); + } catch { + throw new Error("Plugin security artifact tarball structure is invalid."); + } + if ( + inspection.tarballSha256 !== tarballSha256 || + inspection.packageManifest.name !== packageName || + inspection.packageManifest.version !== packageVersion + ) { + throw new Error("Plugin security artifact tarball identity is invalid."); + } return { - artifactKind: "inert-package-input", + artifactKind: "publication-equivalent-plugin-tarball", artifactDir, candidateSha: expectedCandidateSha, + compressedBytes: tarballStat.size, + expandedBytes: inspection.totalFileBytes, extensionId, + packOwner: "scripts/plugin-npm-publish.sh", packageDir, packageName, packageVersion, @@ -640,48 +525,147 @@ function readPluginSecurityArtifact( }; } -export function listPluginNpmSecurityArtifacts(params: { +function resolveAggregateLimit(value: number | undefined, fallback: number, label: string): number { + if (value === undefined) { + return fallback; + } + if (!Number.isSafeInteger(value) || value <= 0 || value > fallback) { + throw new Error(`${label} must be a positive integer no larger than ${fallback}.`); + } + return value; +} + +function artifactDirectoryName(candidateSha: string, extensionId: string): string { + return `${PLUGIN_SECURITY_ARTIFACT_PREFIX}${candidateSha}-${extensionId}`; +} + +function sanitizeArtifactIngestionError( + expectedPackage: PublishablePluginPackage, + error: unknown, +): string { + const knownCategories = new Set([ + "Plugin security artifact metadata is outside the byte limit.", + "Plugin security artifact metadata is not valid JSON.", + "Plugin security artifact metadata has an invalid shape.", + "Plugin security artifact metadata identity is invalid.", + "Plugin security artifact contains unexpected entries.", + "Plugin security artifact tarball structure is invalid.", + "Plugin security artifact tarball identity is invalid.", + ]); + const message = error instanceof Error ? error.message : ""; + const category = + knownCategories.has(message) || message.endsWith("plugin tarball is outside the byte limit.") + ? message.replace(`${expectedPackage.packageName}: `, "") + : "Plugin security artifact validation failed."; + return `${expectedPackage.packageName}: ${category}`; +} + +export function loadPluginNpmSecurityArtifacts(params: { artifactRoot: string; candidateSha: string; expectedPackages: unknown; + limits?: PluginNpmSecurityArtifactLimits; toolingSha: string; -}): PluginNpmSecurityArtifact[] { +}): PluginNpmSecurityArtifactLoadResult { const expectedPackages = parseExpectedPackages(params.expectedPackages); + const rootStat = lstatSync(params.artifactRoot); + if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) { + throw new Error("Plugin security artifact root is not a real directory."); + } const artifactRoot = realpathSync(params.artifactRoot); const entries = readdirSync(artifactRoot, { withFileTypes: true }).toSorted((left, right) => compareCodeUnits(left.name, right.name), ); + const maxCompressedBytes = resolveAggregateLimit( + params.limits?.maxCompressedBytes, + MAX_PLUGIN_TARBALL_TOTAL_BYTES, + "Plugin security compressed-byte limit", + ); + const maxExpandedBytes = resolveAggregateLimit( + params.limits?.maxExpandedBytes, + MAX_PLUGIN_EXPANDED_TOTAL_BYTES, + "Plugin security expanded-byte limit", + ); + const entriesByName = new Map(entries.map((entry) => [entry.name, entry])); + const expectedNames = new Set( + expectedPackages.map((plugin) => + artifactDirectoryName(params.candidateSha, plugin.extensionId), + ), + ); + const ingestionErrors: string[] = []; if (entries.length > MAX_PUBLISHABLE_PLUGIN_PACKAGES) { - throw new Error("Plugin security artifact set exceeds the package-count limit."); + ingestionErrors.push("Plugin security artifact set exceeds the package-count limit."); } - const artifacts = entries.map((entry) => { - if (!entry.isDirectory() || entry.isSymbolicLink()) { - throw new Error("Plugin security artifact root contains a non-directory entry."); - } - return readPluginSecurityArtifact( - join(artifactRoot, entry.name), - params.candidateSha, - params.toolingSha, + const unexpectedEntryCount = entries.filter((entry) => !expectedNames.has(entry.name)).length; + if (unexpectedEntryCount > 0) { + ingestionErrors.push( + `Plugin security artifact root contains ${unexpectedEntryCount} unexpected entries.`, ); - }); - const sorted = artifacts.toSorted((left, right) => - compareCodeUnits(left.packageName, right.packageName), - ); - if (new Set(sorted.map((plugin) => plugin.packageName)).size !== sorted.length) { - throw new Error("Plugin security artifact set contains duplicate package names."); } - const observedPackages = sorted.map( - ({ extensionId, packageDir, packageName, packageVersion }) => ({ - extensionId, - packageDir, - packageName, - packageVersion, - }), - ); - if (JSON.stringify(observedPackages) !== JSON.stringify(expectedPackages)) { - throw new Error("Plugin security artifact set does not match the trusted package plan."); + + const artifacts: PluginNpmSecurityArtifact[] = []; + let compressedBytes = 0; + let expandedBytes = 0; + for (const expectedPackage of expectedPackages) { + const entryName = artifactDirectoryName(params.candidateSha, expectedPackage.extensionId); + const entry = entriesByName.get(entryName); + if (!entry) { + ingestionErrors.push(`${expectedPackage.packageName}: plugin security artifact is missing.`); + continue; + } + if (!entry.isDirectory() || entry.isSymbolicLink()) { + ingestionErrors.push( + `${expectedPackage.packageName}: plugin security artifact is not a real directory.`, + ); + continue; + } + try { + const artifact = readPluginSecurityArtifact( + join(artifactRoot, entry.name), + expectedPackage, + params.candidateSha, + params.toolingSha, + ); + if (compressedBytes + artifact.compressedBytes > maxCompressedBytes) { + ingestionErrors.push( + `${expectedPackage.packageName}: aggregate compressed-byte limit exceeded.`, + ); + continue; + } + if (expandedBytes + artifact.expandedBytes > maxExpandedBytes) { + ingestionErrors.push( + `${expectedPackage.packageName}: aggregate expanded-byte limit exceeded.`, + ); + continue; + } + compressedBytes += artifact.compressedBytes; + expandedBytes += artifact.expandedBytes; + artifacts.push(artifact); + } catch (error) { + ingestionErrors.push(sanitizeArtifactIngestionError(expectedPackage, error)); + } } - return sorted; + + return { + artifacts, + compressedBytes, + expandedBytes, + ingestionErrors: sortStrings(ingestionErrors), + }; +} + +export function listPluginNpmSecurityArtifacts(params: { + artifactRoot: string; + candidateSha: string; + expectedPackages: unknown; + limits?: PluginNpmSecurityArtifactLimits; + toolingSha: string; +}): PluginNpmSecurityArtifact[] { + const result = loadPluginNpmSecurityArtifacts(params); + if (result.ingestionErrors.length > 0) { + throw new Error(result.ingestionErrors.join("\n")); + } + return result.artifacts; } export function stageScannerRelevantPluginTarballFiles(tarballPath: string): { @@ -787,7 +771,7 @@ async function scanPublishablePluginArtifact( staged.inspection.packageManifest.version !== plugin.packageVersion || staged.inspection.tarballSha256 !== plugin.tarballSha256 ) { - throw new Error(`${plugin.packageName}: inert package input identity mismatch.`); + throw new Error(`${plugin.packageName}: publication artifact identity mismatch.`); } for (const packedFile of staged.packedFiles) { expectedReviewedCriticalFindings.push( @@ -999,6 +983,7 @@ export async function runPluginNpmSecurityScan(params: { artifactRoot: string; candidateSha: string; expectedPackages: unknown; + limits?: PluginNpmSecurityArtifactLimits; toolingDir: string; toolingSha: string; }): Promise { @@ -1007,18 +992,19 @@ export async function runPluginNpmSecurityScan(params: { if (toolingSha !== params.toolingSha) { throw new Error("Trusted scanner tooling checkout differs from the expected commit."); } - const packages = listPluginNpmSecurityArtifacts({ + const loaded = loadPluginNpmSecurityArtifacts({ artifactRoot: params.artifactRoot, candidateSha: params.candidateSha, expectedPackages: params.expectedPackages, + limits: params.limits, toolingSha, }); - const { packageResults, scanErrors } = await scanPublishablePluginPackages(packages); + const { packageResults, scanErrors } = await scanPublishablePluginPackages(loaded.artifacts); return constrainPluginNpmSecurityScanReport( buildPluginNpmSecurityScanReport({ candidateSha: params.candidateSha, packageResults, - scanErrors, + scanErrors: [...loaded.ingestionErrors, ...scanErrors], toolingSha, }), ); diff --git a/scripts/plugin-npm-security-prepare.mts b/scripts/plugin-npm-security-prepare.mts index c408d034680b..9c250ca465b0 100644 --- a/scripts/plugin-npm-security-prepare.mts +++ b/scripts/plugin-npm-security-prepare.mts @@ -11,11 +11,10 @@ import { basename, join, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import { resolveNpmJsonEntries } from "./lib/npm-json-output.mts"; import { - collectNpmPackedFiles, listPublishablePluginPackages, + resolveCandidatePluginPackageDir, type PublishablePluginPackage, } from "./lib/plugin-npm-security-scan.mts"; -import { resolveNpmRunner } from "./npm-runner.mts"; import { inspectPackageTarballBytes, readBoundedRegularFile, @@ -156,16 +155,7 @@ async function preparePackage(args: ParsedArgs): Promise { if (!selected) { throw new Error("Selected plugin package is absent from the trusted package plan."); } - - const testPacklistHelper = - process.env.NODE_ENV === "test" - ? process.env.OPENCLAW_PLUGIN_SECURITY_TEST_PACKLIST_HELPER - : undefined; - const expectedPackedFiles = await collectNpmPackedFiles( - selected.packageDir, - selected.packageName, - testPacklistHelper ? { helperPath: testPacklistHelper } : {}, - ); + resolveCandidatePluginPackageDir(candidateRoot, args.extensionId); if (existsSync(args.outputDir)) { if (readdirSync(args.outputDir).length !== 0) { throw new Error("Plugin security artifact output directory must be empty."); @@ -174,39 +164,32 @@ async function preparePackage(args: ParsedArgs): Promise { mkdirSync(args.outputDir, { recursive: true }); } - // Qualification keeps the candidate inert. This packs checked-in package input - // without release builds, asset hooks, installs, lock generation, or overlays. - const npm = resolveNpmRunner({ - env: { - ...process.env, - NPM_CONFIG_AUDIT: "false", - NPM_CONFIG_FUND: "false", - NPM_CONFIG_IGNORE_SCRIPTS: "true", - NPM_CONFIG_PROVENANCE: "false", - NPM_CONFIG_WORKSPACES: "false", + // This unprivileged job is the only candidate-code execution boundary. It + // uses the same pack owner as publication so the trusted scanner sees built, + // overlaid, dependency-complete package bytes without executing candidate code. + const publishScript = + process.env.NODE_ENV === "test" && process.env.OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT + ? process.env.OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT + : join(toolingRoot, "scripts", "plugin-npm-publish.sh"); + const result = spawnSync( + "bash", + [publishScript, "--repo-root", candidateRoot, "--pack", args.packageDir], + { + cwd: toolingRoot, + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR: args.outputDir, + }, + killSignal: "SIGKILL", + maxBuffer: MAX_PACK_STDOUT_BYTES, + shell: false, + stdio: ["ignore", "pipe", "inherit"], + timeout: PACK_TIMEOUT_MS, }, - npmArgs: [ - "pack", - "--json", - "--ignore-scripts", - "--workspaces=false", - "--pack-destination", - args.outputDir, - ], - }); - const result = spawnSync(npm.command, npm.args, { - cwd: selected.packageDir, - encoding: "utf8", - env: npm.env ?? process.env, - killSignal: "SIGKILL", - maxBuffer: MAX_PACK_STDOUT_BYTES, - shell: npm.shell, - stdio: ["ignore", "pipe", "inherit"], - timeout: PACK_TIMEOUT_MS, - windowsVerbatimArguments: npm.windowsVerbatimArguments, - }); + ); if (result.status !== 0 || result.signal || result.error) { - throw new Error(`${selected.packageName}: trusted inert plugin pack failed.`); + throw new Error(`${selected.packageName}: publication-equivalent plugin pack failed.`); } const packEntries = parsePackOutput(result.stdout); if (packEntries.length !== 1) { @@ -228,30 +211,21 @@ async function preparePackage(args: ParsedArgs): Promise { const inspection = inspectPackageTarballBytes(tarballBytes, { maxArchiveBytes: MAX_TARBALL_BYTES, }); - const packedFiles = inspection.inventory - .filter((entry) => entry.type === "file") - .map((entry) => { - if (!entry.path.startsWith("package/")) { - throw new Error(`${selected.packageName}: inert package input escaped package/.`); - } - return entry.path.slice("package/".length); - }) - .toSorted(); if ( inspection.packageManifest.name !== selected.packageName || - inspection.packageManifest.version !== selected.packageVersion || - JSON.stringify(packedFiles) !== JSON.stringify(expectedPackedFiles) + inspection.packageManifest.version !== selected.packageVersion ) { - throw new Error(`${selected.packageName}: inert package input identity mismatch.`); + throw new Error(`${selected.packageName}: publication-equivalent package identity mismatch.`); } const artifactEntries = readdirSync(args.outputDir); if (artifactEntries.length !== 1 || artifactEntries[0] !== tarballName) { throw new Error(`${selected.packageName}: packaging produced unexpected artifact files.`); } const metadata = { - artifactKind: "inert-package-input", + artifactKind: "publication-equivalent-plugin-tarball", candidateSha: args.candidateSha, extensionId: selected.extensionId, + packOwner: "scripts/plugin-npm-publish.sh", packageDir: args.packageDir, packageName: selected.packageName, packageVersion: selected.packageVersion, diff --git a/scripts/plugin-npm-security-scan-runner.mjs b/scripts/plugin-npm-security-scan-runner.mjs index 4260ddd7a2e7..a3add04c6b46 100644 --- a/scripts/plugin-npm-security-scan-runner.mjs +++ b/scripts/plugin-npm-security-scan-runner.mjs @@ -1,9 +1,10 @@ -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const DEFAULT_HEAP_MB = 768; +const DEFAULT_RSS_MB = 1024; const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000; const MAX_CAPTURE_BYTES = 512 * 1024; const MAX_REPORT_BYTES = 1024 * 1024; @@ -53,6 +54,38 @@ function boundedAppend(current, chunk) { return Buffer.concat([current, chunk]).subarray(0, MAX_CAPTURE_BYTES); } +function processGroupRssBytes(pid) { + if (process.platform === "win32") { + return null; + } + const result = spawnSync("ps", ["-o", "rss=", "-g", String(pid)], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5_000, + }); + if (result.status !== 0) { + return null; + } + const samples = result.stdout + .trim() + .split(/\s+/u) + .filter(Boolean) + .map((value) => Number(value)); + if (samples.length === 0 || samples.some((value) => !Number.isSafeInteger(value) || value <= 0)) { + return null; + } + return samples.reduce((total, value) => total + value, 0) * 1024; +} + +function killProcessGroup(child) { + if (!child.pid) { + return; + } + try { + process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL"); + } catch {} +} + function sanitizeOutput(value, args) { let output = value.toString("utf8"); for (const [source, replacement] of [ @@ -121,6 +154,7 @@ async function run(argv) { const args = parseArgs(argv); const scannerPath = testOverride("OPENCLAW_PLUGIN_SECURITY_RUNNER_CHILD", SCANNER_PATH); const heapMb = Number(testOverride("OPENCLAW_PLUGIN_SECURITY_RUNNER_HEAP_MB", DEFAULT_HEAP_MB)); + const rssMb = Number(testOverride("OPENCLAW_PLUGIN_SECURITY_RUNNER_RSS_MB", DEFAULT_RSS_MB)); const timeoutMs = Number( testOverride("OPENCLAW_PLUGIN_SECURITY_RUNNER_TIMEOUT_MS", DEFAULT_TIMEOUT_MS), ); @@ -128,6 +162,9 @@ async function run(argv) { !Number.isSafeInteger(heapMb) || heapMb < 16 || heapMb > 4096 || + !Number.isSafeInteger(rssMb) || + rssMb < 16 || + rssMb > 4096 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 10 || timeoutMs > DEFAULT_TIMEOUT_MS @@ -137,6 +174,7 @@ async function run(argv) { let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); + let rssExceeded = false; let timedOut = false; const child = spawn( process.execPath, @@ -159,19 +197,27 @@ async function run(argv) { child.stderr.on("data", (chunk) => { stderr = boundedAppend(stderr, chunk); }); + const rssLimitBytes = rssMb * 1024 * 1024; + const rssTimer = setInterval(() => { + if (child.exitCode !== null || child.signalCode !== null || !child.pid) { + return; + } + const rssBytes = processGroupRssBytes(child.pid); + if (rssBytes !== null && rssBytes > rssLimitBytes) { + rssExceeded = true; + killProcessGroup(child); + } + }, 250); const timer = setTimeout(() => { timedOut = true; - if (child.pid) { - try { - process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL"); - } catch {} - } + killProcessGroup(child); }, timeoutMs); const result = await new Promise((resolve) => { child.on("error", (error) => resolve({ error, status: null })); child.on("close", (status, signal) => resolve({ error: undefined, signal, status })); }); clearTimeout(timer); + clearInterval(rssTimer); const safeStdout = sanitizeOutput(stdout, args); const safeStderr = sanitizeOutput(stderr, args); @@ -185,6 +231,10 @@ async function run(argv) { writeFailureReport(args, "timed out"); return 1; } + if (rssExceeded) { + writeFailureReport(args, "exceeded its RSS limit"); + return 1; + } if (result.error) { writeFailureReport(args, "could not start"); return 1; diff --git a/test/scripts/plugin-npm-security-scan.test.ts b/test/scripts/plugin-npm-security-scan.test.ts index c9ad959948d5..3744cacb2cd2 100644 --- a/test/scripts/plugin-npm-security-scan.test.ts +++ b/test/scripts/plugin-npm-security-scan.test.ts @@ -1,21 +1,20 @@ import { execFileSync, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { assertCanonicalNpmPackageName, assertCompleteScannerSummary, buildPluginNpmSecurityScanReport, - collectNpmPackedFiles, constrainPluginNpmSecurityScanReport, + loadPluginNpmSecurityArtifacts, listPluginNpmSecurityArtifacts, listPublishablePluginPackages, normalizePackedFindingPath, - parsePacklistFiles, + resolveCandidatePluginPackageDir, resolveReviewedSourceLayout, scanPublishablePluginPackages, - stageScannerRelevantPackedFiles, stageScannerRelevantPluginTarballFiles, type PublishablePluginPackage, type ScanPackageResult, @@ -54,14 +53,21 @@ function writePublishableManifest( } function writePluginArtifact(params: { + artifactRoot?: string; extensionId: string; files: Record; packageName: string; version?: string; }) { - const root = tempDirs.make("openclaw-plugin-npm-security-artifact-"); - const packageRoot = join(root, "source"); - const artifactDir = join(root, "artifacts", params.extensionId); + const root = params.artifactRoot + ? join(params.artifactRoot, "..") + : tempDirs.make("openclaw-plugin-npm-security-artifact-"); + const artifactRoot = params.artifactRoot ?? join(root, "artifacts"); + const packageRoot = join(root, `source-${params.extensionId}`); + const artifactDir = join( + artifactRoot, + `plugin-npm-security-package-${CANDIDATE_SHA}-${params.extensionId}`, + ); const packageVersion = params.version ?? "1.0.0"; mkdirSync(packageRoot, { recursive: true }); mkdirSync(artifactDir, { recursive: true }); @@ -95,9 +101,10 @@ function writePluginArtifact(params: { writeFileSync( join(artifactDir, "plugin-npm-security-artifact.json"), `${JSON.stringify({ - artifactKind: "inert-package-input", + artifactKind: "publication-equivalent-plugin-tarball", candidateSha: CANDIDATE_SHA, extensionId: params.extensionId, + packOwner: "scripts/plugin-npm-publish.sh", packageDir: `extensions/${params.extensionId}`, packageName: params.packageName, packageVersion, @@ -110,10 +117,16 @@ function writePluginArtifact(params: { ); return { artifact: { - artifactKind: "inert-package-input" as const, + artifactKind: "publication-equivalent-plugin-tarball" as const, artifactDir, candidateSha: CANDIDATE_SHA, + compressedBytes: readFileSync(tarballPath).byteLength, + expandedBytes: Object.values(params.files).reduce( + (total, value) => total + Buffer.byteLength(value), + 0, + ), extensionId: params.extensionId, + packOwner: "scripts/plugin-npm-publish.sh" as const, packageDir: `extensions/${params.extensionId}`, packageName: params.packageName, packageVersion, @@ -121,7 +134,7 @@ function writePluginArtifact(params: { tarballSha256, toolingSha: TOOLING_SHA, }, - artifactRoot: join(root, "artifacts"), + artifactRoot, expectedPackage: { extensionId: params.extensionId, packageDir: `extensions/${params.extensionId}`, @@ -179,92 +192,70 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { expect(resolveReviewedSourceLayout([...current, current[0]!])).toBeUndefined(); }); - it("collects package files without running lifecycle or candidate replacement helpers", async () => { - const packageDir = tempDirs.make("openclaw-plugin-npm-security-pack-"); - const replacementMarker = join(packageDir, "replacement-helper-ran"); + it("scans malicious output generated by the canonical publication pack boundary", async () => { + const candidateRoot = tempDirs.make("openclaw-plugin-security-built-pack-"); + const artifactRoot = tempDirs.make("openclaw-plugin-security-built-artifacts-"); + const packageDir = join(candidateRoot, "extensions", "generated"); + const trustedPackScript = join( + tempDirs.make("openclaw-plugin-security-trusted-pack-"), + "pack.sh", + ); + const generatedPath = join(packageDir, "dist", "generated.js"); const lifecycleMarkers = ["prepare", "prepack", "postpack"].map((name) => - join(packageDir, `${name}-ran`), - ); - writeFileSync( - join(packageDir, "package.json"), - `${JSON.stringify({ - name: "@openclaw/test-inert-package", - scripts: Object.fromEntries( - ["prepare", "prepack", "postpack"].map((name, index) => [ - name, - `node -e "require('node:fs').writeFileSync(${JSON.stringify(lifecycleMarkers[index])}, 'ran')"`, - ]), - ), - version: "1.0.0", - })}\n`, - "utf8", - ); - writeFileSync(join(packageDir, "index.js"), "export const value = 1;\n", "utf8"); - writeFileSync( - join(packageDir, "plugin-npm-pack-files.mjs"), - `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(replacementMarker)}, "ran");\n`, - "utf8", - ); - - const trustedHelper = join(tempDirs.make("openclaw-plugin-npm-security-helper-"), "helper.mjs"); - writeFileSync( - trustedHelper, - 'process.stdout.write(JSON.stringify(["index.js", "package.json", "plugin-npm-pack-files.mjs"]));\n', - "utf8", - ); - const packedFiles = await collectNpmPackedFiles(packageDir, "@openclaw/test-inert-package", { - helperPath: trustedHelper, - }); - - expect(packedFiles).toContain("index.js"); - expect(packedFiles).toContain("package.json"); - expect(existsSync(replacementMarker)).toBe(false); - for (const marker of lifecycleMarkers) { - expect(existsSync(marker)).toBe(false); - } - }); - - it("packs candidate input without running lifecycle or asset build hooks", () => { - const candidateRoot = tempDirs.make("openclaw-plugin-security-inert-pack-"); - const outputDir = tempDirs.make("openclaw-plugin-security-inert-output-"); - const packageDir = join(candidateRoot, "extensions", "inert"); - const packlistHelper = join(candidateRoot, "trusted-packlist-helper.mjs"); - const markers = Object.fromEntries( - ["asset", "prepare", "prepack", "postpack"].map((name) => [ - name, - join(candidateRoot, `${name}-ran`), - ]), + join(candidateRoot, `${name}-ran`), ); initGitRepo(candidateRoot); mkdirSync(packageDir, { recursive: true }); const markerCommand = (marker: string) => `node -e "require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ran')"`; + const generatedCode = 'const { execSync } = require("node:child_process");\nexecSync("id");\n'; + const assetBuild = "node build-assets.mjs"; writeFileSync( join(packageDir, "package.json"), `${JSON.stringify({ - name: "@openclaw/test-inert-pack", + files: ["dist", "openclaw.plugin.json", "package.json"], + name: "@openclaw/test-generated-pack", openclaw: { - assetScripts: { build: markerCommand(markers.asset!) }, + assetScripts: { build: assetBuild }, release: { publishToNpm: true }, }, scripts: { - postpack: markerCommand(markers.postpack!), - prepack: markerCommand(markers.prepack!), - prepare: markerCommand(markers.prepare!), + postpack: markerCommand(lifecycleMarkers[2]!), + prepack: markerCommand(lifecycleMarkers[1]!), + prepare: markerCommand(lifecycleMarkers[0]!), }, - version: "1.0.0", + version: "2026.8.1-beta.1", })}\n`, "utf8", ); - writeFileSync(join(packageDir, "index.ts"), "export const inert = true;\n", "utf8"); writeFileSync( join(packageDir, "openclaw.plugin.json"), - `${JSON.stringify({ id: "inert" })}\n`, + `${JSON.stringify({ id: "generated" })}\n`, "utf8", ); writeFileSync( - packlistHelper, - 'process.stdout.write(JSON.stringify(["index.ts", "openclaw.plugin.json", "package.json"]));\n', + join(packageDir, "build-assets.mjs"), + [ + 'import { mkdirSync, writeFileSync } from "node:fs";', + 'mkdirSync("dist", { recursive: true });', + `writeFileSync("dist/generated.js", ${JSON.stringify(generatedCode)});`, + "", + ].join("\n"), + "utf8", + ); + writeFileSync( + trustedPackScript, + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + 'repo_root="$2"', + 'package_dir="$4"', + 'cd "$repo_root/$package_dir"', + 'asset_build="$(node -p "require(\'./package.json\').openclaw.assetScripts.build")"', + 'bash -c "$asset_build"', + 'npm pack --json --ignore-scripts --pack-destination "$OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR"', + "", + ].join("\n"), "utf8", ); execFileSync("git", ["-C", candidateRoot, "add", "."]); @@ -272,8 +263,10 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { const candidateSha = execFileSync("git", ["-C", candidateRoot, "rev-parse", "HEAD"], { encoding: "utf8", }).trim(); + const outputDir = join(artifactRoot, `plugin-npm-security-package-${candidateSha}-generated`); const toolingSha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + mkdirSync(outputDir, { recursive: true }); const result = spawnSync( process.execPath, [ @@ -286,13 +279,13 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { "--candidate-sha", candidateSha, "--extension-id", - "inert", + "generated", "--output-dir", outputDir, "--package-dir", - "extensions/inert", + "extensions/generated", "--package-name", - "@openclaw/test-inert-pack", + "@openclaw/test-generated-pack", "--tooling-sha", toolingSha, ], @@ -302,78 +295,41 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { env: { ...process.env, NODE_ENV: "test", - OPENCLAW_PLUGIN_SECURITY_TEST_PACKLIST_HELPER: packlistHelper, + OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT: trustedPackScript, }, }, ); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - for (const marker of Object.values(markers)) { + expect(readFileSync(generatedPath, "utf8")).toBe(generatedCode); + for (const marker of lifecycleMarkers) { expect(existsSync(marker)).toBe(false); } const metadata = JSON.parse( readFileSync(join(outputDir, "plugin-npm-security-artifact.json"), "utf8"), ) as { artifactKind?: unknown; tarballName?: unknown }; - expect(metadata.artifactKind).toBe("inert-package-input"); + expect(metadata.artifactKind).toBe("publication-equivalent-plugin-tarball"); expect(typeof metadata.tarballName).toBe("string"); - const staged = stageScannerRelevantPluginTarballFiles( - join(outputDir, String(metadata.tarballName)), - ); - try { - expect(staged.packedFiles).toContain("index.ts"); - expect(staged.packedFiles.some((file) => file.startsWith("dist/"))).toBe(false); - } finally { - rmSync(staged.stageDir, { force: true, recursive: true }); - } - }); - - it("rejects malformed, unsafe, duplicate, and excessive packlist entries", () => { - for (const malformed of [ - null, - {}, - 1, - "", - "/absolute.js", - "dir\\file.js", - "../escape.js", - ".hidden.js", - "node_modules/dependency.js", - "a".repeat(4097), - ]) { - expect(() => - parsePacklistFiles(JSON.stringify(["valid.js", malformed]), "@openclaw/test"), - ).toThrow("entry 1 has an invalid path"); - } - expect(() => - parsePacklistFiles(JSON.stringify(["index.js", "index.js"]), "@openclaw/test"), - ).toThrow("duplicate path"); - expect(() => - parsePacklistFiles( - JSON.stringify(Array.from({ length: 20_001 }, (_, index) => `file-${index}.js`)), - "@openclaw/test", - ), - ).toThrow("file-count limit"); - }); - - it("preserves bounded packlist helper failure categories", async () => { - const root = tempDirs.make("openclaw-plugin-packlist-helper-limits-"); - const packageDir = tempDirs.make("openclaw-plugin-packlist-helper-package-"); - const timeoutHelper = join(root, "timeout.mjs"); - const failedHelper = join(root, "failed.mjs"); - writeFileSync(timeoutHelper, "setInterval(() => {}, 1_000);\n", "utf8"); - writeFileSync(failedHelper, "process.exit(7);\n", "utf8"); - - await expect( - collectNpmPackedFiles(packageDir, "@openclaw/test-timeout", { - helperPath: timeoutHelper, - timeoutMs: 25, - }), - ).rejects.toThrow("trusted packlist helper timed out"); - await expect( - collectNpmPackedFiles(packageDir, "@openclaw/test-failed", { - helperPath: failedHelper, - }), - ).rejects.toThrow("trusted packlist helper failed"); + const expectedPackage = { + extensionId: "generated", + packageDir: "extensions/generated", + packageName: "@openclaw/test-generated-pack", + packageVersion: "2026.8.1-beta.1", + }; + const loaded = loadPluginNpmSecurityArtifacts({ + artifactRoot, + candidateSha, + expectedPackages: [expectedPackage], + toolingSha, + }); + expect(loaded.ingestionErrors).toEqual([]); + const scanned = await scanPublishablePluginPackages(loaded.artifacts); + expect(scanned.scanErrors).toEqual([]); + expect(scanned.packageResults[0]?.unexpectedCriticalFindings).toContainEqual({ + line: 2, + path: "dist/generated.js", + ruleId: "dangerous-exec", + }); }); it("bounds manifests and rejects noncanonical or duplicate package identities", async () => { @@ -402,24 +358,27 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ).rejects.toThrow("manifest exceeds the byte limit"); }); - it("fails closed on truncated scans, source escapes, and tarball symlinks", () => { + it("fails closed on truncated scans, candidate package escapes, and tarball symlinks", () => { expect(() => assertCompleteScannerSummary("@openclaw/test", { truncated: true })).toThrow( "security scan reached its file limit", ); - expect(() => - stageScannerRelevantPackedFiles(tempDirs.make("openclaw-plugin-npm-security-path-"), [ - "../escape.ts", - ]), - ).toThrow("npm pack returned an unsafe path"); + + const candidateRoot = tempDirs.make("openclaw-plugin-npm-security-candidate-"); + const outsideDir = tempDirs.make("openclaw-plugin-npm-security-outside-"); + mkdirSync(join(candidateRoot, "extensions"), { recursive: true }); + writeFileSync( + join(outsideDir, "package.json"), + `${JSON.stringify({ name: "@openclaw/escape", version: "1.0.0" })}\n`, + "utf8", + ); + symlinkSync(outsideDir, join(candidateRoot, "extensions", "escape")); + expect(() => resolveCandidatePluginPackageDir(candidateRoot, "escape")).toThrow( + "package directory is not a real directory", + ); const packageDir = tempDirs.make("openclaw-plugin-npm-security-symlink-"); - const outsideDir = tempDirs.make("openclaw-plugin-npm-security-outside-"); const outsideFile = join(outsideDir, "outside.ts"); writeFileSync(outsideFile, "export const value = 1;\n", "utf8"); - symlinkSync(outsideFile, join(packageDir, "escape.ts")); - expect(() => stageScannerRelevantPackedFiles(packageDir, ["escape.ts"])).toThrow( - "not a regular file", - ); const artifact = writePluginArtifact({ extensionId: "symlink", @@ -429,7 +388,13 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { symlinkSync(outsideFile, join(artifact.packageRoot, "escape.ts")); execFileSync( "tar", - ["-czf", artifact.tarballPath, "-C", join(artifact.packageRoot, ".."), "source"], + [ + "-czf", + artifact.tarballPath, + "-C", + join(artifact.packageRoot, ".."), + basename(artifact.packageRoot), + ], { env: { ...process.env, COPYFILE_DISABLE: "1" } }, ); expect(() => stageScannerRelevantPluginTarballFiles(artifact.tarballPath)).toThrow(); @@ -521,7 +486,102 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { expectedPackages: [], toolingSha: TOOLING_SHA, }), - ).toThrow("does not match the trusted package plan"); + ).toThrow("unexpected entries"); + }); + + it("retains valid package scans when a sibling artifact is malformed", async () => { + const artifactRoot = tempDirs.make("openclaw-plugin-npm-security-mixed-"); + const valid = writePluginArtifact({ + artifactRoot, + extensionId: "valid", + files: { "index.js": "export const value = 1;\n" }, + packageName: "@openclaw/test-valid", + }); + const malformed = writePluginArtifact({ + artifactRoot, + extensionId: "malformed", + files: { "index.js": "export const value = 2;\n" }, + packageName: "@openclaw/test-malformed", + }); + writeFileSync( + join(malformed.artifact.artifactDir, "plugin-npm-security-artifact.json"), + "{not-json}\n", + "utf8", + ); + const expectedPackages = [malformed.expectedPackage, valid.expectedPackage].toSorted( + (left, right) => (left.packageName < right.packageName ? -1 : 1), + ); + + const loaded = loadPluginNpmSecurityArtifacts({ + artifactRoot, + candidateSha: CANDIDATE_SHA, + expectedPackages, + toolingSha: TOOLING_SHA, + }); + expect(loaded.artifacts.map((artifact) => artifact.packageName)).toEqual([ + "@openclaw/test-valid", + ]); + expect(loaded.ingestionErrors).toEqual([ + "@openclaw/test-malformed: Plugin security artifact metadata is not valid JSON.", + ]); + + const scanned = await scanPublishablePluginPackages(loaded.artifacts); + expect(scanned.scanErrors).toEqual([]); + expect(scanned.packageResults.map((result) => result.packageName)).toEqual([ + "@openclaw/test-valid", + ]); + }); + + it("bounds aggregate compressed and expanded artifact bytes deterministically", () => { + const artifactRoot = tempDirs.make("openclaw-plugin-npm-security-aggregate-"); + const alpha = writePluginArtifact({ + artifactRoot, + extensionId: "alpha", + files: { "alpha.js": Buffer.alloc(256, 1) }, + packageName: "@openclaw/test-alpha", + }); + const beta = writePluginArtifact({ + artifactRoot, + extensionId: "beta", + files: { "beta.js": Buffer.alloc(256, 2) }, + packageName: "@openclaw/test-beta", + }); + const expectedPackages = [alpha.expectedPackage, beta.expectedPackage]; + const baseline = loadPluginNpmSecurityArtifacts({ + artifactRoot, + candidateSha: CANDIDATE_SHA, + expectedPackages, + toolingSha: TOOLING_SHA, + }); + expect(baseline.ingestionErrors).toEqual([]); + + const compressed = loadPluginNpmSecurityArtifacts({ + artifactRoot, + candidateSha: CANDIDATE_SHA, + expectedPackages, + limits: { maxCompressedBytes: baseline.artifacts[0]!.compressedBytes }, + toolingSha: TOOLING_SHA, + }); + expect(compressed.artifacts.map((artifact) => artifact.packageName)).toEqual([ + "@openclaw/test-alpha", + ]); + expect(compressed.ingestionErrors).toEqual([ + "@openclaw/test-beta: aggregate compressed-byte limit exceeded.", + ]); + + const expanded = loadPluginNpmSecurityArtifacts({ + artifactRoot, + candidateSha: CANDIDATE_SHA, + expectedPackages, + limits: { maxExpandedBytes: baseline.artifacts[0]!.expandedBytes }, + toolingSha: TOOLING_SHA, + }); + expect(expanded.artifacts.map((artifact) => artifact.packageName)).toEqual([ + "@openclaw/test-alpha", + ]); + expect(expanded.ingestionErrors).toEqual([ + "@openclaw/test-beta: aggregate expanded-byte limit exceeded.", + ]); }); it("caps total findings and emits byte-identical bounded reports", () => { @@ -555,19 +615,26 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ]); }); - it("writes sanitized exact-identity reports when the bounded scanner times out or OOMs", () => { + it("writes sanitized exact-identity reports for timeout, heap, and RSS failures", () => { const root = tempDirs.make("openclaw-plugin-npm-security-runner-"); const timeoutChild = join(root, "timeout.mjs"); const oomChild = join(root, "oom.mjs"); + const rssChild = join(root, "rss.mjs"); writeFileSync(timeoutChild, "await new Promise(() => {});\n", "utf8"); writeFileSync( oomChild, "const values = [];\nwhile (true) values.push(new Array(100000).fill(Math.random()));\n", "utf8", ); - for (const [label, child, timeoutMs] of [ - ["timeout", timeoutChild, "25"], - ["oom", oomChild, "10000"], + writeFileSync( + rssChild, + "globalThis.value = Buffer.alloc(64 * 1024 * 1024, 1);\nsetInterval(() => {}, 1_000);\n", + "utf8", + ); + for (const [label, child, timeoutMs, heapMb, rssMb, expectedError] of [ + ["timeout", timeoutChild, "25", "128", "1024", "timed out"], + ["oom", oomChild, "10000", "16", "1024", "exceeded its process limit"], + ["rss", rssChild, "10000", "128", "16", "exceeded its RSS limit"], ] as const) { const reportPath = join(root, `${label}.json`); const result = spawnSync( @@ -592,7 +659,8 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ...process.env, NODE_ENV: "test", OPENCLAW_PLUGIN_SECURITY_RUNNER_CHILD: child, - OPENCLAW_PLUGIN_SECURITY_RUNNER_HEAP_MB: "16", + OPENCLAW_PLUGIN_SECURITY_RUNNER_HEAP_MB: heapMb, + OPENCLAW_PLUGIN_SECURITY_RUNNER_RSS_MB: rssMb, OPENCLAW_PLUGIN_SECURITY_RUNNER_TIMEOUT_MS: timeoutMs, }, timeout: 15_000, @@ -600,6 +668,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ); const report = JSON.parse(readFileSync(reportPath, "utf8")) as { candidateSha: string; + errors: string[]; toolingSha: string; }; expect(result.status).toBe(1); @@ -607,6 +676,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { candidateSha: CANDIDATE_SHA, toolingSha: TOOLING_SHA, }); + expect(report.errors).toContainEqual(expect.stringContaining(expectedError)); expect(`${result.stdout}${result.stderr}${JSON.stringify(report)}`).not.toContain(root); } }, 30_000); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 86a4fc4ea27c..f659cad62cb2 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -300,7 +300,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { (step: WorkflowStep) => step.name === "Install trusted scanner dependencies", ); const runSecurityScan = securityScan.steps.find( - (step: WorkflowStep) => step.name === "Scan inert plugin package inputs", + (step: WorkflowStep) => step.name === "Scan publication-equivalent plugin artifacts", ); const uploadReport = securityScan.steps.find( (step: WorkflowStep) => step.name === "Upload plugin npm security scan report", @@ -310,6 +310,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { ); const releaseWorkflow = readFullReleaseValidationWorkflow(); const releaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8"); + const pluginNpmReleaseSource = readFileSync(".github/workflows/plugin-npm-release.yml", "utf8"); const pluginDispatch = releaseWorkflow.jobs.plugin_prerelease.steps.find( (step: WorkflowStep) => step.name === "Dispatch and monitor plugin prerelease", ); @@ -368,15 +369,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(securityScan.needs).not.toContain("preflight"); expect(securityPackage.permissions).toEqual({ contents: "read" }); expect(securityPackage.secrets).toBeUndefined(); - expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-publish.sh"); - expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-runtime-build"); - expect(JSON.stringify(securityPackage)).not.toContain("generate-npm-package-lock"); - expect(securityPrepareSource).not.toContain("plugin-npm-publish.sh"); - expect(securityPrepareSource).not.toContain("plugin-npm-runtime-build"); - expect(securityPrepareSource).not.toContain("generate-npm-package-lock"); - expect(securityPrepareSource).not.toMatch(/\bnpmArgs:\s*\[\s*["'](?:ci|install)["']/u); - expect(securityPrepareSource).toContain('"--ignore-scripts"'); - expect(securityPrepareSource).toContain('"--workspaces=false"'); + expect(JSON.stringify(securityPackage)).not.toContain("id-token"); + expect(JSON.stringify(securityPackage)).not.toContain("packages: write"); + expect(JSON.stringify(securityPackage)).not.toContain("${{ secrets."); + expect(securityPrepareSource).toContain('"scripts", "plugin-npm-publish.sh"'); + expect(securityPrepareSource).toContain('"--pack"'); + expect(securityPrepareSource).toContain("shell: false"); + expect(securityPrepareSource).toContain("resolveCandidatePluginPackageDir"); + expect(pluginNpmReleaseSource).toContain("bash .release-tooling/scripts/plugin-npm-publish.sh"); + expect(pluginNpmReleaseSource).toContain('--pack "${PACKAGE_DIR}"'); expect(nodeShard.needs).toEqual(["resolve-candidate", "preflight"]); expect(runNodeShard?.run).toContain('spawnSync("pnpm", ["test", "--", ...configs]'); expect(pluginSource).not.toContain("npm-install-security-scan.release.test.ts");