From 7589a67b075ef3eb0ab922bd9d52774d53f989f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 06:10:24 +0000 Subject: [PATCH 1/2] build(deps): bump socket.io-parser from 4.2.6 to 4.2.7 in /plugins/draw Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.6 to 4.2.7. - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.6...socket.io-parser@4.2.7) --- updated-dependencies: - dependency-name: socket.io-parser dependency-version: 4.2.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- plugins/draw/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/draw/package-lock.json b/plugins/draw/package-lock.json index 15ad826..7ef0ee3 100644 --- a/plugins/draw/package-lock.json +++ b/plugins/draw/package-lock.json @@ -402,9 +402,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", From 32451a154904d63d434918df81d210ea2221de0c Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 16 Sep 2026 03:49:22 +0000 Subject: [PATCH 2/2] fix(deps): protect both Draw parser copies and validate room collaboration --- .github/workflows/test.yml | 1 + plugins/draw/README.md | 8 ++- plugins/draw/package.json | 3 +- plugins/draw/test/integration/room.test.mjs | 69 ++++++++++++++++++++ plugins/draw/test/parser.test.mjs | 71 +++++++++++++++++++++ plugins/draw/ui/package-lock.json | 6 +- 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 plugins/draw/test/integration/room.test.mjs create mode 100644 plugins/draw/test/parser.test.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f228cc0..7793118 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,6 +66,7 @@ jobs: working-directory: plugins/draw/ui - run: npm run build working-directory: plugins/draw/ui + - run: npm run test:integration ci-success: name: CI Success diff --git a/plugins/draw/README.md b/plugins/draw/README.md index 2b3f27e..01f4ef6 100644 --- a/plugins/draw/README.md +++ b/plugins/draw/README.md @@ -1,6 +1,6 @@ # Draw development checks -From `plugins/draw`, install the server dependencies and run its build and session tests: +From `plugins/draw`, install the server dependencies and run its build, session, and parser tests: ```sh npm ci @@ -13,6 +13,10 @@ Build the UI from its own lockfile: cd ui npm ci npm run build +cd .. +npm run test:integration ``` -The tests exercise Nano ID's six-character session suffixes and Draw storage in temporary directories. They do not open a browser or sharing tunnel. CI runs these checks on Node.js 20, 22, and 24 alongside the repository's Bats and Shellcheck jobs. +The integration tests connect the UI Socket.IO client to the Draw server over loopback WebSocket and polling transports. They verify room joins, scene updates, cursor updates, state requests, and disconnects. + +The unit tests exercise parser compatibility, rejection of malformed binary headers, Nano ID's six-character session suffixes and Draw storage in temporary directories. They do not open a browser or sharing tunnel. CI runs these checks on Node.js 20, 22, and 24 alongside the repository's Bats and Shellcheck jobs. diff --git a/plugins/draw/package.json b/plugins/draw/package.json index f51cf07..9428d91 100644 --- a/plugins/draw/package.json +++ b/plugins/draw/package.json @@ -12,7 +12,8 @@ "build:server": "tsc", "build:ui": "cd ui && npm install && npm run build", "dev": "tsc --watch", - "test": "npm run build:server && node --test test/*.test.mjs" + "test": "npm run build:server && node --test test/*.test.mjs", + "test:integration": "npm run build:server && node --test test/integration/*.test.mjs" }, "dependencies": { "nanoid": "^5.1.16", diff --git a/plugins/draw/test/integration/room.test.mjs b/plugins/draw/test/integration/room.test.mjs new file mode 100644 index 0000000..867946e --- /dev/null +++ b/plugins/draw/test/integration/room.test.mjs @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { createRequire } from 'node:module'; +import { once } from 'node:events'; +import { test } from 'node:test'; +import { createRoomServer } from '../../dist/server/room.js'; + +// Use the actual UI lockfile's client against the actual Draw room server. +const requireUi = createRequire(new URL('../../ui/package.json', import.meta.url)); +const { io } = requireUi('socket.io-client'); + +function event(socket, name) { + return once(socket, name, { signal: AbortSignal.timeout(5000) }).then(([data]) => data); +} + +for (const transport of ['websocket', 'polling']) { + test(`room collaboration works over ${transport}`, { timeout: 15000 }, async (t) => { + const server = http.createServer(); + const room = createRoomServer(server, 'local-fixture-room', []); + const clients = []; + t.after(async () => { + for (const socket of clients) socket.disconnect(); + await new Promise((resolve) => room.io.close(resolve)); + }); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const url = `http://127.0.0.1:${server.address().port}`; + async function join(username) { + const socket = io(url, { transports: [transport], reconnection: false, autoConnect: false }); + clients.push(socket); + const connected = event(socket, 'connect'); + socket.connect(); + await connected; + const state = event(socket, 'full-state'); + socket.emit('join', { roomId: 'local-fixture-room', username }); + assert.deepEqual((await state).elements, []); + return socket; + } + + const alice = await join('Alice fixture'); + const joined = event(alice, 'participant-joined'); + const bob = await join('Bob fixture'); + assert.equal((await joined).username, 'Bob fixture'); + assert.deepEqual(room.getParticipants().sort(), ['Alice fixture', 'Bob fixture']); + + const elements = [{ id: 'fixture-rectangle', type: 'rectangle', version: 1, x: 10, y: 20 }]; + const update = event(bob, 'scene-update'); + alice.emit('scene-update', { elements }); + assert.deepEqual(await update, { type: 'scene-update', elements, from: 'Alice fixture' }); + assert.deepEqual(room.getElements(), elements); + + const cursor = event(bob, 'cursor-update'); + alice.emit('cursor-update', { pointer: { x: 30, y: 40 } }); + const pointer = await cursor; + assert.deepEqual(pointer.pointer, { x: 30, y: 40 }); + assert.equal(pointer.username, 'Alice fixture'); + + const state = event(bob, 'full-state'); + bob.emit('request-state'); + const snapshot = await state; + assert.deepEqual(snapshot.elements, elements); + assert.deepEqual(snapshot.participants.map((p) => p.username).sort(), ['Alice fixture', 'Bob fixture']); + + const left = event(alice, 'participant-left'); + bob.disconnect(); + assert.equal((await left).username, 'Bob fixture'); + assert.deepEqual(room.getParticipants(), ['Alice fixture']); + }); +} diff --git a/plugins/draw/test/parser.test.mjs b/plugins/draw/test/parser.test.mjs new file mode 100644 index 0000000..f17120f --- /dev/null +++ b/plugins/draw/test/parser.test.mjs @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; +import * as esmParser from 'socket.io-parser'; + +const require = createRequire(import.meta.url); + +// Socket.IO uses the CommonJS entry; also cover the ESM API used by tooling. +for (const [entry, { Encoder, Decoder, PacketType }] of [ + ['CommonJS', require('socket.io-parser')], + ['ESM', esmParser], +]) { + function roundTrip(data) { + const decoder = new Decoder(); + const decoded = []; + decoder.on('decoded', (packet) => decoded.push(packet)); + try { + const encoded = new Encoder().encode({ type: PacketType.EVENT, nsp: '/', data }); + for (const frame of encoded) decoder.add(frame); + assert.equal(decoded.length, 1); + assert.equal(decoded[0].type, PacketType.EVENT); + assert.equal(decoded[0].nsp, '/'); + return decoded[0].data; + } finally { + decoder.destroy(); + } + } + + test(`${entry}: preserves Draw JSON event shapes`, () => { + for (const event of [ + ['join', { roomId: 'local-room', username: 'fixture' }], + ['full-state', { type: 'full-state', elements: [], participants: [] }], + ['scene-update', { elements: [{ id: 'rectangle', version: 1, x: 10, y: 20 }] }], + ['cursor-update', { pointer: { x: 12, y: 34 } }], + ['request-state'], + ]) { + assert.deepEqual(roundTrip(event), event); + } + }); + + test(`${entry}: preserves a normal binary event`, () => { + const event = ['fixture', { bytes: Buffer.from('small local fixture') }]; + assert.deepEqual(roundTrip(event), event); + }); + + test(`${entry}: honors JSON serialization alongside binary data`, () => { + const bytes = Buffer.from('local drawing fixture'); + class DrawingAttachment { + constructor(value) { + this.internalBytes = value; + } + toJSON() { + return { attachment: this.internalBytes, label: 'fixture' }; + } + } + assert.deepEqual(roundTrip(['fixture', new DrawingAttachment(bytes)]), ['fixture', { attachment: bytes, label: 'fixture' }]); + }); + + test(`${entry}: rejects a binary header without attachments`, () => { + const decoder = new Decoder(); + let decoded = false; + decoder.on('decoded', () => { decoded = true; }); + try { + // One short local header; no attachments or resource-exhaustion traffic. + assert.throws(() => decoder.add('50-["fixture"]'), /Illegal attachments/); + assert.equal(decoded, false); + } finally { + decoder.destroy(); + } + }); +} diff --git a/plugins/draw/ui/package-lock.json b/plugins/draw/ui/package-lock.json index 2801e71..200d0b4 100644 --- a/plugins/draw/ui/package-lock.json +++ b/plugins/draw/ui/package-lock.json @@ -3514,9 +3514,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0",