From d242055f9877a47d31b08133e3e8588cbff65f40 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:42:28 +0100 Subject: [PATCH 1/9] test(api): close config-transfer fixture router lifecycles Config-transfer authorization fixtures start router-owned workers but did not register shutdown. Own cleanup at each constructor boundary, including the shared helper used by action authorization, so workers stop before temporary state is removed. Add a subtest-boundary regression that verifies cancellation and worker joining without changing production behavior or qualification thresholds. Change-source: pulse-maintainer --- .../api/config_transfer_authorization_test.go | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) 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") + } +} From 63f9bdcbbe1a5f9b186bc1c3480581ff7bb7ad02 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:53:18 +0100 Subject: [PATCH 2/9] test(api): own password-change fixture router lifecycle Register shared router cleanup at the password-change constructor boundary so workers are cancelled and joined before temporary state is removed. Add a subtest-boundary regression that fails without cleanup and retain the three affected authorisation tests. No production behaviour or qualification threshold changes. Change-source: pulse-maintainer --- .../api/change_password_session_admin_test.go | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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") + } +} From 6edaa56f4f7f502fc3734b9705a1f06093ebc653 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:56:29 +0100 Subject: [PATCH 3/9] fix(web): avoid prescribing permissions from update access failures The update evidence reason represents generic permission errors and HTTP 403, not a verified missing Sys.Audit privilege. Report access denial without prescribing a role change, as illustrated by the new #1802 retest. Preserve unavailable and stale evidence semantics. Both focused presentation and drawer suites pass (6 tests). Change-source: pulse-maintainer --- frontend-modern/browser-verification.json | 22 ++--- ...NodeDrawerOverview.updateEvidence.test.tsx | 2 +- .../__tests__/proxmoxUpdateEvidence.test.ts | 2 +- .../src/utils/proxmoxUpdateEvidence.ts | 2 +- scripts/check-update-access-copy.mjs | 81 +++++++++++++++++++ 5 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 scripts/check-update-access-copy.mjs diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 6f93fc4c5..7d0d17560 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": "d242055f9877a47d31b08133e3e8588cbff65f40", + "verified_at": "2026-09-08T20:59:34.058403Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/utils/alertDestinationsPresentation.ts" + "frontend-modern/src/utils/proxmoxUpdateEvidence.ts" ], "content_sha256": { - "frontend-modern/src/utils/alertDestinationsPresentation.ts": "67f6f9a1c98345949e162bf06a38760e61457aa1e8a2130913646647a4f29e88" + "frontend-modern/src/utils/proxmoxUpdateEvidence.ts": "b99d8b5411e0e894f294f106d970978c8573915ff7ffe0ed60c451128e22d34a" }, "routes": [ - "/qualification (real health and delivery-log cards, scripted props)" + "/qualification.html (production-built real NodeDrawerOverview with synthetic reactive props)" ], "viewports": [ { @@ -27,11 +27,15 @@ } ], "states": [ - "Retained terminal server_error health diagnosis and failed delivery log entry" + "Unavailable permission denial", + "Retained stale count with permission-denial title", + "Checked zero", + "Checked positive count", + "Not-checked permission-denial reason" ], "interactions": [ - "Load both cards at desktop, tablet and phone widths; assert label, guidance, no page errors or horizontal overflow." + "At each width select unavailable, stale, zero, positive, not_checked, then unavailable again; assert text, stale title, no obsolete Sys.Audit prescription, no page errors or horizontal page 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": "Built isolated real component with Vite esnext target, served compiled assets to Chromium. Desktop stale and phone unavailable screenshots visually inspected. Phone uses existing ellipsis for long values; full reason retained in title. No installed backend or reporter credential reproduction. Initial fixture build attempts failed on default target and virtual HTML path; corrected fixture build and all checks passed. Screenshots /tmp/pulse-update-access-copy; log /tmp/web-205503/browser-built.log.", + "command": "pulse-heavy-run -- node scripts/check-update-access-copy.mjs" } 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/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/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/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)); +} From 81877a6cc41f8c69ec7bf1004ca268e8208dc0c6 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:07:39 +0100 Subject: [PATCH 4/9] test(resources): distinguish NAS links from shared bridge identity Retain a synthetic projection discriminator for #1930 without deleting operator associations. Cover standalone shared bridge identity and a reciprocal retained link, including public provider fields and private link hints. Change-source: pulse-maintainer --- .../nas_cluster_projection_test.go | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 internal/unifiedresources/nas_cluster_projection_test.go 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") + } + }) + } +} From 092856b31c09793b60a970baf45559a476139d61 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:32:23 +0100 Subject: [PATCH 5/9] test(proxmox): verify pending update credential routing An admin-browser update read does not establish access for the monitoring token. Cover the proxied target path, configured token, denial propagation and healthy endpoint retention. Remove the unsupported Sys.Audit diagnostic hint: endpoint access is determined by Proxmox, not general inventory access. Change-source: pulse-maintainer Contract-Neutral: Diagnostic wording and method-comment correction plus synthetic regression coverage only; no collection logic, public API, permission policy, freshness or counter semantics change. --- .../monitor_polling_node_helpers.go | 2 +- pkg/proxmox/client.go | 4 +- .../pending_updates_auth_routing_test.go | 64 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 pkg/proxmox/pending_updates_auth_routing_test.go 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/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") + } + }) + } +} From 6f3547bc5fbb10ddf313a8cd2f184b6359173a59 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:02:25 +0100 Subject: [PATCH 6/9] test(web): trace agent-only NAS through Proxmox table projection The new #1930 payload has agent and Docker sources without cluster metadata. Retain a sanitised API-to-table boundary test alongside a real PVE-shaped neighbour to distinguish that evidence from the earlier screenshot; this is diagnostic coverage, not a reproduced reporter fix. Change-source: pulse-maintainer --- .../__tests__/useUnifiedResources.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) 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, From 7de119541685b621dac6839ec3b06f61309aa20c Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:27:00 +0100 Subject: [PATCH 7/9] fix(web): align webhook test custom fields with saved configuration Manually entered Pushover aliases were normalised on save but not on test, so the test could exercise a different payload. Apply the same normalisation and retain a failing-before parity regression; 56 focused webhook tests pass. Change-source: pulse-maintainer --- .../v6/internal/subsystems/alerts.md | 11 +++ frontend-modern/browser-verification.json | 24 +++--- .../components/Alerts/WebhookConfig.test.tsx | 28 ++++++ .../Alerts/useWebhookConfigState.ts | 2 +- scripts/check-webhook-test-save-parity.mjs | 85 +++++++++++++++++++ 5 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 scripts/check-webhook-test-save-parity.mjs 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/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 7d0d17560..da66077c5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,16 +1,16 @@ { "version": 1, - "base_sha": "d242055f9877a47d31b08133e3e8588cbff65f40", - "verified_at": "2026-09-08T20:59:34.058403Z", + "base_sha": "6f3547bc5fbb10ddf313a8cd2f184b6359173a59", + "verified_at": "2026-09-08T22:29:31.643744Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/utils/proxmoxUpdateEvidence.ts" + "frontend-modern/src/components/Alerts/useWebhookConfigState.ts" ], "content_sha256": { - "frontend-modern/src/utils/proxmoxUpdateEvidence.ts": "b99d8b5411e0e894f294f106d970978c8573915ff7ffe0ed60c451128e22d34a" + "frontend-modern/src/components/Alerts/useWebhookConfigState.ts": "14f0dceb069fe52b9a47100c50998ae53a2c5c8a29a524e342f7458546cb4a19" }, "routes": [ - "/qualification.html (production-built real NodeDrawerOverview with synthetic reactive props)" + "/qualification (real WebhookConfigForm and hook; synthetic callbacks)" ], "viewports": [ { @@ -27,15 +27,13 @@ } ], "states": [ - "Unavailable permission denial", - "Retained stale count with permission-denial title", - "Checked zero", - "Checked positive count", - "Not-checked permission-denial reason" + "Unsaved manually entered Pushover aliases", + "Test callback payload", + "Save callback payload" ], "interactions": [ - "At each width select unavailable, stale, zero, positive, not_checked, then unavailable again; assert text, stale title, no obsolete Sys.Audit prescription, no page errors or horizontal page overflow." + "Click Test then Add Webhook at each width; assert both canonical token/user payloads, no page errors or horizontal overflow." ], - "notes": "Built isolated real component with Vite esnext target, served compiled assets to Chromium. Desktop stale and phone unavailable screenshots visually inspected. Phone uses existing ellipsis for long values; full reason retained in title. No installed backend or reporter credential reproduction. Initial fixture build attempts failed on default target and virtual HTML path; corrected fixture build and all checks passed. Screenshots /tmp/pulse-update-access-copy; log /tmp/web-205503/browser-built.log.", - "command": "pulse-heavy-run -- node scripts/check-update-access-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/src/components/Alerts/WebhookConfig.test.tsx b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx index ddd23c2f5..af6b72dc9 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'; @@ -1580,3 +1582,29 @@ 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/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(); +} From 15000f0a673545aa06a1ebf06467e0938d405d22 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:29:10 +0100 Subject: [PATCH 8/9] fix(security): upgrade frontend test tooling past new advisories Use patched Vitest/coverage 4.1.11 and js-yaml 4.3.2. Adapt constructor mocks, callable mock types and explicit call-history cleanup for Vitest 4 without weakening assertions or audit gates. Change-source: pulse-maintainer --- .../subsystems/deployment-installability.md | 12 + frontend-modern/package-lock.json | 804 ++++-------------- frontend-modern/package.json | 6 +- .../Alerts/EmailProviderSelect.test.tsx | 4 +- .../components/Alerts/ResourceTable.test.tsx | 16 +- .../components/Alerts/WebhookConfig.test.tsx | 8 +- .../useDiagnosticsPanelState.test.ts | 12 +- .../shared/__tests__/TagBadges.test.tsx | 2 + .../__tests__/dependencySecurity.test.ts | 30 + .../__tests__/websocket-unified.test.ts | 2 +- ...ourceMetadataEvents.branchcov0712c.test.ts | 2 +- 11 files changed, 224 insertions(+), 674 deletions(-) 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/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..3aae49d18 100644 --- a/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx +++ b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx @@ -102,10 +102,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(); 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/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/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__/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(); From b8257351733dcbb4479210cd0186f1e4fcb6f217 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:50:18 +0100 Subject: [PATCH 9/9] style(web): format webhook parity regression Preserve both accepted candidate changes while satisfying the repository's Prettier check on the newly added regression. Change-source: pulse-maintainer --- .../components/Alerts/WebhookConfig.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx index 0aee91bec..5498be075 100644 --- a/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx +++ b/frontend-modern/src/components/Alerts/WebhookConfig.test.tsx @@ -1590,17 +1590,29 @@ describe('webhook test/save parity', () => { const onTest = vi.fn(); const onAdd = vi.fn(); const state = useWebhookConfigState({ - webhooks: [], onTest, onAdd, onUpdate: vi.fn(), onDelete: vi.fn(), + webhooks: [], + onTest, + onAdd, + onUpdate: vi.fn(), + onDelete: vi.fn(), }); state.openAddForm(); - state.setFormData((data) => ({ ...data, name: 'Example', url: 'https://example.invalid', service: 'pushover' })); + 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(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();