Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
30 changes: 30 additions & 0 deletions connections-routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,36 @@ test('renderConnectPage embeds config as an inert JSON island, never innerHTML',
assert.ok(!page.includes('<script>alert(1)</script>'));
});

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: '<img src=x onerror=alert(1)>' });
assert.ok(!page.includes('<img src=x onerror=alert(1)>'));
Expand Down
101 changes: 52 additions & 49 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.2.0",
"type": "module",
"engines": {
"node": ">=20.19.0"
"node": ">=22.12.0"
},
"scripts": {
"dev": "vite",
Expand Down
7 changes: 5 additions & 2 deletions src/connect-widget/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down