From 923b74fdba7b3406cf2f4c196935cf4c1c5bd2b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 00:40:15 +0000 Subject: [PATCH 1/2] [FIX] Wire up the /connect capture page: drop the dead connect-root mount guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web-hosted connection capture page (renderConnectPage in connections-routes.js) is server-rendered — every element the widget touches already exists in the DOM. But the widget's main() bailed early unless it also found an element with id="connect-root": const root = document.getElementById('connect-root'); if (!root || !statusEl) return; No page ever emits connect-root (grep confirms it was referenced only here), so root was always null, main() returned before adding the form submit handler, and the Connect button silently did nothing — the whole point of the flow (capture a key from any browser) never worked. root was otherwise unused; status-message is the real "are we on the connect page" sentinel and the form controls are re-checked right before the handler is wired up. Add a regression test that parses the widget's own getElementById lookups and asserts renderConnectPage emits each id, so the page and widget can't drift apart silently again (this exact bug is otherwise invisible until someone loads the real page). Verified: the test fails against the pre-fix widget with a precise diagnostic and passes after. Full suite 54/54, build and lint clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014AyPaJTiYLvCp5N6GiKZDS --- connections-routes.test.js | 30 ++++++++++++++++++++++++++++++ src/connect-widget/main.ts | 7 +++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/connections-routes.test.js b/connections-routes.test.js index 32a6ab1..863d7e2 100644 --- a/connections-routes.test.js +++ b/connections-routes.test.js @@ -135,6 +135,36 @@ test('renderConnectPage embeds config as an inert JSON island, never innerHTML', assert.ok(!page.includes('')); }); +test('renderConnectPage emits every element id the connect-widget looks up (page↔widget contract)', () => { + // The widget is server-rendered onto this page; if it getElementById's an id the + // page never emits, main() bails before wiring the submit handler and the Connect + // button silently does nothing. Parse the widget's own lookups and require each. + const widgetSrc = fs.readFileSync( + path.join(import.meta.dirname, 'src/connect-widget/main.ts'), + 'utf8', + ); + const ids = [...widgetSrc.matchAll(/getElementById\(['"]([^'"]+)['"]\)/g)].map((m) => m[1]); + assert.ok(ids.length >= 5, `expected several getElementById lookups, found ${ids.length}`); + + const page = renderConnectPage({ + token: VALID_TOKEN, + providerName: 'Seats.aero', + instructions: 'Do the thing.', + placeholder: 'sk_...', + tokenUrl: 'https://example.com', + publicKey: VALID_PUBLIC_KEY, + expiresAt: futureIso(60_000), + }); + + for (const id of ids) { + assert.ok( + page.includes(`id="${id}"`), + `connect-widget calls getElementById('${id}') but renderConnectPage never emits id="${id}" — ` + + 'the page and widget have drifted and the submit handler will silently never attach', + ); + } +}); + test('renderStatePage escapes its message', () => { const page = renderStatePage({ title: 'Expired', message: '' }); assert.ok(!page.includes('')); diff --git a/src/connect-widget/main.ts b/src/connect-widget/main.ts index ec8d601..1a49d6e 100644 --- a/src/connect-widget/main.ts +++ b/src/connect-widget/main.ts @@ -78,9 +78,12 @@ function setStatus(el: HTMLElement, message: string, kind: 'idle' | 'error' | 's function main() { const config = readConfig(); - const root = document.getElementById('connect-root'); const statusEl = document.getElementById('status-message'); - if (!root || !statusEl) return; + // The page is server-rendered by renderConnectPage (connections-routes.js), so + // every element below already exists in the DOM — there is nothing to mount. + // status-message is the sentinel that we're on the connect page; the form + // controls are re-checked before the submit handler is wired up. + if (!statusEl) return; if (!config) { setStatus(statusEl, 'This link is malformed. Ask the agent to send a new one.', 'error'); From 7912ef3caf28824aaba7610ad1f4a52163a65e1a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 04:43:46 +0000 Subject: [PATCH 2/2] [CI] Move canopy-admin to Node 22 and clear audit advisories (unblock CI + deploy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pre-existing failures were blocking every canopy-admin PR and the Cloud Run deploy, independent of the /connect widget fix on this branch: 1. Node 20 can't run the test suite. CI (security.yml) and the deploy's verify steps (deploy.yml) both pinned `node-version: '20'`, but `connect-widget.test.js` imports `src/connect-widget/main.ts` directly, and Node 20 cannot load `.ts` (ERR_UNKNOWN_FILE_EXTENSION — native type stripping needs Node >=22.6). The repo's own dependencies already require Node 22 (react-router@8.3.0 → >=22.22.0), and canopy standardizes on Node 22, so pinning 20 was the bug. Bump both workflows to '22', `.nvmrc` to 22, and engines to >=22.12.0 to match. 2. `npm audit --audit-level=high` failed on 4 high advisories (multer, nanoid, brace-expansion, browserslist). `npm audit fix` (no --force) clears all 9 within existing semver ranges — package-lock.json only, package.json deps untouched. multer (runtime, multipart uploads) moves to 2.3.0. The Dockerfile build/runtime stay on node:20-alpine intentionally: they don't run the .ts test, and react-router is client-bundled (never executed in node at runtime), with no engine-strict to hard-fail install. Verified on Node 22: audit --audit-level=high exits 0, 54/54 tests, build, lint all pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014AyPaJTiYLvCp5N6GiKZDS --- .github/workflows/deploy.yml | 2 +- .github/workflows/security.yml | 2 +- .nvmrc | 2 +- package-lock.json | 101 +++++++++++++++++---------------- package.json | 2 +- 5 files changed, 56 insertions(+), 53 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 15fd32a..32b9726 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '20' + node-version: '22' cache: npm cache-dependency-path: |- canopy/package-lock.json diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 0499fd4..f3009f3 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '20' + node-version: '22' cache: npm - name: Verify admin server and UI diff --git a/.nvmrc b/.nvmrc index 209e3ef..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/package-lock.json b/package-lock.json index f7c56c0..f92e25e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2192,9 +2192,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2322,9 +2322,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -2348,9 +2348,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -2368,11 +2368,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -2484,9 +2484,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2937,9 +2937,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "version": "1.5.425", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz", + "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==", "dev": true, "license": "ISC" }, @@ -3413,9 +3413,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, "node_modules/file-entry-cache": { @@ -4532,9 +4532,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -4606,9 +4606,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -4671,11 +4671,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4964,9 +4967,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -4984,7 +4987,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5112,9 +5115,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5264,9 +5267,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -6138,9 +6141,9 @@ } }, "node_modules/three-stdlib/node_modules/fflate": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz", + "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==", "license": "MIT" }, "node_modules/tiny-invariant": { @@ -6398,9 +6401,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 0e34bca..c11f1e8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.2.0", "type": "module", "engines": { - "node": ">=20.19.0" + "node": ">=22.12.0" }, "scripts": { "dev": "vite",