diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d5d5ba29..77c352026 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,17 +180,26 @@ jobs: - name: Build Storybook run: npm run build-storybook working-directory: frontend - # Known-accepted as of 2026-07-26: 2 high advisories (react-router via - # react-router-dom, GHSA-qwww-vcr4-c8h2; brace-expansion/minimatch via - # eslint-plugin-jsx-a11y, GHSA-mh99-v99m-4gvg). Neither is exploitable - # here -- the react-router CVE only affects the unstable RSC APIs - # (this app uses plain createBrowserRouter, no RSC anywhere), and the - # jsx-a11y chain only runs against this repo's own hardcoded lint globs, - # never untrusted input. Both "fixes" npm offers are downgrades (no - # patched release exists yet in the currently-depended-upon package - # line), so left as-is rather than trading a real regression for a - # vulnerability that doesn't apply. Re-check on a future bump in case - # upstream ships an actual forward fix. + # Known-accepted as of 2026-07-26: 1 high advisory, + # brace-expansion/minimatch via eslint-plugin-jsx-a11y's own pinned + # minimatch@^3.1.2 (GHSA-mh99-v99m-4gvg). Not exploitable here -- this + # chain only runs against this repo's own hardcoded lint globs, never + # untrusted input. The eslint-core minimatch@10.x instance of the same + # advisory is already fixed via the "minimatch@^10" override below + # (brace-expansion 5.0.8); jsx-a11y's own minimatch is pinned to an old + # major with no patched release in that line, and forcing it to 10.x + # via override breaks npm's own dependency-tree validation (a genuine + # incompatibility, not just an npm quirk) -- so this one can only be + # fixed by an eslint-plugin-jsx-a11y release bumping its own minimatch. + # `npm audit fix --force`'s suggestion is a downgrade to before jsx-a11y + # adopted the vulnerable chain at all; left as-is rather than trading a + # real regression for a vulnerability that doesn't apply. + # The other advisory this comment used to cover (react-router RSC CSRF + # bypass, GHSA-qwww-vcr4-c8h2) is actually fixed now: migrated off the + # abandoned react-router-dom (frozen at 7.18.1, pinning the vulnerable + # react-router 7.18.1) onto react-router 8.3.0 directly, which has the + # real forward fix -- not a downgrade. Re-check on a future bump in + # case upstream ships a real fix for the remaining advisory too. - name: Audit run: npm audit --audit-level=high working-directory: frontend diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1302a516f..799d3de92 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,7 @@ "react-dom": "^19.2.8", "react-hook-form": "^7.82.0", "react-i18next": "^17.0.10", - "react-router-dom": "^7.18.1", + "react-router": "^8.3.0", "swagger-ui-react": "^5.32.11", "uplot": "^1.6.32", "zod": "^4.4.3" @@ -5777,15 +5777,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -6187,6 +6187,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6196,6 +6197,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -10176,20 +10183,19 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "cookie-es": "^3.1.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.22.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=19.2.7", + "react-dom": ">=19.2.7" }, "peerDependenciesMeta": { "react-dom": { @@ -10197,28 +10203,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", - "license": "MIT", - "dependencies": { - "react-router": "7.18.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 604a2327b..7adb11194 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,7 @@ "react-dom": "^19.2.8", "react-hook-form": "^7.82.0", "react-i18next": "^17.0.10", - "react-router-dom": "^7.18.1", + "react-router": "^8.3.0", "swagger-ui-react": "^5.32.11", "uplot": "^1.6.32", "zod": "^4.4.3" @@ -61,6 +61,7 @@ "@types/react-dom": "^19.2.3", "@types/swagger-ui-react": "^5.18.0", "@vitejs/plugin-react": "^6.0.4", + "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", "eslint-plugin-jsx-a11y": "^6.10.2", @@ -70,18 +71,20 @@ "jsdom": "^29.1.1", "lint-staged": "^17.1.1", "msw": "^2.15.0", + "playwright": "^1.61.1", "storybook": "^10.5.3", "typescript": "^6.0.3", "typescript-eslint": "^8.65.0", "vite": "8.1.5", - "vitest": "^4.1.10", - "@vitest/browser-playwright": "4.1.10", - "playwright": "^1.61.1" + "vitest": "^4.1.10" }, "overrides": { "eslint-plugin-jsx-a11y": { "eslint": "$eslint" }, + "minimatch@^10": { + "brace-expansion": "^5.0.8" + }, "dompurify": "^3.4.11", "react-copy-to-clipboard": "^5.1.1", "react-inspector": "^9.0.0", diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js index dc58f3d9e..0c970efc9 100644 --- a/frontend/public/mockServiceWorker.js +++ b/frontend/public/mockServiceWorker.js @@ -7,8 +7,8 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.14.7' -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const PACKAGE_VERSION = '2.15.0' +const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() @@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) { if (client && activeClientIds.has(client.id)) { const serializedRequest = await serializeRequest(requestCloneForEvents) + // Omit the body of server-sent event stream responses. + // Cloning such responses would prevent client-side stream cancelations + // from reaching the original stream (a teed stream only cancels its + // source once both of its branches cancel) and would buffer the + // entire stream into the unconsumed clone indefinitely. + const isEventStreamResponse = response.headers + .get('content-type') + ?.toLowerCase() + .startsWith('text/event-stream') + // Clone the response so both the client and the library could consume it. - const responseClone = response.clone() + const responseClone = isEventStreamResponse ? null : response.clone() sendToClient( client, @@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) { ...serializedRequest, }, response: { - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - headers: Object.fromEntries(responseClone.headers.entries()), - body: responseClone.body, + type: response.type, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: responseClone ? responseClone.body : null, }, }, }, - responseClone.body ? [serializedRequest.body, responseClone.body] : [], + responseClone && responseClone.body + ? [serializedRequest.body, responseClone.body] + : [], ) } diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts index db0022293..c6f181635 100644 --- a/frontend/src/hooks/usePageTitle.ts +++ b/frontend/src/hooks/usePageTitle.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { useTranslation } from 'react-i18next'; const TITLE_KEYS: Record = { diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 3f5993351..ff7e6e124 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ComponentType } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { Drawer, Layout, Menu } from 'antd'; import type { MenuProps } from 'antd'; diff --git a/frontend/src/layouts/PanelLayout.tsx b/frontend/src/layouts/PanelLayout.tsx index bbecf636c..9bf4f652e 100644 --- a/frontend/src/layouts/PanelLayout.tsx +++ b/frontend/src/layouts/PanelLayout.tsx @@ -1,4 +1,4 @@ -import { Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router'; import { useWebSocketBridge } from '@/api/websocketBridge'; import { usePageTitle } from '@/hooks/usePageTitle'; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9c3f15d0a..f848b50b7 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,5 @@ import { createRoot } from 'react-dom/client'; -import { RouterProvider } from 'react-router-dom'; +import { RouterProvider } from 'react-router'; import { message } from 'antd'; import 'antd/dist/reset.css'; import '@/styles/utils.css'; diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index b105299e2..909597cc2 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { Alert, Button, diff --git a/frontend/src/pages/settings/SubscriptionGeneralTab.tsx b/frontend/src/pages/settings/SubscriptionGeneralTab.tsx index 7d80bdf75..e94c8a877 100644 --- a/frontend/src/pages/settings/SubscriptionGeneralTab.tsx +++ b/frontend/src/pages/settings/SubscriptionGeneralTab.tsx @@ -1,7 +1,7 @@ import { Alert, Button, Input, InputNumber, Switch, Tabs } from 'antd'; import { BranchesOutlined, CompassOutlined, IdcardOutlined, InfoCircleOutlined, NodeIndexOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import type { AllSetting } from '@/models/setting'; import { SettingListItem } from '@/components/ui'; import { RemarkTemplateField } from '@/components/form'; diff --git a/frontend/src/pages/xray/XrayPage.tsx b/frontend/src/pages/xray/XrayPage.tsx index d630ccf0d..36d605228 100644 --- a/frontend/src/pages/xray/XrayPage.tsx +++ b/frontend/src/pages/xray/XrayPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { Alert, Button, diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 2b670b173..f8c4185d1 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'react'; -import { createBrowserRouter, type RouteObject } from 'react-router-dom'; +import { createBrowserRouter, type RouteObject } from 'react-router'; import PanelLayout from '@/layouts/PanelLayout'; diff --git a/frontend/src/test/subscription-general-tab.test.tsx b/frontend/src/test/subscription-general-tab.test.tsx index a2cc87484..424046e19 100644 --- a/frontend/src/test/subscription-general-tab.test.tsx +++ b/frontend/src/test/subscription-general-tab.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, screen } from '@testing-library/react'; -import { MemoryRouter, useLocation } from 'react-router-dom'; +import { MemoryRouter, useLocation } from 'react-router'; import { describe, expect, it, vi } from 'vitest'; import { AllSetting } from '@/models/setting';