diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38f5017431..66d7031da0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,6 +231,10 @@ jobs: if: steps.plan.outputs.code == 'true' || steps.plan.outputs.astryx_surface == 'true' || steps.plan.outputs.asf_source == 'true' || steps.plan.outputs.cli_package == 'true' || steps.plan.outputs.release_contract == 'true' run: npm ci + - name: Test dependency workspace adapter + if: steps.plan.outputs.code == 'true' + run: node --test scripts/find-workspace-root.test.mjs + - name: Check localized TUI copy boundaries if: steps.plan.outputs.code == 'true' run: | diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4dc857e29e..a9beca0e01 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -92,7 +92,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "0.12.0", "@xterm/xterm": "^6.0.0", - "electron": "43.4.1", + "electron": "43.5.0", "electron-builder": "26.17.0", "esbuild": "^0.28.1", "linkedom": "^0.18.13", diff --git a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt index 33501a5bc5..9ffbe148bf 100644 --- a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt +++ b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt @@ -8807,7 +8807,7 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ -Package: dompurify@3.4.13 +Package: dompurify@3.4.16 Declared license: (MPL-2.0 OR Apache-2.0) Selected license: Apache-2.0 Repository: git://github.com/cure53/DOMPurify.git diff --git a/package-lock.json b/package-lock.json index f9b0c49065..7c91927030 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@electron/asar": "4.3.0", "@types/node": "^26.6.3", "esbuild": "^0.28.1", + "find-yarn-workspace-root": "file:scripts/npm-compat/find-workspace-root", "husky": "^9.1.7", "knip": "^6.38.0", "patch-package": "8.0.1", @@ -81,7 +82,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "0.12.0", "@xterm/xterm": "^6.0.0", - "electron": "43.4.1", + "electron": "43.5.0", "electron-builder": "26.17.0", "esbuild": "^0.28.1", "linkedom": "^0.18.13", @@ -7345,19 +7346,6 @@ "node": "20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.28.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", @@ -8866,9 +8854,9 @@ "optional": true }, "node_modules/devalue": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.2.tgz", - "integrity": "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==", + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", "dev": true, "license": "MIT" }, @@ -9043,9 +9031,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -9156,9 +9144,9 @@ } }, "node_modules/electron": { - "version": "43.4.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.1.tgz", - "integrity": "sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==", + "version": "43.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.5.0.tgz", + "integrity": "sha512-nV2aWuKatmUrxrAWP2pNIynNX7dy83TCAz+6KnsbK7hDVLE+6fa2iouOtir41AboZTa+J5w2UgicWHAqUaaUJw==", "dev": true, "license": "MIT", "dependencies": { @@ -9986,19 +9974,6 @@ "node": ">=10" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -10060,14 +10035,8 @@ } }, "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "micromatch": "^4.0.2" - } + "resolved": "scripts/npm-compat/find-workspace-root", + "link": true }, "node_modules/flattie": { "version": "1.1.1", @@ -10780,9 +10749,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "dev": true, "license": "BSD-2-Clause" }, @@ -11068,16 +11037,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -12562,33 +12521,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -16173,19 +16105,6 @@ "tmp": "^0.2.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -17427,7 +17346,7 @@ }, "devDependencies": { "@types/ws": "^8.18.1", - "electron": "^43.4.1" + "electron": "^43.5.0" } }, "packages/runtime/node_modules/@slack/socket-mode": { @@ -17555,6 +17474,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "scripts/npm-compat/find-workspace-root": { + "name": "@maka/find-workspace-root", + "version": "1.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "picomatch": "4.0.7" + } + }, "website": { "name": "@maka/website", "version": "0.0.0", diff --git a/package.json b/package.json index 8df8aa2846..46b7d96d82 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "@electron/asar": "4.3.0", "@types/node": "^26.6.3", "esbuild": "^0.28.1", + "find-yarn-workspace-root": "file:scripts/npm-compat/find-workspace-root", "husky": "^9.1.7", "knip": "^6.38.0", "patch-package": "8.0.1", @@ -134,6 +135,9 @@ "node-pty@1.2.0-beta.15": true }, "overrides": { + "patch-package": { + "find-yarn-workspace-root": "$find-yarn-workspace-root" + }, "@ai-sdk/code-mode": { "run": "2.1.4" }, diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json index 9091244230..6e1cd3fdd9 100644 --- a/packages/runtime-host/package.json +++ b/packages/runtime-host/package.json @@ -44,6 +44,6 @@ }, "devDependencies": { "@types/ws": "^8.18.1", - "electron": "^43.4.1" + "electron": "^43.5.0" } } diff --git a/patches/README.md b/patches/README.md index 7df8b0e1fa..6fa3edc795 100644 --- a/patches/README.md +++ b/patches/README.md @@ -26,6 +26,17 @@ After a dependency upgrade, apply the still-needed edits to the new version before regenerating. The command records the installed files, not the old patch text. +The root `overrides` replaces only patch-package's `find-yarn-workspace-root` +with the repository-owned adapter in `scripts/npm-compat/find-workspace-root`. +The upstream helper pulls in `micromatch` and `braces`; braces <=3.0.3 has no +published fix for GHSA-vfj7-8cjw-p6xm. The adapter retains the synchronous +workspace lookup contract using `picomatch` directly, without the vulnerable +brace AST walkers. Patch creation and application still use patch-package 8.0.1. +The direct local devDependency and `$find-yarn-workspace-root` override reference +keep npm's file resolution anchored to the repository root. The adapter is +tested by `node --test scripts/find-workspace-root.test.mjs` in CI. +Remove the override once upstream ships a dependency chain without this advisory. + Keep this directory small. Prefer product code that uses the dependency's published API; only patch for bugs that block shipping and cannot be worked around at the call site. diff --git a/scripts/find-workspace-root.test.mjs b/scripts/find-workspace-root.test.mjs new file mode 100644 index 0000000000..06092230bb --- /dev/null +++ b/scripts/find-workspace-root.test.mjs @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { test } from 'node:test'; +import findWorkspaceRoot from './npm-compat/find-workspace-root/index.cjs'; + +function fixture(t, workspaces) { + const root = mkdtempSync(join(tmpdir(), 'maka-workspace-root-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + writeFileSync(join(root, 'package.json'), JSON.stringify({ private: true, workspaces })); + return root; +} + +for (const workspaces of [['packages/*'], { packages: ['packages/*'] }]) { + test(`finds the root and a member with ${JSON.stringify(workspaces)}`, (t) => { + const root = fixture(t, workspaces); + const member = join(root, 'packages', 'one'); + mkdirSync(member, { recursive: true }); + writeFileSync(join(member, 'package.json'), '{}'); + assert.equal(findWorkspaceRoot(root), root); + assert.equal(findWorkspaceRoot(member), root); + assert.equal(findWorkspaceRoot(join(root, 'unrelated')), null); + }); +} + +test('supports brace patterns, exclusions, and later re-inclusion', (t) => { + const root = fixture(t, ['{packages,apps}/*', '!packages/hidden-*', 'packages/hidden-allowed']); + for (const member of ['packages/core', 'apps/desktop', 'packages/hidden-allowed']) { + assert.equal(findWorkspaceRoot(join(root, member)), root); + } + assert.equal(findWorkspaceRoot(join(root, 'packages/hidden-secret')), null); + assert.equal(findWorkspaceRoot(join(root, 'other/member')), null); +}); + +test('supports exclusion-only patterns and negated extglobs', (t) => { + const root = fixture(t, ['!excluded/**']); + assert.equal(findWorkspaceRoot(join(root, 'included/member')), root); + assert.equal(findWorkspaceRoot(join(root, 'excluded/member')), null); + writeFileSync(join(root, 'package.json'), JSON.stringify({ workspaces: ['!(excluded)'] })); + assert.equal(findWorkspaceRoot(join(root, 'included')), root); + assert.equal(findWorkspaceRoot(join(root, 'excluded')), null); +}); + +test('stops at the nearest workspace boundary even when it excludes the member', (t) => { + const root = fixture(t, ['**']); + const nested = join(root, 'nested'); + mkdirSync(nested); + writeFileSync(join(nested, 'package.json'), JSON.stringify({ workspaces: ['members/*'] })); + assert.equal(findWorkspaceRoot(join(nested, 'members/one')), nested); + assert.equal(findWorkspaceRoot(join(nested, 'other')), null); +}); + +test('empty workspace lists include only their root', (t) => { + const root = fixture(t, []); + assert.equal(findWorkspaceRoot(root), root); + assert.equal(findWorkspaceRoot(join(root, 'member')), null); +}); + +test('deeply nested braces do not exhaust the call stack', (t) => { + const root = fixture(t, [`${'{'.repeat(5000)}member${'}'.repeat(5000)}`]); + assert.doesNotThrow(() => findWorkspaceRoot(join(root, 'member'))); +}); + +test('returns null without a workspace and reports invalid manifests', (t) => { + const root = fixture(t, undefined); + assert.equal(findWorkspaceRoot(root), null); + writeFileSync(join(root, 'package.json'), '{broken'); + assert.throws(() => findWorkspaceRoot(root), SyntaxError); +}); + +test('patch-package resolves the adapter and it recognizes this npm workspace', () => { + const require = createRequire(import.meta.url); + const fromPatchPackage = createRequire(require.resolve('patch-package/package.json')); + const installedFindRoot = fromPatchPackage('find-yarn-workspace-root'); + assert.equal(installedFindRoot, findWorkspaceRoot); + const root = resolve(import.meta.dirname, '..'); + assert.equal(installedFindRoot(join(root, 'apps/desktop')), root); + assert.equal(installedFindRoot(), root); +}); diff --git a/scripts/npm-compat/find-workspace-root/index.cjs b/scripts/npm-compat/find-workspace-root/index.cjs new file mode 100644 index 0000000000..3f7e69f002 --- /dev/null +++ b/scripts/npm-compat/find-workspace-root/index.cjs @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { readFileSync } = require('node:fs'); +const { dirname, join, relative, resolve, sep } = require('node:path'); +const picomatch = require('picomatch'); + +// patch-package expects a synchronous function returning a root path or null. +// Retain ordered exclusions/re-inclusions and both workspace manifest shapes. +function matchesWorkspace(path, patterns) { + const matchers = patterns.map((pattern) => { + const match = picomatch(pattern, {}, true); + return { match, negative: Boolean(match.state.negated || match.state.negatedExtglob) }; + }); + let included = matchers.length > 0 && matchers.every(({ negative }) => negative); + for (const { match, negative } of matchers) { + if (negative ? !match(path) : match(path)) included = !negative; + } + return included; +} + +module.exports = function findWorkspaceRoot(start = process.cwd()) { + const initial = resolve(start); + let directory = initial; + while (true) { + let manifest; + try { + manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + const workspaces = manifest?.workspaces; + const patterns = Array.isArray(workspaces) ? workspaces : workspaces?.packages; + if (Array.isArray(patterns)) { + const path = relative(directory, initial).split(sep).join('/'); + return path === '' || matchesWorkspace(path, patterns) ? directory : null; + } + const parent = dirname(directory); + if (parent === directory) return null; + directory = parent; + } +}; diff --git a/scripts/npm-compat/find-workspace-root/package.json b/scripts/npm-compat/find-workspace-root/package.json new file mode 100644 index 0000000000..47b9eefc87 --- /dev/null +++ b/scripts/npm-compat/find-workspace-root/package.json @@ -0,0 +1,12 @@ +{ + "name": "@maka/find-workspace-root", + "version": "1.0.0", + "private": true, + "license": "Apache-2.0", + "description": "Synchronous workspace-root adapter for patch-package without micromatch/braces", + "type": "commonjs", + "main": "index.cjs", + "dependencies": { + "picomatch": "4.0.7" + } +}