diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 15e6a4a..9b70666 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -13,10 +13,19 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: "20.x" + node-version-file: ".nvmrc" - name: Install dependencies - run: npm install + run: npm ci - name: Check formatting with Prettier run: npx prettier --check . + + - name: Check types + run: npx tsc --noEmit + + - name: Build and test runtime compatibility + run: npm test + + - name: Audit dependencies + run: npm audit --audit-level=high diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ce62013 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.22.1 diff --git a/README.md b/README.md index 83b3e65..83016d8 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,17 @@ A powerful plugin for Elgato Stream Deck that allows you to manage Docker contai Before using this plugin, ensure you have the following installed: -- Elgato Stream Deck Software (latest version) +- Elgato Stream Deck Software 7.1 or newer (latest version recommended) - Docker (with the Docker Engine API enabled) -- Node.js (for development and building the plugin) +- Node.js 24.22.1 or newer within the 24.x line (for development and building the plugin) + +### Node.js security updates + +Development and CI use the patched Node.js version pinned in `.nvmrc`. Node.js 24.22.1 addresses the [October 2026 security advisory](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases). npm rejects unsupported Node.js versions via `engines` and `.npmrc`. + +The installed plugin uses **Stream Deck's bundled Node.js 24 runtime**, not the Node.js installed on your system. Stream Deck 7.1 introduced Node.js 24 support, but this minimum version alone does not guarantee the security fixes: keep Stream Deck updated and verify that its bundled Node.js 24 runtime is at least 24.22.1. The plugin manifest selects only the Node.js major version; it cannot pin a security patch. Updating npm dependencies or your system Node.js does not update this bundled runtime. + +The dependency override for `@elgato/utils@0.4.1` pins patched Zod 4.6.7 to address [GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5). The current Elgato dependency declares Zod 3.25 but imports its bundled Zod 4 Mini API (`zod/v4-mini`), which the patched version also exports. `npm test` checks RPC schema compatibility and built-plugin registration. Reassess the override when Elgato ships a dependency range that includes patched Zod. ## Installation @@ -47,8 +55,12 @@ git clone https://github.com/Darkdragon14/streamdeck-docker.git ### Install the dependencies: +Select the project's Node.js version with [nvm](https://github.com/nvm-sh/nvm) (or install the version from `.nvmrc` using your preferred version manager): + ```bash -npm install +nvm install +nvm use +npm ci ``` ### Build the project: @@ -80,7 +92,9 @@ Clone the repository and navigate to the project directory: ```bash git clone https://github.com/Darkdragon14/streamdeck-docker.git cd streamdeck-docker -npm i +nvm install +nvm use +npm ci npm run watch ``` diff --git a/com.darkdragon14.elgato-docker.sdPlugin/manifest.json b/com.darkdragon14.elgato-docker.sdPlugin/manifest.json index 1f26b2d..560372a 100644 --- a/com.darkdragon14.elgato-docker.sdPlugin/manifest.json +++ b/com.darkdragon14.elgato-docker.sdPlugin/manifest.json @@ -1,6 +1,6 @@ { "Name": "elgato-docker", - "Version": "2.2.0.0", + "Version": "2.2.0.1", "Author": "@Darkdragon14", "Actions": [ { @@ -171,7 +171,7 @@ "Icon": "imgs/plugin/marketplace", "SDKVersion": 3, "Software": { - "MinimumVersion": "6.9" + "MinimumVersion": "7.1" }, "OS": [ { @@ -184,7 +184,7 @@ } ], "Nodejs": { - "Version": "20", + "Version": "24", "Debug": "enabled" }, "UUID": "com.darkdragon14.elgato-docker" diff --git a/package-lock.json b/package-lock.json index 5e65489..be2265f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "dependencies": { "@elgato/streamdeck": "^2.0.1", - "@elgato/utils": "^0.4.1" + "@elgato/utils": "0.4.1" }, "devDependencies": { "@elgato/cli": "^1.7.0", @@ -20,6 +20,9 @@ "rollup": "^4.0.2", "tslib": "^2.6.2", "typescript": "^5.2.2" + }, + "engines": { + "node": ">=24.22.1 <25" } }, "node_modules/@augment-vir/common": { @@ -1503,9 +1506,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1687,10 +1690,20 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true, + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fdir": { @@ -1719,9 +1732,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -2408,9 +2421,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2513,9 +2526,9 @@ } }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2610,9 +2623,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -2667,9 +2680,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 1f58d8e..b1b62c4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,14 @@ { "scripts": { "build": "rollup -c", + "test": "npm run build && node --test tests/*.test.mjs", "watch": "rollup -c -w --watch.onEnd=\"streamdeck restart com.darkdragon14.elgato-docker\"", "prettier": "npx prettier --write ." }, "type": "module", + "engines": { + "node": ">=24.22.1 <25" + }, "devDependencies": { "@elgato/cli": "^1.7.0", "@elgato/prettier-config": "^0.2.4", @@ -20,7 +24,12 @@ }, "dependencies": { "@elgato/streamdeck": "^2.0.1", - "@elgato/utils": "^0.4.1" + "@elgato/utils": "0.4.1" + }, + "overrides": { + "@elgato/utils@0.4.1": { + "zod": "4.6.7" + } }, "prettier": "@elgato/prettier-config" } diff --git a/tests/runtime-compatibility.test.mjs b/tests/runtime-compatibility.test.mjs new file mode 100644 index 0000000..d7d37ac --- /dev/null +++ b/tests/runtime-compatibility.test.mjs @@ -0,0 +1,69 @@ +import { JsonRpcError, JsonRpcRequest, JsonRpcResponse } from "@elgato/utils/rpc"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import { WebSocketServer } from "ws"; + +test("Elgato RPC schemas work with the patched Zod Mini implementation", () => { + const request = { jsonrpc: "2.0", method: "test", params: [1, "value"], id: "1" }; + assert.equal(JsonRpcRequest.safeParse(request).success, true); + assert.equal(JsonRpcRequest.safeParse({ ...request, params: { value: 1 } }).success, true); + assert.equal(JsonRpcRequest.safeParse({ ...request, method: 123 }).success, false); + assert.equal(JsonRpcRequest.safeParse({ ...request, jsonrpc: "1.0" }).success, false); + + const error = { code: -32600, message: "Invalid request", data: null }; + assert.equal(JsonRpcError.safeParse(error).success, true); + assert.equal(JsonRpcError.safeParse({ ...error, code: "invalid" }).success, false); + assert.equal(JsonRpcResponse.safeParse({ jsonrpc: "2.0", result: { ok: true }, id: "1" }).success, true); + assert.equal(JsonRpcResponse.safeParse({ jsonrpc: "2.0", error, id: "1" }).success, true); + assert.equal(JsonRpcResponse.safeParse({ jsonrpc: "2.0", id: "1" }).success, false); +}); + +test("built plugin registers with Stream Deck using the patched dependencies", { timeout: 10000 }, async (t) => { + const pluginDirectory = new URL("../com.darkdragon14.elgato-docker.sdPlugin/", import.meta.url); + const manifest = JSON.parse(await readFile(new URL("manifest.json", pluginDirectory), "utf8")); + const server = new WebSocketServer({ host: "127.0.0.1", port: 0 }); + t.after(() => { + for (const socket of server.clients) socket.terminate(); + server.close(); + }); + await once(server, "listening"); + + const info = { + application: { version: "7.1.0", platform: "windows", language: "en", platformVersion: "10" }, + plugin: { uuid: manifest.UUID, version: manifest.Version }, + devices: [], + colors: {}, + }; + const child = spawn( + process.execPath, + [ + manifest.CodePath, + "-port", + String(server.address().port), + "-pluginUUID", + manifest.UUID, + "-registerEvent", + "registerPlugin", + "-info", + JSON.stringify(info), + ], + { cwd: pluginDirectory, stdio: ["ignore", "ignore", "pipe"] }, + ); + let stderr = ""; + child.stderr.on("data", (data) => (stderr += data)); + t.after(() => child.kill()); + const connection = once(server, "connection"); + const registration = connection.then(async ([socket]) => { + const [data] = await once(socket, "message"); + assert.deepEqual(JSON.parse(data.toString()), { event: "registerPlugin", uuid: manifest.UUID }); + }); + await Promise.race([ + registration, + once(child, "exit").then(([code]) => { + throw new Error(`Plugin exited before registration (${code}): ${stderr}`); + }), + ]); +});