diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 5b12dddb7..4f3717b24 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -2828,3 +2828,14 @@ The presentation unit tests cover the label and terminal guidance. cards in Chromium at desktop, tablet and phone widths, asserting the copy, absence of page errors and horizontal overflow. Scripted props are component evidence only, not installed notification delivery. + +### Webhook test/save custom-field parity + +The webhook form applies the same service-specific custom-field normalisation +when testing unsaved data as when saving it. In particular, manually entered +Pushover app_token/user_token aliases become token/user with the existing +canonical-value precedence. Existing-field editing remains normalised on load. +This changes neither delivery scheduling nor the meaning of a successful test. +The registered WebhookConfig regression covers alias test/save payload equality; +the browser fixture exercises the real form at desktop and phone widths with +synthetic callbacks, not a hosted Pushover destination or installed delivery. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index e0a403e42..23ff141fd 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -3359,6 +3359,18 @@ stages pin the Docker Official Images Linux amd64 manifest the checked-in toolchain files and release-script guards must reject an older compiler so local, exact-candidate, provider control-plane, and container builds cannot silently reintroduce the vulnerable runtime. +The frontend development-tool dependency boundary must retain patched Vitest +and mocker versions at or above `4.1.11`, with the V8 coverage provider matched +to the runner, and a `js-yaml` override at or above `4.3.2`. These floors prevent +reintroducing redirect-mock file reads (GHSA-82fw-gwwq-j7x9) and empty-merge CPU +exhaustion (GHSA-2883-xcg3-v3hh) through the locked development graph. The +`frontend-modern/src/security/__tests__/dependencySecurity.test.ts` proof must +check manifest floors and all installed lockfile copies, including nested +copies; a successful production build alone does not establish this property. +Vitest migration must preserve test assertions while using constructable +constructor mocks and explicit standalone mock-history cleanup. Dependency +updates must retain the required full-graph audit rather than suppressing it. + That same dev-runtime dependency-manifest boundary now also owns the maintained Docker engine module floor. `go.mod`, `go.sum`, and `internal/cloudcp/docker/manager.go` must route hosted runtime orchestration diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 6f93fc4c5..da66077c5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,16 +1,16 @@ { "version": 1, - "base_sha": "550b2d80a175cc66ab4e7ac269b7ec350e101611", - "verified_at": "2026-09-08T11:54:21Z", + "base_sha": "6f3547bc5fbb10ddf313a8cd2f184b6359173a59", + "verified_at": "2026-09-08T22:29:31.643744Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/utils/alertDestinationsPresentation.ts" + "frontend-modern/src/components/Alerts/useWebhookConfigState.ts" ], "content_sha256": { - "frontend-modern/src/utils/alertDestinationsPresentation.ts": "67f6f9a1c98345949e162bf06a38760e61457aa1e8a2130913646647a4f29e88" + "frontend-modern/src/components/Alerts/useWebhookConfigState.ts": "14f0dceb069fe52b9a47100c50998ae53a2c5c8a29a524e342f7458546cb4a19" }, "routes": [ - "/qualification (real health and delivery-log cards, scripted props)" + "/qualification (real WebhookConfigForm and hook; synthetic callbacks)" ], "viewports": [ { @@ -27,11 +27,13 @@ } ], "states": [ - "Retained terminal server_error health diagnosis and failed delivery log entry" + "Unsaved manually entered Pushover aliases", + "Test callback payload", + "Save callback payload" ], "interactions": [ - "Load both cards at desktop, tablet and phone widths; assert label, guidance, no page errors or horizontal overflow." + "Click Test then Add Webhook at each width; assert both canonical token/user payloads, no page errors or horizontal overflow." ], - "notes": "Phone screenshot visually inspected; screenshots retained at /tmp/pulse-server-error-copy. Initial fixture omitted alertIds and timed out; corrected fixture passed all three widths. No installed delivery or receipt claim.", - "command": "pulse-heavy-run -- node scripts/check-delivery-server-error-copy.mjs" + "notes": "Real component with Vite dev transform in Chromium, not production build or installed/provider delivery. Phone screenshot visually inspected; synthetic credentials only. Screenshots /tmp/pulse-webhook-parity.", + "command": "pulse-heavy-run -- node scripts/check-webhook-test-save-parity.mjs" } diff --git a/frontend-modern/package-lock.json b/frontend-modern/package-lock.json index bfea73351..1cd175933 100644 --- a/frontend-modern/package-lock.json +++ b/frontend-modern/package-lock.json @@ -26,7 +26,7 @@ "@types/qrcode": "^1.5.6", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "autoprefixer": "^10.4.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.0.0", @@ -42,7 +42,7 @@ "vite": "^6.4.3", "vite-plugin-solid": "^2.8.0", "vite-plugin-sri-gen": "^1.3.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -65,20 +65,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -1259,88 +1245,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1493,17 +1397,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -1924,6 +1817,13 @@ } } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/typography": { "version": "0.5.20", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", @@ -2336,32 +2236,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", - "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.7", - "vitest": "3.2.7" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2370,39 +2267,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2414,42 +2312,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -2457,28 +2355,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2628,9 +2523,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", "dev": true, "license": "MIT", "dependencies": { @@ -2879,16 +2774,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2971,18 +2856,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -3014,16 +2892,6 @@ "is-regex": "^1.0.3" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3289,16 +3157,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3384,13 +3242,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.420", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", @@ -3448,9 +3299,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -3983,36 +3834,6 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -4158,28 +3979,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4193,39 +3992,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "17.7.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", @@ -4664,21 +4430,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4693,22 +4444,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -4734,9 +4469,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -4982,13 +4717,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5026,15 +4754,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -5195,16 +4923,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5324,6 +5042,20 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5409,13 +5141,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5468,23 +5193,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5492,16 +5200,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6350,9 +6048,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -6370,22 +6068,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6398,20 +6080,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -6448,26 +6116,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/style-to-object": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", @@ -6596,21 +6244,6 @@ "node": ">=4" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6642,11 +6275,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -6696,30 +6332,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -7007,29 +6623,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-solid": { "version": "2.11.12", "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz", @@ -7149,65 +6742,79 @@ } }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -7218,13 +6825,16 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -7370,110 +6980,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/frontend-modern/package.json b/frontend-modern/package.json index b4e463dfa..6289dc6c4 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -66,7 +66,7 @@ "seroval": "^1.4.1", "seroval-plugins": "^1.4.1", "@babel/core": "^7.29.6", - "js-yaml": "^4.2.0" + "js-yaml": "^4.3.2" }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -77,7 +77,7 @@ "@types/qrcode": "^1.5.6", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "autoprefixer": "^10.4.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.0.0", @@ -93,6 +93,6 @@ "vite": "^6.4.3", "vite-plugin-solid": "^2.8.0", "vite-plugin-sri-gen": "^1.3.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } } diff --git a/frontend-modern/src/components/Alerts/EmailProviderSelect.test.tsx b/frontend-modern/src/components/Alerts/EmailProviderSelect.test.tsx index f8f2f59de..45d441eee 100644 --- a/frontend-modern/src/components/Alerts/EmailProviderSelect.test.tsx +++ b/frontend-modern/src/components/Alerts/EmailProviderSelect.test.tsx @@ -79,8 +79,8 @@ const mockProviders = [gmailProvider, sendgridProvider, outlookProvider]; // --- Tests --- describe('EmailProviderSelect', () => { - let onChangeMock: ReturnType; - let onTestMock: ReturnType; + let onChangeMock: ReturnType any>>; + let onTestMock: ReturnType any>>; beforeEach(() => { onChangeMock = vi.fn(); diff --git a/frontend-modern/src/components/Alerts/ResourceTable.test.tsx b/frontend-modern/src/components/Alerts/ResourceTable.test.tsx index 65ded2ac2..46bbf72bb 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.test.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.test.tsx @@ -168,17 +168,17 @@ interface DefaultProps { title: string; resources: Resource[]; columns: string[]; - onEdit: ReturnType; - onSaveEdit: ReturnType; - onCancelEdit: ReturnType; - onRemoveOverride: ReturnType; + onEdit: ReturnType any>>; + onSaveEdit: ReturnType any>>; + onCancelEdit: ReturnType any>>; + onRemoveOverride: ReturnType any>>; editingId: () => string | null; editingThresholds: () => Record; - setEditingThresholds: ReturnType; - formatMetricValue: ReturnType; - hasActiveAlert: ReturnType; + setEditingThresholds: ReturnType any>>; + formatMetricValue: ReturnType any>>; + hasActiveAlert: ReturnType any>>; editingNote: () => string; - setEditingNote: ReturnType; + setEditingNote: ReturnType any>>; } function makeProps( diff --git a/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx index ddd23c2f5..5498be075 100644 --- a/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx +++ b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx @@ -1,3 +1,5 @@ +import { createRoot } from 'solid-js'; +import { useWebhookConfigState } from './useWebhookConfigState'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; import webhookConfigSource from '@/components/Alerts/WebhookConfig.tsx?raw'; @@ -102,10 +104,10 @@ const mockTemplates: WebhookTemplate[] = [ // --- Tests --- describe('WebhookConfig', () => { - let onAddMock: ReturnType; - let onUpdateMock: ReturnType; - let onDeleteMock: ReturnType; - let onTestMock: ReturnType; + let onAddMock: ReturnType any>>; + let onUpdateMock: ReturnType any>>; + let onDeleteMock: ReturnType any>>; + let onTestMock: ReturnType any>>; beforeEach(() => { onAddMock = vi.fn(); @@ -1580,3 +1582,41 @@ describe('WebhookConfig', () => { expect(payloadTextareaAfter.value).toBe(''); }); }); + +describe('webhook test/save parity', () => { + it('normalises manually entered Pushover aliases identically for test and save', () => { + createRoot((dispose) => { + try { + const onTest = vi.fn(); + const onAdd = vi.fn(); + const state = useWebhookConfigState({ + webhooks: [], + onTest, + onAdd, + onUpdate: vi.fn(), + onDelete: vi.fn(), + }); + state.openAddForm(); + state.setFormData((data) => ({ + ...data, + name: 'Example', + url: 'https://example.invalid', + service: 'pushover', + })); + for (const [index, key] of ['app_token', 'user_token'].entries()) { + state.addCustomFieldInput(); + state.updateCustomFieldInput(index, { key, value: `synthetic-${index}` }); + } + state.testWebhookForm(); + state.saveWebhook(); + expect(onAdd.mock.calls[0][0].customFields).toEqual({ + token: 'synthetic-0', + user: 'synthetic-1', + }); + expect(onTest.mock.calls[0][1].customFields).toEqual(onAdd.mock.calls[0][0].customFields); + } finally { + dispose(); + } + }); + }); +}); diff --git a/frontend-modern/src/components/Alerts/useWebhookConfigState.ts b/frontend-modern/src/components/Alerts/useWebhookConfigState.ts index 080e44d0e..e7086e25b 100644 --- a/frontend-modern/src/components/Alerts/useWebhookConfigState.ts +++ b/frontend-modern/src/components/Alerts/useWebhookConfigState.ts @@ -284,7 +284,7 @@ export function useWebhookConfigState(props: WebhookConfigProps): WebhookConfigS const testPayload = { ...restFormData, headers, - customFields, + customFields: normalizeAlertWebhookCustomFields(data.service, customFields), template: payloadTemplate ?? restFormData.template ?? '', }; const tempId = editingId() || 'temp-new-webhook'; diff --git a/frontend-modern/src/components/Settings/__tests__/useDiagnosticsPanelState.test.ts b/frontend-modern/src/components/Settings/__tests__/useDiagnosticsPanelState.test.ts index 09d7520b3..dd01e9690 100644 --- a/frontend-modern/src/components/Settings/__tests__/useDiagnosticsPanelState.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/useDiagnosticsPanelState.test.ts @@ -65,12 +65,12 @@ const currentExportDate = (): string => new Date().toISOString().split('T')[0]; describe('useDiagnosticsPanelState', () => { let useDiagnosticsPanelState: UseDiagnosticsPanelStateModule['useDiagnosticsPanelState']; - let apiFetchJSONMock: ReturnType; - let showErrorMock: ReturnType; - let showSuccessMock: ReturnType; - let createObjectURLMock: ReturnType; - let revokeObjectURLMock: ReturnType; - let anchorClickMock: ReturnType; + let apiFetchJSONMock: ReturnType any>>; + let showErrorMock: ReturnType any>>; + let showSuccessMock: ReturnType any>>; + let createObjectURLMock: ReturnType any>>; + let revokeObjectURLMock: ReturnType any>>; + let anchorClickMock: ReturnType any>>; let createdAnchor: HTMLAnchorElement | null; let originalCreateObjectURL: typeof URL.createObjectURL | undefined; let originalRevokeObjectURL: typeof URL.revokeObjectURL | undefined; diff --git a/frontend-modern/src/components/Workloads/__tests__/NodeDrawerOverview.updateEvidence.test.tsx b/frontend-modern/src/components/Workloads/__tests__/NodeDrawerOverview.updateEvidence.test.tsx index 52f83acd3..14136132a 100644 --- a/frontend-modern/src/components/Workloads/__tests__/NodeDrawerOverview.updateEvidence.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/NodeDrawerOverview.updateEvidence.test.tsx @@ -52,6 +52,6 @@ describe('NodeDrawerOverview update evidence', () => { /> )); - expect(screen.getByText('Unavailable · Sys.Audit permission required')).toBeInTheDocument(); + expect(screen.getByText('Unavailable · Update check access denied')).toBeInTheDocument(); }); }); diff --git a/frontend-modern/src/components/shared/__tests__/TagBadges.test.tsx b/frontend-modern/src/components/shared/__tests__/TagBadges.test.tsx index 1f2373e5a..54e277de4 100644 --- a/frontend-modern/src/components/shared/__tests__/TagBadges.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/TagBadges.test.tsx @@ -84,6 +84,8 @@ function getTagDotRing(dot: Element) { afterEach(() => { cleanup(); vi.restoreAllMocks(); + // Vitest 4 restores spies without clearing standalone vi.fn call history. + vi.clearAllMocks(); // Reset dark mode to default (false) for isolation darkModeMock.mockReturnValue(false); }); diff --git a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts index 45ea92111..761ed090b 100644 --- a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts +++ b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts @@ -1,6 +1,7 @@ import { batch, createRoot, createSignal } from 'solid-js'; import { createStore, reconcile, type SetStoreFunction } from 'solid-js/store'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { buildProxmoxPageModel } from '@/features/proxmox/proxmoxPageModel'; import type { State } from '@/types/api'; import type { Resource } from '@/types/resource'; import useUnifiedResourcesSource from '../useUnifiedResources.ts?raw'; @@ -1339,6 +1340,54 @@ describe('useUnifiedResources', () => { dispose(); }); + it('keeps an agent-plus-docker NAS out of the Proxmox table after API projection', async () => { + // #1930's supplied field shape, with entirely synthetic identities and no + // addresses or token metadata. This does not reproduce the earlier screenshot. + apiFetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [ + { + ...v2Resource, + id: 'synthetic-nas', + name: 'synthetic-nas', + technology: 'linux', + sources: ['agent', 'docker'], + platformScopes: ['agent', 'docker'], + canonicalIdentity: { displayName: 'synthetic-nas', platformId: 'synthetic-nas' }, + identity: { hostnames: ['synthetic-nas'] }, + agent: { platform: 'linux', osName: 'Synology DSM' }, + }, + { + ...v2Resource, + id: 'synthetic-pve', + sources: ['proxmox'], + proxmox: { nodeName: 'synthetic-pve', clusterName: 'synthetic-cluster' }, + }, + ], + }), + }); + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + result = useUnifiedResources({ query: 'type=agent', cacheKey: 'nas-boundary' }); + }); + try { + await result!.refetch(); + const resources = result!.resources(); + const nas = resources.find((resource) => resource.id === 'synthetic-nas'); + expect(nas).toBeDefined(); + expect(nas?.clusterId).toBeUndefined(); + expect(nas?.proxmox).toBeUndefined(); + expect(buildProxmoxPageModel(resources).pveNodes.map((node) => node.id)).toEqual([ + 'synthetic-pve', + ]); + } finally { + dispose(); + } + }); + it('projects proxmox clusterId from the shared cluster helper', async () => { apiFetchMock.mockResolvedValueOnce({ ok: true, diff --git a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts index ec392f81c..1cd69547c 100644 --- a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts +++ b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts @@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest'; interface PackageManifest { dependencies: Record; + devDependencies: Record; + overrides: Record; } interface PackageLock { @@ -58,6 +60,34 @@ const nanoidIsPatched = (version: string): boolean => { }; describe('frontend dependency security floors', () => { + it('keeps Vitest and its mocker above the redirect-mock file-read floor', () => { + // GHSA-82fw-gwwq-j7x9: the maintained 4.x fix starts at 4.1.11. + expect(manifest.devDependencies.vitest).toBe('^4.1.11'); + expect(manifest.devDependencies['@vitest/coverage-v8']).toBe('^4.1.11'); + const runner = lockedVersions('vitest'); + expect(runner).toHaveLength(1); + for (const name of ['vitest', '@vitest/mocker', '@vitest/coverage-v8']) { + const versions = lockedVersions(name); + expect(versions).not.toHaveLength(0); + for (const version of versions) { + expect(version).not.toContain('-'); + expect(atLeast(version, [4, 1, 11]), `${name} ${version} is vulnerable`).toBe(true); + expect(version, `${name} must match the runner`).toBe(runner[0]); + } + } + }); + + it('keeps every js-yaml copy above the empty-merge CPU exhaustion floor', () => { + // GHSA-2883-xcg3-v3hh: our override keeps every transitive copy on patched 4.x. + expect(manifest.overrides['js-yaml']).toBe('^4.3.2'); + const versions = lockedVersions('js-yaml'); + expect(versions).not.toHaveLength(0); + for (const version of versions) { + expect(version).not.toContain('-'); + expect(atLeast(version, [4, 3, 2]), `js-yaml ${version} is vulnerable`).toBe(true); + } + }); + it('keeps DOMPurify above the hook-detachment XSS floor', () => { expect(manifest.dependencies.dompurify).toBe('^3.4.13'); const versions = lockedVersions('dompurify'); diff --git a/frontend-modern/src/stores/__tests__/websocket-unified.test.ts b/frontend-modern/src/stores/__tests__/websocket-unified.test.ts index 61073809c..c630e3535 100644 --- a/frontend-modern/src/stores/__tests__/websocket-unified.test.ts +++ b/frontend-modern/src/stores/__tests__/websocket-unified.test.ts @@ -26,7 +26,7 @@ interface MockWebSocketInstance { let mockWsInstance: MockWebSocketInstance | null = null; -const MockWebSocket = vi.fn().mockImplementation((url: string): MockWebSocketInstance => { +const MockWebSocket = vi.fn().mockImplementation(function (url: string): MockWebSocketInstance { const instance: MockWebSocketInstance = { url, readyState: 1, diff --git a/frontend-modern/src/utils/__tests__/proxmoxUpdateEvidence.test.ts b/frontend-modern/src/utils/__tests__/proxmoxUpdateEvidence.test.ts index 07a1fefdd..f2dabdf3b 100644 --- a/frontend-modern/src/utils/__tests__/proxmoxUpdateEvidence.test.ts +++ b/frontend-modern/src/utils/__tests__/proxmoxUpdateEvidence.test.ts @@ -51,7 +51,7 @@ describe('Proxmox update evidence presentation', () => { pendingUpdatesReason: 'permission_denied', }), ).toMatchObject({ - value: 'Unavailable · Sys.Audit permission required', + value: 'Unavailable · Update check access denied', current: false, }); }); diff --git a/frontend-modern/src/utils/__tests__/resourceMetadataEvents.branchcov0712c.test.ts b/frontend-modern/src/utils/__tests__/resourceMetadataEvents.branchcov0712c.test.ts index b40b1c224..c20fcda7b 100644 --- a/frontend-modern/src/utils/__tests__/resourceMetadataEvents.branchcov0712c.test.ts +++ b/frontend-modern/src/utils/__tests__/resourceMetadataEvents.branchcov0712c.test.ts @@ -25,7 +25,7 @@ import type { ResourceMetadataChangedDetail } from '@/utils/resourceMetadataEven const win: Window & typeof globalThis = window; describe('dispatchResourceMetadataChanged', () => { - let listener: ReturnType; + let listener: ReturnType any>>; beforeEach(() => { listener = vi.fn(); diff --git a/frontend-modern/src/utils/proxmoxUpdateEvidence.ts b/frontend-modern/src/utils/proxmoxUpdateEvidence.ts index a8c144ae1..4b90f7a1d 100644 --- a/frontend-modern/src/utils/proxmoxUpdateEvidence.ts +++ b/frontend-modern/src/utils/proxmoxUpdateEvidence.ts @@ -16,7 +16,7 @@ type UpdateEvidenceNode = Pick< const reasonLabel = (reason: Node['pendingUpdatesReason']): string => { switch (reason) { case 'permission_denied': - return 'Sys.Audit permission required'; + return 'Update check access denied'; case 'source_unavailable': return 'Proxmox source unavailable'; case 'check_failed': diff --git a/internal/api/change_password_session_admin_test.go b/internal/api/change_password_session_admin_test.go index af56a32c6..8f2bd313b 100644 --- a/internal/api/change_password_session_admin_test.go +++ b/internal/api/change_password_session_admin_test.go @@ -23,7 +23,9 @@ func newChangePasswordRouter(t *testing.T) (*Router, *config.Config) { t.Fatalf("bcrypt: %v", err) } cfg := &config.Config{DataPath: dir, ConfigPath: dir, AuthUser: "admin", AuthPass: string(hashed)} - return NewRouter(cfg, nil, nil, nil, nil, "1.0.0"), cfg + router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") + cleanupTestRouter(t, router) + return router, cfg } func changePasswordRequest(t *testing.T, current string) *http.Request { @@ -87,3 +89,27 @@ func TestChangePasswordBasicAuthPathUnaffected(t *testing.T) { t.Fatalf("Basic Auth change-password must not be refused by the session gate, got 403 (body %s)", rec.Body.String()) } } + +func TestChangePasswordFixtureOwnsRouterLifecycle(t *testing.T) { + var router *Router + workerDone := make(chan struct{}) + t.Run("fixture", func(t *testing.T) { + router, _ = newChangePasswordRouter(t) + router.startLifecycleWorker(func() { + <-router.lifecycleCtx.Done() + close(workerDone) + }) + }) + // Ensure a failing regression does not itself leave workers running. + cleanupTestRouter(t, router) + select { + case <-router.lifecycleCtx.Done(): + default: + t.Fatal("password-change fixture left router lifecycle active") + } + select { + case <-workerDone: + default: + t.Fatal("password-change fixture did not join its router worker") + } +} diff --git a/internal/api/config_transfer_authorization_test.go b/internal/api/config_transfer_authorization_test.go index d81dba15b..a3b94a9cf 100644 --- a/internal/api/config_transfer_authorization_test.go +++ b/internal/api/config_transfer_authorization_test.go @@ -43,7 +43,9 @@ func newConfigTransferTestRouter(t *testing.T, hosted bool, sso *config.SSOConfi t.Fatalf("save synthetic SSO config: %v", err) } } - return NewRouter(cfg, nil, nil, nil, nil, "test") + router := NewRouter(cfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, router) + return router } func enabledConfigTransferSSO(providerType config.SSOProviderType) *config.SSOConfig { @@ -129,6 +131,7 @@ func TestConfigTransferEnvironmentOIDCAndSSOLoadFailureFailClosed(t *testing.T) } cfg := &config.Config{DataPath: dataDir, ConfigPath: dataDir} router := NewRouter(cfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, router) if !router.ssoAuthenticationLoadFailed() { t.Fatal("precondition: corrupt persisted SSO was not recorded as a load failure") } @@ -220,6 +223,7 @@ func TestConfigTransferAuthorizedInstanceModesReachHandler(t *testing.T) { t.Setenv("PULSE_DATA_DIR", dataDir) cfg := &config.Config{DataPath: dataDir, ConfigPath: dataDir, AuthUser: "admin", AuthPass: hash} router := NewRouter(cfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, router) for _, path := range []string{"/api/config/export", "/api/config/import"} { body := &countingConfigTransferBody{reader: strings.NewReader(`{not-json`)} req := httptest.NewRequest(http.MethodPost, path, body) @@ -266,6 +270,7 @@ func TestConfigTransferAuthorizedInstanceModesReachHandler(t *testing.T) { ProxyAuthRoleHeader: "X-Proxy-Roles", ProxyAuthAdminRole: "admin", } router := NewRouter(cfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, router) for _, path := range []string{"/api/config/export", "/api/config/import"} { body := &countingConfigTransferBody{reader: strings.NewReader(`{not-json`)} req := httptest.NewRequest(http.MethodPost, path, body) @@ -289,6 +294,7 @@ func TestConfigTransferTokenScopesAndOrganizationBinding(t *testing.T) { cfg := newTestConfigWithTokens(t, readRecord, writeRecord) t.Setenv("PULSE_DATA_DIR", cfg.DataPath) router := NewRouter(cfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, router) tests := []struct { name string @@ -331,6 +337,7 @@ func TestConfigTransferTokenScopesAndOrganizationBinding(t *testing.T) { boundCfg := newTestConfigWithTokens(t, boundRecord) t.Setenv("PULSE_DATA_DIR", boundCfg.DataPath) boundRouter := NewRouter(boundCfg, nil, nil, nil, nil, "test") + cleanupTestRouter(t, boundRouter) body := &countingConfigTransferBody{reader: strings.NewReader(`{not-json`)} req := httptest.NewRequest(http.MethodPost, "/api/config/export", body) req.Header.Set("X-API-Token", boundRaw) @@ -404,6 +411,7 @@ func TestConfigTransferTenantSessionsRequireManagement(t *testing.T) { mtm := monitoring.NewMultiTenantMonitor(cfg, mtp, nil) t.Cleanup(mtm.Stop) router := NewRouter(cfg, nil, mtm, nil, nil, "test") + cleanupTestRouter(t, router) tests := []struct { user string @@ -517,6 +525,7 @@ func TestDeniedConfigTransferDoesNotMutateOrReload(t *testing.T) { reloadCalls++ return nil }, "test") + cleanupTestRouter(t, router) requests := []struct { path string @@ -547,3 +556,29 @@ func TestDeniedConfigTransferDoesNotMutateOrReload(t *testing.T) { t.Fatalf("denied import mutated live config: %+v", cfg) } } + +func TestConfigTransferFixtureOwnsRouterLifecycle(t *testing.T) { + var router *Router + workerDone := make(chan struct{}) + t.Run("fixture", func(t *testing.T) { + router = newConfigTransferTestRouter(t, false, nil) + router.startLifecycleWorker(func() { + <-router.lifecycleCtx.Done() + close(workerDone) + }) + }) + // Retain cleanup even if the helper regresses. + defer router.shutdownBackgroundWorkers() + defer router.ShutdownResourceStores() + defer router.ShutdownRBAC() + select { + case <-router.lifecycleCtx.Done(): + default: + t.Fatal("config-transfer helper left router lifecycle active") + } + select { + case <-workerDone: + default: + t.Fatal("config-transfer helper did not join its router worker") + } +} diff --git a/internal/monitoring/monitor_polling_node_helpers.go b/internal/monitoring/monitor_polling_node_helpers.go index d6984f45f..03156560d 100644 --- a/internal/monitoring/monitor_polling_node_helpers.go +++ b/internal/monitoring/monitor_polling_node_helpers.go @@ -378,7 +378,7 @@ func (m *Monitor) applyNodePendingUpdates(ctx context.Context, instanceName stri Err(err). Str("node", node.Node). Str("instance", instanceName). - Msg("Could not check pending apt updates (may require Sys.Audit permission)") + Msg("Could not check pending apt updates with configured Proxmox credentials") modelNode.PendingUpdatesReason = pendingUpdatesFailureReason(err) if hasCached { modelNode.PendingUpdates = cached.count diff --git a/internal/unifiedresources/nas_cluster_projection_test.go b/internal/unifiedresources/nas_cluster_projection_test.go new file mode 100644 index 000000000..f9ecdde30 --- /dev/null +++ b/internal/unifiedresources/nas_cluster_projection_test.go @@ -0,0 +1,64 @@ +package unifiedresources + +import ( + "encoding/json" + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/models" +) + +// A cluster label is a projection of provider association, not subnet membership. +// Keep the public discriminator covered: link hints themselves are not JSON fields. +func TestNASClusterProjectionPublicEvidence(t *testing.T) { + for _, linked := range []bool{false, true} { + name := "unlinked-shared-bridge" + if linked { + name = "retained-reciprocal-link" + } + t.Run(name, func(t *testing.T) { + node := models.Node{ID: "pve-node", Name: "pve", Instance: "cluster", ClusterName: "cluster", IsClusterMember: true, Host: "https://192.0.2.10:8006", NetworkInterfaces: []models.HostNetworkInterface{{Name: "docker0", Addresses: []string{"172.17.0.1/16"}}}} + host := models.Host{ID: "nas-agent", MachineID: "nas-machine", Hostname: "nas", NetworkInterfaces: []models.HostNetworkInterface{{Name: "eth0", Addresses: []string{"198.51.100.20/24"}}, {Name: "docker0", Addresses: []string{"172.17.0.1/16"}}}} + if linked { + host.LinkedNodeID = node.ID + host.NodeLinkSource = "manual" + node.LinkedAgentID = host.ID + } + registry := NewRegistry(NewMemoryStore()) + registry.IngestSnapshot(models.StateSnapshot{Nodes: []models.Node{node}, Hosts: []models.Host{host}}) + found := false + for _, resource := range registry.ListForPresentation() { + if resource.Agent == nil || resource.Agent.AgentID != host.ID { + continue + } + found = true + if (resource.Proxmox != nil) != linked { + t.Fatalf("provider projection linked=%v: %+v", linked, resource.Proxmox) + } + if !linked && resource.Identity.ClusterName != "" { + t.Fatalf("unlinked NAS inherited cluster %q", resource.Identity.ClusterName) + } + raw, err := json.Marshal(resource) + if err != nil { + t.Fatal(err) + } + var public map[string]any + if err := json.Unmarshal(raw, &public); err != nil { + t.Fatal(err) + } + agent := public["agent"].(map[string]any) + if _, exposed := agent["linkedNodeId"]; exposed { + t.Fatal("internal link hint unexpectedly exposed") + } + if linked { + provider := public["proxmox"].(map[string]any) + if provider["nodeName"] != node.Name || provider["clusterName"] != node.ClusterName { + t.Fatalf("missing public association discriminator: %v", provider) + } + } + } + if !found { + t.Fatal("NAS agent missing from presentation") + } + }) + } +} diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 34367a0ba..5dc74c8d6 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -2944,7 +2944,9 @@ type AptPackage struct { } // GetNodePendingUpdates returns the list of pending apt updates for a node -// Requires Sys.Audit permission on /nodes/{node} +// Access is evaluated by Proxmox for the configured credential on /nodes/{node}. +// Do not infer access from general node-audit permission; endpoint requirements +// can differ from other monitoring reads. func (c *Client) GetNodePendingUpdates(ctx context.Context, node string) ([]AptPackage, error) { resp, err := c.get(ctx, fmt.Sprintf("/nodes/%s/apt/update", node)) if err != nil { diff --git a/pkg/proxmox/pending_updates_auth_routing_test.go b/pkg/proxmox/pending_updates_auth_routing_test.go new file mode 100644 index 000000000..0854e21eb --- /dev/null +++ b/pkg/proxmox/pending_updates_auth_routing_test.go @@ -0,0 +1,64 @@ +package proxmox + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" +) + +// A cluster entry endpoint may proxy a different target node. Neither the +// selected endpoint nor its successful inventory read grants apt access. +func TestClusterClient_PendingUpdatesConfiguredCredentialAndTarget(t *testing.T) { + for _, status := range []int{http.StatusOK, http.StatusForbidden} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + var reads atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected method %s", r.Method) + } + if r.Header.Get("Authorization") != "PVEAPIToken=monitor@pve!readonly=synthetic" { + t.Error("configured token was not preserved") + } + if r.Header.Get("Cookie") != "" { + t.Error("unexpected browser/session credential") + } + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api2/json/nodes": + fmt.Fprint(w, `{"data":[{"node":"entry","status":"online"},{"node":"target","status":"online"}]}`) + case "/api2/json/nodes/target/apt/update": + reads.Add(1) + w.WriteHeader(status) + if status == http.StatusOK { + fmt.Fprint(w, `{"data":[{"Package":"example","Version":"2"}]}`) + } else { + fmt.Fprint(w, `{"message":"Permission check failed"}`) + } + default: + t.Errorf("unexpected request path %s", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + cfg := ClientConfig{Host: server.URL, TokenName: "monitor@pve!readonly", TokenValue: "synthetic"} + cc := NewClusterClient("routing-test", cfg, []string{server.URL}, nil) + updates, err := cc.GetNodePendingUpdates(context.Background(), "target") + if status == http.StatusOK { + if err != nil || len(updates) != 1 || updates[0].Package != "example" { + t.Fatalf("unexpected successful result: %v, %v", updates, err) + } + } else if err == nil || extractStatusCode(err.Error()) != http.StatusForbidden || len(updates) != 0 { + t.Fatalf("denial not preserved: %v, %v", updates, err) + } + if reads.Load() != 1 { + t.Fatalf("apt reads = %d; expected one without auth retry", reads.Load()) + } + if !cc.GetHealthStatus()[server.URL] { + t.Error("apt permission failure must not poison cluster endpoint health") + } + }) + } +} diff --git a/scripts/check-update-access-copy.mjs b/scripts/check-update-access-copy.mjs new file mode 100644 index 000000000..47c0b2a00 --- /dev/null +++ b/scripts/check-update-access-copy.mjs @@ -0,0 +1,81 @@ +// Isolated real-browser component qualification; no installed backend or delivery claim. +import { build, preview } from "../frontend-modern/node_modules/vite/dist/node/index.js"; +import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs"; +import { chromium } from "@playwright/test"; +import { resolve } from "node:path"; +import { mkdirSync } from "node:fs"; +import assert from "node:assert/strict"; +const root = resolve("frontend-modern"); +process.chdir(root); +const fixture = ` +import { render } from 'solid-js/web'; +import { createSignal } from 'solid-js'; +import { NodeDrawerOverview } from '/src/components/Workloads/NodeDrawerOverview'; +import '/src/index.css'; +const base = {id:'lab-pve1',name:'pve1',instance:'lab',status:'online',type:'node',cpu:0,memory:{total:1024,used:256,free:768,usage:25},disk:{total:1024,used:256,free:768,usage:25},uptime:3600,loadAverage:[],kernelVersion:'6.8.12',pveVersion:'9.0.1',cpuInfo:{model:'CPU',cores:4,sockets:1,mhz:'2400'},lastSeen:new Date().toISOString(),connectionHealth:'healthy'}; +const states = { + not_checked:{pendingUpdatesStatus:'not_checked',pendingUpdatesReason:'permission_denied'}, + unavailable:{pendingUpdatesStatus:'unavailable',pendingUpdatesReason:'permission_denied'}, + stale:{pendingUpdates:1,pendingUpdatesStatus:'stale',pendingUpdatesReason:'permission_denied',pendingUpdatesCheckedAt:new Date().toISOString()}, + zero:{pendingUpdates:0,pendingUpdatesStatus:'checked',pendingUpdatesCheckedAt:new Date().toISOString()}, + positive:{pendingUpdates:1,pendingUpdatesStatus:'checked',pendingUpdatesCheckedAt:new Date().toISOString()} +}; +const [state,setState] = createSignal('unavailable'); +render(() =>
, document.getElementById('root')); +`; +const config = { + root, + configFile: false, + optimizeDeps: { + noDiscovery: true, + entries: [], + esbuildOptions: { target: "esnext" }, + }, + esbuild: { target: "esnext" }, + plugins: [ + solid(), + { + name: "update-access-fixture", + resolveId(id) { + if (id === "/update-access-fixture.tsx" || id === resolve(root, "qualification.html")) return id; + }, + load(id) { + if (id === "/update-access-fixture.tsx") return fixture; + if (id === resolve(root, "qualification.html")) return '
'; + }, + }, + ], + resolve: { alias: { "@": resolve(root, "src") } }, + build: { target: "esnext", outDir: "/tmp/pulse-update-access-build", emptyOutDir: true, rollupOptions: { input: resolve(root, "qualification.html") } }, +}; +await build(config); +const server = await preview({root, configFile:false, build:{outDir:"/tmp/pulse-update-access-build"}, preview:{host:"127.0.0.1",port:5198,strictPort:true}}); + +let browser; +try { + + browser = await chromium.launch({ headless: true }); + mkdirSync("/tmp/pulse-update-access-copy", { recursive: true }); + for (const width of [1440, 900, 390]) { + const page = await browser.newPage({ viewport: { width, height: 900 } }); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto("http://127.0.0.1:5198/qualification.html"); + for (const state of ['unavailable', 'stale', 'zero', 'positive', 'not_checked', 'unavailable']) { + await page.getByRole('button', {name:state,exact:true}).click(); + const expected = state === 'not_checked' ? 'Not checked · Update check access denied' : state === 'unavailable' ? 'Unavailable · Update check access denied' : state === 'stale' ? /1 pending · stale/ : state === 'zero' ? /No pending updates · checked/ : /1 pending · checked/; + await page.getByText(expected,{exact:typeof expected === 'string'}).waitFor(); + if (state === 'stale') assert.match(await page.getByText(/1 pending · stale/).getAttribute('title'), /Update check access denied/); + assert.equal(await page.getByText('Sys.Audit permission required',{exact:false}).count(),0); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + assert.deepEqual(errors, []); + await page.screenshot({path: `/tmp/pulse-update-access-copy/${width}-${state}.png`, fullPage:true}); + } + await page.close(); + } + console.log(JSON.stringify({result:'passed',viewports:[1440,900,390],scope:'Real NodeDrawerOverview, Chromium, scripted props; no installed delivery claim'})); + +} finally { + await browser?.close(); + await new Promise(resolve => server.httpServer.close(resolve)); +} diff --git a/scripts/check-webhook-test-save-parity.mjs b/scripts/check-webhook-test-save-parity.mjs new file mode 100644 index 000000000..9a746512e --- /dev/null +++ b/scripts/check-webhook-test-save-parity.mjs @@ -0,0 +1,85 @@ +// Isolated real-browser component qualification; no installed backend or delivery claim. +import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js"; +import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs"; +import { chromium } from "@playwright/test"; +import { resolve } from "node:path"; +import { mkdirSync } from "node:fs"; +import assert from "node:assert/strict"; +const root = resolve("frontend-modern"); +process.chdir(root); +const fixture = ` +import { render } from 'solid-js/web'; +import { useWebhookConfigState } from '/src/components/Alerts/useWebhookConfigState'; +import { WebhookConfigForm } from '/src/components/Alerts/WebhookConfigForm'; +import '/src/index.css'; +render(() => { + const state = useWebhookConfigState({webhooks:[], onAdd: data => window.saved = data, onUpdate:()=>{}, onDelete:()=>{}, onTest: (_id,data) => window.tested = data}); + state.openAddForm(); + state.setFormData(data => ({...data,name:'Synthetic destination',url:'https://example.invalid',service:'pushover'})); + for (const [index,key] of ['app_token','user_token'].entries()) { + state.addCustomFieldInput(); + state.updateCustomFieldInput(index,{key,value:'synthetic-'+index}); + } + return
; +}, document.getElementById('root')); +`; + +const server = await createServer({ + root, + configFile: false, + optimizeDeps: { + noDiscovery: true, + entries: [], + esbuildOptions: { target: "esnext" }, + }, + esbuild: { target: "esnext" }, + plugins: [ + solid(), + { + name: "ordering-fixture", + configureServer(s) { + s.middlewares.use((req, res, next) => { + if (req.url === "/qualification") { + res.setHeader("Content-Type", "text/html"); + res.end( + '
', + ); + } else next(); + }); + }, + resolveId(id) { + if (id === "/ordering-fixture.tsx") return id; + }, + load(id) { + if (id === "/ordering-fixture.tsx") return fixture; + }, + }, + ], + resolve: { alias: { "@": resolve(root, "src") } }, + server: { host: "127.0.0.1", port: 5197, strictPort: true }, +}); +let browser; +try { + await server.listen(); + browser = await chromium.launch({ headless: true }); + mkdirSync("/tmp/pulse-webhook-parity", { recursive: true }); + for (const width of [1440, 900, 390]) { + const page = await browser.newPage({ viewport: { width, height: 900 } }); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto("http://127.0.0.1:5197/qualification"); + await page.getByRole('button', {name:'Test', exact:true}).click(); + assert.deepEqual(await page.evaluate(() => window.tested.customFields), {token:'synthetic-0',user:'synthetic-1'}); + await page.screenshot({path: `/tmp/pulse-webhook-parity/${width}.png`, fullPage:true}); + await page.getByRole('button', {name:'Add Webhook', exact:true}).click(); + assert.deepEqual(await page.evaluate(() => window.saved.customFields), {token:'synthetic-0',user:'synthetic-1'}); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + assert.deepEqual(errors, []); + await page.close(); + } + console.log(JSON.stringify({result:'passed',viewports:[1440,900,390],scope:'Real webhook form, Chromium, scripted props; no installed delivery claim'})); + +} finally { + await browser?.close(); + await server.close(); +}