From 9634fd99d1be11b08fa5524bacf30a95d58058af Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 4 Feb 2026 12:55:24 +0800 Subject: [PATCH 01/43] web: ensure npm install runs before build (#5154) --- .github/workflows/golangci-lint.yml | 4 +- .github/workflows/goreleaser.yml | 4 +- web/frpc/Makefile | 2 +- web/frpc/package-lock.json | 64 +++++++++++++++++++++-------- web/frps/Makefile | 2 +- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e8cc1fe9..6a96ce6a 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -23,10 +23,10 @@ jobs: with: node-version: '22' - name: Build web assets (frps) - run: make install build + run: make build working-directory: web/frps - name: Build web assets (frpc) - run: make install build + run: make build working-directory: web/frpc - name: golangci-lint uses: golangci/golangci-lint-action@v8 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 502d0032..c00a6d29 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -20,10 +20,10 @@ jobs: with: node-version: '22' - name: Build web assets (frps) - run: make install build + run: make build working-directory: web/frps - name: Build web assets (frpc) - run: make install build + run: make build working-directory: web/frpc - name: Make All run: | diff --git a/web/frpc/Makefile b/web/frpc/Makefile index 57a88918..6d2bdcf8 100644 --- a/web/frpc/Makefile +++ b/web/frpc/Makefile @@ -3,7 +3,7 @@ install: @npm install -build: +build: install @npm run build dev: diff --git a/web/frpc/package-lock.json b/web/frpc/package-lock.json index 6b0912eb..7ff79c09 100644 --- a/web/frpc/package-lock.json +++ b/web/frpc/package-lock.json @@ -1611,7 +1611,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2052,7 +2054,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2061,11 +2065,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2256,7 +2262,9 @@ } }, "node_modules/cross-spawn": { - "version": "6.0.5", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2271,7 +2279,9 @@ } }, "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -2959,7 +2969,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -3182,7 +3194,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3725,6 +3739,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -3850,7 +3866,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3949,11 +3967,15 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash-unified": { @@ -4002,11 +4024,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4141,7 +4165,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -5118,6 +5144,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5342,7 +5370,9 @@ } }, "node_modules/unplugin-auto-import/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5504,7 +5534,9 @@ } }, "node_modules/unplugin-vue-components/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/web/frps/Makefile b/web/frps/Makefile index 57a88918..6d2bdcf8 100644 --- a/web/frps/Makefile +++ b/web/frps/Makefile @@ -3,7 +3,7 @@ install: @npm install -build: +build: install @npm run build dev: From 519368b1fd22bd180baba3da6f4c3f46816f600b Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Feb 2026 11:22:34 +0800 Subject: [PATCH 02/43] server/api: fix DeleteProxies endpoint returning empty response instead of JSON (#5163) --- server/api/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/controller.go b/server/api/controller.go index 8c9827d8..861316c4 100644 --- a/server/api/controller.go +++ b/server/api/controller.go @@ -254,7 +254,7 @@ func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) { } cleared, total := mem.StatsCollector.ClearOfflineProxies() log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total) - return nil, nil + return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil } func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) { From d0347325fc1c41ad31026fba08b6c0277fae8e9b Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 13 Feb 2026 14:10:18 +0800 Subject: [PATCH 03/43] pkg/config: fix custom domain validation to prevent false matches with subdomain host (#5178) --- pkg/config/v1/validation/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/v1/validation/proxy.go b/pkg/config/v1/validation/proxy.go index d8e3d01e..744620f0 100644 --- a/pkg/config/v1/validation/proxy.go +++ b/pkg/config/v1/validation/proxy.go @@ -81,7 +81,7 @@ func validateDomainConfigForClient(c *v1.DomainConfig) error { func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error { for _, domain := range c.CustomDomains { if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) { - if strings.Contains(domain, s.SubDomainHost) { + if strings.HasSuffix(domain, "."+s.SubDomainHost) { return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost) } } From 01997deb98f159f798fc010e1f06d8c3c7a5089a Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 2 Mar 2026 01:09:59 +0800 Subject: [PATCH 04/43] add persistent proxy/visitor store with CRUD API and web UI (#5188) --- .circleci/config.yml | 2 +- .github/workflows/golangci-lint.yml | 6 +- .github/workflows/goreleaser.yml | 2 +- .gitignore | 14 +- .golangci.yml | 11 +- README.md | 27 +- README_zh.md | 19 +- Release.md | 7 +- client/admin_api.go | 23 +- client/api/controller.go | 329 +- client/api/controller_test.go | 390 ++ client/api/types.go | 27 + client/config_manager.go | 365 ++ client/config_manager_test.go | 134 + client/configmgmt/types.go | 42 + client/control.go | 10 +- client/proxy/proxy_manager.go | 2 +- client/proxy/proxy_wrapper.go | 7 +- client/proxy/xtcp.go | 3 +- client/service.go | 126 +- client/service_test.go | 140 + client/visitor/stcp.go | 4 +- client/visitor/sudp.go | 4 +- client/visitor/xtcp.go | 6 +- cmd/frpc/sub/proxy.go | 30 +- cmd/frpc/sub/root.go | 72 +- conf/frpc_full_example.toml | 3 + dockerfiles/Dockerfile-for-frpc | 2 +- dockerfiles/Dockerfile-for-frps | 2 +- go.mod | 2 +- pkg/config/load.go | 225 +- pkg/config/load_test.go | 164 + pkg/config/source/aggregator.go | 117 + pkg/config/source/aggregator_test.go | 217 + pkg/config/source/base_source.go | 65 + pkg/config/source/base_source_test.go | 48 + pkg/config/source/clone.go | 43 + pkg/config/source/config_source.go | 65 + pkg/config/source/config_source_test.go | 173 + pkg/config/source/source.go | 37 + pkg/config/source/store.go | 359 ++ pkg/config/source/store_test.go | 144 + pkg/config/v1/client.go | 3 + pkg/config/v1/clone_test.go | 109 + pkg/config/v1/common.go | 35 +- pkg/config/v1/proxy.go | 96 +- pkg/config/v1/proxy_plugin.go | 95 + pkg/config/v1/store.go | 26 + pkg/config/v1/visitor.go | 53 +- pkg/config/v1/visitor_plugin.go | 17 + pkg/naming/names.go | 33 + pkg/naming/names_test.go | 27 + pkg/nathole/controller.go | 2 +- pkg/policy/featuregate/feature_gate.go | 5 +- pkg/ssh/server.go | 2 +- pkg/util/util/util.go | 9 + pkg/util/util/util_test.go | 13 + pkg/virtual/client.go | 8 +- server/api/controller.go | 38 +- server/api/types.go | 3 +- server/registry/registry.go | 20 +- server/service.go | 2 +- test/e2e/v1/features/store.go | 230 + web/frpc/.eslintrc.cjs | 30 - web/frpc/components.d.ts | 10 +- web/frpc/eslint.config.js | 36 + web/frpc/package-lock.json | 4673 +++++++++++++------ web/frpc/package.json | 12 +- web/frpc/src/App.vue | 6 + web/frpc/src/api/frpc.ts | 63 +- web/frpc/src/components/KeyValueEditor.vue | 153 + web/frpc/src/components/ProxyCard.vue | 349 +- web/frpc/src/router/index.ts | 56 + web/frpc/src/types/proxy.ts | 632 +++ web/frpc/src/views/Overview.vue | 759 +++- web/frpc/src/views/ProxyEdit.vue | 1238 +++++ web/frpc/src/views/VisitorEdit.vue | 606 +++ web/frps/.eslintrc.cjs | 30 - web/frps/eslint.config.js | 36 + web/frps/package-lock.json | 4802 ++++++++++++-------- web/frps/package.json | 10 +- web/frps/src/components/ClientCard.vue | 3 + web/frps/src/components/Traffic.vue | 2 +- web/frps/src/types/client.ts | 1 + web/frps/src/types/proxy.ts | 1 - web/frps/src/utils/client.ts | 2 + web/frps/src/utils/proxy.ts | 2 - web/frps/src/views/ClientDetail.vue | 16 +- web/frps/src/views/ServerOverview.vue | 2 +- 89 files changed, 13960 insertions(+), 3864 deletions(-) create mode 100644 client/api/controller_test.go create mode 100644 client/config_manager.go create mode 100644 client/config_manager_test.go create mode 100644 client/configmgmt/types.go create mode 100644 client/service_test.go create mode 100644 pkg/config/source/aggregator.go create mode 100644 pkg/config/source/aggregator_test.go create mode 100644 pkg/config/source/base_source.go create mode 100644 pkg/config/source/base_source_test.go create mode 100644 pkg/config/source/clone.go create mode 100644 pkg/config/source/config_source.go create mode 100644 pkg/config/source/config_source_test.go create mode 100644 pkg/config/source/source.go create mode 100644 pkg/config/source/store.go create mode 100644 pkg/config/source/store_test.go create mode 100644 pkg/config/v1/clone_test.go create mode 100644 pkg/config/v1/store.go create mode 100644 pkg/naming/names.go create mode 100644 pkg/naming/names_test.go create mode 100644 test/e2e/v1/features/store.go delete mode 100644 web/frpc/.eslintrc.cjs create mode 100644 web/frpc/eslint.config.js create mode 100644 web/frpc/src/components/KeyValueEditor.vue create mode 100644 web/frpc/src/views/ProxyEdit.vue create mode 100644 web/frpc/src/views/VisitorEdit.vue delete mode 100644 web/frps/.eslintrc.cjs create mode 100644 web/frps/eslint.config.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 817e1297..21f4159d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: go-version-latest: docker: - - image: cimg/go:1.24-node + - image: cimg/go:1.25-node resource_class: large steps: - checkout diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6a96ce6a..4e42f0fe 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: '1.25' cache: false - uses: actions/setup-node@v4 with: @@ -29,7 +29,7 @@ jobs: run: make build working-directory: web/frpc - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v2.3 + version: v2.10 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index c00a6d29..ebc0dca9 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: '1.25' - uses: actions/setup-node@v4 with: node-version: '22' diff --git a/.gitignore b/.gitignore index c6480f59..fdb32dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,18 +7,6 @@ _obj _test -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - *.exe *.test *.prof @@ -42,3 +30,5 @@ client.key # AI CLAUDE.md +AGENTS.md +.sisyphus/ diff --git a/.golangci.yml b/.golangci.yml index f7c0e8bd..cbdb4510 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,13 +33,7 @@ linters: disabled-checks: - exitAfterDefer gosec: - excludes: - - G401 - - G402 - - G404 - - G501 - - G115 - - G204 + excludes: ["G115", "G117", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"] severity: low confidence: low govet: @@ -77,6 +71,9 @@ linters: - linters: - revive text: "avoid meaningless package names" + - linters: + - revive + text: "Go standard library package names" - linters: - unparam text: is always false diff --git a/README.md b/README.md index ac4591c2..a080b431 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+
+ +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +
+

@@ -40,15 +50,6 @@ frp is an open source project with its ongoing development made possible entirel An open source, self-hosted alternative to public clouds, built for data ownership and privacy

-
- -## Recall.ai - API for meeting recordings - -If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), - -an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. - -
## What is frp? @@ -800,6 +801,14 @@ Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to l **Note that global client parameters won't be modified except 'start'.** +`start` is a global allowlist evaluated after all sources are merged (config file/include/store). +If `start` is non-empty, any proxy or visitor not listed there will not be started, including +entries created via Store API. + +`start` is kept mainly for compatibility and is generally not recommended for new configurations. +Prefer per-proxy/per-visitor `enabled`, and keep `start` empty unless you explicitly want this +global allowlist behavior. + You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors. ### Get proxy status from client diff --git a/README_zh.md b/README_zh.md index 210bfb0e..1b4b6862 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,6 +15,16 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

Gold Sponsors

+
+ +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +
+

@@ -42,15 +52,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者 An open source, self-hosted alternative to public clouds, built for data ownership and privacy

-
- -## Recall.ai - API for meeting recordings - -If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), - -an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. - -
## 为什么使用 frp ? diff --git a/Release.md b/Release.md index 69032dcc..5aaf921f 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,7 @@ ## Features -* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments. -* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation. +* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard. -## Fixes +## Improvements -* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session. +* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic. diff --git a/client/admin_api.go b/client/admin_api.go index 09936352..1343931d 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -38,6 +38,20 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet) subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet) subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut) + + if svr.storeSource != nil { + subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete) + subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete) + } + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") subRouter.PathPrefix("/static/").Handler( netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), @@ -52,13 +66,10 @@ func healthz(w http.ResponseWriter, _ *http.Request) { } func newAPIController(svr *Service) *api.Controller { + manager := newServiceConfigManager(svr) return api.NewController(api.ControllerParams{ - GetProxyStatus: svr.getAllProxyStatus, - ServerAddr: svr.common.ServerAddr, - ConfigFilePath: svr.configFilePath, - UnsafeFeatures: svr.unsafeFeatures, - UpdateConfig: svr.UpdateAllConfigurer, - GracefulClose: svr.GracefulClose, + ServerAddr: svr.common.ServerAddr, + Manager: manager, }) } diff --git a/client/api/controller.go b/client/api/controller.go index 6874b724..2ba44216 100644 --- a/client/api/controller.go +++ b/client/api/controller.go @@ -16,67 +16,66 @@ package api import ( "cmp" + "encoding/json" + "errors" "fmt" "net" "net/http" - "os" "slices" "strconv" "time" + "github.com/fatedier/frp/client/configmgmt" "github.com/fatedier/frp/client/proxy" - "github.com/fatedier/frp/pkg/config" v1 "github.com/fatedier/frp/pkg/config/v1" - "github.com/fatedier/frp/pkg/config/v1/validation" - "github.com/fatedier/frp/pkg/policy/security" httppkg "github.com/fatedier/frp/pkg/util/http" - "github.com/fatedier/frp/pkg/util/log" ) // Controller handles HTTP API requests for frpc. type Controller struct { - // getProxyStatus returns the current proxy status. - // Returns nil if the control connection is not established. - getProxyStatus func() []*proxy.WorkingStatus - - // serverAddr is the frps server address for display. serverAddr string - - // configFilePath is the path to the configuration file. - configFilePath string - - // unsafeFeatures is used for validation. - unsafeFeatures *security.UnsafeFeatures - - // updateConfig updates proxy and visitor configurations. - updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error - - // gracefulClose gracefully stops the service. - gracefulClose func(d time.Duration) + manager configmgmt.ConfigManager } // ControllerParams contains parameters for creating an APIController. type ControllerParams struct { - GetProxyStatus func() []*proxy.WorkingStatus - ServerAddr string - ConfigFilePath string - UnsafeFeatures *security.UnsafeFeatures - UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error - GracefulClose func(d time.Duration) + ServerAddr string + Manager configmgmt.ConfigManager } -// NewController creates a new Controller. func NewController(params ControllerParams) *Controller { return &Controller{ - getProxyStatus: params.GetProxyStatus, - serverAddr: params.ServerAddr, - configFilePath: params.ConfigFilePath, - unsafeFeatures: params.UnsafeFeatures, - updateConfig: params.UpdateConfig, - gracefulClose: params.GracefulClose, + serverAddr: params.ServerAddr, + manager: params.Manager, } } +func (c *Controller) toHTTPError(err error) error { + if err == nil { + return nil + } + + code := http.StatusInternalServerError + switch { + case errors.Is(err, configmgmt.ErrInvalidArgument): + code = http.StatusBadRequest + case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled): + code = http.StatusNotFound + case errors.Is(err, configmgmt.ErrConflict): + code = http.StatusConflict + } + return httppkg.NewError(code, err.Error()) +} + +// TODO(fatedier): Remove this lock wrapper after migrating typed config +// decoding to encoding/json/v2 with per-call options. +// TypedProxyConfig/TypedVisitorConfig currently read global strictness state. +func unmarshalTypedConfig[T any](body []byte, out *T) error { + return v1.WithDisallowUnknownFields(false, func() error { + return json.Unmarshal(body, out) + }) +} + // Reload handles GET /api/reload func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { strictConfigMode := false @@ -85,36 +84,22 @@ func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { strictConfigMode, _ = strconv.ParseBool(strictStr) } - cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode) - if err != nil { - log.Warnf("reload frpc proxy config error: %s", err.Error()) - return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + if err := c.manager.ReloadFromFile(strictConfigMode); err != nil { + return nil, c.toHTTPError(err) } - - if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil { - log.Warnf("reload frpc proxy config error: %s", err.Error()) - return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) - } - - if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil { - log.Warnf("reload frpc proxy config error: %s", err.Error()) - return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) - } - - log.Infof("success reload conf") return nil, nil } // Stop handles POST /api/stop func (c *Controller) Stop(ctx *httppkg.Context) (any, error) { - go c.gracefulClose(100 * time.Millisecond) + go c.manager.GracefulClose(100 * time.Millisecond) return nil, nil } // Status handles GET /api/status func (c *Controller) Status(ctx *httppkg.Context) (any, error) { res := make(StatusResp) - ps := c.getProxyStatus() + ps := c.manager.GetProxyStatus() if ps == nil { return res, nil } @@ -136,16 +121,11 @@ func (c *Controller) Status(ctx *httppkg.Context) (any, error) { // GetConfig handles GET /api/config func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) { - if c.configFilePath == "" { - return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path") - } - - content, err := os.ReadFile(c.configFilePath) + content, err := c.manager.ReadConfigFile() if err != nil { - log.Warnf("load frpc config file error: %s", err.Error()) - return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + return nil, c.toHTTPError(err) } - return string(content), nil + return content, nil } // PutConfig handles PUT /api/config @@ -159,13 +139,12 @@ func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty") } - if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil { - return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err)) + if err := c.manager.WriteConfigFile(body); err != nil { + return nil, c.toHTTPError(err) } return nil, nil } -// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp { psr := ProxyStatusResp{ Name: status.Name, @@ -185,5 +164,227 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStat psr.RemoteAddr = c.serverAddr + psr.RemoteAddr } } + + if c.manager.IsStoreProxyEnabled(status.Name) { + psr.Source = SourceStore + } return psr } + +func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) { + proxies, err := c.manager.ListStoreProxies() + if err != nil { + return nil, c.toHTTPError(err) + } + + resp := ProxyListResp{Proxies: make([]ProxyConfig, 0, len(proxies))} + for _, p := range proxies { + cfg, err := configurerToMap(p) + if err != nil { + continue + } + resp.Proxies = append(resp.Proxies, ProxyConfig{ + Name: p.GetBaseConfig().Name, + Type: p.GetBaseConfig().Type, + Config: cfg, + }) + } + return resp, nil +} + +func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + p, err := c.manager.GetStoreProxy(name) + if err != nil { + return nil, c.toHTTPError(err) + } + + cfg, err := configurerToMap(p) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return ProxyConfig{ + Name: p.GetBaseConfig().Name, + Type: p.GetBaseConfig().Type, + Config: cfg, + }, nil +} + +func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var typed v1.TypedProxyConfig + if err := unmarshalTypedConfig(body, &typed); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if typed.ProxyConfigurer == nil { + return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required") + } + + if err := c.manager.CreateStoreProxy(typed.ProxyConfigurer); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var typed v1.TypedProxyConfig + if err := unmarshalTypedConfig(body, &typed); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if typed.ProxyConfigurer == nil { + return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required") + } + + if err := c.manager.UpdateStoreProxy(name, typed.ProxyConfigurer); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + if err := c.manager.DeleteStoreProxy(name); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) { + visitors, err := c.manager.ListStoreVisitors() + if err != nil { + return nil, c.toHTTPError(err) + } + + resp := VisitorListResp{Visitors: make([]VisitorConfig, 0, len(visitors))} + for _, v := range visitors { + cfg, err := configurerToMap(v) + if err != nil { + continue + } + resp.Visitors = append(resp.Visitors, VisitorConfig{ + Name: v.GetBaseConfig().Name, + Type: v.GetBaseConfig().Type, + Config: cfg, + }) + } + return resp, nil +} + +func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + v, err := c.manager.GetStoreVisitor(name) + if err != nil { + return nil, c.toHTTPError(err) + } + + cfg, err := configurerToMap(v) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return VisitorConfig{ + Name: v.GetBaseConfig().Name, + Type: v.GetBaseConfig().Type, + Config: cfg, + }, nil +} + +func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var typed v1.TypedVisitorConfig + if err := unmarshalTypedConfig(body, &typed); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if typed.VisitorConfigurer == nil { + return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required") + } + + if err := c.manager.CreateStoreVisitor(typed.VisitorConfigurer); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var typed v1.TypedVisitorConfig + if err := unmarshalTypedConfig(body, &typed); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if typed.VisitorConfigurer == nil { + return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required") + } + + if err := c.manager.UpdateStoreVisitor(name, typed.VisitorConfigurer); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + if err := c.manager.DeleteStoreVisitor(name); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func configurerToMap(v any) (map[string]any, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + return m, nil +} diff --git a/client/api/controller_test.go b/client/api/controller_test.go new file mode 100644 index 00000000..d237b6ba --- /dev/null +++ b/client/api/controller_test.go @@ -0,0 +1,390 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +type fakeConfigManager struct { + reloadFromFileFn func(strict bool) error + readConfigFileFn func() (string, error) + writeConfigFileFn func(content []byte) error + getProxyStatusFn func() []*proxy.WorkingStatus + isStoreProxyEnabledFn func(name string) bool + storeEnabledFn func() bool + + listStoreProxiesFn func() ([]v1.ProxyConfigurer, error) + getStoreProxyFn func(name string) (v1.ProxyConfigurer, error) + createStoreProxyFn func(cfg v1.ProxyConfigurer) error + updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) error + deleteStoreProxyFn func(name string) error + listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error) + getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error) + createStoreVisitFn func(cfg v1.VisitorConfigurer) error + updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) error + deleteStoreVisitFn func(name string) error + gracefulCloseFn func(d time.Duration) +} + +func (m *fakeConfigManager) ReloadFromFile(strict bool) error { + if m.reloadFromFileFn != nil { + return m.reloadFromFileFn(strict) + } + return nil +} + +func (m *fakeConfigManager) ReadConfigFile() (string, error) { + if m.readConfigFileFn != nil { + return m.readConfigFileFn() + } + return "", nil +} + +func (m *fakeConfigManager) WriteConfigFile(content []byte) error { + if m.writeConfigFileFn != nil { + return m.writeConfigFileFn(content) + } + return nil +} + +func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus { + if m.getProxyStatusFn != nil { + return m.getProxyStatusFn() + } + return nil +} + +func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool { + if m.isStoreProxyEnabledFn != nil { + return m.isStoreProxyEnabledFn(name) + } + return false +} + +func (m *fakeConfigManager) StoreEnabled() bool { + if m.storeEnabledFn != nil { + return m.storeEnabledFn() + } + return false +} + +func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { + if m.listStoreProxiesFn != nil { + return m.listStoreProxiesFn() + } + return nil, nil +} + +func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) { + if m.getStoreProxyFn != nil { + return m.getStoreProxyFn(name) + } + return nil, nil +} + +func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error { + if m.createStoreProxyFn != nil { + return m.createStoreProxyFn(cfg) + } + return nil +} + +func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error { + if m.updateStoreProxyFn != nil { + return m.updateStoreProxyFn(name, cfg) + } + return nil +} + +func (m *fakeConfigManager) DeleteStoreProxy(name string) error { + if m.deleteStoreProxyFn != nil { + return m.deleteStoreProxyFn(name) + } + return nil +} + +func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) { + if m.listStoreVisitorsFn != nil { + return m.listStoreVisitorsFn() + } + return nil, nil +} + +func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) { + if m.getStoreVisitorFn != nil { + return m.getStoreVisitorFn(name) + } + return nil, nil +} + +func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error { + if m.createStoreVisitFn != nil { + return m.createStoreVisitFn(cfg) + } + return nil +} + +func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error { + if m.updateStoreVisitFn != nil { + return m.updateStoreVisitFn(name, cfg) + } + return nil +} + +func (m *fakeConfigManager) DeleteStoreVisitor(name string) error { + if m.deleteStoreVisitFn != nil { + return m.deleteStoreVisitFn(name) + } + return nil +} + +func (m *fakeConfigManager) GracefulClose(d time.Duration) { + if m.gracefulCloseFn != nil { + m.gracefulCloseFn(d) + } +} + +func setDisallowUnknownFieldsForTest(t *testing.T, value bool) func() { + t.Helper() + v1.DisallowUnknownFieldsMu.Lock() + prev := v1.DisallowUnknownFields + v1.DisallowUnknownFields = value + v1.DisallowUnknownFieldsMu.Unlock() + return func() { + v1.DisallowUnknownFieldsMu.Lock() + v1.DisallowUnknownFields = prev + v1.DisallowUnknownFieldsMu.Unlock() + } +} + +func getDisallowUnknownFieldsForTest() bool { + v1.DisallowUnknownFieldsMu.Lock() + defer v1.DisallowUnknownFieldsMu.Unlock() + return v1.DisallowUnknownFields +} + +func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig { + return &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 10080, + }, + }, + } +} + +func newRawXTCPVisitorConfig(name string) *v1.XTCPVisitorConfig { + return &v1.XTCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: name, + Type: "xtcp", + ServerName: "server", + BindPort: 10081, + SecretKey: "secret", + }, + } +} + +func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) { + status := &proxy.WorkingStatus{ + Name: "shared-proxy", + Type: "tcp", + Phase: proxy.ProxyPhaseRunning, + RemoteAddr: ":8080", + Cfg: newRawTCPProxyConfig("shared-proxy"), + } + + controller := &Controller{ + serverAddr: "127.0.0.1", + manager: &fakeConfigManager{ + isStoreProxyEnabledFn: func(name string) bool { + return name == "shared-proxy" + }, + }, + } + + resp := controller.buildProxyStatusResp(status) + if resp.Source != "store" { + t.Fatalf("unexpected source: %q", resp.Source) + } + if resp.RemoteAddr != "127.0.0.1:8080" { + t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr) + } +} + +func TestReloadErrorMapping(t *testing.T) { + tests := []struct { + name string + err error + expectedCode int + }{ + {name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest}, + {name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }}, + } + ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil)) + _, err := controller.Reload(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, tc.expectedCode) + }) + } +} + +func TestStoreProxyErrorMapping(t *testing.T) { + tests := []struct { + name string + err error + expectedCode int + }{ + {name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound}, + {name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict}, + {name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + body, err := json.Marshal(newRawTCPProxyConfig("shared-proxy")) + if err != nil { + t.Fatalf("marshal body: %v", err) + } + + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + controller := &Controller{ + manager: &fakeConfigManager{ + updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) error { return tc.err }, + }, + } + + _, err = controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, tc.expectedCode) + }) + } +} + +func TestStoreVisitorErrorMapping(t *testing.T) { + body, err := json.Marshal(newRawXTCPVisitorConfig("shared-visitor")) + if err != nil { + t.Fatalf("marshal body: %v", err) + } + + req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + controller := &Controller{ + manager: &fakeConfigManager{ + deleteStoreVisitFn: func(string) error { + return fmtError(configmgmt.ErrStoreDisabled, "disabled") + }, + }, + } + + _, err = controller.DeleteStoreVisitor(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} + +func TestCreateStoreProxy_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { + restore := setDisallowUnknownFieldsForTest(t, true) + t.Cleanup(restore) + + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreProxyFn: func(cfg v1.ProxyConfigurer) error { + gotName = cfg.GetBaseConfig().Name + return nil + }, + }, + } + + body := []byte(`{"name":"raw-proxy","type":"tcp","localPort":10080,"unexpected":"value"}`) + req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.CreateStoreProxy(ctx) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + if gotName != "raw-proxy" { + t.Fatalf("unexpected proxy name: %q", gotName) + } + if !getDisallowUnknownFieldsForTest() { + t.Fatal("global strictness flag was not restored") + } +} + +func TestCreateStoreVisitor_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { + restore := setDisallowUnknownFieldsForTest(t, true) + t.Cleanup(restore) + + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreVisitFn: func(cfg v1.VisitorConfigurer) error { + gotName = cfg.GetBaseConfig().Name + return nil + }, + }, + } + + body := []byte(`{"name":"raw-visitor","type":"xtcp","serverName":"server","bindPort":10081,"secretKey":"secret","unexpected":"value"}`) + req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.CreateStoreVisitor(ctx) + if err != nil { + t.Fatalf("create store visitor: %v", err) + } + if gotName != "raw-visitor" { + t.Fatalf("unexpected visitor name: %q", gotName) + } + if !getDisallowUnknownFieldsForTest() { + t.Fatal("global strictness flag was not restored") + } +} + +func fmtError(sentinel error, msg string) error { + return fmt.Errorf("%w: %s", sentinel, msg) +} + +func assertHTTPCode(t *testing.T, err error, expected int) { + t.Helper() + var httpErr *httppkg.Error + if !errors.As(err, &httpErr) { + t.Fatalf("unexpected error type: %T", err) + } + if httpErr.Code != expected { + t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected) + } +} diff --git a/client/api/types.go b/client/api/types.go index d7c930d0..8f7bece2 100644 --- a/client/api/types.go +++ b/client/api/types.go @@ -14,6 +14,8 @@ package api +const SourceStore = "store" + // StatusResp is the response for GET /api/status type StatusResp map[string][]ProxyStatusResp @@ -26,4 +28,29 @@ type ProxyStatusResp struct { LocalAddr string `json:"local_addr"` Plugin string `json:"plugin"` RemoteAddr string `json:"remote_addr"` + Source string `json:"source,omitempty"` // "store" or "config" +} + +// ProxyConfig wraps proxy configuration for API requests/responses. +type ProxyConfig struct { + Name string `json:"name"` + Type string `json:"type"` + Config map[string]any `json:"config"` +} + +// VisitorConfig wraps visitor configuration for API requests/responses. +type VisitorConfig struct { + Name string `json:"name"` + Type string `json:"type"` + Config map[string]any `json:"config"` +} + +// ProxyListResp is the response for GET /api/store/proxies +type ProxyListResp struct { + Proxies []ProxyConfig `json:"proxies"` +} + +// VisitorListResp is the response for GET /api/store/visitors +type VisitorListResp struct { + Visitors []VisitorConfig `json:"visitors"` } diff --git a/client/config_manager.go b/client/config_manager.go new file mode 100644 index 00000000..0ed6b3c2 --- /dev/null +++ b/client/config_manager.go @@ -0,0 +1,365 @@ +package client + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/source" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/util/log" +) + +type serviceConfigManager struct { + svr *Service +} + +func newServiceConfigManager(svr *Service) configmgmt.ConfigManager { + return &serviceConfigManager{svr: svr} +} + +func (m *serviceConfigManager) ReloadFromFile(strict bool) error { + if m.svr.configFilePath == "" { + return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument) + } + + result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict) + if err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + + proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers( + result.Common, + result.Proxies, + result.Visitors, + ) + proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation) + visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation) + + if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err) + } + + log.Infof("success reload conf") + return nil +} + +func (m *serviceConfigManager) ReadConfigFile() (string, error) { + if m.svr.configFilePath == "" { + return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument) + } + + content, err := os.ReadFile(m.svr.configFilePath) + if err != nil { + return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + return string(content), nil +} + +func (m *serviceConfigManager) WriteConfigFile(content []byte) error { + if len(content) == 0 { + return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument) + } + + if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil { + return err + } + return nil +} + +func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus { + return m.svr.getAllProxyStatus() +} + +func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool { + if name == "" { + return false + } + + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource == nil { + return false + } + + cfg := storeSource.GetProxy(name) + if cfg == nil { + return false + } + enabled := cfg.GetBaseConfig().Enabled + return enabled == nil || *enabled +} + +func (m *serviceConfigManager) StoreEnabled() bool { + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + return storeSource != nil +} + +func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + return storeSource.GetAllProxies() +} + +func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + + cfg := storeSource.GetProxy(name) + if cfg == nil { + return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name) + } + return cfg, nil +} + +func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error { + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.AddProxy(cfg); err != nil { + if errors.Is(err, source.ErrAlreadyExists) { + return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: created proxy %q", cfg.GetBaseConfig().Name) + return nil +} + +func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error { + if name == "" { + return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.UpdateProxy(cfg); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: updated proxy %q", name) + return nil +} + +func (m *serviceConfigManager) DeleteStoreProxy(name string) error { + if name == "" { + return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.RemoveProxy(name); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: deleted proxy %q", name) + return nil +} + +func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) { + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + return storeSource.GetAllVisitors() +} + +func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + + cfg := storeSource.GetVisitor(name) + if cfg == nil { + return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name) + } + return cfg, nil +} + +func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error { + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.AddVisitor(cfg); err != nil { + if errors.Is(err, source.ErrAlreadyExists) { + return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: created visitor %q", cfg.GetBaseConfig().Name) + return nil +} + +func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error { + if name == "" { + return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.UpdateVisitor(cfg); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: updated visitor %q", name) + return nil +} + +func (m *serviceConfigManager) DeleteStoreVisitor(name string) error { + if name == "" { + return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.RemoveVisitor(name); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: deleted visitor %q", name) + return nil +} + +func (m *serviceConfigManager) GracefulClose(d time.Duration) { + m.svr.GracefulClose(d) +} + +func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) { + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + return storeSource, nil +} + +func (m *serviceConfigManager) withStoreMutationAndReload( + fn func(storeSource *source.StoreSource) error, +) error { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return err + } + + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + return nil +} + +func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error { + if cfg == nil { + return fmt.Errorf("invalid proxy config") + } + runtimeCfg := cfg.Clone() + if runtimeCfg == nil { + return fmt.Errorf("invalid proxy config") + } + runtimeCfg.Complete() + return validation.ValidateProxyConfigurerForClient(runtimeCfg) +} + +func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error { + if cfg == nil { + return fmt.Errorf("invalid visitor config") + } + runtimeCfg := cfg.Clone() + if runtimeCfg == nil { + return fmt.Errorf("invalid visitor config") + } + runtimeCfg.Complete() + return validation.ValidateVisitorConfigurer(runtimeCfg) +} diff --git a/client/config_manager_test.go b/client/config_manager_test.go new file mode 100644 index 00000000..13152497 --- /dev/null +++ b/client/config_manager_test.go @@ -0,0 +1,134 @@ +package client + +import ( + "errors" + "path/filepath" + "testing" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/pkg/config/source" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig { + return &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 10080, + }, + }, + } +} + +func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil { + t.Fatalf("seed proxy: %v", err) + } + + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + + mgr := &serviceConfigManager{ + svr: &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected conflict error") + } + if !errors.Is(err, configmgmt.ErrConflict) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + + mgr := &serviceConfigManager{ + svr: &Service{ + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected apply config error") + } + if !errors.Is(err, configmgmt.ErrApplyConfig) { + t.Fatalf("unexpected error: %v", err) + } + if storeSource.GetProxy("p1") == nil { + t.Fatal("proxy should remain in store after reload failure") + } +} + +func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) { + mgr := &serviceConfigManager{ + svr: &Service{ + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected store disabled error") + } + if !errors.Is(err, configmgmt.ErrStoreDisabled) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + + mgr := &serviceConfigManager{ + svr: &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy")) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + + got := storeSource.GetProxy("raw-proxy") + if got == nil { + t.Fatal("proxy not found in store") + } + if got.GetBaseConfig().LocalIP != "" { + t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP) + } + if got.GetBaseConfig().Transport.BandwidthLimitMode != "" { + t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode) + } +} diff --git a/client/configmgmt/types.go b/client/configmgmt/types.go new file mode 100644 index 00000000..5da75fb8 --- /dev/null +++ b/client/configmgmt/types.go @@ -0,0 +1,42 @@ +package configmgmt + +import ( + "errors" + "time" + + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +var ( + ErrInvalidArgument = errors.New("invalid argument") + ErrNotFound = errors.New("not found") + ErrConflict = errors.New("conflict") + ErrStoreDisabled = errors.New("store disabled") + ErrApplyConfig = errors.New("apply config failed") +) + +type ConfigManager interface { + ReloadFromFile(strict bool) error + + ReadConfigFile() (string, error) + WriteConfigFile(content []byte) error + + GetProxyStatus() []*proxy.WorkingStatus + IsStoreProxyEnabled(name string) bool + StoreEnabled() bool + + ListStoreProxies() ([]v1.ProxyConfigurer, error) + GetStoreProxy(name string) (v1.ProxyConfigurer, error) + CreateStoreProxy(cfg v1.ProxyConfigurer) error + UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error + DeleteStoreProxy(name string) error + + ListStoreVisitors() ([]v1.VisitorConfigurer, error) + GetStoreVisitor(name string) (v1.VisitorConfigurer, error) + CreateStoreVisitor(cfg v1.VisitorConfigurer) error + UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error + DeleteStoreVisitor(name string) error + + GracefulClose(d time.Duration) +} diff --git a/client/control.go b/client/control.go index 0f48c36d..020ac94f 100644 --- a/client/control.go +++ b/client/control.go @@ -25,6 +25,7 @@ import ( "github.com/fatedier/frp/pkg/auth" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/wait" @@ -156,6 +157,8 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) { return } + startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName) + // dispatch this work connection to related proxy ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) } @@ -165,11 +168,12 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) { inMsg := m.(*msg.NewProxyResp) // Server will return NewProxyResp message to each NewProxy message. // Start a new proxy handler if no error got - err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) + proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName) + err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error) if err != nil { - xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err) + xl.Warnf("[%s] start error: %v", proxyName, err) } else { - xl.Infof("[%s] start proxy success", inMsg.ProxyName) + xl.Infof("[%s] start proxy success", proxyName) } } diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 4615e9a2..42f9f589 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -118,9 +118,9 @@ func (pm *Manager) HandleEvent(payload any) error { } func (pm *Manager) GetAllProxyStatus() []*WorkingStatus { - ps := make([]*WorkingStatus, 0) pm.mu.RLock() defer pm.mu.RUnlock() + ps := make([]*WorkingStatus, 0, len(pm.proxies)) for _, pxy := range pm.proxies { ps = append(ps, pxy.GetStatus()) } diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 4698320a..718c02e6 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -29,6 +29,7 @@ import ( "github.com/fatedier/frp/client/health" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/vnet" @@ -86,6 +87,8 @@ type Wrapper struct { xl *xlog.Logger ctx context.Context + + wireName string } func NewWrapper( @@ -113,6 +116,7 @@ func NewWrapper( vnetController: vnetController, xl: xl, ctx: xlog.NewContext(ctx, xl), + wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name), } if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 { @@ -182,7 +186,7 @@ func (pw *Wrapper) Stop() { func (pw *Wrapper) close() { _ = pw.handler(&event.CloseProxyPayload{ CloseProxyMsg: &msg.CloseProxy{ - ProxyName: pw.Name, + ProxyName: pw.wireName, }, }) } @@ -208,6 +212,7 @@ func (pw *Wrapper) checkWorker() { var newProxyMsg msg.NewProxy pw.Cfg.MarshalToMsg(&newProxyMsg) + newProxyMsg.ProxyName = pw.wireName pw.lastSendStartMsg = now _ = pw.handler(&event.StartProxyPayload{ NewProxyMsg: &newProxyMsg, diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 6e1deac3..808e0ebe 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -27,6 +27,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" @@ -85,7 +86,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC transactionID := nathole.NewTransactionID() natHoleClientMsg := &msg.NatHoleClient{ TransactionID: transactionID, - ProxyName: pxy.cfg.Name, + ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name), Sid: natHoleSidMsg.Sid, MappedAddrs: prepareResult.Addrs, AssistedAddrs: prepareResult.AssistedAddrs, diff --git a/client/service.go b/client/service.go index 8d639698..26f1db18 100644 --- a/client/service.go +++ b/client/service.go @@ -29,6 +29,8 @@ import ( "github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/pkg/auth" + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/policy/security" @@ -61,9 +63,11 @@ func (e cancelErr) Error() string { // ServiceOptions contains options for creating a new client service. type ServiceOptions struct { - Common *v1.ClientCommonConfig - ProxyCfgs []v1.ProxyConfigurer - VisitorCfgs []v1.VisitorConfigurer + Common *v1.ClientCommonConfig + + // ConfigSourceAggregator manages internal config and optional store sources. + // It is required for creating a Service. + ConfigSourceAggregator *source.Aggregator UnsafeFeatures *security.UnsafeFeatures @@ -119,11 +123,23 @@ type Service struct { vnetController *vnet.Controller - cfgMu sync.RWMutex - common *v1.ClientCommonConfig - proxyCfgs []v1.ProxyConfigurer - visitorCfgs []v1.VisitorConfigurer - clientSpec *msg.ClientSpec + cfgMu sync.RWMutex + // reloadMu serializes reload transactions to keep reloadCommon and applied + // config in sync across concurrent API operations. + reloadMu sync.Mutex + common *v1.ClientCommonConfig + // reloadCommon is used for filtering/defaulting during config-source reloads. + // It can be updated by /api/reload without mutating startup-only common behavior. + reloadCommon *v1.ClientCommonConfig + proxyCfgs []v1.ProxyConfigurer + visitorCfgs []v1.VisitorConfigurer + clientSpec *msg.ClientSpec + + // aggregator manages multiple configuration sources. + // When set, the service watches for config changes and reloads automatically. + aggregator *source.Aggregator + configSource *source.ConfigSource + storeSource *source.StoreSource unsafeFeatures *security.UnsafeFeatures @@ -160,19 +176,39 @@ func NewService(options ServiceOptions) (*Service, error) { return nil, err } + if options.ConfigSourceAggregator == nil { + return nil, fmt.Errorf("config source aggregator is required") + } + + configSource := options.ConfigSourceAggregator.ConfigSource() + storeSource := options.ConfigSourceAggregator.StoreSource() + + proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load() + if loadErr != nil { + return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr) + } + proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs) + proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs) + s := &Service{ ctx: context.Background(), auth: authRuntime, webServer: webServer, common: options.Common, + reloadCommon: options.Common, configFilePath: options.ConfigFilePath, unsafeFeatures: options.UnsafeFeatures, - proxyCfgs: options.ProxyCfgs, - visitorCfgs: options.VisitorCfgs, + proxyCfgs: proxyCfgs, + visitorCfgs: visitorCfgs, clientSpec: options.ClientSpec, + aggregator: options.ConfigSourceAggregator, + configSource: configSource, + storeSource: storeSource, connectorCreator: options.ConnectorCreator, handleWorkConnCb: options.HandleWorkConnCb, } + if webServer != nil { webServer.RouteRegister(s.registerRouteHandlers) } @@ -403,6 +439,35 @@ func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorC return nil } +func (svr *Service) UpdateConfigSource( + common *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, +) error { + svr.reloadMu.Lock() + defer svr.reloadMu.Unlock() + + cfgSource := svr.configSource + if cfgSource == nil { + return fmt.Errorf("config source is not available") + } + + if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil { + return err + } + + // Non-atomic update semantics: source has been updated at this point. + // Even if reload fails below, keep this common config for subsequent reloads. + svr.cfgMu.Lock() + svr.reloadCommon = common + svr.cfgMu.Unlock() + + if err := svr.reloadConfigFromSourcesLocked(); err != nil { + return err + } + return nil +} + func (svr *Service) Close() { svr.GracefulClose(time.Duration(0)) } @@ -413,6 +478,15 @@ func (svr *Service) GracefulClose(d time.Duration) { } func (svr *Service) stop() { + // Coordinate shutdown with reload/update paths that read source pointers. + svr.reloadMu.Lock() + if svr.aggregator != nil { + svr.aggregator = nil + } + svr.configSource = nil + svr.storeSource = nil + svr.reloadMu.Unlock() + svr.ctlMu.Lock() defer svr.ctlMu.Unlock() if svr.ctl != nil { @@ -453,3 +527,35 @@ type statusExporterImpl struct { func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) { return s.getProxyStatusFunc(name) } + +func (svr *Service) reloadConfigFromSources() error { + svr.reloadMu.Lock() + defer svr.reloadMu.Unlock() + return svr.reloadConfigFromSourcesLocked() +} + +func (svr *Service) reloadConfigFromSourcesLocked() error { + aggregator := svr.aggregator + if aggregator == nil { + return errors.New("config aggregator is not initialized") + } + + svr.cfgMu.RLock() + reloadCommon := svr.reloadCommon + svr.cfgMu.RUnlock() + + proxies, visitors, err := aggregator.Load() + if err != nil { + return fmt.Errorf("reload config from sources failed: %w", err) + } + + proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors) + proxies = config.CompleteProxyConfigurers(proxies) + visitors = config.CompleteVisitorConfigurers(visitors) + + // Atomically replace the entire configuration + if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil { + return err + } + return nil +} diff --git a/client/service_test.go b/client/service_test.go new file mode 100644 index 00000000..e1c6b587 --- /dev/null +++ b/client/service_test.go @@ -0,0 +1,140 @@ +package client + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/fatedier/frp/pkg/config/source" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) { + prevCommon := &v1.ClientCommonConfig{User: "old-user"} + newCommon := &v1.ClientCommonConfig{User: "new-user"} + + svr := &Service{ + configSource: source.NewConfigSource(), + reloadCommon: prevCommon, + } + + invalidProxy := &v1.TCPProxyConfig{} + err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "proxy name cannot be empty") { + t.Fatalf("unexpected error: %v", err) + } + + if svr.reloadCommon != prevCommon { + t.Fatalf("reloadCommon should roll back on ReplaceAll failure") + } +} + +func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) { + prevCommon := &v1.ClientCommonConfig{User: "old-user"} + newCommon := &v1.ClientCommonConfig{User: "new-user"} + + svr := &Service{ + // Keep configSource valid so ReplaceAll succeeds first. + configSource: source.NewConfigSource(), + reloadCommon: prevCommon, + // Keep aggregator nil to force reload failure. + aggregator: nil, + } + + validProxy := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "p1", + Type: "tcp", + }, + } + err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "config aggregator is not initialized") { + t.Fatalf("unexpected error: %v", err) + } + + if svr.reloadCommon != newCommon { + t.Fatalf("reloadCommon should keep new value on reload failure") + } +} + +func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + + proxyCfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "store-proxy", + Type: "tcp", + }, + } + visitorCfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "store-visitor", + Type: "stcp", + }, + } + if err := storeSource.AddProxy(proxyCfg); err != nil { + t.Fatalf("add proxy to store: %v", err) + } + if err := storeSource.AddVisitor(visitorCfg); err != nil { + t.Fatalf("add visitor to store: %v", err) + } + + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + svr := &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + } + + if err := svr.reloadConfigFromSources(); err != nil { + t.Fatalf("reload config from sources: %v", err) + } + + gotProxy := storeSource.GetProxy("store-proxy") + if gotProxy == nil { + t.Fatalf("proxy not found in store") + } + if gotProxy.GetBaseConfig().LocalIP != "" { + t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP) + } + + gotVisitor := storeSource.GetVisitor("store-visitor") + if gotVisitor == nil { + t.Fatalf("visitor not found in store") + } + if gotVisitor.GetBaseConfig().BindAddr != "" { + t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr) + } + + svr.cfgMu.RLock() + defer svr.cfgMu.RUnlock() + + if len(svr.proxyCfgs) != 1 { + t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs)) + } + if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" { + t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP) + } + + if len(svr.visitorCfgs) != 1 { + t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs)) + } + if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" { + t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr) + } +} diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index 31f6f174..870e48d9 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -25,6 +25,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -103,9 +104,10 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { defer visitorConn.Close() now := time.Now().Unix() + targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) newVisitorConnMsg := &msg.NewVisitorConn{ RunID: sv.helper.RunID(), - ProxyName: sv.cfg.ServerName, + ProxyName: targetProxyName, SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, UseEncryption: sv.cfg.Transport.UseEncryption, diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 284aee10..8c7d793c 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -27,6 +27,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/proto/udp" netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" @@ -205,9 +206,10 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { } now := time.Now().Unix() + targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) newVisitorConnMsg := &msg.NewVisitorConn{ RunID: sv.helper.RunID(), - ProxyName: sv.cfg.ServerName, + ProxyName: targetProxyName, SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, UseEncryption: sv.cfg.Transport.UseEncryption, diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index cdfeb1ab..dcfba505 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -31,6 +31,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" @@ -280,8 +281,9 @@ func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) { // 4. Create a tunnel session using an underlying UDP connection. func (sv *XTCPVisitor) makeNatHole() { xl := xlog.FromContextSafe(sv.ctx) + targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) xl.Tracef("makeNatHole start") - if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil { + if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil { xl.Warnf("nathole precheck error: %v", err) return } @@ -310,7 +312,7 @@ func (sv *XTCPVisitor) makeNatHole() { transactionID := nathole.NewTransactionID() natHoleVisitorMsg := &msg.NatHoleVisitor{ TransactionID: transactionID, - ProxyName: sv.cfg.ServerName, + ProxyName: targetProxyName, Protocol: sv.cfg.Protocol, SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index ef7fe67f..8651f0b3 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/policy/security" @@ -86,13 +87,14 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm os.Exit(1) } - c.Complete(clientCfg.User) c.GetBaseConfig().Type = name - if err := validation.ValidateProxyConfigurerForClient(c); err != nil { + c.Complete() + proxyCfg := c + if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil { fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "") + err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "") if err != nil { fmt.Println(err) os.Exit(1) @@ -117,13 +119,14 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client os.Exit(1) } - c.Complete(clientCfg) c.GetBaseConfig().Type = name - if err := validation.ValidateVisitorConfigurer(c); err != nil { + c.Complete() + visitorCfg := c + if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil { fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "") + err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "") if err != nil { fmt.Println(err) os.Exit(1) @@ -131,3 +134,18 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client }, } } + +func startService( + cfg *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures *security.UnsafeFeatures, + cfgFile string, +) error { + configSource := source.NewConfigSource() + if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil { + return fmt.Errorf("failed to set config source: %w", err) + } + aggregator := source.NewAggregator(configSource) + return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile) +} diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 1c2d8d5e..5d83cabf 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -30,6 +30,7 @@ import ( "github.com/fatedier/frp/client" "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/policy/featuregate" @@ -120,22 +121,64 @@ func handleTermSignal(svr *client.Service) { } func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error { - cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) + // Load configuration + result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode) if err != nil { return err } - if isLegacyFormat { + if result.IsLegacyFormat { fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " + "please use yaml/json/toml format instead!\n") } - if len(cfg.FeatureGates) > 0 { - if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil { + if len(result.Common.FeatureGates) > 0 { + if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil { return err } } - warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures) + return runClientWithAggregator(result, unsafeFeatures, cfgFilePath) +} + +// runClientWithAggregator runs the client using the internal source aggregator. +func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error { + configSource := source.NewConfigSource() + if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil { + return fmt.Errorf("failed to set config source: %w", err) + } + + var storeSource *source.StoreSource + + if result.Common.Store.IsEnabled() { + storePath := result.Common.Store.Path + if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) { + storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath) + } + + s, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: storePath, + }) + if err != nil { + return fmt.Errorf("failed to create store source: %w", err) + } + storeSource = s + } + + aggregator := source.NewAggregator(configSource) + if storeSource != nil { + aggregator.SetStoreSource(storeSource) + } + + proxyCfgs, visitorCfgs, err := aggregator.Load() + if err != nil { + return fmt.Errorf("failed to load config from sources: %w", err) + } + + proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs) + proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs) + + warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } @@ -143,35 +186,32 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro return err } - return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath) + return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath) } -func startService( +func startServiceWithAggregator( cfg *v1.ClientCommonConfig, - proxyCfgs []v1.ProxyConfigurer, - visitorCfgs []v1.VisitorConfigurer, + aggregator *source.Aggregator, unsafeFeatures *security.UnsafeFeatures, cfgFile string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) if cfgFile != "" { - log.Infof("start frpc service for config file [%s]", cfgFile) + log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile) defer log.Infof("frpc service for config file [%s] stopped", cfgFile) } svr, err := client.NewService(client.ServiceOptions{ - Common: cfg, - ProxyCfgs: proxyCfgs, - VisitorCfgs: visitorCfgs, - UnsafeFeatures: unsafeFeatures, - ConfigFilePath: cfgFile, + Common: cfg, + ConfigSourceAggregator: aggregator, + UnsafeFeatures: unsafeFeatures, + ConfigFilePath: cfgFile, }) if err != nil { return err } shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic" - // Capture the exit signal if we use kcp or quic. if shouldGracefulClose { go handleTermSignal(svr) } diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 5a414137..50a00cfd 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -143,6 +143,9 @@ transport.tls.enable = true # Proxy names you want to start. # Default is empty, means all proxies. +# This list is a global allowlist after config + store are merged, so entries +# created via Store API are also filtered by this list. +# If start is non-empty, any proxy/visitor not listed here will not be started. # start = ["ssh", "dns"] # Alternative to 'start': You can control each proxy individually using the 'enabled' field. diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index df25542e..881b0cc2 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -5,7 +5,7 @@ COPY web/frpc/ ./ RUN npm install RUN npm run build -FROM golang:1.24 AS building +FROM golang:1.25 AS building COPY . /building COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 5a86b2b6..3df253e2 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -5,7 +5,7 @@ COPY web/frps/ ./ RUN npm install RUN npm run build -FROM golang:1.24 AS building +FROM golang:1.25 AS building COPY . /building COPY --from=web-builder /web/frps/dist /building/web/frps/dist diff --git a/go.mod b/go.mod index e23facde..bfede7a9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.24.0 +go 1.25.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 diff --git a/pkg/config/load.go b/pkg/config/load.go index 6e8c251d..a9f98552 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -141,34 +141,33 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error { // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. func LoadConfigure(b []byte, c any, strict bool) error { - v1.DisallowUnknownFieldsMu.Lock() - defer v1.DisallowUnknownFieldsMu.Unlock() - v1.DisallowUnknownFields = strict - - var tomlObj any - // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). - if err := toml.Unmarshal(b, &tomlObj); err == nil { - b, err = json.Marshal(&tomlObj) - if err != nil { - return err + return v1.WithDisallowUnknownFields(strict, func() error { + var tomlObj any + // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). + if err := toml.Unmarshal(b, &tomlObj); err == nil { + var err error + b, err = json.Marshal(&tomlObj) + if err != nil { + return err + } } - } - // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. - if yaml.IsJSONBuffer(b) { - decoder := json.NewDecoder(bytes.NewBuffer(b)) + // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. + if yaml.IsJSONBuffer(b) { + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if strict { + decoder.DisallowUnknownFields() + } + return decoder.Decode(c) + } + + // Handle YAML content if strict { - decoder.DisallowUnknownFields() + // In strict mode, always use our custom handler to support YAML merge + return parseYAMLWithDotFieldsHandling(b, c) } - return decoder.Decode(c) - } - - // Handle YAML content - if strict { - // In strict mode, always use our custom handler to support YAML merge - return parseYAMLWithDotFieldsHandling(b, c) - } - // Non-strict mode, parse normally - return yaml.Unmarshal(b, c) + // Non-strict mode, parse normally + return yaml.Unmarshal(b, c) + }) } func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { @@ -180,7 +179,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1. } configurer.UnmarshalFromMsg(m) - configurer.Complete("") + configurer.Complete() if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil { return nil, err @@ -219,60 +218,132 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) return svrCfg, isLegacyFormat, nil } +// ClientConfigLoadResult contains the result of loading a client configuration file. +type ClientConfigLoadResult struct { + // Common contains the common client configuration. + Common *v1.ClientCommonConfig + + // Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles. + // These are NOT completed (user prefix not added). + Proxies []v1.ProxyConfigurer + + // Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles. + // These are NOT completed. + Visitors []v1.VisitorConfigurer + + // IsLegacyFormat indicates whether the config file is in legacy INI format. + IsLegacyFormat bool +} + +// LoadClientConfigResult loads and parses a client configuration file. +// It returns the raw configuration without completing proxies/visitors. +// The caller should call Complete on the configs manually for legacy behavior. +func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) { + result := &ClientConfigLoadResult{ + Proxies: make([]v1.ProxyConfigurer, 0), + Visitors: make([]v1.VisitorConfigurer, 0), + } + + if DetectLegacyINIFormatFromFile(path) { + legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) + if err != nil { + return nil, err + } + result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) + for _, c := range legacyProxyCfgs { + result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c)) + } + for _, c := range legacyVisitorCfgs { + result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c)) + } + result.IsLegacyFormat = true + } else { + allCfg := v1.ClientConfig{} + if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil { + return nil, err + } + result.Common = &allCfg.ClientCommonConfig + for _, c := range allCfg.Proxies { + result.Proxies = append(result.Proxies, c.ProxyConfigurer) + } + for _, c := range allCfg.Visitors { + result.Visitors = append(result.Visitors, c.VisitorConfigurer) + } + } + + // Load additional config from includes. + // legacy ini format already handle this in ParseClientConfig. + if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat { + extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict) + if err != nil { + return nil, err + } + result.Proxies = append(result.Proxies, extProxyCfgs...) + result.Visitors = append(result.Visitors, extVisitorCfgs...) + } + + // Complete the common config + if result.Common != nil { + if err := result.Common.Complete(); err != nil { + return nil, err + } + } + + return result, nil +} + func LoadClientConfig(path string, strict bool) ( *v1.ClientCommonConfig, []v1.ProxyConfigurer, []v1.VisitorConfigurer, bool, error, ) { - var ( - cliCfg *v1.ClientCommonConfig - proxyCfgs = make([]v1.ProxyConfigurer, 0) - visitorCfgs = make([]v1.VisitorConfigurer, 0) - isLegacyFormat bool - ) - - if DetectLegacyINIFormatFromFile(path) { - legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) - if err != nil { - return nil, nil, nil, true, err - } - cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) - for _, c := range legacyProxyCfgs { - proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) - } - for _, c := range legacyVisitorCfgs { - visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c)) - } - isLegacyFormat = true - } else { - allCfg := v1.ClientConfig{} - if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil { - return nil, nil, nil, false, err - } - cliCfg = &allCfg.ClientCommonConfig - for _, c := range allCfg.Proxies { - proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) - } - for _, c := range allCfg.Visitors { - visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) - } + result, err := LoadClientConfigResult(path, strict) + if err != nil { + return nil, nil, nil, result != nil && result.IsLegacyFormat, err } - // Load additional config from includes. - // legacy ini format already handle this in ParseClientConfig. - if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat { - extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict) - if err != nil { - return nil, nil, nil, isLegacyFormat, err - } - proxyCfgs = append(proxyCfgs, extProxyCfgs...) - visitorCfgs = append(visitorCfgs, extVisitorCfgs...) + proxyCfgs := result.Proxies + visitorCfgs := result.Visitors + + proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs) + proxyCfgs = CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = CompleteVisitorConfigurers(visitorCfgs) + return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil +} + +func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer { + proxyCfgs := proxies + for _, c := range proxyCfgs { + c.Complete() + } + return proxyCfgs +} + +func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer { + visitorCfgs := visitors + for _, c := range visitorCfgs { + c.Complete() + } + return visitorCfgs +} + +func FilterClientConfigurers( + common *v1.ClientCommonConfig, + proxies []v1.ProxyConfigurer, + visitors []v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { + if common == nil { + common = &v1.ClientCommonConfig{} } - // Filter by start - if len(cliCfg.Start) > 0 { - startSet := sets.New(cliCfg.Start...) + proxyCfgs := proxies + visitorCfgs := visitors + + // Filter by start across merged configurers from all sources. + // For example, store entries are also filtered by this set. + if len(common.Start) > 0 { + startSet := sets.New(common.Start...) proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { return startSet.Has(c.GetBaseConfig().Name) }) @@ -291,19 +362,7 @@ func LoadClientConfig(path string, strict bool) ( enabled := c.GetBaseConfig().Enabled return enabled == nil || *enabled }) - - if cliCfg != nil { - if err := cliCfg.Complete(); err != nil { - return nil, nil, nil, isLegacyFormat, err - } - } - for _, c := range proxyCfgs { - c.Complete(cliCfg.User) - } - for _, c := range visitorCfgs { - c.Complete(cliCfg) - } - return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil + return proxyCfgs, visitorCfgs } func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 95d6101e..2675f636 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -15,6 +15,7 @@ package config import ( + "encoding/json" "fmt" "strings" "testing" @@ -273,6 +274,169 @@ proxies: require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type) } +func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) { + require := require.New(t) + + enabled := true + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy-raw" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + proxyCfg.Enabled = &enabled + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor-raw" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server-raw" + visitorCfg.FallbackTo = "fallback-raw" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + visitorCfg.Enabled = &enabled + + common := &v1.ClientCommonConfig{ + User: "alice", + } + + proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.Len(proxies, 1) + require.Len(visitors, 1) + + p := proxies[0].GetBaseConfig() + require.Equal("proxy-raw", p.Name) + require.Empty(p.LocalIP) + + v := visitors[0].GetBaseConfig() + require.Equal("visitor-raw", v.Name) + require.Equal("server-raw", v.ServerName) + require.Empty(v.BindAddr) + + xtcp := visitors[0].(*v1.XTCPVisitorConfig) + require.Equal("fallback-raw", xtcp.FallbackTo) + require.Empty(xtcp.Protocol) +} + +func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) { + require := require.New(t) + + enabled := true + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy-raw" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + proxyCfg.Enabled = &enabled + + proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg}) + require.Len(proxies, 1) + + p := proxies[0].GetBaseConfig() + require.Equal("proxy-raw", p.Name) + require.Equal("127.0.0.1", p.LocalIP) +} + +func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) { + require := require.New(t) + + enabled := true + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor-raw" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server-raw" + visitorCfg.FallbackTo = "fallback-raw" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + visitorCfg.Enabled = &enabled + + visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg}) + require.Len(visitors, 1) + + v := visitors[0].GetBaseConfig() + require.Equal("visitor-raw", v.Name) + require.Equal("server-raw", v.ServerName) + require.Equal("127.0.0.1", v.BindAddr) + + xtcp := visitors[0].(*v1.XTCPVisitorConfig) + require.Equal("fallback-raw", xtcp.FallbackTo) + require.Equal("quic", xtcp.Protocol) +} + +func TestCompleteProxyConfigurers_Idempotent(t *testing.T) { + require := require.New(t) + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg}) + firstProxyJSON, err := json.Marshal(proxies[0]) + require.NoError(err) + + proxies = CompleteProxyConfigurers(proxies) + secondProxyJSON, err := json.Marshal(proxies[0]) + require.NoError(err) + + require.Equal(string(firstProxyJSON), string(secondProxyJSON)) +} + +func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) { + require := require.New(t) + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg}) + firstVisitorJSON, err := json.Marshal(visitors[0]) + require.NoError(err) + + visitors = CompleteVisitorConfigurers(visitors) + secondVisitorJSON, err := json.Marshal(visitors[0]) + require.NoError(err) + + require.Equal(string(firstVisitorJSON), string(secondVisitorJSON)) +} + +func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) { + require := require.New(t) + + enabled := true + disabled := false + + proxyKeep := &v1.TCPProxyConfig{} + proxyKeep.Name = "keep" + proxyKeep.Type = "tcp" + proxyKeep.LocalPort = 10080 + proxyKeep.Enabled = &enabled + + proxyDropByStart := &v1.TCPProxyConfig{} + proxyDropByStart.Name = "drop-by-start" + proxyDropByStart.Type = "tcp" + proxyDropByStart.LocalPort = 10081 + proxyDropByStart.Enabled = &enabled + + proxyDropByEnabled := &v1.TCPProxyConfig{} + proxyDropByEnabled.Name = "drop-by-enabled" + proxyDropByEnabled.Type = "tcp" + proxyDropByEnabled.LocalPort = 10082 + proxyDropByEnabled.Enabled = &disabled + + common := &v1.ClientCommonConfig{ + Start: []string{"keep"}, + } + + proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{ + proxyKeep, + proxyDropByStart, + proxyDropByEnabled, + }, nil) + require.Len(visitors, 0) + require.Len(proxies, 1) + require.Equal("keep", proxies[0].GetBaseConfig().Name) +} + // TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types func TestYAMLEdgeCases(t *testing.T) { require := require.New(t) diff --git a/pkg/config/source/aggregator.go b/pkg/config/source/aggregator.go new file mode 100644 index 00000000..f3be67bd --- /dev/null +++ b/pkg/config/source/aggregator.go @@ -0,0 +1,117 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "errors" + "fmt" + "sort" + "sync" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type Aggregator struct { + mu sync.RWMutex + + configSource *ConfigSource + storeSource *StoreSource +} + +func NewAggregator(configSource *ConfigSource) *Aggregator { + if configSource == nil { + configSource = NewConfigSource() + } + return &Aggregator{ + configSource: configSource, + } +} + +func (a *Aggregator) SetStoreSource(storeSource *StoreSource) { + a.mu.Lock() + defer a.mu.Unlock() + + a.storeSource = storeSource +} + +func (a *Aggregator) ConfigSource() *ConfigSource { + return a.configSource +} + +func (a *Aggregator) StoreSource() *StoreSource { + return a.storeSource +} + +func (a *Aggregator) getSourcesLocked() []Source { + sources := make([]Source, 0, 2) + if a.configSource != nil { + sources = append(sources, a.configSource) + } + if a.storeSource != nil { + sources = append(sources, a.storeSource) + } + return sources +} + +func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + a.mu.RLock() + entries := a.getSourcesLocked() + a.mu.RUnlock() + + if len(entries) == 0 { + return nil, nil, errors.New("no sources configured") + } + + proxyMap := make(map[string]v1.ProxyConfigurer) + visitorMap := make(map[string]v1.VisitorConfigurer) + + for _, src := range entries { + proxies, visitors, err := src.Load() + if err != nil { + return nil, nil, fmt.Errorf("load source: %w", err) + } + for _, p := range proxies { + proxyMap[p.GetBaseConfig().Name] = p + } + for _, v := range visitors { + visitorMap[v.GetBaseConfig().Name] = v + } + } + proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap) + return proxies, visitors, nil +} + +func (a *Aggregator) mapsToSortedSlices( + proxyMap map[string]v1.ProxyConfigurer, + visitorMap map[string]v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { + proxies := make([]v1.ProxyConfigurer, 0, len(proxyMap)) + for _, p := range proxyMap { + proxies = append(proxies, p) + } + sort.Slice(proxies, func(i, j int) bool { + return proxies[i].GetBaseConfig().Name < proxies[j].GetBaseConfig().Name + }) + + visitors := make([]v1.VisitorConfigurer, 0, len(visitorMap)) + for _, v := range visitorMap { + visitors = append(visitors, v) + } + sort.Slice(visitors, func(i, j int) bool { + return visitors[i].GetBaseConfig().Name < visitors[j].GetBaseConfig().Name + }) + + return proxies, visitors +} diff --git a/pkg/config/source/aggregator_test.go b/pkg/config/source/aggregator_test.go new file mode 100644 index 00000000..5fc9636a --- /dev/null +++ b/pkg/config/source/aggregator_test.go @@ -0,0 +1,217 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// mockProxy creates a TCP proxy config for testing +func mockProxy(name string) v1.ProxyConfigurer { + cfg := &v1.TCPProxyConfig{} + cfg.Name = name + cfg.Type = "tcp" + cfg.LocalPort = 8080 + cfg.RemotePort = 9090 + return cfg +} + +// mockVisitor creates a STCP visitor config for testing +func mockVisitor(name string) v1.VisitorConfigurer { + cfg := &v1.STCPVisitorConfig{} + cfg.Name = name + cfg.Type = "stcp" + cfg.ServerName = "test-server" + return cfg +} + +func newTestStoreSource(t *testing.T) *StoreSource { + t.Helper() + + path := filepath.Join(t.TempDir(), "store.json") + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(t, err) + return storeSource +} + +func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator { + t.Helper() + + configSource := NewConfigSource() + agg := NewAggregator(configSource) + if storeSource != nil { + agg.SetStoreSource(storeSource) + } + return agg +} + +func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) { + require := require.New(t) + + agg := NewAggregator(nil) + require.NotNil(agg) + require.NotNil(agg.ConfigSource()) + require.Nil(agg.StoreSource()) +} + +func TestNewAggregator_WithoutStore(t *testing.T) { + require := require.New(t) + + configSource := NewConfigSource() + agg := NewAggregator(configSource) + require.NotNil(agg) + require.Same(configSource, agg.ConfigSource()) + require.Nil(agg.StoreSource()) +} + +func TestNewAggregator_WithStore(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + configSource := NewConfigSource() + agg := NewAggregator(configSource) + agg.SetStoreSource(storeSource) + + require.Same(configSource, agg.ConfigSource()) + require.Same(storeSource, agg.StoreSource()) +} + +func TestAggregator_SetStoreSource_Overwrite(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + first := newTestStoreSource(t) + second := newTestStoreSource(t) + + agg.SetStoreSource(first) + require.Same(first, agg.StoreSource()) + + agg.SetStoreSource(second) + require.Same(second, agg.StoreSource()) + + agg.SetStoreSource(nil) + require.Nil(agg.StoreSource()) +} + +func TestAggregator_MergeBySourceOrder(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + + configSource := agg.ConfigSource() + + configShared := mockProxy("shared").(*v1.TCPProxyConfig) + configShared.LocalPort = 1111 + configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig) + configOnly.LocalPort = 1112 + + err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil) + require.NoError(err) + + storeShared := mockProxy("shared").(*v1.TCPProxyConfig) + storeShared.LocalPort = 2222 + storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig) + storeOnly.LocalPort = 2223 + err = storeSource.AddProxy(storeShared) + require.NoError(err) + err = storeSource.AddProxy(storeOnly) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(visitors, 0) + require.Len(proxies, 3) + + var sharedProxy *v1.TCPProxyConfig + for _, p := range proxies { + if p.GetBaseConfig().Name == "shared" { + sharedProxy = p.(*v1.TCPProxyConfig) + break + } + } + require.NotNil(sharedProxy) + require.Equal(2222, sharedProxy.LocalPort) +} + +func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + configSource := agg.ConfigSource() + + lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig) + lowProxy.LocalPort = 1111 + err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil) + require.NoError(err) + + disabled := false + highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig) + highProxy.LocalPort = 2222 + highProxy.Enabled = &disabled + err = storeSource.AddProxy(highProxy) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 0) + + proxy := proxies[0].(*v1.TCPProxyConfig) + require.Equal("shared-proxy", proxy.Name) + require.Equal(1111, proxy.LocalPort) +} + +func TestAggregator_VisitorMerge(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + + err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")}) + require.NoError(err) + err = storeSource.AddVisitor(mockVisitor("visitor2")) + require.NoError(err) + + _, visitors, err := agg.Load() + require.NoError(err) + require.Len(visitors, 2) +} + +func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil) + require.NoError(err) + + proxies, _, err := agg.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Equal("ssh", proxies[0].GetBaseConfig().Name) + + proxies[0].GetBaseConfig().Name = "alice.ssh" + + proxies2, _, err := agg.Load() + require.NoError(err) + require.Len(proxies2, 1) + require.Equal("ssh", proxies2[0].GetBaseConfig().Name) +} diff --git a/pkg/config/source/base_source.go b/pkg/config/source/base_source.go new file mode 100644 index 00000000..ab1f59fe --- /dev/null +++ b/pkg/config/source/base_source.go @@ -0,0 +1,65 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "sync" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// baseSource provides shared state and behavior for Source implementations. +// It manages proxy/visitor storage. +// Concrete types (ConfigSource, StoreSource) embed this struct. +type baseSource struct { + mu sync.RWMutex + + proxies map[string]v1.ProxyConfigurer + visitors map[string]v1.VisitorConfigurer +} + +func newBaseSource() baseSource { + return baseSource{ + proxies: make(map[string]v1.ProxyConfigurer), + visitors: make(map[string]v1.VisitorConfigurer), + } +} + +// Load returns all enabled proxy and visitor configurations. +// Configurations with Enabled explicitly set to false are filtered out. +func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies)) + for _, p := range s.proxies { + // Filter out disabled proxies (nil or true means enabled) + if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled { + continue + } + proxies = append(proxies, p) + } + + visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors)) + for _, v := range s.visitors { + // Filter out disabled visitors (nil or true means enabled) + if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled { + continue + } + visitors = append(visitors, v) + } + + return cloneConfigurers(proxies, visitors) +} diff --git a/pkg/config/source/base_source_test.go b/pkg/config/source/base_source_test.go new file mode 100644 index 00000000..34ea9d9d --- /dev/null +++ b/pkg/config/source/base_source_test.go @@ -0,0 +1,48 @@ +package source + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + proxyCfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "proxy1", + Type: "tcp", + }, + } + visitorCfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "visitor1", + Type: "stcp", + }, + } + + err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.NoError(err) + + firstProxies, firstVisitors, err := src.Load() + require.NoError(err) + require.Len(firstProxies, 1) + require.Len(firstVisitors, 1) + + // Mutate loaded objects as runtime completion would do. + firstProxies[0].Complete() + firstVisitors[0].Complete() + + secondProxies, secondVisitors, err := src.Load() + require.NoError(err) + require.Len(secondProxies, 1) + require.Len(secondVisitors, 1) + + require.Empty(secondProxies[0].GetBaseConfig().LocalIP) + require.Empty(secondVisitors[0].GetBaseConfig().BindAddr) +} diff --git a/pkg/config/source/clone.go b/pkg/config/source/clone.go new file mode 100644 index 00000000..cacd5dbb --- /dev/null +++ b/pkg/config/source/clone.go @@ -0,0 +1,43 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "fmt" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func cloneConfigurers( + proxies []v1.ProxyConfigurer, + visitors []v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + clonedProxies := make([]v1.ProxyConfigurer, 0, len(proxies)) + clonedVisitors := make([]v1.VisitorConfigurer, 0, len(visitors)) + + for _, cfg := range proxies { + if cfg == nil { + return nil, nil, fmt.Errorf("proxy cannot be nil") + } + clonedProxies = append(clonedProxies, cfg.Clone()) + } + for _, cfg := range visitors { + if cfg == nil { + return nil, nil, fmt.Errorf("visitor cannot be nil") + } + clonedVisitors = append(clonedVisitors, cfg.Clone()) + } + return clonedProxies, clonedVisitors, nil +} diff --git a/pkg/config/source/config_source.go b/pkg/config/source/config_source.go new file mode 100644 index 00000000..ea8a2af6 --- /dev/null +++ b/pkg/config/source/config_source.go @@ -0,0 +1,65 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "fmt" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// ConfigSource implements Source for in-memory configuration. +// All operations are thread-safe. +type ConfigSource struct { + baseSource +} + +func NewConfigSource() *ConfigSource { + return &ConfigSource{ + baseSource: newBaseSource(), + } +} + +// ReplaceAll replaces all proxy and visitor configurations atomically. +func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error { + s.mu.Lock() + defer s.mu.Unlock() + + nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies)) + for _, p := range proxies { + if p == nil { + return fmt.Errorf("proxy cannot be nil") + } + name := p.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + nextProxies[name] = p + } + nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors)) + for _, v := range visitors { + if v == nil { + return fmt.Errorf("visitor cannot be nil") + } + name := v.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + nextVisitors[name] = v + } + s.proxies = nextProxies + s.visitors = nextVisitors + return nil +} diff --git a/pkg/config/source/config_source_test.go b/pkg/config/source/config_source_test.go new file mode 100644 index 00000000..793284e1 --- /dev/null +++ b/pkg/config/source/config_source_test.go @@ -0,0 +1,173 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestNewConfigSource(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + require.NotNil(src) +} + +func TestConfigSource_ReplaceAll(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + err := src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")}, + []v1.VisitorConfigurer{mockVisitor("visitor1")}, + ) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2) + require.Len(visitors, 1) + + // ReplaceAll again should replace everything + err = src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy3")}, + nil, + ) + require.NoError(err) + + proxies, visitors, err = src.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 0) + require.Equal("proxy3", proxies[0].GetBaseConfig().Name) + + // ReplaceAll with nil proxy should fail + err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil) + require.Error(err) + + // ReplaceAll with empty name proxy should fail + err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil) + require.Error(err) +} + +func TestConfigSource_Load(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + err := src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")}, + []v1.VisitorConfigurer{mockVisitor("visitor1")}, + ) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2) + require.Len(visitors, 1) +} + +// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out +// proxies and visitors with Enabled explicitly set to false. +func TestConfigSource_Load_FiltersDisabled(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + disabled := false + enabled := true + + // Create enabled proxy (nil Enabled = enabled by default) + enabledProxy := mockProxy("enabled-proxy") + + // Create disabled proxy + disabledProxy := &v1.TCPProxyConfig{} + disabledProxy.Name = "disabled-proxy" + disabledProxy.Type = "tcp" + disabledProxy.Enabled = &disabled + + // Create explicitly enabled proxy + explicitEnabledProxy := &v1.TCPProxyConfig{} + explicitEnabledProxy.Name = "explicit-enabled-proxy" + explicitEnabledProxy.Type = "tcp" + explicitEnabledProxy.Enabled = &enabled + + // Create enabled visitor (nil Enabled = enabled by default) + enabledVisitor := mockVisitor("enabled-visitor") + + // Create disabled visitor + disabledVisitor := &v1.STCPVisitorConfig{} + disabledVisitor.Name = "disabled-visitor" + disabledVisitor.Type = "stcp" + disabledVisitor.Enabled = &disabled + + err := src.ReplaceAll( + []v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy}, + []v1.VisitorConfigurer{enabledVisitor, disabledVisitor}, + ) + require.NoError(err) + + // Load should filter out disabled configs + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2, "Should have 2 enabled proxies") + require.Len(visitors, 1, "Should have 1 enabled visitor") + + // Verify the correct proxies are returned + proxyNames := make([]string, 0, len(proxies)) + for _, p := range proxies { + proxyNames = append(proxyNames, p.GetBaseConfig().Name) + } + require.Contains(proxyNames, "enabled-proxy") + require.Contains(proxyNames, "explicit-enabled-proxy") + require.NotContains(proxyNames, "disabled-proxy") + + // Verify the correct visitor is returned + require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name) +} + +func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 1) + require.Empty(proxies[0].GetBaseConfig().LocalIP) + require.Empty(visitors[0].GetBaseConfig().BindAddr) + require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol) +} diff --git a/pkg/config/source/source.go b/pkg/config/source/source.go new file mode 100644 index 00000000..a6cff226 --- /dev/null +++ b/pkg/config/source/source.go @@ -0,0 +1,37 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// Source is the interface for configuration sources. +// A Source provides proxy and visitor configurations from various backends. +// Aggregator currently uses the built-in config source as base and an optional +// store source as higher-priority overlay. +type Source interface { + // Load loads the proxy and visitor configurations from this source. + // Returns the loaded configurations and any error encountered. + // A disabled entry in one source is source-local filtering, not a cross-source + // tombstone for entries from lower-priority sources. + // + // Error handling contract with Aggregator: + // - When err is nil, returned slices are consumed. + // - When err is non-nil, Aggregator aborts the merge and returns the error. + // - To publish best-effort or partial results, return those results with + // err set to nil. + Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error) +} diff --git a/pkg/config/source/store.go b/pkg/config/source/store.go new file mode 100644 index 00000000..22024136 --- /dev/null +++ b/pkg/config/source/store.go @@ -0,0 +1,359 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type StoreSourceConfig struct { + Path string `json:"path"` +} + +type storeData struct { + Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"` + Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"` +} + +type StoreSource struct { + baseSource + config StoreSourceConfig +} + +var ( + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") +) + +func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) { + if cfg.Path == "" { + return nil, fmt.Errorf("path is required") + } + + s := &StoreSource{ + baseSource: newBaseSource(), + config: cfg, + } + + if err := s.loadFromFile(); err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to load existing data: %w", err) + } + } + + return s, nil +} + +func (s *StoreSource) loadFromFile() error { + s.mu.Lock() + defer s.mu.Unlock() + return s.loadFromFileUnlocked() +} + +func (s *StoreSource) loadFromFileUnlocked() error { + data, err := os.ReadFile(s.config.Path) + if err != nil { + return err + } + + var stored storeData + if err := v1.WithDisallowUnknownFields(false, func() error { + return json.Unmarshal(data, &stored) + }); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + + s.proxies = make(map[string]v1.ProxyConfigurer) + s.visitors = make(map[string]v1.VisitorConfigurer) + + for _, tp := range stored.Proxies { + if tp.ProxyConfigurer != nil { + proxyCfg := tp.ProxyConfigurer + name := proxyCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + s.proxies[name] = proxyCfg + } + } + + for _, tv := range stored.Visitors { + if tv.VisitorConfigurer != nil { + visitorCfg := tv.VisitorConfigurer + name := visitorCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + s.visitors[name] = visitorCfg + } + } + + return nil +} + +func (s *StoreSource) saveToFileUnlocked() error { + stored := storeData{ + Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)), + Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)), + } + + for _, p := range s.proxies { + stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p}) + } + for _, v := range s.visitors { + stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v}) + } + + data, err := json.MarshalIndent(stored, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + dir := filepath.Dir(s.config.Path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + tmpPath := s.config.Path + ".tmp" + + f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(tmpPath) + return fmt.Errorf("failed to write temp file: %w", err) + } + + if err := f.Sync(); err != nil { + f.Close() + os.Remove(tmpPath) + return fmt.Errorf("failed to sync temp file: %w", err) + } + + if err := f.Close(); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to close temp file: %w", err) + } + + if err := os.Rename(tmpPath, s.config.Path); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to rename temp file: %w", err) + } + + return nil +} + +func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error { + if proxy == nil { + return fmt.Errorf("proxy cannot be nil") + } + + name := proxy.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.proxies[name]; exists { + return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name) + } + + s.proxies[name] = proxy + + if err := s.saveToFileUnlocked(); err != nil { + delete(s.proxies, name) + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error { + if proxy == nil { + return fmt.Errorf("proxy cannot be nil") + } + + name := proxy.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + oldProxy, exists := s.proxies[name] + if !exists { + return fmt.Errorf("%w: proxy %q", ErrNotFound, name) + } + + s.proxies[name] = proxy + + if err := s.saveToFileUnlocked(); err != nil { + s.proxies[name] = oldProxy + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) RemoveProxy(name string) error { + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + oldProxy, exists := s.proxies[name] + if !exists { + return fmt.Errorf("%w: proxy %q", ErrNotFound, name) + } + + delete(s.proxies, name) + + if err := s.saveToFileUnlocked(); err != nil { + s.proxies[name] = oldProxy + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer { + s.mu.RLock() + defer s.mu.RUnlock() + + p, exists := s.proxies[name] + if !exists { + return nil + } + return p +} + +func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error { + if visitor == nil { + return fmt.Errorf("visitor cannot be nil") + } + + name := visitor.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.visitors[name]; exists { + return fmt.Errorf("%w: visitor %q", ErrAlreadyExists, name) + } + + s.visitors[name] = visitor + + if err := s.saveToFileUnlocked(); err != nil { + delete(s.visitors, name) + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error { + if visitor == nil { + return fmt.Errorf("visitor cannot be nil") + } + + name := visitor.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + oldVisitor, exists := s.visitors[name] + if !exists { + return fmt.Errorf("%w: visitor %q", ErrNotFound, name) + } + + s.visitors[name] = visitor + + if err := s.saveToFileUnlocked(); err != nil { + s.visitors[name] = oldVisitor + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) RemoveVisitor(name string) error { + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + + s.mu.Lock() + defer s.mu.Unlock() + + oldVisitor, exists := s.visitors[name] + if !exists { + return fmt.Errorf("%w: visitor %q", ErrNotFound, name) + } + + delete(s.visitors, name) + + if err := s.saveToFileUnlocked(); err != nil { + s.visitors[name] = oldVisitor + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer { + s.mu.RLock() + defer s.mu.RUnlock() + + v, exists := s.visitors[name] + if !exists { + return nil + } + return v +} + +func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make([]v1.ProxyConfigurer, 0, len(s.proxies)) + for _, p := range s.proxies { + result = append(result, p) + } + return result, nil +} + +func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make([]v1.VisitorConfigurer, 0, len(s.visitors)) + for _, v := range s.visitors { + result = append(result, v) + } + return result, nil +} diff --git a/pkg/config/source/store_test.go b/pkg/config/source/store_test.go new file mode 100644 index 00000000..801115fa --- /dev/null +++ b/pkg/config/source/store_test.go @@ -0,0 +1,144 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func setDisallowUnknownFieldsForStoreTest(t *testing.T, value bool) func() { + t.Helper() + v1.DisallowUnknownFieldsMu.Lock() + prev := v1.DisallowUnknownFields + v1.DisallowUnknownFields = value + v1.DisallowUnknownFieldsMu.Unlock() + return func() { + v1.DisallowUnknownFieldsMu.Lock() + v1.DisallowUnknownFields = prev + v1.DisallowUnknownFieldsMu.Unlock() + } +} + +func getDisallowUnknownFieldsForStoreTest() bool { + v1.DisallowUnknownFieldsMu.Lock() + defer v1.DisallowUnknownFieldsMu.Unlock() + return v1.DisallowUnknownFields +} + +func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + path := filepath.Join(t.TempDir(), "store.json") + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + err = storeSource.AddProxy(proxyCfg) + require.NoError(err) + err = storeSource.AddVisitor(visitorCfg) + require.NoError(err) + + gotProxy := storeSource.GetProxy("proxy1") + require.NotNil(gotProxy) + require.Empty(gotProxy.GetBaseConfig().LocalIP) + + gotVisitor := storeSource.GetVisitor("visitor1") + require.NotNil(gotVisitor) + require.Empty(gotVisitor.GetBaseConfig().BindAddr) + require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol) +} + +func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + path := filepath.Join(t.TempDir(), "store.json") + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + stored := storeData{ + Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}}, + Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}}, + } + data, err := json.Marshal(stored) + require.NoError(err) + err = os.WriteFile(path, data, 0o600) + require.NoError(err) + + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + gotProxy := storeSource.GetProxy("proxy1") + require.NotNil(gotProxy) + require.Empty(gotProxy.GetBaseConfig().LocalIP) + + gotVisitor := storeSource.GetVisitor("visitor1") + require.NotNil(gotVisitor) + require.Empty(gotVisitor.GetBaseConfig().BindAddr) + require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol) +} + +func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { + require := require.New(t) + + restore := setDisallowUnknownFieldsForStoreTest(t, true) + t.Cleanup(restore) + + path := filepath.Join(t.TempDir(), "store.json") + raw := []byte(`{ + "proxies": [ + {"name":"proxy1","type":"tcp","localPort":10080,"unexpected":"value"} + ], + "visitors": [ + {"name":"visitor1","type":"xtcp","serverName":"server1","secretKey":"secret","bindPort":10081,"unexpected":"value"} + ] + }`) + err := os.WriteFile(path, raw, 0o600) + require.NoError(err) + + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + require.NotNil(storeSource.GetProxy("proxy1")) + require.NotNil(storeSource.GetVisitor("visitor1")) + require.True(getDisallowUnknownFieldsForStoreTest()) +} diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index bb95b6cd..783eee8e 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -77,6 +77,9 @@ type ClientCommonConfig struct { // Include other config files for proxies. IncludeConfigFiles []string `json:"includes,omitempty"` + + // Store config enables the built-in store source (not configurable via sources list). + Store StoreConfig `json:"store,omitempty"` } func (c *ClientCommonConfig) Complete() error { diff --git a/pkg/config/v1/clone_test.go b/pkg/config/v1/clone_test.go new file mode 100644 index 00000000..9b108528 --- /dev/null +++ b/pkg/config/v1/clone_test.go @@ -0,0 +1,109 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProxyCloneDeepCopy(t *testing.T) { + require := require.New(t) + + enabled := true + pluginHTTP2 := true + cfg := &HTTPProxyConfig{ + ProxyBaseConfig: ProxyBaseConfig{ + Name: "p1", + Type: "http", + Enabled: &enabled, + Annotations: map[string]string{"a": "1"}, + Metadatas: map[string]string{"m": "1"}, + HealthCheck: HealthCheckConfig{ + Type: "http", + HTTPHeaders: []HTTPHeader{ + {Name: "X-Test", Value: "v1"}, + }, + }, + ProxyBackend: ProxyBackend{ + Plugin: TypedClientPluginOptions{ + Type: PluginHTTPS2HTTP, + ClientPluginOptions: &HTTPS2HTTPPluginOptions{ + Type: PluginHTTPS2HTTP, + EnableHTTP2: &pluginHTTP2, + RequestHeaders: HeaderOperations{Set: map[string]string{"k": "v"}}, + }, + }, + }, + }, + DomainConfig: DomainConfig{ + CustomDomains: []string{"a.example.com"}, + SubDomain: "a", + }, + Locations: []string{"/api"}, + RequestHeaders: HeaderOperations{Set: map[string]string{"h1": "v1"}}, + ResponseHeaders: HeaderOperations{Set: map[string]string{"h2": "v2"}}, + } + + cloned := cfg.Clone().(*HTTPProxyConfig) + + *cloned.Enabled = false + cloned.Annotations["a"] = "changed" + cloned.Metadatas["m"] = "changed" + cloned.HealthCheck.HTTPHeaders[0].Value = "changed" + cloned.CustomDomains[0] = "b.example.com" + cloned.Locations[0] = "/new" + cloned.RequestHeaders.Set["h1"] = "changed" + cloned.ResponseHeaders.Set["h2"] = "changed" + clientPlugin := cloned.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions) + *clientPlugin.EnableHTTP2 = false + clientPlugin.RequestHeaders.Set["k"] = "changed" + + require.True(*cfg.Enabled) + require.Equal("1", cfg.Annotations["a"]) + require.Equal("1", cfg.Metadatas["m"]) + require.Equal("v1", cfg.HealthCheck.HTTPHeaders[0].Value) + require.Equal("a.example.com", cfg.CustomDomains[0]) + require.Equal("/api", cfg.Locations[0]) + require.Equal("v1", cfg.RequestHeaders.Set["h1"]) + require.Equal("v2", cfg.ResponseHeaders.Set["h2"]) + + origPlugin := cfg.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions) + require.True(*origPlugin.EnableHTTP2) + require.Equal("v", origPlugin.RequestHeaders.Set["k"]) +} + +func TestVisitorCloneDeepCopy(t *testing.T) { + require := require.New(t) + + enabled := true + cfg := &XTCPVisitorConfig{ + VisitorBaseConfig: VisitorBaseConfig{ + Name: "v1", + Type: "xtcp", + Enabled: &enabled, + ServerName: "server", + BindPort: 7000, + Plugin: TypedVisitorPluginOptions{ + Type: VisitorPluginVirtualNet, + VisitorPluginOptions: &VirtualNetVisitorPluginOptions{ + Type: VisitorPluginVirtualNet, + DestinationIP: "10.0.0.1", + }, + }, + }, + NatTraversal: &NatTraversalConfig{ + DisableAssistedAddrs: true, + }, + } + + cloned := cfg.Clone().(*XTCPVisitorConfig) + *cloned.Enabled = false + cloned.NatTraversal.DisableAssistedAddrs = false + visitorPlugin := cloned.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions) + visitorPlugin.DestinationIP = "10.0.0.2" + + require.True(*cfg.Enabled) + require.True(cfg.NatTraversal.DisableAssistedAddrs) + origPlugin := cfg.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions) + require.Equal("10.0.0.1", origPlugin.DestinationIP) +} diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index 89898554..df1867a2 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -15,15 +15,15 @@ package v1 import ( + "maps" "sync" "github.com/fatedier/frp/pkg/util/util" ) -// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method -// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder. -// Here, a global variable is temporarily used to control whether unknown fields are allowed. -// Once the v2 version is implemented by the community, we can switch to a standardized approach. +// TODO(fatedier): Migrate typed config decoding to encoding/json/v2 when it is stable for production use. +// The current encoding/json(v1) path cannot propagate DisallowUnknownFields into custom UnmarshalJSON +// methods, so we temporarily keep this global strictness flag protected by a mutex. // // https://github.com/golang/go/issues/41144 // https://github.com/golang/go/discussions/63397 @@ -32,6 +32,19 @@ var ( DisallowUnknownFieldsMu sync.Mutex ) +// WithDisallowUnknownFields temporarily overrides typed config JSON strictness. +// It restores the previous value before returning. +func WithDisallowUnknownFields(disallow bool, fn func() error) error { + DisallowUnknownFieldsMu.Lock() + prev := DisallowUnknownFields + DisallowUnknownFields = disallow + defer func() { + DisallowUnknownFields = prev + DisallowUnknownFieldsMu.Unlock() + }() + return fn() +} + type AuthScope string const ( @@ -104,6 +117,14 @@ type NatTraversalConfig struct { DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"` } +func (c *NatTraversalConfig) Clone() *NatTraversalConfig { + if c == nil { + return nil + } + out := *c + return &out +} + type LogConfig struct { // This is destination where frp should write the logs. // If "console" is used, logs will be printed to stdout, otherwise, @@ -138,6 +159,12 @@ type HeaderOperations struct { Set map[string]string `json:"set,omitempty"` } +func (o HeaderOperations) Clone() HeaderOperations { + return HeaderOperations{ + Set: maps.Clone(o.Set), + } +} + type HTTPHeader struct { Name string `json:"name"` Value string `json:"value"` diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 1bbe5ac3..fb868775 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -19,9 +19,9 @@ import ( "encoding/json" "errors" "fmt" + "maps" "reflect" - - "github.com/samber/lo" + "slices" "github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/msg" @@ -102,11 +102,23 @@ type HealthCheckConfig struct { HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"` } +func (c HealthCheckConfig) Clone() HealthCheckConfig { + out := c + out.HTTPHeaders = slices.Clone(c.HTTPHeaders) + return out +} + type DomainConfig struct { CustomDomains []string `json:"customDomains,omitempty"` SubDomain string `json:"subdomain,omitempty"` } +func (c DomainConfig) Clone() DomainConfig { + out := c + out.CustomDomains = slices.Clone(c.CustomDomains) + return out +} + type ProxyBaseConfig struct { Name string `json:"name"` Type string `json:"type"` @@ -122,12 +134,27 @@ type ProxyBaseConfig struct { ProxyBackend } +func (c ProxyBaseConfig) Clone() ProxyBaseConfig { + out := c + out.Enabled = util.ClonePtr(c.Enabled) + out.Annotations = maps.Clone(c.Annotations) + out.Metadatas = maps.Clone(c.Metadatas) + out.HealthCheck = c.HealthCheck.Clone() + out.ProxyBackend = c.ProxyBackend.Clone() + return out +} + +func (c ProxyBackend) Clone() ProxyBackend { + out := c + out.Plugin = c.Plugin.Clone() + return out +} + func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig { return c } -func (c *ProxyBaseConfig) Complete(namePrefix string) { - c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name +func (c *ProxyBaseConfig) Complete() { c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1") c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) @@ -207,8 +234,9 @@ func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) { } type ProxyConfigurer interface { - Complete(namePrefix string) + Complete() GetBaseConfig() *ProxyBaseConfig + Clone() ProxyConfigurer // MarshalToMsg marshals this config into a msg.NewProxy message. This // function will be called on the frpc side. MarshalToMsg(*msg.NewProxy) @@ -271,6 +299,12 @@ func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.RemotePort = m.RemotePort } +func (c *TCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + return &out +} + var _ ProxyConfigurer = &UDPProxyConfig{} type UDPProxyConfig struct { @@ -291,6 +325,12 @@ func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.RemotePort = m.RemotePort } +func (c *UDPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + return &out +} + var _ ProxyConfigurer = &HTTPProxyConfig{} type HTTPProxyConfig struct { @@ -334,6 +374,16 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.RouteByHTTPUser = m.RouteByHTTPUser } +func (c *HTTPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + out.Locations = slices.Clone(c.Locations) + out.RequestHeaders = c.RequestHeaders.Clone() + out.ResponseHeaders = c.ResponseHeaders.Clone() + return &out +} + var _ ProxyConfigurer = &HTTPSProxyConfig{} type HTTPSProxyConfig struct { @@ -355,6 +405,13 @@ func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.SubDomain = m.SubDomain } +func (c *HTTPSProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + return &out +} + type TCPMultiplexerType string const ( @@ -395,6 +452,13 @@ func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.RouteByHTTPUser = m.RouteByHTTPUser } +func (c *TCPMuxProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + return &out +} + var _ ProxyConfigurer = &STCPProxyConfig{} type STCPProxyConfig struct { @@ -418,6 +482,13 @@ func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.AllowUsers = m.AllowUsers } +func (c *STCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + return &out +} + var _ ProxyConfigurer = &XTCPProxyConfig{} type XTCPProxyConfig struct { @@ -444,6 +515,14 @@ func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.AllowUsers = m.AllowUsers } +func (c *XTCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + out.NatTraversal = c.NatTraversal.Clone() + return &out +} + var _ ProxyConfigurer = &SUDPProxyConfig{} type SUDPProxyConfig struct { @@ -466,3 +545,10 @@ func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.Secretkey = m.Sk c.AllowUsers = m.AllowUsers } + +func (c *SUDPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + return &out +} diff --git a/pkg/config/v1/proxy_plugin.go b/pkg/config/v1/proxy_plugin.go index 128ccae6..908b30ff 100644 --- a/pkg/config/v1/proxy_plugin.go +++ b/pkg/config/v1/proxy_plugin.go @@ -54,6 +54,7 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{ type ClientPluginOptions interface { Complete() + Clone() ClientPluginOptions } type TypedClientPluginOptions struct { @@ -61,6 +62,14 @@ type TypedClientPluginOptions struct { ClientPluginOptions } +func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions { + out := c + if c.ClientPluginOptions != nil { + out.ClientPluginOptions = c.ClientPluginOptions.Clone() + } + return out +} + func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { if len(b) == 4 && string(b) == "null" { return nil @@ -109,6 +118,15 @@ type HTTP2HTTPSPluginOptions struct { func (o *HTTP2HTTPSPluginOptions) Complete() {} +func (o *HTTP2HTTPSPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + return &out +} + type HTTPProxyPluginOptions struct { Type string `json:"type,omitempty"` HTTPUser string `json:"httpUser,omitempty"` @@ -117,6 +135,14 @@ type HTTPProxyPluginOptions struct { func (o *HTTPProxyPluginOptions) Complete() {} +func (o *HTTPProxyPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + type HTTPS2HTTPPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` @@ -131,6 +157,16 @@ func (o *HTTPS2HTTPPluginOptions) Complete() { o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) } +func (o *HTTPS2HTTPPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2) + return &out +} + type HTTPS2HTTPSPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` @@ -145,6 +181,16 @@ func (o *HTTPS2HTTPSPluginOptions) Complete() { o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) } +func (o *HTTPS2HTTPSPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2) + return &out +} + type HTTP2HTTPPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` @@ -154,6 +200,15 @@ type HTTP2HTTPPluginOptions struct { func (o *HTTP2HTTPPluginOptions) Complete() {} +func (o *HTTP2HTTPPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + return &out +} + type Socks5PluginOptions struct { Type string `json:"type,omitempty"` Username string `json:"username,omitempty"` @@ -162,6 +217,14 @@ type Socks5PluginOptions struct { func (o *Socks5PluginOptions) Complete() {} +func (o *Socks5PluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + type StaticFilePluginOptions struct { Type string `json:"type,omitempty"` LocalPath string `json:"localPath,omitempty"` @@ -172,6 +235,14 @@ type StaticFilePluginOptions struct { func (o *StaticFilePluginOptions) Complete() {} +func (o *StaticFilePluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + type UnixDomainSocketPluginOptions struct { Type string `json:"type,omitempty"` UnixPath string `json:"unixPath,omitempty"` @@ -179,6 +250,14 @@ type UnixDomainSocketPluginOptions struct { func (o *UnixDomainSocketPluginOptions) Complete() {} +func (o *UnixDomainSocketPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + type TLS2RawPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` @@ -188,8 +267,24 @@ type TLS2RawPluginOptions struct { func (o *TLS2RawPluginOptions) Complete() {} +func (o *TLS2RawPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + type VirtualNetPluginOptions struct { Type string `json:"type,omitempty"` } func (o *VirtualNetPluginOptions) Complete() {} + +func (o *VirtualNetPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} diff --git a/pkg/config/v1/store.go b/pkg/config/v1/store.go new file mode 100644 index 00000000..7f992c3b --- /dev/null +++ b/pkg/config/v1/store.go @@ -0,0 +1,26 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// StoreConfig configures the built-in store source. +type StoreConfig struct { + // Path is the store file path. + Path string `json:"path,omitempty"` +} + +// IsEnabled returns true if the store is configured with a valid path. +func (c *StoreConfig) IsEnabled() bool { + return c.Path != "" +} diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 5017f57d..b0ff1e34 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -21,8 +21,6 @@ import ( "fmt" "reflect" - "github.com/samber/lo" - "github.com/fatedier/frp/pkg/util/util" ) @@ -52,31 +50,27 @@ type VisitorBaseConfig struct { Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"` } +func (c VisitorBaseConfig) Clone() VisitorBaseConfig { + out := c + out.Enabled = util.ClonePtr(c.Enabled) + out.Plugin = c.Plugin.Clone() + return out +} + func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { return c } -func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) { +func (c *VisitorBaseConfig) Complete() { if c.BindAddr == "" { c.BindAddr = "127.0.0.1" } - - namePrefix := "" - if g.User != "" { - namePrefix = g.User + "." - } - c.Name = namePrefix + c.Name - - if c.ServerUser != "" { - c.ServerName = c.ServerUser + "." + c.ServerName - } else { - c.ServerName = namePrefix + c.ServerName - } } type VisitorConfigurer interface { - Complete(*ClientCommonConfig) + Complete() GetBaseConfig() *VisitorBaseConfig + Clone() VisitorConfigurer } type VisitorType string @@ -146,12 +140,24 @@ type STCPVisitorConfig struct { VisitorBaseConfig } +func (c *STCPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + return &out +} + var _ VisitorConfigurer = &SUDPVisitorConfig{} type SUDPVisitorConfig struct { VisitorBaseConfig } +func (c *SUDPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + return &out +} + var _ VisitorConfigurer = &XTCPVisitorConfig{} type XTCPVisitorConfig struct { @@ -168,15 +174,18 @@ type XTCPVisitorConfig struct { NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` } -func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { - c.VisitorBaseConfig.Complete(g) +func (c *XTCPVisitorConfig) Complete() { + c.VisitorBaseConfig.Complete() c.Protocol = util.EmptyOr(c.Protocol, "quic") c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8) c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90) c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000) - - if c.FallbackTo != "" { - c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo - } +} + +func (c *XTCPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + out.NatTraversal = c.NatTraversal.Clone() + return &out } diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go index 5a4909bd..9fa28a0b 100644 --- a/pkg/config/v1/visitor_plugin.go +++ b/pkg/config/v1/visitor_plugin.go @@ -32,6 +32,7 @@ var visitorPluginOptionsTypeMap = map[string]reflect.Type{ type VisitorPluginOptions interface { Complete() + Clone() VisitorPluginOptions } type TypedVisitorPluginOptions struct { @@ -39,6 +40,14 @@ type TypedVisitorPluginOptions struct { VisitorPluginOptions } +func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions { + out := c + if c.VisitorPluginOptions != nil { + out.VisitorPluginOptions = c.VisitorPluginOptions.Clone() + } + return out +} + func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { if len(b) == 4 && string(b) == "null" { return nil @@ -84,3 +93,11 @@ type VirtualNetVisitorPluginOptions struct { } func (o *VirtualNetVisitorPluginOptions) Complete() {} + +func (o *VirtualNetVisitorPluginOptions) Clone() VisitorPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} diff --git a/pkg/naming/names.go b/pkg/naming/names.go new file mode 100644 index 00000000..0e4d4e6a --- /dev/null +++ b/pkg/naming/names.go @@ -0,0 +1,33 @@ +package naming + +import "strings" + +// AddUserPrefix builds the wire-level proxy name for frps by prefixing user. +func AddUserPrefix(user, name string) string { + if user == "" { + return name + } + return user + "." + name +} + +// StripUserPrefix converts a wire-level proxy name to an internal raw name. +// It strips only one exact "{user}." prefix. +func StripUserPrefix(user, name string) string { + if user == "" { + return name + } + prefix := user + "." + if strings.HasPrefix(name, prefix) { + return strings.TrimPrefix(name, prefix) + } + return name +} + +// BuildTargetServerProxyName resolves visitor target proxy name for wire-level +// protocol messages. serverUser overrides local user when set. +func BuildTargetServerProxyName(localUser, serverUser, serverName string) string { + if serverUser != "" { + return AddUserPrefix(serverUser, serverName) + } + return AddUserPrefix(localUser, serverName) +} diff --git a/pkg/naming/names_test.go b/pkg/naming/names_test.go new file mode 100644 index 00000000..3792af42 --- /dev/null +++ b/pkg/naming/names_test.go @@ -0,0 +1,27 @@ +package naming + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAddUserPrefix(t *testing.T) { + require := require.New(t) + require.Equal("test", AddUserPrefix("", "test")) + require.Equal("alice.test", AddUserPrefix("alice", "test")) +} + +func TestStripUserPrefix(t *testing.T) { + require := require.New(t) + require.Equal("test", StripUserPrefix("", "test")) + require.Equal("test", StripUserPrefix("alice", "alice.test")) + require.Equal("alice.test", StripUserPrefix("alice", "alice.alice.test")) + require.Equal("bob.test", StripUserPrefix("alice", "bob.test")) +} + +func TestBuildTargetServerProxyName(t *testing.T) { + require := require.New(t) + require.Equal("alice.test", BuildTargetServerProxyName("alice", "", "test")) + require.Equal("bob.test", BuildTargetServerProxyName("alice", "bob", "test")) +} diff --git a/pkg/nathole/controller.go b/pkg/nathole/controller.go index c08c81c9..88d788d6 100644 --- a/pkg/nathole/controller.go +++ b/pkg/nathole/controller.go @@ -375,7 +375,7 @@ func getRangePorts(addrs []string, difference, maxNumber int) []msg.PortsRange { if !isLast { return nil } - var ports []msg.PortsRange + ports := make([]msg.PortsRange, 0, 1) _, portStr, err := net.SplitHostPort(addr) if err != nil { return nil diff --git a/pkg/policy/featuregate/feature_gate.go b/pkg/policy/featuregate/feature_gate.go index c5fd684b..81a392b7 100644 --- a/pkg/policy/featuregate/feature_gate.go +++ b/pkg/policy/featuregate/feature_gate.go @@ -171,8 +171,9 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error { // String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..." func (f *featureGate) String() string { - pairs := []string{} - for k, v := range f.enabled.Load().(map[Feature]bool) { + enabled := f.enabled.Load().(map[Feature]bool) + pairs := make([]string, 0, len(enabled)) + for k, v := range enabled { pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) } sort.Strings(pairs) diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index 378c6098..bffc40bc 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -112,7 +112,7 @@ func (s *TunnelServer) Run() error { if sshConn.Permissions != nil { clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) } - pc.Complete(clientCfg.User) + pc.Complete() vc, err := virtual.NewClient(virtual.ClientOptions{ Common: clientCfg, diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 774af2cf..84408385 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -134,3 +134,12 @@ func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Durati func ConstantTimeEqString(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } + +// ClonePtr returns a pointer to a copied value. If v is nil, it returns nil. +func ClonePtr[T any](v *T) *T { + if v == nil { + return nil + } + out := *v + return &out +} diff --git a/pkg/util/util/util_test.go b/pkg/util/util/util_test.go index 0a63ba6d..bd53f48e 100644 --- a/pkg/util/util/util_test.go +++ b/pkg/util/util/util_test.go @@ -41,3 +41,16 @@ func TestParseRangeNumbers(t *testing.T) { _, err = ParseRangeNumbers("3-a") require.Error(err) } + +func TestClonePtr(t *testing.T) { + require := require.New(t) + + var nilInt *int + require.Nil(ClonePtr(nilInt)) + + v := 42 + cloned := ClonePtr(&v) + require.NotNil(cloned) + require.Equal(v, *cloned) + require.NotSame(&v, cloned) +} diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go index 8fec28c8..c7006ce7 100644 --- a/pkg/virtual/client.go +++ b/pkg/virtual/client.go @@ -19,6 +19,7 @@ import ( "net" "github.com/fatedier/frp/client" + "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" netpkg "github.com/fatedier/frp/pkg/util/net" @@ -43,10 +44,13 @@ func NewClient(options ClientOptions) (*Client, error) { } ln := netpkg.NewInternalListener() + configSource := source.NewConfigSource() + aggregator := source.NewAggregator(configSource) serviceOptions := client.ServiceOptions{ - Common: options.Common, - ClientSpec: options.Spec, + Common: options.Common, + ConfigSourceAggregator: aggregator, + ClientSpec: options.Spec, ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) client.Connector { return &pipeConnector{ peerListener: ln, diff --git a/server/api/controller.go b/server/api/controller.go index 861316c4..f691fcad 100644 --- a/server/api/controller.go +++ b/server/api/controller.go @@ -236,9 +236,6 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error") } proxyInfo.Status = "online" - c.fillProxyClientInfo(&proxyClientInfo{ - clientVersion: &proxyInfo.ClientVersion, - }, pxy) } else { proxyInfo.Status = "offline" } @@ -277,9 +274,6 @@ func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyS continue } proxyInfo.Status = "online" - c.fillProxyClientInfo(&proxyClientInfo{ - clientVersion: &proxyInfo.ClientVersion, - }, pxy) } else { proxyInfo.Status = "offline" } @@ -339,6 +333,7 @@ func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp { User: info.User, ClientID: info.ClientID(), RunID: info.RunID, + Version: info.Version, Hostname: info.Hostname, ClientIP: info.IP, FirstConnectedAt: toUnix(info.FirstConnectedAt), @@ -351,37 +346,6 @@ func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp { return resp } -type proxyClientInfo struct { - user *string - clientID *string - clientVersion *string -} - -func (c *Controller) fillProxyClientInfo(proxyInfo *proxyClientInfo, pxy proxy.Proxy) { - loginMsg := pxy.GetLoginMsg() - if loginMsg == nil { - return - } - if proxyInfo.user != nil { - *proxyInfo.user = loginMsg.User - } - if proxyInfo.clientVersion != nil { - *proxyInfo.clientVersion = loginMsg.Version - } - if info, ok := c.clientRegistry.GetByRunID(loginMsg.RunID); ok { - if proxyInfo.clientID != nil { - *proxyInfo.clientID = info.ClientID() - } - return - } - if proxyInfo.clientID != nil { - *proxyInfo.clientID = loginMsg.ClientID - if *proxyInfo.clientID == "" { - *proxyInfo.clientID = loginMsg.RunID - } - } -} - func toUnix(t time.Time) int64 { if t.IsZero() { return 0 diff --git a/server/api/types.go b/server/api/types.go index b91422be..0a72af1e 100644 --- a/server/api/types.go +++ b/server/api/types.go @@ -45,6 +45,7 @@ type ClientInfoResp struct { User string `json:"user"` ClientID string `json:"clientID"` RunID string `json:"runID"` + Version string `json:"version,omitempty"` Hostname string `json:"hostname"` ClientIP string `json:"clientIP,omitempty"` FirstConnectedAt int64 `json:"firstConnectedAt"` @@ -100,7 +101,6 @@ type ProxyStatsInfo struct { Conf any `json:"conf"` User string `json:"user,omitempty"` ClientID string `json:"clientID,omitempty"` - ClientVersion string `json:"clientVersion,omitempty"` TodayTrafficIn int64 `json:"todayTrafficIn"` TodayTrafficOut int64 `json:"todayTrafficOut"` CurConns int64 `json:"curConns"` @@ -119,7 +119,6 @@ type GetProxyStatsResp struct { Conf any `json:"conf"` User string `json:"user,omitempty"` ClientID string `json:"clientID,omitempty"` - ClientVersion string `json:"clientVersion,omitempty"` TodayTrafficIn int64 `json:"todayTrafficIn"` TodayTrafficOut int64 `json:"todayTrafficOut"` CurConns int64 `json:"curConns"` diff --git a/server/registry/registry.go b/server/registry/registry.go index 24751bf6..01c44947 100644 --- a/server/registry/registry.go +++ b/server/registry/registry.go @@ -28,6 +28,7 @@ type ClientInfo struct { RunID string Hostname string IP string + Version string FirstConnectedAt time.Time LastConnectedAt time.Time DisconnectedAt time.Time @@ -50,7 +51,7 @@ func NewClientRegistry() *ClientRegistry { } // Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client. -func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, remoteAddr string) (key string, conflict bool) { +func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr string) (key string, conflict bool) { if runID == "" { return "", false } @@ -86,6 +87,7 @@ func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, remoteAdd info.RunID = runID info.Hostname = hostname info.IP = remoteAddr + info.Version = version if info.FirstConnectedAt.IsZero() { info.FirstConnectedAt = now } @@ -151,22 +153,6 @@ func (info ClientInfo) ClientID() string { return info.RunID } -// GetByRunID retrieves a client by its run ID. -func (cr *ClientRegistry) GetByRunID(runID string) (ClientInfo, bool) { - cr.mu.RLock() - defer cr.mu.RUnlock() - - key, ok := cr.runIndex[runID] - if !ok { - return ClientInfo{}, false - } - info, ok := cr.clients[key] - if !ok { - return ClientInfo{}, false - } - return *info, true -} - func (cr *ClientRegistry) composeClientKey(user, id string) string { switch { case user == "": diff --git a/server/service.go b/server/service.go index 6106dad6..62f9ac8e 100644 --- a/server/service.go +++ b/server/service.go @@ -622,7 +622,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter if host, _, err := net.SplitHostPort(remoteAddr); err == nil { remoteAddr = host } - _, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, remoteAddr) + _, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Version, remoteAddr) if conflict { svr.ctlManager.Del(loginMsg.RunID, ctl) ctl.Close() diff --git a/test/e2e/v1/features/store.go b/test/e2e/v1/features/store.go new file mode 100644 index 00000000..8479b9be --- /dev/null +++ b/test/e2e/v1/features/store.go @@ -0,0 +1,230 @@ +package features + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Store]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Store API", func() { + ginkgo.It("create proxy via API and verify connection", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(time.Second) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("update proxy via API", func() { + adminPort := f.AllocPort() + remotePort1 := f.AllocPort() + remotePort2 := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort1, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort1).Ensure() + + proxyConfig["remotePort"] = remotePort2 + proxyBody, _ = json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp").HTTPParams("PUT", "", "/api/store/proxies/test-tcp", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort2).Ensure() + framework.NewRequestExpect(f).Port(remotePort1).ExpectError(true).Ensure() + }) + + ginkgo.It("delete proxy via API", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp").HTTPParams("DELETE", "", "/api/store/proxies/test-tcp", nil) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + }) + + ginkgo.It("list and get proxy via API", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + time.Sleep(500 * time.Millisecond) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 && strings.Contains(string(resp.Content), "test-tcp") + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 && strings.Contains(string(resp.Content), "test-tcp") + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/nonexistent") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 404 + }) + }) + + ginkgo.It("store disabled returns 404", func() { + adminPort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + `, adminPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 404 + }) + }) + }) +}) diff --git a/web/frpc/.eslintrc.cjs b/web/frpc/.eslintrc.cjs deleted file mode 100644 index d1ed4702..00000000 --- a/web/frpc/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - extends: [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/eslint-config-typescript', - '@vue/eslint-config-prettier', - ], - parserOptions: { - ecmaVersion: 'latest', - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - 'vue/multi-word-component-names': [ - 'error', - { - ignores: ['Overview'], - }, - ], - }, -} diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index f9d4522a..700f3c42 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -10,13 +10,21 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] + ElSelect: typeof import('element-plus/es')['ElSelect'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] + KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default'] ProxyCard: typeof import('./src/components/ProxyCard.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/web/frpc/eslint.config.js b/web/frpc/eslint.config.js new file mode 100644 index 00000000..a4cce6d4 --- /dev/null +++ b/web/frpc/eslint.config.js @@ -0,0 +1,36 @@ +import pluginVue from 'eslint-plugin-vue' +import vueTsEslintConfig from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default [ + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + { + name: 'app/files-to-ignore', + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], + }, + ...pluginVue.configs['flat/essential'], + ...vueTsEslintConfig(), + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'vue/multi-word-component-names': [ + 'error', + { + ignores: ['Overview'], + }, + ], + }, + }, + skipFormatting, +] diff --git a/web/frpc/package-lock.json b/web/frpc/package-lock.json index 7ff79c09..204e6bf6 100644 --- a/web/frpc/package-lock.json +++ b/web/frpc/package-lock.json @@ -13,14 +13,13 @@ "vue-router": "^4.6.4" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.15.0", "@types/node": "24", "@vitejs/plugin-vue": "^6.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^8.56.0", + "eslint": "^9.39.0", "eslint-plugin-vue": "^9.33.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", @@ -35,16 +34,10 @@ "vue-tsc": "^3.2.2" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@antfu/utils": { - "version": "0.7.7", + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", "dev": true, "license": "MIT", "funding": { @@ -53,6 +46,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -60,16 +55,20 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -79,7 +78,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -90,7 +91,9 @@ } }, "node_modules/@ctrl/tinycolor": { - "version": "3.6.0", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "license": "MIT", "engines": { "node": ">=10" @@ -98,15 +101,17 @@ }, "node_modules/@element-plus/icons-vue": { "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", "license": "MIT", "peerDependencies": { "vue": "^3.2.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -121,9 +126,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -138,9 +143,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -155,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -172,9 +177,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -189,7 +194,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -204,9 +211,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -221,9 +228,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -238,9 +245,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -255,9 +262,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -272,9 +279,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -289,9 +296,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -306,9 +313,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -323,9 +330,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -340,9 +347,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -357,9 +364,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -374,9 +381,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -391,9 +398,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -408,9 +415,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -425,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -442,9 +449,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -459,9 +466,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -476,9 +483,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -493,9 +500,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -510,9 +517,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -527,9 +534,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -544,83 +551,251 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.56.0", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.2.1", - "license": "MIT" - }, - "node_modules/@floating-ui/dom": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.2.1" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.2" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -631,13 +806,24 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -647,6 +833,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -656,6 +844,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -664,6 +854,8 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -673,10 +865,14 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -686,6 +882,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -698,6 +896,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -706,6 +906,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -717,16 +919,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -736,25 +940,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -773,9 +977,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -794,7 +998,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -813,9 +1019,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -834,9 +1040,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -855,9 +1061,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -876,9 +1082,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -897,9 +1103,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -918,9 +1124,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -939,9 +1145,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -960,9 +1166,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -981,9 +1187,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1002,9 +1208,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1022,20 +1228,38 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pkgr/core": { - "version": "0.1.1", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", - "version": "2.11.7", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1043,18 +1267,22 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -1068,10 +1296,23 @@ } } }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1083,9 +1324,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1097,9 +1338,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1111,7 +1352,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1123,9 +1366,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1137,9 +1380,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1151,9 +1394,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1165,9 +1408,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1179,9 +1422,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1193,9 +1436,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1207,9 +1450,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1221,9 +1464,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1235,9 +1478,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1249,9 +1492,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1263,9 +1506,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1277,9 +1520,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1291,9 +1534,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1305,9 +1548,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1319,9 +1562,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1333,9 +1576,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1347,9 +1590,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1361,9 +1604,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1375,9 +1618,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1389,9 +1632,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1403,9 +1646,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1416,13 +1659,10 @@ "win32" ] }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "dev": true, - "license": "MIT" - }, "node_modules/@trysound/sax": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true, "license": "ISC", "engines": { @@ -1431,152 +1671,204 @@ }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.21", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", "license": "MIT" }, "node_modules/@types/lodash-es": { "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/node": { - "version": "24.10.4", + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/semver": { - "version": "7.5.6", - "dev": true, - "license": "MIT" - }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.20.0", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/type-utils": "6.20.0", - "@typescript-eslint/utils": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.20.0", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/utils": "6.20.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "6.20.0", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1584,90 +1876,69 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.20.0", - "eslint-visitor-keys": "^3.4.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1675,27 +1946,26 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1706,41 +1976,51 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.27", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.27" + "@volar/source-map": "2.4.28" } }, "node_modules/@volar/source-map": { - "version": "2.4.27", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.27", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-core/node_modules/entities": { - "version": "7.0.0", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1750,22 +2030,26 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.26", - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -1773,46 +2057,55 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/devtools-api": { "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, "node_modules/@vue/eslint-config-prettier": { - "version": "9.0.0", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0" + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { - "eslint": ">= 8.0.0", + "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "node_modules/@vue/eslint-config-typescript": { - "version": "12.0.0", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", + "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "vue-eslint-parser": "^9.3.1" + "@typescript-eslint/utils": "^8.56.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.56.0", + "vue-eslint-parser": "^10.4.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", - "eslint-plugin-vue": "^9.0.0", - "typescript": "*" + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" }, "peerDependenciesMeta": { "typescript": { @@ -1821,11 +2114,13 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.2", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", + "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", @@ -1836,6 +2131,8 @@ }, "node_modules/@vue/language-core/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -1846,47 +2143,59 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.26" + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { - "vue": "3.5.26" + "vue": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@vue/tsconfig": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1903,13 +2212,15 @@ } }, "node_modules/@vueuse/core": { - "version": "14.1.0", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1919,7 +2230,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.1.0", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", "dev": true, "license": "MIT", "funding": { @@ -1927,7 +2240,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "14.1.0", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", "dev": true, "license": "MIT", "funding": { @@ -1938,7 +2253,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1950,6 +2267,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1957,7 +2276,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1973,30 +2294,31 @@ }, "node_modules/alien-signals": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", "dev": true, "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { - "version": "3.2.1", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -2009,25 +2331,75 @@ }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/async-validator": { "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", "license": "MIT" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2036,32 +2408,46 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/boolbase": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2079,11 +2465,15 @@ }, "node_modules/buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/c12": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2111,6 +2501,8 @@ }, "node_modules/c12/node_modules/chokidar": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { @@ -2123,13 +2515,17 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/c12/node_modules/pathe": { - "version": "2.0.3", + "node_modules/c12/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "dev": true, "license": "MIT" }, "node_modules/c12/node_modules/pkg-types": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "dev": true, "license": "MIT", "dependencies": { @@ -2138,8 +2534,21 @@ "pathe": "^2.0.3" } }, + "node_modules/c12/node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/c12/node_modules/readdirp": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", "engines": { @@ -2151,12 +2560,50 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2164,6 +2611,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -2171,54 +2620,42 @@ } }, "node_modules/chalk": { - "version": "2.4.2", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chokidar": { - "version": "3.5.3", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/citty": { "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2226,35 +2663,50 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, "license": "MIT", "engines": { @@ -2262,34 +2714,24 @@ } }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">= 8" } }, "node_modules/css-select": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2305,6 +2747,8 @@ }, "node_modules/css-tree": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, "license": "MIT", "dependencies": { @@ -2317,6 +2761,8 @@ }, "node_modules/css-what": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2328,6 +2774,8 @@ }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { @@ -2339,6 +2787,8 @@ }, "node_modules/csso": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2351,6 +2801,8 @@ }, "node_modules/csso/node_modules/css-tree": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "dev": true, "license": "MIT", "dependencies": { @@ -2364,23 +2816,85 @@ }, "node_modules/csso/node_modules/mdn-data": { "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true, "license": "CC0-1.0" }, "node_modules/csstype": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/dayjs": { - "version": "1.11.19", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2393,14 +2907,37 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, - "node_modules/define-properties": { - "version": "1.2.0", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -2413,58 +2950,33 @@ }, "node_modules/defu": { "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "dev": true, "license": "MIT" }, "node_modules/destr": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "dev": true, "license": "MIT" }, "node_modules/detect-libc": { - "version": "1.0.3", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/dir-glob/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { @@ -2478,6 +2990,8 @@ }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -2489,6 +3003,8 @@ }, "node_modules/domhandler": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2503,6 +3019,8 @@ }, "node_modules/domutils": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2515,7 +3033,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2525,8 +3045,25 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/element-plus": { - "version": "2.13.0", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.3.tgz", + "integrity": "sha512-RwLVtFpeHjZ4UCtHxVi1/sGR2cr2xOL7hqWa7qJc/+gdO6QavVG8Nw1C647obCb3tIg2ztMhNbIIjZUv+6z1og==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.4.1", @@ -2538,8 +3075,8 @@ "@vueuse/core": "^10.11.0", "async-validator": "^4.2.5", "dayjs": "^1.11.19", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", "normalize-wheel-es": "^1.2.0" @@ -2550,10 +3087,14 @@ }, "node_modules/element-plus/node_modules/@types/web-bluetooth": { "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", "license": "MIT" }, "node_modules/element-plus/node_modules/@vueuse/core": { "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", @@ -2567,6 +3108,8 @@ }, "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": { "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2591,6 +3134,8 @@ }, "node_modules/element-plus/node_modules/@vueuse/metadata": { "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -2598,6 +3143,8 @@ }, "node_modules/element-plus/node_modules/@vueuse/shared": { "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", "license": "MIT", "dependencies": { "vue-demi": ">=0.14.8" @@ -2608,6 +3155,8 @@ }, "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": { "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2632,6 +3181,8 @@ }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2642,7 +3193,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2651,47 +3204,72 @@ }, "node_modules/errx": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", "dev": true, "license": "MIT" }, "node_modules/es-abstract": { - "version": "1.21.1", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2700,32 +3278,72 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2735,7 +3353,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2746,36 +3366,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -2786,77 +3408,90 @@ } }, "node_modules/eslint": { - "version": "8.56.0", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2867,7 +3502,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -2881,6 +3516,8 @@ }, "node_modules/eslint-plugin-vue": { "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -2900,8 +3537,10 @@ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-scope": { + "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2915,158 +3554,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/espree": { + "node_modules/eslint-plugin-vue/node_modules/espree": { "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3081,8 +3572,68 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3092,8 +3643,85 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esquery": { - "version": "1.5.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3105,6 +3733,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3116,6 +3746,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3124,10 +3756,14 @@ }, "node_modules/estree-walker": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3136,21 +3772,29 @@ }, "node_modules/exsolve": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-diff": { - "version": "1.2.0", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3158,24 +3802,43 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { - "version": "1.15.0", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3183,14 +3846,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -3208,6 +3873,8 @@ }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -3222,38 +3889,48 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.7", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { - "version": "2.3.2", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -3264,19 +3941,28 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -3287,32 +3973,73 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.0", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3323,6 +4050,8 @@ }, "node_modules/giget": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", "dev": true, "license": "MIT", "dependencies": { @@ -3337,61 +4066,41 @@ "giget": "dist/cli.mjs" } }, - "node_modules/giget/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { - "version": "5.1.2", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/globals": { - "version": "13.24.0", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -3400,103 +4109,71 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/gopd": { - "version": "1.0.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { - "version": "4.2.10", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { - "version": "1.0.2", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { - "version": "3.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3505,7 +4182,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -3516,11 +4195,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3530,7 +4211,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3540,21 +4223,17 @@ "node": ">= 0.4" } }, - "node_modules/hasown/node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, "license": "ISC" }, "node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3563,11 +4242,15 @@ }, "node_modules/immutable": { "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "dev": true, "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3583,47 +4266,42 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, "node_modules/internal-slot": { - "version": "1.0.5", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/is-array-buffer": { - "version": "3.0.1", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3631,15 +4309,42 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, - "node_modules/is-bigint": { - "version": "1.0.4", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3647,6 +4352,8 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -3657,12 +4364,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3673,6 +4382,8 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -3683,22 +4394,48 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-date-object": { - "version": "1.0.5", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3709,14 +4446,54 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3726,8 +4503,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -3748,11 +4540,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3761,21 +4556,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { - "version": "1.1.4", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3784,23 +4575,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-string": { - "version": "1.0.7", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3810,11 +4622,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3824,15 +4640,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -3841,30 +4655,83 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakref": { - "version": "1.0.2", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/jiti": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "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/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -3878,28 +4745,48 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, - "node_modules/jsonc-parser": { - "version": "3.2.0", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } }, "node_modules/klona": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "dev": true, "license": "MIT", "engines": { @@ -3908,11 +4795,15 @@ }, "node_modules/knitwork": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz", + "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==", "dev": true, "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3925,6 +4816,8 @@ }, "node_modules/load-json-file": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3938,12 +4831,14 @@ } }, "node_modules/local-pkg": { - "version": "0.5.0", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", "dev": true, "license": "MIT", "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" }, "engines": { "node": ">=14" @@ -3954,6 +4849,8 @@ }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -3980,6 +4877,8 @@ }, "node_modules/lodash-unified": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", "license": "MIT", "peerDependencies": { "@types/lodash-es": "*", @@ -3989,27 +4888,47 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/magic-string": { "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true, "license": "CC0-1.0" }, "node_modules/memoize-one": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, "node_modules/memorystream": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", "dev": true, "engines": { "node": ">= 0.10.0" @@ -4017,6 +4936,8 @@ }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -4038,18 +4959,25 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", "dev": true, "license": "MIT", "dependencies": { @@ -4059,38 +4987,24 @@ "ufo": "^1.6.1" } }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/ms": { - "version": "2.1.2", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/muggle-string": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4107,27 +5021,37 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true, "license": "MIT" }, "node_modules/node-addon-api": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-fetch-native": { "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "dev": true, "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4137,33 +5061,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "node_modules/normalize-package-data/node_modules/is-core-module": { - "version": "2.11.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/normalize-package-data/node_modules/resolve": { - "version": "1.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -4176,6 +5073,8 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -4184,10 +5083,14 @@ }, "node_modules/normalize-wheel-es": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "license": "BSD-3-Clause" }, "node_modules/npm-run-all": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4210,8 +5113,192 @@ "node": ">= 4" } }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4222,48 +5309,47 @@ } }, "node_modules/nypm": { - "version": "0.6.2", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, - "node_modules/nypm/node_modules/pathe": { - "version": "2.0.3", + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", "dev": true, "license": "MIT" }, - "node_modules/nypm/node_modules/pkg-types": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, "node_modules/object-inspect": { - "version": "1.12.3", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -4271,13 +5357,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -4289,35 +5379,51 @@ }, "node_modules/ohash": { "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "dev": true, "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { - "version": "0.9.3", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4332,6 +5438,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -4346,6 +5454,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4357,6 +5467,8 @@ }, "node_modules/parse-json": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "license": "MIT", "dependencies": { @@ -4369,40 +5481,42 @@ }, "node_modules/path-browserify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true, "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { - "version": "2.0.1", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "license": "MIT", "dependencies": { @@ -4413,21 +5527,29 @@ } }, "node_modules/pathe": { - "version": "1.1.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "2.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -4439,6 +5561,8 @@ }, "node_modules/pidtree": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true, "license": "MIT", "bin": { @@ -4450,6 +5574,8 @@ }, "node_modules/pify": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, "license": "MIT", "engines": { @@ -4457,22 +5583,31 @@ } }, "node_modules/pkg-types": { - "version": "1.0.3", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, "license": "MIT", "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/pkg-types/node_modules/pathe": { + "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -4498,7 +5633,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -4511,6 +5648,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -4518,7 +5657,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -4532,7 +5673,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -4543,15 +5686,36 @@ } }, "node_modules/punycode": { - "version": "2.3.0", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -4570,16 +5734,20 @@ "license": "MIT" }, "node_modules/rc9": { - "version": "2.1.2", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.0.tgz", + "integrity": "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA==", "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", - "destr": "^2.0.3" + "destr": "^2.0.5" } }, "node_modules/read-pkg": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, "license": "MIT", "dependencies": { @@ -4592,24 +5760,55 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { - "node": ">=8.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4619,23 +5818,30 @@ } }, "node_modules/resolve": { - "version": "1.22.8", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -4643,7 +5849,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -4651,22 +5859,10 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rolldown-string": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rolldown-string/-/rolldown-string-0.2.1.tgz", + "integrity": "sha512-7H8oH5A8+L96pbBTPCt/rZrwayEhZY5/ejhdk9nRODH32H1v7+bfkaCr+kS15DcGQ7VC1HcWdQVNABFYgrMOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -4680,7 +5876,9 @@ } }, "node_modules/rollup": { - "version": "4.55.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4694,36 +5892,38 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -4744,21 +5944,65 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex-test": { - "version": "1.0.0", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sass": { - "version": "1.97.2", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -4776,39 +6020,17 @@ "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/scule": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "dev": true, "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4818,56 +6040,171 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "1.2.0", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.8.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4876,6 +6213,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4883,6 +6222,8 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -4891,7 +6232,9 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4900,12 +6243,16 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4914,18 +6261,59 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, - "node_modules/string.prototype.padend": { - "version": "3.1.4", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4935,44 +6323,46 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -4981,6 +6371,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -4991,29 +6383,35 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^9.0.1" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/supports-color": { - "version": "5.5.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -5025,6 +6423,8 @@ }, "node_modules/svgo": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, "license": "MIT", "dependencies": { @@ -5049,6 +6449,8 @@ }, "node_modules/svgo/node_modules/commander": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "license": "MIT", "engines": { @@ -5056,22 +6458,25 @@ } }, "node_modules/synckit": { - "version": "0.8.8", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/terser": { - "version": "5.44.1", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5087,13 +6492,10 @@ "node": ">=10" } }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/tinyexec": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { @@ -5102,6 +6504,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5117,6 +6521,8 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -5133,6 +6539,8 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5156,23 +6564,22 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16.13.0" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.6.2", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -5184,6 +6591,8 @@ }, "node_modules/type-fest": { "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -5193,14 +6602,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-array-length": { - "version": "1.0.4", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5208,6 +6682,8 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -5218,20 +6694,51 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/ufo": { - "version": "1.6.2", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "dev": true, "license": "MIT" }, "node_modules/unbox-primitive": { - "version": "1.0.2", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5239,6 +6746,8 @@ }, "node_modules/unctx": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz", + "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==", "dev": true, "license": "MIT", "dependencies": { @@ -5250,6 +6759,8 @@ }, "node_modules/unctx/node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -5258,6 +6769,8 @@ }, "node_modules/unctx/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5269,6 +6782,8 @@ }, "node_modules/unctx/node_modules/unplugin": { "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -5283,31 +6798,45 @@ }, "node_modules/undici-types": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/unimport": { - "version": "3.7.1", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "acorn": "^8.11.2", + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", - "fast-glob": "^3.3.2", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "mlly": "^1.4.2", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "scule": "^1.1.1", - "strip-literal": "^1.3.0", - "unplugin": "^1.5.1" + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" } }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unimport/node_modules/escape-string-regexp": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", "engines": { @@ -5319,36 +6848,86 @@ }, "node_modules/unimport/node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } }, - "node_modules/unplugin": { - "version": "1.6.0", + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.2", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.1" + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/unplugin-auto-import": { - "version": "0.17.5", + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", + "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", "dev": true, "license": "MIT", "dependencies": { - "@antfu/utils": "^0.7.7", + "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.0", "fast-glob": "^3.3.2", "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "minimatch": "^9.0.3", - "unimport": "^3.7.1", - "unplugin": "^1.6.0" + "magic-string": "^0.30.10", + "minimatch": "^9.0.4", + "unimport": "^3.7.2", + "unplugin": "^1.11.0" }, "engines": { "node": ">=14" @@ -5369,6 +6948,13 @@ } } }, + "node_modules/unplugin-auto-import/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/unplugin-auto-import/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -5380,11 +6966,13 @@ } }, "node_modules/unplugin-auto-import/node_modules/minimatch": { - "version": "9.0.3", + "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.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5395,6 +6983,8 @@ }, "node_modules/unplugin-element-plus": { "version": "0.11.2", + "resolved": "https://registry.npmjs.org/unplugin-element-plus/-/unplugin-element-plus-0.11.2.tgz", + "integrity": "sha512-jr88ePpv43h8cCmVW0SqM73sTD+g1n9Rmy4uMbTh+pSmceH9ZdKteWX9f+twC4aDlP3svdZuKMqLoUNBT2V6Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -5409,11 +6999,13 @@ } }, "node_modules/unplugin-element-plus/node_modules/@nuxt/kit": { - "version": "4.2.2", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.1.tgz", + "integrity": "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==", "dev": true, "license": "MIT", "dependencies": { - "c12": "^3.3.2", + "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", @@ -5426,20 +7018,29 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", - "rc9": "^2.1.2", + "rc9": "^3.0.0", "scule": "^1.3.0", - "semver": "^7.7.3", + "semver": "^7.7.4", "tinyglobby": "^0.2.15", - "ufo": "^1.6.1", - "unctx": "^2.4.1", + "ufo": "^1.6.3", + "unctx": "^2.5.0", "untyped": "^2.0.0" }, "engines": { "node": ">=18.12.0" } }, + "node_modules/unplugin-element-plus/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unplugin-element-plus/node_modules/escape-string-regexp": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", "engines": { @@ -5451,19 +7052,18 @@ }, "node_modules/unplugin-element-plus/node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/unplugin-element-plus/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, "node_modules/unplugin-element-plus/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5475,6 +7075,8 @@ }, "node_modules/unplugin-element-plus/node_modules/pkg-types": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "dev": true, "license": "MIT", "dependencies": { @@ -5485,6 +7087,8 @@ }, "node_modules/unplugin-element-plus/node_modules/unplugin": { "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -5499,6 +7103,8 @@ }, "node_modules/unplugin-vue-components": { "version": "0.26.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", + "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5533,6 +7139,13 @@ } } }, + "node_modules/unplugin-vue-components/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/unplugin-vue-components/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -5543,8 +7156,48 @@ "balanced-match": "^1.0.0" } }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/unplugin-vue-components/node_modules/local-pkg": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", "dev": true, "license": "MIT", "engines": { @@ -5555,11 +7208,13 @@ } }, "node_modules/unplugin-vue-components/node_modules/minimatch": { - "version": "9.0.3", + "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.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5568,8 +7223,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/untyped": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", + "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==", "dev": true, "license": "MIT", "dependencies": { @@ -5585,6 +7255,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5593,11 +7265,15 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5607,6 +7283,8 @@ }, "node_modules/vite": { "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -5680,6 +7358,8 @@ }, "node_modules/vite-svg-loader": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz", + "integrity": "sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==", "dev": true, "license": "MIT", "dependencies": { @@ -5691,6 +7371,8 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -5705,20 +7387,10 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5730,18 +7402,22 @@ }, "node_modules/vscode-uri": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "dev": true, "license": "MIT" }, "node_modules/vue": { - "version": "3.5.26", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-sfc": "3.5.26", - "@vue/runtime-dom": "3.5.26", - "@vue/server-renderer": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" @@ -5753,30 +7429,46 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.4.3", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": ">=6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/vue-router": { "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.4" @@ -5789,12 +7481,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.2.2", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", + "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.27", - "@vue/language-core": "3.2.2" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.5" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -5803,56 +7497,41 @@ "typescript": ">=5.0.0" } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, "license": "MIT" }, "node_modules/which": { - "version": "1.3.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -5861,13 +7540,89 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrappy": { - "version": "1.0.2", + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/xml-name-validator": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5876,6 +7631,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { diff --git a/web/frpc/package.json b/web/frpc/package.json index b41d048a..14eb419e 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -2,13 +2,14 @@ "name": "frpc-dashboard", "version": "0.0.1", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "run-p type-check build-only", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint --fix" }, "dependencies": { "element-plus": "^2.13.0", @@ -16,14 +17,13 @@ "vue-router": "^4.6.4" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.15.0", "@types/node": "24", "@vitejs/plugin-vue": "^6.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^8.56.0", + "eslint": "^9.39.0", "eslint-plugin-vue": "^9.33.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", @@ -37,4 +37,4 @@ "vite-svg-loader": "^5.1.0", "vue-tsc": "^3.2.2" } -} \ No newline at end of file +} diff --git a/web/frpc/src/App.vue b/web/frpc/src/App.vue index 2bfded92..44ebcbc9 100644 --- a/web/frpc/src/App.vue +++ b/web/frpc/src/App.vue @@ -65,6 +65,12 @@ const isDark = useDark() const currentRouteName = computed(() => { if (route.path === '/') return 'Overview' if (route.path === '/configure') return 'Configure' + if (route.path === '/proxies/create') return 'Create Proxy' + if (route.path.startsWith('/proxies/') && route.path.endsWith('/edit')) + return 'Edit Proxy' + if (route.path === '/visitors/create') return 'Create Visitor' + if (route.path.startsWith('/visitors/') && route.path.endsWith('/edit')) + return 'Edit Visitor' return '' }) diff --git a/web/frpc/src/api/frpc.ts b/web/frpc/src/api/frpc.ts index 63aeeb31..bacfdcdc 100644 --- a/web/frpc/src/api/frpc.ts +++ b/web/frpc/src/api/frpc.ts @@ -1,5 +1,11 @@ import { http } from './http' -import type { StatusResponse } from '../types/proxy' +import type { + StatusResponse, + StoreProxyListResp, + StoreProxyConfig, + StoreVisitorListResp, + StoreVisitorConfig, +} from '../types/proxy' export const getStatus = () => { return http.get('/api/status') @@ -16,3 +22,58 @@ export const putConfig = (content: string) => { export const reloadConfig = () => { return http.get('/api/reload') } + +// Store API - Proxies +export const listStoreProxies = () => { + return http.get('/api/store/proxies') +} + +export const getStoreProxy = (name: string) => { + return http.get( + `/api/store/proxies/${encodeURIComponent(name)}`, + ) +} + +export const createStoreProxy = (config: Record) => { + return http.post('/api/store/proxies', config) +} + +export const updateStoreProxy = (name: string, config: Record) => { + return http.put( + `/api/store/proxies/${encodeURIComponent(name)}`, + config, + ) +} + +export const deleteStoreProxy = (name: string) => { + return http.delete(`/api/store/proxies/${encodeURIComponent(name)}`) +} + +// Store API - Visitors +export const listStoreVisitors = () => { + return http.get('/api/store/visitors') +} + +export const getStoreVisitor = (name: string) => { + return http.get( + `/api/store/visitors/${encodeURIComponent(name)}`, + ) +} + +export const createStoreVisitor = (config: Record) => { + return http.post('/api/store/visitors', config) +} + +export const updateStoreVisitor = ( + name: string, + config: Record, +) => { + return http.put( + `/api/store/visitors/${encodeURIComponent(name)}`, + config, + ) +} + +export const deleteStoreVisitor = (name: string) => { + return http.delete(`/api/store/visitors/${encodeURIComponent(name)}`) +} diff --git a/web/frpc/src/components/KeyValueEditor.vue b/web/frpc/src/components/KeyValueEditor.vue new file mode 100644 index 00000000..3472ebb4 --- /dev/null +++ b/web/frpc/src/components/KeyValueEditor.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue index 70246f8c..f253ae15 100644 --- a/web/frpc/src/components/ProxyCard.vue +++ b/web/frpc/src/components/ProxyCard.vue @@ -1,23 +1,46 @@ @@ -68,10 +102,46 @@ v-for="proxy in filteredStatus" :key="proxy.name" :proxy="proxy" + @edit="handleEdit" + @delete="handleDelete" />
- +
+
+ + + + + + +
+

No proxies configured

+

+ Add proxies in your configuration file or use Store to create + dynamic proxies +

+ + Create First Proxy + +
@@ -90,7 +160,9 @@ v-for="(count, type) in proxyTypeCounts" :key="type" class="proxy-type-item" + :class="{ active: filterType === type }" v-show="count > 0" + @click="toggleTypeFilter(String(type))" >
{{ String(type).toUpperCase() }} @@ -125,6 +197,178 @@
+ + + + +
+ + +
+
+ + + + + + + + + +
+
+
+ {{ proxy.name }} + {{ + proxy.type.toUpperCase() + }} + Disabled +
+
+ + Edit + + + Delete + +
+
+
+ +

+ Edit a proxy and enable it to make it active again. +

+
+
+
+ + + + + + +
+
+
+
+ {{ visitor.name }} + {{ + visitor.type.toUpperCase() + }} +
+
+ + Edit + + + Delete + +
+
+
+ + Server: {{ visitor.config.serverName }} + + + Bind: {{ visitor.config.bindAddr || '127.0.0.1' + }} + +
+
+
+
+
+

No visitors configured

+

+ Create your first visitor to connect to secure proxies. +

+ + Create First Visitor + +
+
+
@@ -132,17 +376,37 @@ @@ -222,19 +626,22 @@ fetchData() .proxy-list-card, .types-card, -.status-summary-card { +.status-summary-card, +.store-status-card { border-radius: 12px; border: 1px solid #e4e7ed; } html.dark .proxy-list-card, html.dark .types-card, -html.dark .status-summary-card { +html.dark .status-summary-card, +html.dark .store-status-card { border-color: #3a3d5c; background: #27293d; } -.status-summary-card { +.status-summary-card, +.store-status-card { margin-top: 20px; } @@ -255,7 +662,7 @@ html.dark .status-summary-card { .header-actions { display: flex; align-items: center; - gap: 12px; + gap: 8px; } .card-title { @@ -268,8 +675,12 @@ html.dark .card-title { color: #e5e7eb; } +.filter-select { + width: 100px; +} + .search-input { - width: 200px; + width: 180px; } .proxy-list-content { @@ -282,8 +693,45 @@ html.dark .card-title { gap: 12px; } +/* Empty State */ .empty-state { - padding: 40px 0; + padding: 48px 24px; +} + +.empty-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.empty-icon { + width: 80px; + height: 80px; + margin-bottom: 20px; + color: #c0c4cc; +} + +html.dark .empty-icon { + color: #4b5563; +} + +.empty-text { + font-size: 16px; + font-weight: 500; + color: #606266; + margin: 0 0 8px; +} + +html.dark .empty-text { + color: #9ca3af; +} + +.empty-hint { + font-size: 14px; + color: #909399; + margin: 0 0 20px; + max-width: 320px; } /* Proxy Types Grid */ @@ -303,6 +751,7 @@ html.dark .card-title { background: #f8f9fa; border-radius: 8px; transition: all 0.2s; + cursor: pointer; } .proxy-type-item:hover { @@ -310,6 +759,19 @@ html.dark .card-title { transform: translateY(-2px); } +.proxy-type-item.active { + background: var(--el-color-primary-light-8); + box-shadow: 0 0 0 2px var(--el-color-primary-light-5); +} + +.proxy-type-item.active .proxy-type-name { + color: var(--el-color-primary); +} + +.proxy-type-item.active .proxy-type-count { + color: var(--el-color-primary); +} + html.dark .proxy-type-item { background: #1e1e2d; } @@ -318,6 +780,11 @@ html.dark .proxy-type-item:hover { background: #2a2a3c; } +html.dark .proxy-type-item.active { + background: var(--el-color-primary-dark-2); + box-shadow: 0 0 0 2px var(--el-color-primary); +} + .proxy-type-name { font-size: 11px; color: #909399; @@ -410,6 +877,213 @@ html.dark .status-item:hover { color: var(--el-text-color-primary); } +/* Store Status Card */ +.store-info { + min-height: 60px; +} + +.store-stat { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: linear-gradient( + 135deg, + rgba(102, 126, 234, 0.08) 0%, + rgba(118, 75, 162, 0.08) 100% + ); + border-radius: 8px; + margin-bottom: 12px; +} + +html.dark .store-stat { + background: linear-gradient( + 135deg, + rgba(129, 140, 248, 0.12) 0%, + rgba(167, 139, 250, 0.12) 100% + ); +} + +.store-stat-label { + font-size: 14px; + color: #606266; + font-weight: 500; +} + +html.dark .store-stat-label { + color: #9ca3af; +} + +.store-stat-value { + font-size: 24px; + font-weight: 600; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +html.dark .store-stat-value { + background: linear-gradient(135deg, #818cf8 0%, #a78bfa 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.store-hint { + font-size: 12px; + color: #909399; + margin: 0; + line-height: 1.5; +} + +.store-disabled-text { + font-size: 13px; + color: #909399; + margin: 0; + line-height: 1.6; +} + +/* Disabled Proxies Card */ +.disabled-proxies-card { + border-radius: 12px; + border: 1px solid #e4e7ed; + margin-top: 20px; +} + +html.dark .disabled-proxies-card { + border-color: #3a3d5c; + background: #27293d; +} + +.disabled-proxy-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.disabled-proxy-card { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: 8px; + background: #faf7f0; + border: 1px solid #f1d9a6; +} + +html.dark .disabled-proxy-card { + background: rgba(161, 98, 7, 0.14); + border-color: rgba(245, 158, 11, 0.45); +} + +.disabled-proxy-info { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.disabled-proxy-name { + font-size: 15px; + font-weight: 600; + color: #303133; +} + +html.dark .disabled-proxy-name { + color: #e5e7eb; +} + +.disabled-proxy-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.disabled-proxy-hint { + margin: 12px 2px 0; + font-size: 13px; + color: #909399; +} + +/* Visitors Card */ +.visitors-card { + border-radius: 12px; + border: 1px solid #e4e7ed; + margin-top: 20px; +} + +html.dark .visitors-card { + border-color: #3a3d5c; + background: #27293d; +} + +.visitor-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.visitor-card { + padding: 16px; + background: #f8f9fa; + border-radius: 8px; + transition: all 0.2s; +} + +.visitor-card:hover { + background: #f0f2f5; +} + +html.dark .visitor-card { + background: #1e1e2d; +} + +html.dark .visitor-card:hover { + background: #2a2a3c; +} + +.visitor-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.visitor-info { + display: flex; + align-items: center; + gap: 12px; +} + +.visitor-name { + font-size: 15px; + font-weight: 600; + color: #303133; +} + +html.dark .visitor-name { + color: #e5e7eb; +} + +.visitor-actions { + display: flex; + gap: 8px; +} + +.visitor-card-body { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: #606266; +} + +html.dark .visitor-card-body { + color: #9ca3af; +} + @media (max-width: 768px) { .card-header { flex-direction: column; @@ -432,10 +1106,21 @@ html.dark .status-item:hover { .proxy-types-grid { grid-template-columns: repeat(3, 1fr); } + + .disabled-proxy-card { + flex-direction: column; + align-items: flex-start; + } + + .disabled-proxy-actions { + width: 100%; + justify-content: flex-end; + } } @media (max-width: 992px) { - .status-summary-card { + .status-summary-card, + .store-status-card { margin-top: 0; } } diff --git a/web/frpc/src/views/ProxyEdit.vue b/web/frpc/src/views/ProxyEdit.vue new file mode 100644 index 00000000..e94dce88 --- /dev/null +++ b/web/frpc/src/views/ProxyEdit.vue @@ -0,0 +1,1238 @@ + + + + + + + diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue new file mode 100644 index 00000000..429ed4ad --- /dev/null +++ b/web/frpc/src/views/VisitorEdit.vue @@ -0,0 +1,606 @@ + + + + + + + diff --git a/web/frps/.eslintrc.cjs b/web/frps/.eslintrc.cjs deleted file mode 100644 index 70463782..00000000 --- a/web/frps/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - extends: [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/eslint-config-typescript', - '@vue/eslint-config-prettier', - ], - parserOptions: { - ecmaVersion: 'latest', - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - 'vue/multi-word-component-names': [ - 'error', - { - ignores: ['Traffic', 'Proxies', 'Clients'], - }, - ], - }, -} diff --git a/web/frps/eslint.config.js b/web/frps/eslint.config.js new file mode 100644 index 00000000..e13d4e48 --- /dev/null +++ b/web/frps/eslint.config.js @@ -0,0 +1,36 @@ +import pluginVue from 'eslint-plugin-vue' +import vueTsEslintConfig from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default [ + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + { + name: 'app/files-to-ignore', + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], + }, + ...pluginVue.configs['flat/essential'], + ...vueTsEslintConfig(), + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'vue/multi-word-component-names': [ + 'error', + { + ignores: ['Traffic', 'Proxies', 'Clients'], + }, + ], + }, + }, + skipFormatting, +] diff --git a/web/frps/package-lock.json b/web/frps/package-lock.json index 07c3ad48..287498dd 100644 --- a/web/frps/package-lock.json +++ b/web/frps/package-lock.json @@ -13,14 +13,13 @@ "vue-router": "^4.6.4" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.15.0", "@types/node": "24", "@vitejs/plugin-vue": "^6.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^8.56.0", + "eslint": "^9.39.0", "eslint-plugin-vue": "^9.33.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", @@ -35,16 +34,6 @@ "vue-tsc": "^3.2.2" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@antfu/utils": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", @@ -74,12 +63,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -89,9 +78,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -102,9 +91,9 @@ } }, "node_modules/@ctrl/tinycolor": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz", - "integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "license": "MIT", "engines": { "node": ">=10" @@ -120,9 +109,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -137,9 +126,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -154,9 +143,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -171,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -188,9 +177,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -205,9 +194,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -222,9 +211,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -239,9 +228,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -256,9 +245,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -273,9 +262,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -290,9 +279,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -307,9 +296,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -324,9 +313,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -341,9 +330,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -358,9 +347,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -375,9 +364,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -392,9 +381,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -409,9 +398,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -426,9 +415,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -443,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -460,9 +449,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -477,9 +466,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -494,9 +483,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -511,9 +500,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -528,9 +517,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -545,9 +534,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -562,122 +551,245 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.1.tgz", - "integrity": "sha512-PL7g3dhA4dHgZfujkuD8Q+tfJJynEtnNQSPzmucCnxMvkxf4cLBJw/ZYqZUn4HCh33U3WHrAfv2R2tbi9UCSmw==", - "license": "MIT" - }, - "node_modules/@floating-ui/dom": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", - "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.1.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.2" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -694,13 +806,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", @@ -801,18 +919,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -822,25 +940,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -859,9 +977,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -880,9 +998,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -901,9 +1019,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -922,9 +1040,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -943,9 +1061,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -964,9 +1082,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -985,9 +1103,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -1006,9 +1124,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -1027,9 +1145,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -1048,9 +1166,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -1069,9 +1187,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1090,9 +1208,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1110,24 +1228,38 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", - "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1135,9 +1267,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "dev": true, "license": "MIT" }, @@ -1164,10 +1296,23 @@ } } }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1179,9 +1324,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1193,9 +1338,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1207,9 +1352,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1221,9 +1366,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1235,9 +1380,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1249,9 +1394,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1263,9 +1408,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1277,9 +1422,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1291,9 +1436,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1305,9 +1450,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1319,9 +1464,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1333,9 +1478,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1347,9 +1492,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1361,9 +1506,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1375,9 +1520,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1389,9 +1534,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1403,9 +1548,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1417,9 +1562,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1431,9 +1576,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1445,9 +1590,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1459,9 +1604,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1473,9 +1618,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1487,9 +1632,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1501,9 +1646,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1514,13 +1659,6 @@ "win32" ] }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", - "dev": true, - "license": "MIT" - }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -1546,9 +1684,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", "license": "MIT" }, "node_modules/@types/lodash-es": { @@ -1561,22 +1699,15 @@ } }, "node_modules/@types/node": { - "version": "24.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", - "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", @@ -1585,124 +1716,159 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1710,109 +1876,96 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1823,68 +1976,80 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", - "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.27" + "@volar/source-map": "2.4.28" } }, "node_modules/@volar/source-map": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz", - "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz", - "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", - "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@vue/compiler-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", - "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", - "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.26", - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -1892,13 +2057,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", - "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/devtools-api": { @@ -1908,38 +2073,39 @@ "license": "MIT" }, "node_modules/@vue/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0" + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { - "eslint": ">= 8.0.0", + "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "node_modules/@vue/eslint-config-typescript": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", - "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", + "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "vue-eslint-parser": "^9.3.1" + "@typescript-eslint/utils": "^8.56.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.56.0", + "vue-eslint-parser": "^10.4.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", - "eslint-plugin-vue": "^9.0.0", - "typescript": "*" + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" }, "peerDependenciesMeta": { "typescript": { @@ -1948,13 +2114,13 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.2.tgz", - "integrity": "sha512-5DAuhxsxBN9kbriklh3Q5AMaJhyOCNiQJvCskN9/30XOpdLiqZU9Q+WvjArP17ubdGEyZtBzlIeG5nIjEbNOrQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", + "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", @@ -1963,54 +2129,67 @@ "picomatch": "^4.0.2" } }, + "node_modules/@vue/language-core/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@vue/reactivity": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", - "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.26" + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", - "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", - "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", - "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { - "vue": "3.5.26" + "vue": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", - "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -2033,15 +2212,15 @@ } }, "node_modules/@vueuse/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", - "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -2051,9 +2230,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", - "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", "dev": true, "license": "MIT", "funding": { @@ -2061,9 +2240,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", - "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", "dev": true, "license": "MIT", "funding": { @@ -2074,9 +2253,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2086,10 +2265,20 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2110,27 +2299,20 @@ "dev": true, "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { @@ -2147,19 +2329,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2167,14 +2336,53 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/async-validator": { @@ -2184,11 +2392,14 @@ "license": "MIT" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2197,11 +2408,14 @@ } }, "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==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -2224,13 +2438,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2298,6 +2515,36 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/c12/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/c12/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/c12/node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/c12/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -2313,14 +2560,50 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2337,79 +2620,36 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/chokidar/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" } }, "node_modules/citty": { @@ -2423,19 +2663,22 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, @@ -2454,9 +2697,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, "license": "MIT" }, @@ -2471,30 +2714,18 @@ } }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">= 8" } }, "node_modules/css-select": { @@ -2596,6 +2827,60 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -2627,13 +2912,32 @@ "dev": true, "license": "MIT" }, - "node_modules/define-properties": { + "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -2659,55 +2963,16 @@ "license": "MIT" }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/dir-glob/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2723,19 +2988,6 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -2781,9 +3033,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2793,10 +3045,25 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/element-plus": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz", - "integrity": "sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.3.tgz", + "integrity": "sha512-RwLVtFpeHjZ4UCtHxVi1/sGR2cr2xOL7hqWa7qJc/+gdO6QavVG8Nw1C647obCb3tIg2ztMhNbIIjZUv+6z1og==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.4.1", @@ -2808,8 +3075,8 @@ "@vueuse/core": "^10.11.0", "async-validator": "^4.2.5", "dayjs": "^1.11.19", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", "normalize-wheel-es": "^1.2.0" @@ -2839,6 +3106,32 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/element-plus/node_modules/@vueuse/metadata": { "version": "10.11.1", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", @@ -2860,10 +3153,37 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -2873,9 +3193,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2890,45 +3210,66 @@ "license": "MIT" }, "node_modules/es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2937,6 +3278,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", @@ -2944,31 +3305,45 @@ "dev": true, "license": "MIT" }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2978,9 +3353,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2991,126 +3366,132 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3121,7 +3502,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3156,6 +3537,41 @@ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, + "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-plugin-vue/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3198,9 +3614,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3208,7 +3624,10 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -3224,262 +3643,85 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/eslint/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/eslint/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "node": "*" } }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3543,9 +3785,9 @@ "license": "MIT" }, "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, "license": "Apache-2.0" }, @@ -3566,6 +3808,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3590,35 +3845,17 @@ "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -3652,48 +3889,48 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -3704,23 +3941,28 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -3739,30 +3981,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3789,64 +4066,41 @@ "giget": "dist/cli.mjs" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -3855,106 +4109,71 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3963,9 +4182,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -3976,13 +4195,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4004,16 +4223,6 @@ "node": ">= 0.4" } }, - "node_modules/hasown/node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4022,9 +4231,9 @@ "license": "ISC" }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -4039,9 +4248,9 @@ "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4065,49 +4274,34 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4120,14 +4314,37 @@ "dev": true, "license": "MIT" }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4147,14 +4364,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4192,14 +4409,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4218,6 +4454,42 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4231,10 +4503,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -4255,13 +4540,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4270,25 +4556,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4297,27 +4575,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4327,13 +4622,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4343,17 +4640,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4362,19 +4655,59 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4412,6 +4745,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4433,6 +4773,16 @@ "dev": true, "license": "MIT" }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -4481,11 +4831,15 @@ } }, "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", "dev": true, "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, "engines": { "node": ">=14" }, @@ -4548,6 +4902,16 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -4594,41 +4958,20 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, "engines": { - "node": ">=8.6" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { @@ -4644,25 +4987,6 @@ "ufo": "^1.6.1" } }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4789,6 +5113,188 @@ "node": ">= 4" } }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4803,31 +5309,39 @@ } }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4843,15 +5357,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -4868,34 +5384,42 @@ "dev": true, "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4972,24 +5496,14 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -5020,27 +5534,26 @@ "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5070,15 +5583,25 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/postcss": { @@ -5123,12 +5646,6 @@ "node": ">=4" } }, - "node_modules/postcss/node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5140,9 +5657,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -5156,9 +5673,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -5169,9 +5686,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -5217,14 +5734,14 @@ "license": "MIT" }, "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.0.tgz", + "integrity": "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA==", "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", - "destr": "^2.0.3" + "destr": "^2.0.5" } }, "node_modules/read-pkg": { @@ -5256,16 +5773,42 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5316,22 +5859,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rolldown-string": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/rolldown-string/-/rolldown-string-0.2.1.tgz", @@ -5349,9 +5876,9 @@ } }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5365,31 +5892,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -5417,25 +5944,65 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sass": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", - "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -5453,22 +6020,6 @@ "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -5477,9 +6028,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5489,62 +6040,165 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", - "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/source-map": { @@ -5578,9 +6232,9 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5589,9 +6243,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, @@ -5607,22 +6261,59 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, - "node_modules/string.prototype.padend": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", - "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5632,48 +6323,42 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/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-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5711,16 +6396,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -5773,26 +6458,25 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5808,13 +6492,6 @@ "node": ">=10" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -5842,6 +6519,37 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5856,25 +6564,18 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16.13.0" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5901,16 +6602,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5930,24 +6694,51 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/ufo": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", - "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "dev": true, "license": "MIT" }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5976,6 +6767,19 @@ "@types/estree": "^1.0.0" } }, + "node_modules/unctx/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/unctx/node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -6023,12 +6827,25 @@ } }, "node_modules/unimport/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "dev": true, "license": "MIT" }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unimport/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -6057,13 +6874,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/unimport/node_modules/local-pkg/node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -6076,16 +6886,17 @@ "pathe": "^2.0.3" } }, - "node_modules/unimport/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/unplugin": { @@ -6137,38 +6948,31 @@ } } }, - "node_modules/unplugin-auto-import/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/unplugin-auto-import/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/unplugin-auto-import/node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "node_modules/unplugin-auto-import/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "balanced-match": "^1.0.0" } }, "node_modules/unplugin-auto-import/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6177,18 +6981,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/unplugin-auto-import/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/unplugin-element-plus": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/unplugin-element-plus/-/unplugin-element-plus-0.11.2.tgz", @@ -6207,13 +6999,13 @@ } }, "node_modules/unplugin-element-plus/node_modules/@nuxt/kit": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz", - "integrity": "sha512-ZAgYBrPz/yhVgDznBNdQj2vhmOp31haJbO0I0iah/P9atw+OHH7NJLUZ3PK+LOz/0fblKTN1XJVSi8YQ1TQ0KA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.1.tgz", + "integrity": "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==", "dev": true, "license": "MIT", "dependencies": { - "c12": "^3.3.2", + "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", @@ -6226,18 +7018,38 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", - "rc9": "^2.1.2", + "rc9": "^3.0.0", "scule": "^1.3.0", - "semver": "^7.7.3", + "semver": "^7.7.4", "tinyglobby": "^0.2.15", - "ufo": "^1.6.1", - "unctx": "^2.4.1", + "ufo": "^1.6.3", + "unctx": "^2.5.0", "untyped": "^2.0.0" }, "engines": { "node": ">=18.12.0" } }, + "node_modules/unplugin-element-plus/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin-element-plus/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unplugin-element-plus/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -6248,6 +7060,31 @@ "node": ">= 4" } }, + "node_modules/unplugin-element-plus/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-element-plus/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/unplugin-element-plus/node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -6302,14 +7139,82 @@ } } }, - "node_modules/unplugin-vue-components/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/unplugin-vue-components/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/unplugin-vue-components/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-vue-components/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" @@ -6318,6 +7223,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/untyped": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", @@ -6364,9 +7282,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -6451,6 +7369,37 @@ "vue": ">=3.2.13" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -6459,16 +7408,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", - "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-sfc": "3.5.26", - "@vue/runtime-dom": "3.5.26", - "@vue/server-renderer": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" @@ -6479,55 +7428,41 @@ } } }, - "node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": ">=6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/vue-router": { @@ -6546,14 +7481,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.2.tgz", - "integrity": "sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", + "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.27", - "@vue/language-core": "3.2.2" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.5" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -6570,48 +7505,33 @@ "license": "MIT" }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -6620,12 +7540,84 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/xml-name-validator": { "version": "4.0.0", diff --git a/web/frps/package.json b/web/frps/package.json index cbb0f578..c44c0dd4 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -2,13 +2,14 @@ "name": "frps-dashboard", "version": "0.0.1", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "run-p type-check build-only", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint --fix" }, "dependencies": { "element-plus": "^2.13.0", @@ -16,14 +17,13 @@ "vue-router": "^4.6.4" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.15.0", "@types/node": "24", "@vitejs/plugin-vue": "^6.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "@vueuse/core": "^14.1.0", - "eslint": "^8.56.0", + "eslint": "^9.39.0", "eslint-plugin-vue": "^9.33.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", diff --git a/web/frps/src/components/ClientCard.vue b/web/frps/src/components/ClientCard.vue index d44ef36a..531a210f 100644 --- a/web/frps/src/components/ClientCard.vue +++ b/web/frps/src/components/ClientCard.vue @@ -13,6 +13,9 @@ {{ client.hostname }} + v{{ client.version }}
diff --git a/web/frps/src/components/Traffic.vue b/web/frps/src/components/Traffic.vue index 04fffa6c..9142f3e0 100644 --- a/web/frps/src/components/Traffic.vue +++ b/web/frps/src/components/Traffic.vue @@ -86,7 +86,7 @@ const processData = (trafficIn: number[], trafficOut: number[]) => { // Calculate dates (last 7 days ending today) const dates: string[] = [] - let d = new Date() + const d = new Date() d.setDate(d.getDate() - 6) for (let i = 0; i < 7; i++) { diff --git a/web/frps/src/types/client.ts b/web/frps/src/types/client.ts index e33c4479..31d846a7 100644 --- a/web/frps/src/types/client.ts +++ b/web/frps/src/types/client.ts @@ -3,6 +3,7 @@ export interface ClientInfoData { user: string clientID: string runID: string + version?: string hostname: string clientIP?: string metas?: Record diff --git a/web/frps/src/types/proxy.ts b/web/frps/src/types/proxy.ts index eec1cb43..f5bc8e65 100644 --- a/web/frps/src/types/proxy.ts +++ b/web/frps/src/types/proxy.ts @@ -3,7 +3,6 @@ export interface ProxyStatsInfo { conf: any user: string clientID: string - clientVersion: string todayTrafficIn: number todayTrafficOut: number curConns: number diff --git a/web/frps/src/utils/client.ts b/web/frps/src/utils/client.ts index 272361a1..d7f95a3f 100644 --- a/web/frps/src/utils/client.ts +++ b/web/frps/src/utils/client.ts @@ -6,6 +6,7 @@ export class Client { user: string clientID: string runID: string + version: string hostname: string ip: string metas: Map @@ -19,6 +20,7 @@ export class Client { this.user = data.user this.clientID = data.clientID this.runID = data.runID + this.version = data.version || '' this.hostname = data.hostname this.ip = data.clientIP || '' this.metas = new Map() diff --git a/web/frps/src/utils/proxy.ts b/web/frps/src/utils/proxy.ts index a2285c62..a883f097 100644 --- a/web/frps/src/utils/proxy.ts +++ b/web/frps/src/utils/proxy.ts @@ -12,7 +12,6 @@ class BaseProxy { status: string user: string clientID: string - clientVersion: string addr: string port: number @@ -49,7 +48,6 @@ class BaseProxy { this.status = proxyStats.status this.user = proxyStats.user || '' this.clientID = proxyStats.clientID || '' - this.clientVersion = proxyStats.clientVersion this.addr = '' this.port = 0 diff --git a/web/frps/src/views/ClientDetail.vue b/web/frps/src/views/ClientDetail.vue index aa064579..1468a243 100644 --- a/web/frps/src/views/ClientDetail.vue +++ b/web/frps/src/views/ClientDetail.vue @@ -22,7 +22,12 @@ {{ client.displayName.charAt(0).toUpperCase() }}
-

{{ client.displayName }}

+
+

{{ client.displayName }}

+ v{{ client.version }} +
{{ client.ip @@ -354,11 +359,18 @@ onMounted(() => { min-width: 0; } +.client-name-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} + .client-name { font-size: 20px; font-weight: 500; color: var(--text-primary); - margin: 0 0 4px 0; + margin: 0; line-height: 1.3; } diff --git a/web/frps/src/views/ServerOverview.vue b/web/frps/src/views/ServerOverview.vue index 9d1493ad..ed3c4a64 100644 --- a/web/frps/src/views/ServerOverview.vue +++ b/web/frps/src/views/ServerOverview.vue @@ -230,7 +230,7 @@ const fetchData = async () => { data.value.proxyCounts += count || 0 }) } - } catch (err) { + } catch { ElMessage({ showClose: true, message: 'Get server info from frps failed!', From 381245a4399e1e20e841f7b977305b46e69a856e Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 2 Mar 2026 01:32:19 +0800 Subject: [PATCH 05/43] build: add noweb tag to allow building without frontend assets (#5189) --- Makefile | 21 +++++++++++---------- Release.md | 1 + assets/assets.go | 13 +++++++++++-- pkg/util/version/version.go | 2 +- web/frpc/embed.go | 2 ++ web/frpc/embed_stub.go | 3 +++ web/frps/embed.go | 2 ++ web/frps/embed_stub.go | 3 +++ 8 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 web/frpc/embed_stub.go create mode 100644 web/frps/embed_stub.go diff --git a/Makefile b/Makefile index a4df78f7..26e40b80 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ export PATH := $(PATH):`go env GOPATH`/bin export GO111MODULE=on LDFLAGS := -s -w +NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb') .PHONY: web frps-web frpc-web frps frpc @@ -28,23 +29,23 @@ fmt-more: gci: gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./ -vet: web - go vet ./... +vet: + go vet -tags "$(NOWEB_TAG)" ./... frps: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps frpc: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc test: gotest -gotest: web - go test -v --cover ./assets/... - go test -v --cover ./cmd/... - go test -v --cover ./client/... - go test -v --cover ./server/... - go test -v --cover ./pkg/... +gotest: + go test -tags "$(NOWEB_TAG)" -v --cover ./assets/... + go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/... + go test -tags "$(NOWEB_TAG)" -v --cover ./client/... + go test -tags "$(NOWEB_TAG)" -v --cover ./server/... + go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/... e2e: ./hack/run-e2e.sh diff --git a/Release.md b/Release.md index 5aaf921f..c4425976 100644 --- a/Release.md +++ b/Release.md @@ -5,3 +5,4 @@ ## Improvements * Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic. +* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded. diff --git a/assets/assets.go b/assets/assets.go index 2d7a164e..713087ad 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -29,14 +29,23 @@ var ( prefixPath string ) +type emptyFS struct{} + +func (emptyFS) Open(name string) (http.File, error) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} + // if path is empty, load assets in memory // or set FileSystem using disk files func Load(path string) { prefixPath = path - if prefixPath != "" { + switch { + case prefixPath != "": FileSystem = http.Dir(prefixPath) - } else { + case content != nil: FileSystem = http.FS(content) + default: + FileSystem = emptyFS{} } } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 4a601f32..c98b7cfb 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.67.0" +var version = "0.68.0" func Full() string { return version diff --git a/web/frpc/embed.go b/web/frpc/embed.go index ce06ff90..77b4863e 100644 --- a/web/frpc/embed.go +++ b/web/frpc/embed.go @@ -1,3 +1,5 @@ +//go:build !noweb + package frpc import ( diff --git a/web/frpc/embed_stub.go b/web/frpc/embed_stub.go new file mode 100644 index 00000000..591a770a --- /dev/null +++ b/web/frpc/embed_stub.go @@ -0,0 +1,3 @@ +//go:build noweb + +package frpc diff --git a/web/frps/embed.go b/web/frps/embed.go index 6bdc475f..a1264d7c 100644 --- a/web/frps/embed.go +++ b/web/frps/embed.go @@ -1,3 +1,5 @@ +//go:build !noweb + package frps import ( diff --git a/web/frps/embed_stub.go b/web/frps/embed_stub.go new file mode 100644 index 00000000..35c9d9c8 --- /dev/null +++ b/web/frps/embed_stub.go @@ -0,0 +1,3 @@ +//go:build noweb + +package frps From fbeb6ca43affc8878a074a08d463644aad59cf15 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 4 Mar 2026 17:38:43 +0800 Subject: [PATCH 06/43] refactor: restructure API packages into client/http and server/http with typed proxy/visitor models (#5193) --- client/{admin_api.go => api_router.go} | 6 +- client/config_manager.go | 123 +++++++--- client/config_manager_test.go | 11 +- client/configmgmt/types.go | 8 +- client/{api => http}/controller.go | 175 +++++++------- client/{api => http}/controller_test.go | 295 +++++++++++++++++------- client/http/model/proxy_definition.go | 148 ++++++++++++ client/{api => http/model}/types.go | 20 +- client/http/model/visitor_definition.go | 107 +++++++++ pkg/config/load.go | 69 +++--- pkg/config/load_test.go | 25 ++ pkg/config/source/store.go | 52 +++-- pkg/config/source/store_test.go | 29 +-- pkg/config/v1/common.go | 25 -- pkg/config/v1/decode.go | 195 ++++++++++++++++ pkg/config/v1/decode_test.go | 86 +++++++ pkg/config/v1/proxy.go | 30 +-- pkg/config/v1/proxy_plugin.go | 39 +--- pkg/config/v1/visitor.go | 30 +-- pkg/config/v1/visitor_plugin.go | 40 +--- pkg/sdk/client/client.go | 10 +- pkg/util/jsonx/json_v1.go | 45 ++++ pkg/util/jsonx/raw_message.go | 36 +++ server/api_router.go | 64 +++++ server/{api => http}/controller.go | 133 ++++------- server/http/controller_test.go | 71 ++++++ server/{api => http/model}/types.go | 2 +- server/service.go | 41 ---- test/e2e/v1/features/store.go | 135 +++++++++-- web/frpc/src/api/frpc.ts | 32 +-- web/frpc/src/types/proxy.ts | 288 +++++++++++++++-------- web/frpc/src/views/Overview.vue | 61 +++-- 32 files changed, 1704 insertions(+), 727 deletions(-) rename client/{admin_api.go => api_router.go} (95%) rename client/{api => http}/controller.go (68%) rename client/{api => http}/controller_test.go (52%) create mode 100644 client/http/model/proxy_definition.go rename client/{api => http/model}/types.go (71%) create mode 100644 client/http/model/visitor_definition.go create mode 100644 pkg/config/v1/decode.go create mode 100644 pkg/config/v1/decode_test.go create mode 100644 pkg/util/jsonx/json_v1.go create mode 100644 pkg/util/jsonx/raw_message.go create mode 100644 server/api_router.go rename server/{api => http}/controller.go (71%) create mode 100644 server/http/controller_test.go rename server/{api => http/model}/types.go (99%) diff --git a/client/admin_api.go b/client/api_router.go similarity index 95% rename from client/admin_api.go rename to client/api_router.go index 1343931d..ceab4fbc 100644 --- a/client/admin_api.go +++ b/client/api_router.go @@ -17,7 +17,7 @@ package client import ( "net/http" - "github.com/fatedier/frp/client/api" + adminapi "github.com/fatedier/frp/client/http" "github.com/fatedier/frp/client/proxy" httppkg "github.com/fatedier/frp/pkg/util/http" netpkg "github.com/fatedier/frp/pkg/util/net" @@ -65,9 +65,9 @@ func healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) } -func newAPIController(svr *Service) *api.Controller { +func newAPIController(svr *Service) *adminapi.Controller { manager := newServiceConfigManager(svr) - return api.NewController(api.ControllerParams{ + return adminapi.NewController(adminapi.ControllerParams{ ServerAddr: svr.common.ServerAddr, Manager: manager, }) diff --git a/client/config_manager.go b/client/config_manager.go index 0ed6b3c2..1d3fb0ec 100644 --- a/client/config_manager.go +++ b/client/config_manager.go @@ -133,12 +133,13 @@ func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, e return cfg, nil } -func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error { +func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { if err := m.validateStoreProxyConfigurer(cfg); err != nil { - return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) } - if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + name := cfg.GetBaseConfig().Name + persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error { if err := storeSource.AddProxy(cfg); err != nil { if errors.Is(err, source.ErrAlreadyExists) { return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) @@ -146,30 +147,30 @@ func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error { return err } return nil - }); err != nil { - return err + }) + if err != nil { + return nil, err } - - log.Infof("store: created proxy %q", cfg.GetBaseConfig().Name) - return nil + log.Infof("store: created proxy %q", name) + return persisted, nil } -func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error { +func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { if name == "" { - return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) } if cfg == nil { - return fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument) } bodyName := cfg.GetBaseConfig().Name if bodyName != name { - return fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument) } if err := m.validateStoreProxyConfigurer(cfg); err != nil { - return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) } - if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error { if err := storeSource.UpdateProxy(cfg); err != nil { if errors.Is(err, source.ErrNotFound) { return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) @@ -177,12 +178,13 @@ func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigu return err } return nil - }); err != nil { - return err + }) + if err != nil { + return nil, err } log.Infof("store: updated proxy %q", name) - return nil + return persisted, nil } func (m *serviceConfigManager) DeleteStoreProxy(name string) error { @@ -231,12 +233,13 @@ func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigure return cfg, nil } -func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error { +func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { if err := m.validateStoreVisitorConfigurer(cfg); err != nil { - return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) } - if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + name := cfg.GetBaseConfig().Name + persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error { if err := storeSource.AddVisitor(cfg); err != nil { if errors.Is(err, source.ErrAlreadyExists) { return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) @@ -244,30 +247,31 @@ func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) erro return err } return nil - }); err != nil { - return err + }) + if err != nil { + return nil, err } - log.Infof("store: created visitor %q", cfg.GetBaseConfig().Name) - return nil + log.Infof("store: created visitor %q", name) + return persisted, nil } -func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error { +func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { if name == "" { - return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) } if cfg == nil { - return fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument) } bodyName := cfg.GetBaseConfig().Name if bodyName != name { - return fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument) + return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument) } if err := m.validateStoreVisitorConfigurer(cfg); err != nil { - return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) } - if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error { if err := storeSource.UpdateVisitor(cfg); err != nil { if errors.Is(err, source.ErrNotFound) { return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) @@ -275,12 +279,13 @@ func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorCon return err } return nil - }); err != nil { - return err + }) + if err != nil { + return nil, err } log.Infof("store: updated visitor %q", name) - return nil + return persisted, nil } func (m *serviceConfigManager) DeleteStoreVisitor(name string) error { @@ -340,6 +345,58 @@ func (m *serviceConfigManager) withStoreMutationAndReload( return nil } +func (m *serviceConfigManager) withStoreProxyMutationAndReload( + name string, + fn func(storeSource *source.StoreSource) error, +) (v1.ProxyConfigurer, error) { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return nil, err + } + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + + persisted := storeSource.GetProxy(name) + if persisted == nil { + return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name) + } + return persisted.Clone(), nil +} + +func (m *serviceConfigManager) withStoreVisitorMutationAndReload( + name string, + fn func(storeSource *source.StoreSource) error, +) (v1.VisitorConfigurer, error) { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return nil, err + } + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + + persisted := storeSource.GetVisitor(name) + if persisted == nil { + return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name) + } + return persisted.Clone(), nil +} + func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error { if cfg == nil { return fmt.Errorf("invalid proxy config") diff --git a/client/config_manager_test.go b/client/config_manager_test.go index 13152497..07ae3297 100644 --- a/client/config_manager_test.go +++ b/client/config_manager_test.go @@ -45,7 +45,7 @@ func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) { }, } - err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + _, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) if err == nil { t.Fatal("expected conflict error") } @@ -69,7 +69,7 @@ func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testin }, } - err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + _, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) if err == nil { t.Fatal("expected apply config error") } @@ -88,7 +88,7 @@ func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) { }, } - err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + _, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) if err == nil { t.Fatal("expected store disabled error") } @@ -116,10 +116,13 @@ func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *te }, } - err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy")) + persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy")) if err != nil { t.Fatalf("create store proxy: %v", err) } + if persisted == nil { + t.Fatal("expected persisted proxy to be returned") + } got := storeSource.GetProxy("raw-proxy") if got == nil { diff --git a/client/configmgmt/types.go b/client/configmgmt/types.go index 5da75fb8..090d0b7b 100644 --- a/client/configmgmt/types.go +++ b/client/configmgmt/types.go @@ -28,14 +28,14 @@ type ConfigManager interface { ListStoreProxies() ([]v1.ProxyConfigurer, error) GetStoreProxy(name string) (v1.ProxyConfigurer, error) - CreateStoreProxy(cfg v1.ProxyConfigurer) error - UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error + CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) DeleteStoreProxy(name string) error ListStoreVisitors() ([]v1.VisitorConfigurer, error) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) - CreateStoreVisitor(cfg v1.VisitorConfigurer) error - UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error + CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) DeleteStoreVisitor(name string) error GracefulClose(d time.Duration) diff --git a/client/api/controller.go b/client/http/controller.go similarity index 68% rename from client/api/controller.go rename to client/http/controller.go index 2ba44216..d06396dc 100644 --- a/client/api/controller.go +++ b/client/http/controller.go @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package api +package http import ( "cmp" - "encoding/json" "errors" "fmt" "net" @@ -26,9 +25,10 @@ import ( "time" "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/http/model" "github.com/fatedier/frp/client/proxy" - v1 "github.com/fatedier/frp/pkg/config/v1" httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/pkg/util/jsonx" ) // Controller handles HTTP API requests for frpc. @@ -67,15 +67,6 @@ func (c *Controller) toHTTPError(err error) error { return httppkg.NewError(code, err.Error()) } -// TODO(fatedier): Remove this lock wrapper after migrating typed config -// decoding to encoding/json/v2 with per-call options. -// TypedProxyConfig/TypedVisitorConfig currently read global strictness state. -func unmarshalTypedConfig[T any](body []byte, out *T) error { - return v1.WithDisallowUnknownFields(false, func() error { - return json.Unmarshal(body, out) - }) -} - // Reload handles GET /api/reload func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { strictConfigMode := false @@ -98,7 +89,7 @@ func (c *Controller) Stop(ctx *httppkg.Context) (any, error) { // Status handles GET /api/status func (c *Controller) Status(ctx *httppkg.Context) (any, error) { - res := make(StatusResp) + res := make(model.StatusResp) ps := c.manager.GetProxyStatus() if ps == nil { return res, nil @@ -112,7 +103,7 @@ func (c *Controller) Status(ctx *httppkg.Context) (any, error) { if len(arrs) <= 1 { continue } - slices.SortFunc(arrs, func(a, b ProxyStatusResp) int { + slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int { return cmp.Compare(a.Name, b.Name) }) } @@ -145,8 +136,8 @@ func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) { return nil, nil } -func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp { - psr := ProxyStatusResp{ +func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp { + psr := model.ProxyStatusResp{ Name: status.Name, Type: status.Type, Status: status.Phase, @@ -166,7 +157,7 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStat } if c.manager.IsStoreProxyEnabled(status.Name) { - psr.Source = SourceStore + psr.Source = model.SourceStore } return psr } @@ -177,18 +168,17 @@ func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) { return nil, c.toHTTPError(err) } - resp := ProxyListResp{Proxies: make([]ProxyConfig, 0, len(proxies))} + resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))} for _, p := range proxies { - cfg, err := configurerToMap(p) + payload, err := model.ProxyDefinitionFromConfigurer(p) if err != nil { - continue + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) } - resp.Proxies = append(resp.Proxies, ProxyConfig{ - Name: p.GetBaseConfig().Name, - Type: p.GetBaseConfig().Type, - Config: cfg, - }) + resp.Proxies = append(resp.Proxies, payload) } + slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int { + return cmp.Compare(a.Name, b.Name) + }) return resp, nil } @@ -203,16 +193,12 @@ func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) { return nil, c.toHTTPError(err) } - cfg, err := configurerToMap(p) + payload, err := model.ProxyDefinitionFromConfigurer(p) if err != nil { return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) } - return ProxyConfig{ - Name: p.GetBaseConfig().Name, - Type: p.GetBaseConfig().Type, - Config: cfg, - }, nil + return payload, nil } func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) { @@ -221,19 +207,28 @@ func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) } - var typed v1.TypedProxyConfig - if err := unmarshalTypedConfig(body, &typed); err != nil { + var payload model.ProxyDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) } - if typed.ProxyConfigurer == nil { - return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required") + if err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) } - - if err := c.manager.CreateStoreProxy(typed.ProxyConfigurer); err != nil { + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + created, err := c.manager.CreateStoreProxy(cfg) + if err != nil { return nil, c.toHTTPError(err) } - return nil, nil + + resp, err := model.ProxyDefinitionFromConfigurer(created) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil } func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) { @@ -247,19 +242,28 @@ func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) } - var typed v1.TypedProxyConfig - if err := unmarshalTypedConfig(body, &typed); err != nil { + var payload model.ProxyDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) } - if typed.ProxyConfigurer == nil { - return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required") + if err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) } - - if err := c.manager.UpdateStoreProxy(name, typed.ProxyConfigurer); err != nil { + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + updated, err := c.manager.UpdateStoreProxy(name, cfg) + if err != nil { return nil, c.toHTTPError(err) } - return nil, nil + + resp, err := model.ProxyDefinitionFromConfigurer(updated) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil } func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) { @@ -280,18 +284,17 @@ func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) { return nil, c.toHTTPError(err) } - resp := VisitorListResp{Visitors: make([]VisitorConfig, 0, len(visitors))} + resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))} for _, v := range visitors { - cfg, err := configurerToMap(v) + payload, err := model.VisitorDefinitionFromConfigurer(v) if err != nil { - continue + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) } - resp.Visitors = append(resp.Visitors, VisitorConfig{ - Name: v.GetBaseConfig().Name, - Type: v.GetBaseConfig().Type, - Config: cfg, - }) + resp.Visitors = append(resp.Visitors, payload) } + slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int { + return cmp.Compare(a.Name, b.Name) + }) return resp, nil } @@ -306,16 +309,12 @@ func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) { return nil, c.toHTTPError(err) } - cfg, err := configurerToMap(v) + payload, err := model.VisitorDefinitionFromConfigurer(v) if err != nil { return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) } - return VisitorConfig{ - Name: v.GetBaseConfig().Name, - Type: v.GetBaseConfig().Type, - Config: cfg, - }, nil + return payload, nil } func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) { @@ -324,19 +323,28 @@ func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) } - var typed v1.TypedVisitorConfig - if err := unmarshalTypedConfig(body, &typed); err != nil { + var payload model.VisitorDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) } - if typed.VisitorConfigurer == nil { - return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required") + if err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) } - - if err := c.manager.CreateStoreVisitor(typed.VisitorConfigurer); err != nil { + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + created, err := c.manager.CreateStoreVisitor(cfg) + if err != nil { return nil, c.toHTTPError(err) } - return nil, nil + + resp, err := model.VisitorDefinitionFromConfigurer(created) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil } func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) { @@ -350,19 +358,28 @@ func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) } - var typed v1.TypedVisitorConfig - if err := unmarshalTypedConfig(body, &typed); err != nil { + var payload model.VisitorDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) } - if typed.VisitorConfigurer == nil { - return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required") + if err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) } - - if err := c.manager.UpdateStoreVisitor(name, typed.VisitorConfigurer); err != nil { + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + updated, err := c.manager.UpdateStoreVisitor(name, cfg) + if err != nil { return nil, c.toHTTPError(err) } - return nil, nil + + resp, err := model.VisitorDefinitionFromConfigurer(updated) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil } func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) { @@ -376,15 +393,3 @@ func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) { } return nil, nil } - -func configurerToMap(v any) (map[string]any, error) { - data, err := json.Marshal(v) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err != nil { - return nil, err - } - return m, nil -} diff --git a/client/api/controller_test.go b/client/http/controller_test.go similarity index 52% rename from client/api/controller_test.go rename to client/http/controller_test.go index d237b6ba..aa88c545 100644 --- a/client/api/controller_test.go +++ b/client/http/controller_test.go @@ -1,4 +1,4 @@ -package api +package http import ( "bytes" @@ -13,6 +13,7 @@ import ( "github.com/gorilla/mux" "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/http/model" "github.com/fatedier/frp/client/proxy" v1 "github.com/fatedier/frp/pkg/config/v1" httppkg "github.com/fatedier/frp/pkg/util/http" @@ -28,13 +29,13 @@ type fakeConfigManager struct { listStoreProxiesFn func() ([]v1.ProxyConfigurer, error) getStoreProxyFn func(name string) (v1.ProxyConfigurer, error) - createStoreProxyFn func(cfg v1.ProxyConfigurer) error - updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) error + createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) deleteStoreProxyFn func(name string) error listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error) getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error) - createStoreVisitFn func(cfg v1.VisitorConfigurer) error - updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) error + createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) deleteStoreVisitFn func(name string) error gracefulCloseFn func(d time.Duration) } @@ -95,18 +96,18 @@ func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, erro return nil, nil } -func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error { +func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { if m.createStoreProxyFn != nil { return m.createStoreProxyFn(cfg) } - return nil + return cfg, nil } -func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error { +func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { if m.updateStoreProxyFn != nil { return m.updateStoreProxyFn(name, cfg) } - return nil + return cfg, nil } func (m *fakeConfigManager) DeleteStoreProxy(name string) error { @@ -130,18 +131,18 @@ func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, return nil, nil } -func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error { +func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { if m.createStoreVisitFn != nil { return m.createStoreVisitFn(cfg) } - return nil + return cfg, nil } -func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error { +func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { if m.updateStoreVisitFn != nil { return m.updateStoreVisitFn(name, cfg) } - return nil + return cfg, nil } func (m *fakeConfigManager) DeleteStoreVisitor(name string) error { @@ -157,25 +158,6 @@ func (m *fakeConfigManager) GracefulClose(d time.Duration) { } } -func setDisallowUnknownFieldsForTest(t *testing.T, value bool) func() { - t.Helper() - v1.DisallowUnknownFieldsMu.Lock() - prev := v1.DisallowUnknownFields - v1.DisallowUnknownFields = value - v1.DisallowUnknownFieldsMu.Unlock() - return func() { - v1.DisallowUnknownFieldsMu.Lock() - v1.DisallowUnknownFields = prev - v1.DisallowUnknownFieldsMu.Unlock() - } -} - -func getDisallowUnknownFieldsForTest() bool { - v1.DisallowUnknownFieldsMu.Lock() - defer v1.DisallowUnknownFieldsMu.Unlock() - return v1.DisallowUnknownFields -} - func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig { return &v1.TCPProxyConfig{ ProxyBaseConfig: v1.ProxyBaseConfig{ @@ -188,18 +170,6 @@ func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig { } } -func newRawXTCPVisitorConfig(name string) *v1.XTCPVisitorConfig { - return &v1.XTCPVisitorConfig{ - VisitorBaseConfig: v1.VisitorBaseConfig{ - Name: name, - Type: "xtcp", - ServerName: "server", - BindPort: 10081, - SecretKey: "secret", - }, - } -} - func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) { status := &proxy.WorkingStatus{ Name: "shared-proxy", @@ -265,22 +235,20 @@ func TestStoreProxyErrorMapping(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - body, err := json.Marshal(newRawTCPProxyConfig("shared-proxy")) - if err != nil { - t.Fatalf("marshal body: %v", err) - } - + body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`) req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body)) req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"}) ctx := httppkg.NewContext(httptest.NewRecorder(), req) controller := &Controller{ manager: &fakeConfigManager{ - updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) error { return tc.err }, + updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + return nil, tc.err + }, }, } - _, err = controller.UpdateStoreProxy(ctx) + _, err := controller.UpdateStoreProxy(ctx) if err == nil { t.Fatal("expected error") } @@ -290,11 +258,7 @@ func TestStoreProxyErrorMapping(t *testing.T) { } func TestStoreVisitorErrorMapping(t *testing.T) { - body, err := json.Marshal(newRawXTCPVisitorConfig("shared-visitor")) - if err != nil { - t.Fatalf("marshal body: %v", err) - } - + body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`) req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body)) req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"}) ctx := httppkg.NewContext(httptest.NewRecorder(), req) @@ -307,70 +271,208 @@ func TestStoreVisitorErrorMapping(t *testing.T) { }, } - _, err = controller.DeleteStoreVisitor(ctx) + _, err := controller.DeleteStoreVisitor(ctx) if err == nil { t.Fatal("expected error") } assertHTTPCode(t, err, http.StatusNotFound) } -func TestCreateStoreProxy_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { - restore := setDisallowUnknownFieldsForTest(t, true) - t.Cleanup(restore) - +func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) { var gotName string controller := &Controller{ manager: &fakeConfigManager{ - createStoreProxyFn: func(cfg v1.ProxyConfigurer) error { + createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { gotName = cfg.GetBaseConfig().Name - return nil + return cfg, nil }, }, } - body := []byte(`{"name":"raw-proxy","type":"tcp","localPort":10080,"unexpected":"value"}`) + body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`) req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body)) ctx := httppkg.NewContext(httptest.NewRecorder(), req) - _, err := controller.CreateStoreProxy(ctx) + resp, err := controller.CreateStoreProxy(ctx) if err != nil { t.Fatalf("create store proxy: %v", err) } if gotName != "raw-proxy" { t.Fatalf("unexpected proxy name: %q", gotName) } - if !getDisallowUnknownFieldsForTest() { - t.Fatal("global strictness flag was not restored") + + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Type != "tcp" || payload.TCP == nil { + t.Fatalf("unexpected payload: %#v", payload) } } -func TestCreateStoreVisitor_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { - restore := setDisallowUnknownFieldsForTest(t, true) - t.Cleanup(restore) - +func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) { var gotName string controller := &Controller{ manager: &fakeConfigManager{ - createStoreVisitFn: func(cfg v1.VisitorConfigurer) error { + createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { gotName = cfg.GetBaseConfig().Name - return nil + return cfg, nil }, }, } - body := []byte(`{"name":"raw-visitor","type":"xtcp","serverName":"server","bindPort":10081,"secretKey":"secret","unexpected":"value"}`) + body := []byte(`{ + "name":"raw-visitor","type":"xtcp","unexpected":"value", + "xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"} + }`) req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body)) ctx := httppkg.NewContext(httptest.NewRecorder(), req) - _, err := controller.CreateStoreVisitor(ctx) + resp, err := controller.CreateStoreVisitor(ctx) if err != nil { t.Fatalf("create store visitor: %v", err) } if gotName != "raw-visitor" { t.Fatalf("unexpected visitor name: %q", gotName) } - if !getDisallowUnknownFieldsForTest() { - t.Fatal("global strictness flag was not restored") + + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Type != "xtcp" || payload.XTCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) { + var gotPluginType string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + gotPluginType = cfg.GetBaseConfig().Plugin.Type + return cfg, nil + }, + }, + } + + body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`) + req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreProxy(ctx) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + if gotPluginType != "http2https" { + t.Fatalf("unexpected plugin type: %q", gotPluginType) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.TCP == nil { + t.Fatalf("unexpected response payload: %#v", payload) + } + pluginType := payload.TCP.Plugin.Type + + if pluginType != "http2https" { + t.Fatalf("unexpected plugin type in response payload: %q", pluginType) + } +} + +func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) { + var gotPluginType string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + gotPluginType = cfg.GetBaseConfig().Plugin.Type + return cfg, nil + }, + }, + } + + body := []byte(`{ + "name":"plugin-visitor","type":"stcp", + "stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}} + }`) + req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreVisitor(ctx) + if err != nil { + t.Fatalf("create store visitor: %v", err) + } + if gotPluginType != "virtual_net" { + t.Fatalf("unexpected plugin type: %q", gotPluginType) + } + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.STCP == nil { + t.Fatalf("unexpected response payload: %#v", payload) + } + pluginType := payload.STCP.Plugin.Type + + if pluginType != "virtual_net" { + t.Fatalf("unexpected plugin type in response payload: %q", pluginType) + } +} + +func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) { + controller := &Controller{manager: &fakeConfigManager{}} + body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`) + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "p1"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusBadRequest) +} + +func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) { + controller := &Controller{manager: &fakeConfigManager{}} + body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`) + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "p1"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusBadRequest) +} + +func TestListStoreProxiesReturnsSortedPayload(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) { + b := newRawTCPProxyConfig("b") + a := newRawTCPProxyConfig("a") + return []v1.ProxyConfigurer{b, a}, nil + }, + }, + } + ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil)) + + resp, err := controller.ListStoreProxies(ctx) + if err != nil { + t.Fatalf("list store proxies: %v", err) + } + out, ok := resp.(model.ProxyListResp) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if len(out.Proxies) != 2 { + t.Fatalf("unexpected proxy count: %d", len(out.Proxies)) + } + if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" { + t.Fatalf("proxies are not sorted by name: %#v", out.Proxies) } } @@ -388,3 +490,42 @@ func assertHTTPCode(t *testing.T, err error, expected int) { t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected) } } + +func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + return cfg, nil + }, + }, + } + + body := map[string]any{ + "name": "shared-proxy", + "type": "tcp", + "tcp": map[string]any{ + "localPort": 10080, + "remotePort": 7000, + }, + } + data, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.UpdateStoreProxy(ctx) + if err != nil { + t.Fatalf("update store proxy: %v", err) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.TCP == nil || payload.TCP.RemotePort != 7000 { + t.Fatalf("unexpected response payload: %#v", payload) + } +} diff --git a/client/http/model/proxy_definition.go b/client/http/model/proxy_definition.go new file mode 100644 index 00000000..dae4b4b8 --- /dev/null +++ b/client/http/model/proxy_definition.go @@ -0,0 +1,148 @@ +package model + +import ( + "fmt" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type ProxyDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + + TCP *v1.TCPProxyConfig `json:"tcp,omitempty"` + UDP *v1.UDPProxyConfig `json:"udp,omitempty"` + HTTP *v1.HTTPProxyConfig `json:"http,omitempty"` + HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"` + TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"` + STCP *v1.STCPProxyConfig `json:"stcp,omitempty"` + SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"` + XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"` +} + +func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error { + if strings.TrimSpace(p.Name) == "" { + return fmt.Errorf("proxy name is required") + } + if !IsProxyType(p.Type) { + return fmt.Errorf("invalid proxy type: %s", p.Type) + } + if isUpdate && pathName != "" && pathName != p.Name { + return fmt.Errorf("proxy name in URL must match name in body") + } + + _, blockType, blockCount := p.activeBlock() + if blockCount != 1 { + return fmt.Errorf("exactly one proxy type block is required") + } + if blockType != p.Type { + return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type) + } + return nil +} + +func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) { + block, _, _ := p.activeBlock() + if block == nil { + return nil, fmt.Errorf("exactly one proxy type block is required") + } + + cfg := block + cfg.GetBaseConfig().Name = p.Name + cfg.GetBaseConfig().Type = p.Type + return cfg, nil +} + +func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) { + if cfg == nil { + return ProxyDefinition{}, fmt.Errorf("proxy config is nil") + } + + base := cfg.GetBaseConfig() + payload := ProxyDefinition{ + Name: base.Name, + Type: base.Type, + } + + switch c := cfg.(type) { + case *v1.TCPProxyConfig: + payload.TCP = c + case *v1.UDPProxyConfig: + payload.UDP = c + case *v1.HTTPProxyConfig: + payload.HTTP = c + case *v1.HTTPSProxyConfig: + payload.HTTPS = c + case *v1.TCPMuxProxyConfig: + payload.TCPMux = c + case *v1.STCPProxyConfig: + payload.STCP = c + case *v1.SUDPProxyConfig: + payload.SUDP = c + case *v1.XTCPProxyConfig: + payload.XTCP = c + default: + return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg) + } + + return payload, nil +} + +func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) { + count := 0 + var block v1.ProxyConfigurer + var blockType string + + if p.TCP != nil { + count++ + block = p.TCP + blockType = "tcp" + } + if p.UDP != nil { + count++ + block = p.UDP + blockType = "udp" + } + if p.HTTP != nil { + count++ + block = p.HTTP + blockType = "http" + } + if p.HTTPS != nil { + count++ + block = p.HTTPS + blockType = "https" + } + if p.TCPMux != nil { + count++ + block = p.TCPMux + blockType = "tcpmux" + } + if p.STCP != nil { + count++ + block = p.STCP + blockType = "stcp" + } + if p.SUDP != nil { + count++ + block = p.SUDP + blockType = "sudp" + } + if p.XTCP != nil { + count++ + block = p.XTCP + blockType = "xtcp" + } + + return block, blockType, count +} + +func IsProxyType(typ string) bool { + switch typ { + case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp": + return true + default: + return false + } +} diff --git a/client/api/types.go b/client/http/model/types.go similarity index 71% rename from client/api/types.go rename to client/http/model/types.go index 8f7bece2..63704850 100644 --- a/client/api/types.go +++ b/client/http/model/types.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package api +package model const SourceStore = "store" @@ -31,26 +31,12 @@ type ProxyStatusResp struct { Source string `json:"source,omitempty"` // "store" or "config" } -// ProxyConfig wraps proxy configuration for API requests/responses. -type ProxyConfig struct { - Name string `json:"name"` - Type string `json:"type"` - Config map[string]any `json:"config"` -} - -// VisitorConfig wraps visitor configuration for API requests/responses. -type VisitorConfig struct { - Name string `json:"name"` - Type string `json:"type"` - Config map[string]any `json:"config"` -} - // ProxyListResp is the response for GET /api/store/proxies type ProxyListResp struct { - Proxies []ProxyConfig `json:"proxies"` + Proxies []ProxyDefinition `json:"proxies"` } // VisitorListResp is the response for GET /api/store/visitors type VisitorListResp struct { - Visitors []VisitorConfig `json:"visitors"` + Visitors []VisitorDefinition `json:"visitors"` } diff --git a/client/http/model/visitor_definition.go b/client/http/model/visitor_definition.go new file mode 100644 index 00000000..a108982d --- /dev/null +++ b/client/http/model/visitor_definition.go @@ -0,0 +1,107 @@ +package model + +import ( + "fmt" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type VisitorDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + + STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"` + SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"` + XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"` +} + +func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error { + if strings.TrimSpace(p.Name) == "" { + return fmt.Errorf("visitor name is required") + } + if !IsVisitorType(p.Type) { + return fmt.Errorf("invalid visitor type: %s", p.Type) + } + if isUpdate && pathName != "" && pathName != p.Name { + return fmt.Errorf("visitor name in URL must match name in body") + } + + _, blockType, blockCount := p.activeBlock() + if blockCount != 1 { + return fmt.Errorf("exactly one visitor type block is required") + } + if blockType != p.Type { + return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type) + } + return nil +} + +func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) { + block, _, _ := p.activeBlock() + if block == nil { + return nil, fmt.Errorf("exactly one visitor type block is required") + } + + cfg := block + cfg.GetBaseConfig().Name = p.Name + cfg.GetBaseConfig().Type = p.Type + return cfg, nil +} + +func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) { + if cfg == nil { + return VisitorDefinition{}, fmt.Errorf("visitor config is nil") + } + + base := cfg.GetBaseConfig() + payload := VisitorDefinition{ + Name: base.Name, + Type: base.Type, + } + + switch c := cfg.(type) { + case *v1.STCPVisitorConfig: + payload.STCP = c + case *v1.SUDPVisitorConfig: + payload.SUDP = c + case *v1.XTCPVisitorConfig: + payload.XTCP = c + default: + return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg) + } + + return payload, nil +} + +func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) { + count := 0 + var block v1.VisitorConfigurer + var blockType string + + if p.STCP != nil { + count++ + block = p.STCP + blockType = "stcp" + } + if p.SUDP != nil { + count++ + block = p.SUDP + blockType = "sudp" + } + if p.XTCP != nil { + count++ + block = p.XTCP + blockType = "xtcp" + } + return block, blockType, count +} + +func IsVisitorType(typ string) bool { + switch typ { + case "stcp", "sudp", "xtcp": + return true + default: + return false + } +} diff --git a/pkg/config/load.go b/pkg/config/load.go index a9f98552..159cd097 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -16,7 +16,6 @@ package config import ( "bytes" - "encoding/json" "fmt" "os" "path/filepath" @@ -33,6 +32,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/jsonx" "github.com/fatedier/frp/pkg/util/util" ) @@ -129,45 +129,54 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error { } // Convert to JSON and decode with strict validation - jsonBytes, err := json.Marshal(temp) + jsonBytes, err := jsonx.Marshal(temp) if err != nil { return err } - decoder := json.NewDecoder(bytes.NewReader(jsonBytes)) - decoder.DisallowUnknownFields() - return decoder.Decode(target) + return decodeJSONContent(jsonBytes, target, true) +} + +func decodeJSONContent(content []byte, target any, strict bool) error { + if clientCfg, ok := target.(*v1.ClientConfig); ok { + decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{ + DisallowUnknownFields: strict, + }) + if err != nil { + return err + } + *clientCfg = decoded + return nil + } + + return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{ + RejectUnknownMembers: strict, + }) } // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. func LoadConfigure(b []byte, c any, strict bool) error { - return v1.WithDisallowUnknownFields(strict, func() error { - var tomlObj any - // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). - if err := toml.Unmarshal(b, &tomlObj); err == nil { - var err error - b, err = json.Marshal(&tomlObj) - if err != nil { - return err - } - } - // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. - if yaml.IsJSONBuffer(b) { - decoder := json.NewDecoder(bytes.NewBuffer(b)) - if strict { - decoder.DisallowUnknownFields() - } - return decoder.Decode(c) + var tomlObj any + // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). + if err := toml.Unmarshal(b, &tomlObj); err == nil { + var err error + b, err = jsonx.Marshal(&tomlObj) + if err != nil { + return err } + } + // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. + if yaml.IsJSONBuffer(b) { + return decodeJSONContent(b, c, strict) + } - // Handle YAML content - if strict { - // In strict mode, always use our custom handler to support YAML merge - return parseYAMLWithDotFieldsHandling(b, c) - } - // Non-strict mode, parse normally - return yaml.Unmarshal(b, c) - }) + // Handle YAML content + if strict { + // In strict mode, always use our custom handler to support YAML merge + return parseYAMLWithDotFieldsHandling(b, c) + } + // Non-strict mode, parse normally + return yaml.Unmarshal(b, c) } func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 2675f636..b23955b8 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -189,6 +189,31 @@ unixPath = "/tmp/uds.sock" require.Error(err) } +func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) { + require := require.New(t) + + content := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +localPort = 6000 +[proxies.plugin] +type = "http2https" +localAddr = "127.0.0.1:8080" +unknownInPlugin = "value" +` + + clientCfg := v1.ClientConfig{} + + err := LoadConfigure([]byte(content), &clientCfg, false) + require.NoError(err) + + err = LoadConfigure([]byte(content), &clientCfg, true) + require.ErrorContains(err, "unknownInPlugin") +} + // TestYAMLMergeInStrictMode tests that YAML merge functionality works // even in strict mode by properly handling dot-prefixed fields func TestYAMLMergeInStrictMode(t *testing.T) { diff --git a/pkg/config/source/store.go b/pkg/config/source/store.go index 22024136..d1bf4fb5 100644 --- a/pkg/config/source/store.go +++ b/pkg/config/source/store.go @@ -15,13 +15,13 @@ package source import ( - "encoding/json" "errors" "fmt" "os" "path/filepath" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" ) type StoreSourceConfig struct { @@ -74,36 +74,44 @@ func (s *StoreSource) loadFromFileUnlocked() error { return err } - var stored storeData - if err := v1.WithDisallowUnknownFields(false, func() error { - return json.Unmarshal(data, &stored) - }); err != nil { + type rawStoreData struct { + Proxies []jsonx.RawMessage `json:"proxies,omitempty"` + Visitors []jsonx.RawMessage `json:"visitors,omitempty"` + } + stored := rawStoreData{} + if err := jsonx.Unmarshal(data, &stored); err != nil { return fmt.Errorf("failed to parse JSON: %w", err) } s.proxies = make(map[string]v1.ProxyConfigurer) s.visitors = make(map[string]v1.VisitorConfigurer) - for _, tp := range stored.Proxies { - if tp.ProxyConfigurer != nil { - proxyCfg := tp.ProxyConfigurer - name := proxyCfg.GetBaseConfig().Name - if name == "" { - return fmt.Errorf("proxy name cannot be empty") - } - s.proxies[name] = proxyCfg + for i, proxyData := range stored.Proxies { + proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{ + DisallowUnknownFields: false, + }) + if err != nil { + return fmt.Errorf("failed to decode proxy at index %d: %w", i, err) } + name := proxyCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + s.proxies[name] = proxyCfg } - for _, tv := range stored.Visitors { - if tv.VisitorConfigurer != nil { - visitorCfg := tv.VisitorConfigurer - name := visitorCfg.GetBaseConfig().Name - if name == "" { - return fmt.Errorf("visitor name cannot be empty") - } - s.visitors[name] = visitorCfg + for i, visitorData := range stored.Visitors { + visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{ + DisallowUnknownFields: false, + }) + if err != nil { + return fmt.Errorf("failed to decode visitor at index %d: %w", i, err) } + name := visitorCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + s.visitors[name] = visitorCfg } return nil @@ -122,7 +130,7 @@ func (s *StoreSource) saveToFileUnlocked() error { stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v}) } - data, err := json.MarshalIndent(stored, "", " ") + data, err := jsonx.MarshalIndent(stored, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } diff --git a/pkg/config/source/store_test.go b/pkg/config/source/store_test.go index 801115fa..bb5382b0 100644 --- a/pkg/config/source/store_test.go +++ b/pkg/config/source/store_test.go @@ -15,7 +15,6 @@ package source import ( - "encoding/json" "os" "path/filepath" "testing" @@ -23,27 +22,9 @@ import ( "github.com/stretchr/testify/require" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" ) -func setDisallowUnknownFieldsForStoreTest(t *testing.T, value bool) func() { - t.Helper() - v1.DisallowUnknownFieldsMu.Lock() - prev := v1.DisallowUnknownFields - v1.DisallowUnknownFields = value - v1.DisallowUnknownFieldsMu.Unlock() - return func() { - v1.DisallowUnknownFieldsMu.Lock() - v1.DisallowUnknownFields = prev - v1.DisallowUnknownFieldsMu.Unlock() - } -} - -func getDisallowUnknownFieldsForStoreTest() bool { - v1.DisallowUnknownFieldsMu.Lock() - defer v1.DisallowUnknownFieldsMu.Unlock() - return v1.DisallowUnknownFields -} - func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) { require := require.New(t) @@ -99,7 +80,7 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) { Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}}, Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}}, } - data, err := json.Marshal(stored) + data, err := jsonx.Marshal(stored) require.NoError(err) err = os.WriteFile(path, data, 0o600) require.NoError(err) @@ -117,12 +98,9 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) { require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol) } -func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) { +func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) { require := require.New(t) - restore := setDisallowUnknownFieldsForStoreTest(t, true) - t.Cleanup(restore) - path := filepath.Join(t.TempDir(), "store.json") raw := []byte(`{ "proxies": [ @@ -140,5 +118,4 @@ func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t require.NotNil(storeSource.GetProxy("proxy1")) require.NotNil(storeSource.GetVisitor("visitor1")) - require.True(getDisallowUnknownFieldsForStoreTest()) } diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index df1867a2..41bd9a4c 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -16,35 +16,10 @@ package v1 import ( "maps" - "sync" "github.com/fatedier/frp/pkg/util/util" ) -// TODO(fatedier): Migrate typed config decoding to encoding/json/v2 when it is stable for production use. -// The current encoding/json(v1) path cannot propagate DisallowUnknownFields into custom UnmarshalJSON -// methods, so we temporarily keep this global strictness flag protected by a mutex. -// -// https://github.com/golang/go/issues/41144 -// https://github.com/golang/go/discussions/63397 -var ( - DisallowUnknownFields = false - DisallowUnknownFieldsMu sync.Mutex -) - -// WithDisallowUnknownFields temporarily overrides typed config JSON strictness. -// It restores the previous value before returning. -func WithDisallowUnknownFields(disallow bool, fn func() error) error { - DisallowUnknownFieldsMu.Lock() - prev := DisallowUnknownFields - DisallowUnknownFields = disallow - defer func() { - DisallowUnknownFields = prev - DisallowUnknownFieldsMu.Unlock() - }() - return fn() -} - type AuthScope string const ( diff --git a/pkg/config/v1/decode.go b/pkg/config/v1/decode.go new file mode 100644 index 00000000..427cea7f --- /dev/null +++ b/pkg/config/v1/decode.go @@ -0,0 +1,195 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "errors" + "fmt" + "reflect" + + "github.com/fatedier/frp/pkg/util/jsonx" +) + +type DecodeOptions struct { + DisallowUnknownFields bool +} + +func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error { + return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{ + RejectUnknownMembers: options.DisallowUnknownFields, + }) +} + +func isJSONNull(b []byte) bool { + return len(b) == 0 || string(b) == "null" +} + +type typedEnvelope struct { + Type string `json:"type"` + Plugin jsonx.RawMessage `json:"plugin,omitempty"` +} + +func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) { + if isJSONNull(b) { + return nil, errors.New("type is required") + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return nil, err + } + + configurer := NewProxyConfigurerByType(ProxyType(env.Type)) + if configurer == nil { + return nil, fmt.Errorf("unknown proxy type: %s", env.Type) + } + if err := decodeJSONWithOptions(b, configurer, options); err != nil { + return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err) + } + + if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) { + plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options) + if err != nil { + return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err) + } + configurer.GetBaseConfig().Plugin = plugin + } + return configurer, nil +} + +func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) { + if isJSONNull(b) { + return nil, errors.New("type is required") + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return nil, err + } + + configurer := NewVisitorConfigurerByType(VisitorType(env.Type)) + if configurer == nil { + return nil, fmt.Errorf("unknown visitor type: %s", env.Type) + } + if err := decodeJSONWithOptions(b, configurer, options); err != nil { + return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err) + } + + if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) { + plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options) + if err != nil { + return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err) + } + configurer.GetBaseConfig().Plugin = plugin + } + return configurer, nil +} + +func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) { + if isJSONNull(b) { + return TypedClientPluginOptions{}, nil + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return TypedClientPluginOptions{}, err + } + if env.Type == "" { + return TypedClientPluginOptions{}, errors.New("plugin type is empty") + } + + v, ok := clientPluginOptionsTypeMap[env.Type] + if !ok { + return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type) + } + optionsStruct := reflect.New(v).Interface().(ClientPluginOptions) + if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil { + return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err) + } + return TypedClientPluginOptions{ + Type: env.Type, + ClientPluginOptions: optionsStruct, + }, nil +} + +func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) { + if isJSONNull(b) { + return TypedVisitorPluginOptions{}, nil + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return TypedVisitorPluginOptions{}, err + } + if env.Type == "" { + return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty") + } + + v, ok := visitorPluginOptionsTypeMap[env.Type] + if !ok { + return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type) + } + optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions) + if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil { + return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err) + } + return TypedVisitorPluginOptions{ + Type: env.Type, + VisitorPluginOptions: optionsStruct, + }, nil +} + +func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) { + type rawClientConfig struct { + ClientCommonConfig + Proxies []jsonx.RawMessage `json:"proxies,omitempty"` + Visitors []jsonx.RawMessage `json:"visitors,omitempty"` + } + + raw := rawClientConfig{} + if err := decodeJSONWithOptions(b, &raw, options); err != nil { + return ClientConfig{}, err + } + + cfg := ClientConfig{ + ClientCommonConfig: raw.ClientCommonConfig, + Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)), + Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)), + } + + for i, proxyData := range raw.Proxies { + proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options) + if err != nil { + return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err) + } + cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{ + Type: proxyCfg.GetBaseConfig().Type, + ProxyConfigurer: proxyCfg, + }) + } + + for i, visitorData := range raw.Visitors { + visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options) + if err != nil { + return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err) + } + cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{ + Type: visitorCfg.GetBaseConfig().Type, + VisitorConfigurer: visitorCfg, + }) + } + + return cfg, nil +} diff --git a/pkg/config/v1/decode_test.go b/pkg/config/v1/decode_test.go new file mode 100644 index 00000000..cba433ad --- /dev/null +++ b/pkg/config/v1/decode_test.go @@ -0,0 +1,86 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "name":"p1", + "type":"tcp", + "localPort":10080, + "plugin":{ + "type":"http2https", + "localAddr":"127.0.0.1:8080", + "unknownInPlugin":"value" + } + }`) + + _, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownInPlugin") +} + +func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "name":"v1", + "type":"stcp", + "serverName":"server", + "bindPort":10081, + "plugin":{ + "type":"virtual_net", + "destinationIP":"10.0.0.1", + "unknownInPlugin":"value" + } + }`) + + _, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownInPlugin") +} + +func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "serverPort":7000, + "proxies":[ + { + "name":"p1", + "type":"tcp", + "localPort":10080, + "unknownField":"value" + } + ] + }`) + + _, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownField") +} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index fb868775..97bfab22 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -15,16 +15,13 @@ package v1 import ( - "bytes" - "encoding/json" - "errors" - "fmt" "maps" "reflect" "slices" "github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/jsonx" "github.com/fatedier/frp/pkg/util/util" ) @@ -202,35 +199,18 @@ type TypedProxyConfig struct { } func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return errors.New("type is required") - } - - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { + configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{}) + if err != nil { return err } - c.Type = typeStruct.Type - configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type)) - if configurer == nil { - return fmt.Errorf("unknown proxy type: %s", typeStruct.Type) - } - decoder := json.NewDecoder(bytes.NewBuffer(b)) - if DisallowUnknownFields { - decoder.DisallowUnknownFields() - } - if err := decoder.Decode(configurer); err != nil { - return fmt.Errorf("unmarshal ProxyConfig error: %v", err) - } + c.Type = configurer.GetBaseConfig().Type c.ProxyConfigurer = configurer return nil } func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) { - return json.Marshal(c.ProxyConfigurer) + return jsonx.Marshal(c.ProxyConfigurer) } type ProxyConfigurer interface { diff --git a/pkg/config/v1/proxy_plugin.go b/pkg/config/v1/proxy_plugin.go index 908b30ff..003f825f 100644 --- a/pkg/config/v1/proxy_plugin.go +++ b/pkg/config/v1/proxy_plugin.go @@ -15,14 +15,11 @@ package v1 import ( - "bytes" - "encoding/json" - "errors" - "fmt" "reflect" "github.com/samber/lo" + "github.com/fatedier/frp/pkg/util/jsonx" "github.com/fatedier/frp/pkg/util/util" ) @@ -71,42 +68,16 @@ func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions { } func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return nil - } - - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { + decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{}) + if err != nil { return err } - - c.Type = typeStruct.Type - if c.Type == "" { - return errors.New("plugin type is empty") - } - - v, ok := clientPluginOptionsTypeMap[typeStruct.Type] - if !ok { - return fmt.Errorf("unknown plugin type: %s", typeStruct.Type) - } - options := reflect.New(v).Interface().(ClientPluginOptions) - - decoder := json.NewDecoder(bytes.NewBuffer(b)) - if DisallowUnknownFields { - decoder.DisallowUnknownFields() - } - - if err := decoder.Decode(options); err != nil { - return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err) - } - c.ClientPluginOptions = options + *c = decoded return nil } func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) { - return json.Marshal(c.ClientPluginOptions) + return jsonx.Marshal(c.ClientPluginOptions) } type HTTP2HTTPSPluginOptions struct { diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index b0ff1e34..8f94f672 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -15,12 +15,9 @@ package v1 import ( - "bytes" - "encoding/json" - "errors" - "fmt" "reflect" + "github.com/fatedier/frp/pkg/util/jsonx" "github.com/fatedier/frp/pkg/util/util" ) @@ -93,35 +90,18 @@ type TypedVisitorConfig struct { } func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return errors.New("type is required") - } - - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { + configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{}) + if err != nil { return err } - c.Type = typeStruct.Type - configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type)) - if configurer == nil { - return fmt.Errorf("unknown visitor type: %s", typeStruct.Type) - } - decoder := json.NewDecoder(bytes.NewBuffer(b)) - if DisallowUnknownFields { - decoder.DisallowUnknownFields() - } - if err := decoder.Decode(configurer); err != nil { - return fmt.Errorf("unmarshal VisitorConfig error: %v", err) - } + c.Type = configurer.GetBaseConfig().Type c.VisitorConfigurer = configurer return nil } func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) { - return json.Marshal(c.VisitorConfigurer) + return jsonx.Marshal(c.VisitorConfigurer) } func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer { diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go index 9fa28a0b..2e2d0494 100644 --- a/pkg/config/v1/visitor_plugin.go +++ b/pkg/config/v1/visitor_plugin.go @@ -15,11 +15,9 @@ package v1 import ( - "bytes" - "encoding/json" - "errors" - "fmt" "reflect" + + "github.com/fatedier/frp/pkg/util/jsonx" ) const ( @@ -49,42 +47,16 @@ func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions { } func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return nil - } - - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { + decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{}) + if err != nil { return err } - - c.Type = typeStruct.Type - if c.Type == "" { - return errors.New("visitor plugin type is empty") - } - - v, ok := visitorPluginOptionsTypeMap[typeStruct.Type] - if !ok { - return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type) - } - options := reflect.New(v).Interface().(VisitorPluginOptions) - - decoder := json.NewDecoder(bytes.NewBuffer(b)) - if DisallowUnknownFields { - decoder.DisallowUnknownFields() - } - - if err := decoder.Decode(options); err != nil { - return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err) - } - c.VisitorPluginOptions = options + *c = decoded return nil } func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) { - return json.Marshal(c.VisitorPluginOptions) + return jsonx.Marshal(c.VisitorPluginOptions) } type VirtualNetVisitorPluginOptions struct { diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go index dfad996d..088d3f8f 100644 --- a/pkg/sdk/client/client.go +++ b/pkg/sdk/client/client.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "github.com/fatedier/frp/client/api" + "github.com/fatedier/frp/client/http/model" httppkg "github.com/fatedier/frp/pkg/util/http" ) @@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) { c.authPwd = pwd } -func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) { +func (c *Client) GetProxyStatus(ctx context.Context, name string) (*model.ProxyStatusResp, error) { req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) if err != nil { return nil, err @@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta if err != nil { return nil, err } - allStatus := make(api.StatusResp) + allStatus := make(model.StatusResp) if err = json.Unmarshal([]byte(content), &allStatus); err != nil { return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) } @@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta return nil, fmt.Errorf("no proxy status found") } -func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) { +func (c *Client) GetAllProxyStatus(ctx context.Context) (model.StatusResp, error) { req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) if err != nil { return nil, err @@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) if err != nil { return nil, err } - allStatus := make(api.StatusResp) + allStatus := make(model.StatusResp) if err = json.Unmarshal([]byte(content), &allStatus); err != nil { return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) } diff --git a/pkg/util/jsonx/json_v1.go b/pkg/util/jsonx/json_v1.go new file mode 100644 index 00000000..1fb98290 --- /dev/null +++ b/pkg/util/jsonx/json_v1.go @@ -0,0 +1,45 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jsonx + +import ( + "bytes" + "encoding/json" +) + +type DecodeOptions struct { + RejectUnknownMembers bool +} + +func Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +func MarshalIndent(v any, prefix, indent string) ([]byte, error) { + return json.MarshalIndent(v, prefix, indent) +} + +func Unmarshal(data []byte, out any) error { + return json.Unmarshal(data, out) +} + +func UnmarshalWithOptions(data []byte, out any, options DecodeOptions) error { + if !options.RejectUnknownMembers { + return json.Unmarshal(data, out) + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + return decoder.Decode(out) +} diff --git a/pkg/util/jsonx/raw_message.go b/pkg/util/jsonx/raw_message.go new file mode 100644 index 00000000..3eda4628 --- /dev/null +++ b/pkg/util/jsonx/raw_message.go @@ -0,0 +1,36 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jsonx + +import "fmt" + +// RawMessage stores a raw encoded JSON value. +// It is equivalent to encoding/json.RawMessage behavior. +type RawMessage []byte + +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + return m, nil +} + +func (m *RawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return fmt.Errorf("jsonx.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[:0], data...) + return nil +} diff --git a/server/api_router.go b/server/api_router.go new file mode 100644 index 00000000..bb9e44ed --- /dev/null +++ b/server/api_router.go @@ -0,0 +1,64 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + httppkg "github.com/fatedier/frp/pkg/util/http" + netpkg "github.com/fatedier/frp/pkg/util/net" + adminapi "github.com/fatedier/frp/server/http" +) + +func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { + helper.Router.HandleFunc("/healthz", healthz) + subRouter := helper.Router.NewRoute().Subrouter() + + subRouter.Use(helper.AuthMiddleware) + subRouter.Use(httppkg.NewRequestLogger) + + // metrics + if svr.cfg.EnablePrometheus { + subRouter.Handle("/metrics", promhttp.Handler()) + } + + apiController := adminapi.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager) + + // apis + subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET") + subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET") + subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET") + subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET") + subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET") + subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE") + + // view + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") + subRouter.PathPrefix("/static/").Handler( + netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), + ).Methods("GET") + + subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/", http.StatusMovedPermanently) + }) +} + +func healthz(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(200) +} diff --git a/server/api/controller.go b/server/http/controller.go similarity index 71% rename from server/api/controller.go rename to server/http/controller.go index f691fcad..a1842788 100644 --- a/server/api/controller.go +++ b/server/http/controller.go @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package api +package http import ( "cmp" - "encoding/json" "fmt" "net/http" "slices" @@ -29,6 +28,7 @@ import ( httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" + "github.com/fatedier/frp/server/http/model" "github.com/fatedier/frp/server/proxy" "github.com/fatedier/frp/server/registry" ) @@ -59,7 +59,7 @@ func NewController( // /api/serverinfo func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) { serverStats := mem.StatsCollector.GetServer() - svrResp := ServerInfoResp{ + svrResp := model.ServerInfoResp{ Version: version.Full(), BindPort: c.serverCfg.BindPort, VhostHTTPPort: c.serverCfg.VhostHTTPPort, @@ -80,22 +80,6 @@ func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) { ClientCounts: serverStats.ClientCounts, ProxyTypeCounts: serverStats.ProxyTypeCounts, } - // For API that returns struct, we can just return it. - // But current GeneralResponse.Msg in legacy code expects a JSON string. - // Since MakeHTTPHandlerFunc handles struct by encoding to JSON, we can return svrResp directly? - // The original code wraps it in GeneralResponse{Msg: string(json)}. - // If we return svrResp, the response body will be the JSON of svrResp. - // We should check if the frontend expects { "code": 200, "msg": "{...}" } or just {...}. - // Looking at previous code: - // res := GeneralResponse{Code: 200} - // buf, _ := json.Marshal(&svrResp) - // res.Msg = string(buf) - // Response body: {"code": 200, "msg": "{\"version\":...}"} - // Wait, is it double encoded JSON? Yes it seems so! - // Let's check dashboard_api.go original code again. - // Yes: res.Msg = string(buf). - // So the frontend expects { "code": 200, "msg": "JSON_STRING" }. - // This is kind of ugly, but we must preserve compatibility. return svrResp, nil } @@ -112,7 +96,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) { statusFilter := strings.ToLower(ctx.Query("status")) records := c.clientRegistry.List() - items := make([]ClientInfoResp, 0, len(records)) + items := make([]model.ClientInfoResp, 0, len(records)) for _, info := range records { if userFilter != "" && info.User != userFilter { continue @@ -129,7 +113,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) { items = append(items, buildClientInfoResp(info)) } - slices.SortFunc(items, func(a, b ClientInfoResp) int { + slices.SortFunc(items, func(a, b model.ClientInfoResp) int { if v := cmp.Compare(a.User, b.User); v != 0 { return v } @@ -165,9 +149,9 @@ func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) { func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) { proxyType := ctx.Param("type") - proxyInfoResp := GetProxyInfoResp{} + proxyInfoResp := model.GetProxyInfoResp{} proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType) - slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int { + slices.SortFunc(proxyInfoResp.Proxies, func(a, b *model.ProxyStatsInfo) int { return cmp.Compare(a.Name, b.Name) }) @@ -191,7 +175,7 @@ func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) { func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) { name := ctx.Param("name") - trafficResp := GetProxyTrafficResp{} + trafficResp := model.GetProxyTrafficResp{} trafficResp.Name = name proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name) @@ -213,7 +197,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) { return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found") } - proxyInfo := GetProxyStatsResp{ + proxyInfo := model.GetProxyStatsResp{ Name: ps.Name, User: ps.User, ClientID: ps.ClientID, @@ -225,16 +209,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) { } if pxy, ok := c.pxyManager.GetByName(name); ok { - content, err := json.Marshal(pxy.GetConfigurer()) - if err != nil { - log.Warnf("marshal proxy [%s] conf info error: %v", name, err) - return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error") - } - proxyInfo.Conf = getConfByType(ps.Type) - if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { - log.Warnf("unmarshal proxy [%s] conf info error: %v", name, err) - return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error") - } + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) proxyInfo.Status = "online" } else { proxyInfo.Status = "offline" @@ -254,25 +229,16 @@ func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) { return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil } -func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) { +func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*model.ProxyStatsInfo) { proxyStats := mem.StatsCollector.GetProxiesByType(proxyType) - proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats)) + proxyInfos = make([]*model.ProxyStatsInfo, 0, len(proxyStats)) for _, ps := range proxyStats { - proxyInfo := &ProxyStatsInfo{ + proxyInfo := &model.ProxyStatsInfo{ User: ps.User, ClientID: ps.ClientID, } if pxy, ok := c.pxyManager.GetByName(ps.Name); ok { - content, err := json.Marshal(pxy.GetConfigurer()) - if err != nil { - log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err) - continue - } - proxyInfo.Conf = getConfByType(ps.Type) - if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { - log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err) - continue - } + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) proxyInfo.Status = "online" } else { proxyInfo.Status = "offline" @@ -288,7 +254,7 @@ func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyS return } -func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) { +func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo model.GetProxyStatsResp, code int, msg string) { proxyInfo.Name = proxyName ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName) if ps == nil { @@ -298,20 +264,7 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri proxyInfo.User = ps.User proxyInfo.ClientID = ps.ClientID if pxy, ok := c.pxyManager.GetByName(proxyName); ok { - content, err := json.Marshal(pxy.GetConfigurer()) - if err != nil { - log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err) - code = 400 - msg = "parse conf error" - return - } - proxyInfo.Conf = getConfByType(ps.Type) - if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { - log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err) - code = 400 - msg = "parse conf error" - return - } + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) proxyInfo.Status = "online" } else { proxyInfo.Status = "offline" @@ -327,8 +280,8 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri return } -func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp { - resp := ClientInfoResp{ +func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp { + resp := model.ClientInfoResp{ Key: info.Key, User: info.User, ClientID: info.ClientID(), @@ -366,23 +319,37 @@ func matchStatusFilter(online bool, filter string) bool { } } -func getConfByType(proxyType string) any { - switch v1.ProxyType(proxyType) { - case v1.ProxyTypeTCP: - return &TCPOutConf{} - case v1.ProxyTypeTCPMUX: - return &TCPMuxOutConf{} - case v1.ProxyTypeUDP: - return &UDPOutConf{} - case v1.ProxyTypeHTTP: - return &HTTPOutConf{} - case v1.ProxyTypeHTTPS: - return &HTTPSOutConf{} - case v1.ProxyTypeSTCP: - return &STCPOutConf{} - case v1.ProxyTypeXTCP: - return &XTCPOutConf{} - default: - return nil +func getConfFromConfigurer(cfg v1.ProxyConfigurer) any { + outBase := model.BaseOutConf{ProxyBaseConfig: *cfg.GetBaseConfig()} + + switch c := cfg.(type) { + case *v1.TCPProxyConfig: + return &model.TCPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort} + case *v1.UDPProxyConfig: + return &model.UDPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort} + case *v1.HTTPProxyConfig: + return &model.HTTPOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + Locations: c.Locations, + HostHeaderRewrite: c.HostHeaderRewrite, + } + case *v1.HTTPSProxyConfig: + return &model.HTTPSOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + } + case *v1.TCPMuxProxyConfig: + return &model.TCPMuxOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + Multiplexer: c.Multiplexer, + RouteByHTTPUser: c.RouteByHTTPUser, + } + case *v1.STCPProxyConfig: + return &model.STCPOutConf{BaseOutConf: outBase} + case *v1.XTCPProxyConfig: + return &model.XTCPOutConf{BaseOutConf: outBase} } + return outBase } diff --git a/server/http/controller_test.go b/server/http/controller_test.go new file mode 100644 index 00000000..3ef50776 --- /dev/null +++ b/server/http/controller_test.go @@ -0,0 +1,71 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "encoding/json" + "testing" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) { + cfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "test-proxy", + Type: string(v1.ProxyTypeTCP), + ProxyBackend: v1.ProxyBackend{ + Plugin: v1.TypedClientPluginOptions{ + Type: v1.PluginHTTPProxy, + ClientPluginOptions: &v1.HTTPProxyPluginOptions{ + Type: v1.PluginHTTPProxy, + HTTPUser: "user", + HTTPPassword: "password", + }, + }, + }, + }, + RemotePort: 6000, + } + + content, err := json.Marshal(getConfFromConfigurer(cfg)) + if err != nil { + t.Fatalf("marshal conf failed: %v", err) + } + + var out map[string]any + if err := json.Unmarshal(content, &out); err != nil { + t.Fatalf("unmarshal conf failed: %v", err) + } + + pluginValue, ok := out["plugin"] + if !ok { + t.Fatalf("plugin field missing in output: %v", out) + } + plugin, ok := pluginValue.(map[string]any) + if !ok { + t.Fatalf("plugin field should be object, got: %#v", pluginValue) + } + + if got := plugin["type"]; got != v1.PluginHTTPProxy { + t.Fatalf("plugin type mismatch, want %q got %#v", v1.PluginHTTPProxy, got) + } + if got := plugin["httpUser"]; got != "user" { + t.Fatalf("plugin httpUser mismatch, want %q got %#v", "user", got) + } + if got := plugin["httpPassword"]; got != "password" { + t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got) + } +} diff --git a/server/api/types.go b/server/http/model/types.go similarity index 99% rename from server/api/types.go rename to server/http/model/types.go index 0a72af1e..92467e4b 100644 --- a/server/api/types.go +++ b/server/http/model/types.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package api +package model import ( v1 "github.com/fatedier/frp/pkg/config/v1" diff --git a/server/service.go b/server/service.go index 62f9ac8e..b0db0327 100644 --- a/server/service.go +++ b/server/service.go @@ -28,7 +28,6 @@ import ( "github.com/fatedier/golib/crypto" "github.com/fatedier/golib/net/mux" fmux "github.com/hashicorp/yamux" - "github.com/prometheus/client_golang/prometheus/promhttp" quic "github.com/quic-go/quic-go" "github.com/samber/lo" @@ -48,7 +47,6 @@ import ( "github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/pkg/util/xlog" - "github.com/fatedier/frp/server/api" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/group" "github.com/fatedier/frp/server/metrics" @@ -690,42 +688,3 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey, newMsg.UseEncryption, newMsg.UseCompression, visitorUser) } - -func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { - helper.Router.HandleFunc("/healthz", healthz) - subRouter := helper.Router.NewRoute().Subrouter() - - subRouter.Use(helper.AuthMiddleware) - subRouter.Use(httppkg.NewRequestLogger) - - // metrics - if svr.cfg.EnablePrometheus { - subRouter.Handle("/metrics", promhttp.Handler()) - } - - apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager) - - // apis - subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET") - subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET") - subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET") - subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET") - subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET") - subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE") - - // view - subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") - subRouter.PathPrefix("/static/").Handler( - netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), - ).Methods("GET") - - subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/static/", http.StatusMovedPermanently) - }) -} - -func healthz(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(200) -} diff --git a/test/e2e/v1/features/store.go b/test/e2e/v1/features/store.go index 8479b9be..284c85f4 100644 --- a/test/e2e/v1/features/store.go +++ b/test/e2e/v1/features/store.go @@ -34,11 +34,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ - "name": "test-tcp", - "type": "tcp", - "localIP": "127.0.0.1", - "localPort": f.PortByName(framework.TCPEchoServerPort), - "remotePort": remotePort, + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, } proxyBody, _ := json.Marshal(proxyConfig) @@ -73,11 +75,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ - "name": "test-tcp", - "type": "tcp", - "localIP": "127.0.0.1", - "localPort": f.PortByName(framework.TCPEchoServerPort), - "remotePort": remotePort1, + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort1, + }, } proxyBody, _ := json.Marshal(proxyConfig) @@ -92,7 +96,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { time.Sleep(time.Second) framework.NewRequestExpect(f).Port(remotePort1).Ensure() - proxyConfig["remotePort"] = remotePort2 + proxyConfig["tcp"].(map[string]any)["remotePort"] = remotePort2 proxyBody, _ = json.Marshal(proxyConfig) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { @@ -125,11 +129,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ - "name": "test-tcp", - "type": "tcp", - "localIP": "127.0.0.1", - "localPort": f.PortByName(framework.TCPEchoServerPort), - "remotePort": remotePort, + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, } proxyBody, _ := json.Marshal(proxyConfig) @@ -171,11 +177,13 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ - "name": "test-tcp", - "type": "tcp", - "localIP": "127.0.0.1", - "localPort": f.PortByName(framework.TCPEchoServerPort), - "remotePort": remotePort, + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, } proxyBody, _ := json.Marshal(proxyConfig) @@ -226,5 +234,90 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 404 }) }) + + ginkgo.It("rejects mismatched type block", func() { + adminPort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + invalidBody, _ := json.Marshal(map[string]any{ + "name": "bad-proxy", + "type": "tcp", + "udp": map[string]any{ + "localPort": 1234, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(invalidBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 400 + }) + }) + + ginkgo.It("rejects path/body name mismatch on update", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + time.Sleep(500 * time.Millisecond) + + createBody, _ := json.Marshal(map[string]any{ + "name": "proxy-a", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(createBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + updateBody, _ := json.Marshal(map[string]any{ + "name": "proxy-b", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/proxy-a").HTTPParams("PUT", "", "/api/store/proxies/proxy-a", map[string]string{ + "Content-Type": "application/json", + }).Body(updateBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 400 + }) + }) }) }) diff --git a/web/frpc/src/api/frpc.ts b/web/frpc/src/api/frpc.ts index bacfdcdc..213c04fa 100644 --- a/web/frpc/src/api/frpc.ts +++ b/web/frpc/src/api/frpc.ts @@ -1,10 +1,10 @@ import { http } from './http' import type { StatusResponse, - StoreProxyListResp, - StoreProxyConfig, - StoreVisitorListResp, - StoreVisitorConfig, + ProxyListResp, + ProxyDefinition, + VisitorListResp, + VisitorDefinition, } from '../types/proxy' export const getStatus = () => { @@ -25,21 +25,21 @@ export const reloadConfig = () => { // Store API - Proxies export const listStoreProxies = () => { - return http.get('/api/store/proxies') + return http.get('/api/store/proxies') } export const getStoreProxy = (name: string) => { - return http.get( + return http.get( `/api/store/proxies/${encodeURIComponent(name)}`, ) } -export const createStoreProxy = (config: Record) => { - return http.post('/api/store/proxies', config) +export const createStoreProxy = (config: ProxyDefinition) => { + return http.post('/api/store/proxies', config) } -export const updateStoreProxy = (name: string, config: Record) => { - return http.put( +export const updateStoreProxy = (name: string, config: ProxyDefinition) => { + return http.put( `/api/store/proxies/${encodeURIComponent(name)}`, config, ) @@ -51,24 +51,24 @@ export const deleteStoreProxy = (name: string) => { // Store API - Visitors export const listStoreVisitors = () => { - return http.get('/api/store/visitors') + return http.get('/api/store/visitors') } export const getStoreVisitor = (name: string) => { - return http.get( + return http.get( `/api/store/visitors/${encodeURIComponent(name)}`, ) } -export const createStoreVisitor = (config: Record) => { - return http.post('/api/store/visitors', config) +export const createStoreVisitor = (config: VisitorDefinition) => { + return http.post('/api/store/visitors', config) } export const updateStoreVisitor = ( name: string, - config: Record, + config: VisitorDefinition, ) => { - return http.put( + return http.put( `/api/store/visitors/${encodeURIComponent(name)}`, config, ) diff --git a/web/frpc/src/types/proxy.ts b/web/frpc/src/types/proxy.ts index b4c37eee..a4b9b88a 100644 --- a/web/frpc/src/types/proxy.ts +++ b/web/frpc/src/types/proxy.ts @@ -20,24 +20,33 @@ export type StatusResponse = Record // STORE API TYPES // ======================================== -export interface StoreProxyConfig { +export interface ProxyDefinition { name: string - type: string - config: Record + type: ProxyType + tcp?: Record + udp?: Record + http?: Record + https?: Record + tcpmux?: Record + stcp?: Record + sudp?: Record + xtcp?: Record } -export interface StoreVisitorConfig { +export interface VisitorDefinition { name: string - type: string - config: Record + type: VisitorType + stcp?: Record + sudp?: Record + xtcp?: Record } -export interface StoreProxyListResp { - proxies: StoreProxyConfig[] +export interface ProxyListResp { + proxies: ProxyDefinition[] } -export interface StoreVisitorListResp { - visitors: StoreVisitorConfig[] +export interface VisitorListResp { + visitors: VisitorDefinition[] } // ======================================== @@ -255,29 +264,24 @@ export function createDefaultVisitorForm(): VisitorFormData { // CONVERTERS: Form -> Store API // ======================================== -export function formToStoreProxy(form: ProxyFormData): Record { - const config: Record = { - name: form.name, - type: form.type, - } +export function formToStoreProxy(form: ProxyFormData): ProxyDefinition { + const block: Record = {} // Enabled (nil/true = enabled, false = disabled) if (!form.enabled) { - config.enabled = false + block.enabled = false } // Backend - LocalIP/LocalPort if (form.pluginType === '') { - // No plugin, use local backend if (form.localIP && form.localIP !== '127.0.0.1') { - config.localIP = form.localIP + block.localIP = form.localIP } if (form.localPort != null) { - config.localPort = form.localPort + block.localPort = form.localPort } } else { - // Plugin backend - config.plugin = { + block.plugin = { type: form.pluginType, ...form.pluginConfig, } @@ -291,109 +295,102 @@ export function formToStoreProxy(form: ProxyFormData): Record { (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') || form.proxyProtocolVersion ) { - config.transport = {} - if (form.useEncryption) config.transport.useEncryption = true - if (form.useCompression) config.transport.useCompression = true - if (form.bandwidthLimit) - config.transport.bandwidthLimit = form.bandwidthLimit + block.transport = {} + if (form.useEncryption) block.transport.useEncryption = true + if (form.useCompression) block.transport.useCompression = true + if (form.bandwidthLimit) block.transport.bandwidthLimit = form.bandwidthLimit if (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') { - config.transport.bandwidthLimitMode = form.bandwidthLimitMode + block.transport.bandwidthLimitMode = form.bandwidthLimitMode } if (form.proxyProtocolVersion) { - config.transport.proxyProtocolVersion = form.proxyProtocolVersion + block.transport.proxyProtocolVersion = form.proxyProtocolVersion } } // Load Balancer if (form.loadBalancerGroup) { - config.loadBalancer = { + block.loadBalancer = { group: form.loadBalancerGroup, } if (form.loadBalancerGroupKey) { - config.loadBalancer.groupKey = form.loadBalancerGroupKey + block.loadBalancer.groupKey = form.loadBalancerGroupKey } } // Health Check if (form.healthCheckType) { - config.healthCheck = { + block.healthCheck = { type: form.healthCheckType, } if (form.healthCheckTimeoutSeconds != null) { - config.healthCheck.timeoutSeconds = form.healthCheckTimeoutSeconds + block.healthCheck.timeoutSeconds = form.healthCheckTimeoutSeconds } if (form.healthCheckMaxFailed != null) { - config.healthCheck.maxFailed = form.healthCheckMaxFailed + block.healthCheck.maxFailed = form.healthCheckMaxFailed } if (form.healthCheckIntervalSeconds != null) { - config.healthCheck.intervalSeconds = form.healthCheckIntervalSeconds + block.healthCheck.intervalSeconds = form.healthCheckIntervalSeconds } if (form.healthCheckPath) { - config.healthCheck.path = form.healthCheckPath + block.healthCheck.path = form.healthCheckPath } if (form.healthCheckHTTPHeaders.length > 0) { - config.healthCheck.httpHeaders = form.healthCheckHTTPHeaders + block.healthCheck.httpHeaders = form.healthCheckHTTPHeaders } } // Metadata if (form.metadatas.length > 0) { - config.metadatas = Object.fromEntries( + block.metadatas = Object.fromEntries( form.metadatas.map((m) => [m.key, m.value]), ) } // Annotations if (form.annotations.length > 0) { - config.annotations = Object.fromEntries( + block.annotations = Object.fromEntries( form.annotations.map((a) => [a.key, a.value]), ) } // Type-specific fields - if (form.type === 'tcp' || form.type === 'udp') { - if (form.remotePort != null) { - config.remotePort = form.remotePort - } + if ((form.type === 'tcp' || form.type === 'udp') && form.remotePort != null) { + block.remotePort = form.remotePort } if (form.type === 'http' || form.type === 'https' || form.type === 'tcpmux') { - // Domain config if (form.customDomains) { - config.customDomains = form.customDomains + block.customDomains = form.customDomains .split(',') .map((s) => s.trim()) .filter(Boolean) } if (form.subdomain) { - config.subdomain = form.subdomain + block.subdomain = form.subdomain } } if (form.type === 'http') { - // HTTP specific if (form.locations) { - config.locations = form.locations + block.locations = form.locations .split(',') .map((s) => s.trim()) .filter(Boolean) } - if (form.httpUser) config.httpUser = form.httpUser - if (form.httpPassword) config.httpPassword = form.httpPassword - if (form.hostHeaderRewrite) - config.hostHeaderRewrite = form.hostHeaderRewrite - if (form.routeByHTTPUser) config.routeByHTTPUser = form.routeByHTTPUser + if (form.httpUser) block.httpUser = form.httpUser + if (form.httpPassword) block.httpPassword = form.httpPassword + if (form.hostHeaderRewrite) block.hostHeaderRewrite = form.hostHeaderRewrite + if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser - // Header operations if (form.requestHeaders.length > 0) { - config.requestHeaders = { + block.requestHeaders = { set: Object.fromEntries( form.requestHeaders.map((h) => [h.key, h.value]), ), } } if (form.responseHeaders.length > 0) { - config.responseHeaders = { + block.responseHeaders = { set: Object.fromEntries( form.responseHeaders.map((h) => [h.key, h.value]), ), @@ -402,107 +399,194 @@ export function formToStoreProxy(form: ProxyFormData): Record { } if (form.type === 'tcpmux') { - // TCPMux specific - if (form.httpUser) config.httpUser = form.httpUser - if (form.httpPassword) config.httpPassword = form.httpPassword - if (form.routeByHTTPUser) config.routeByHTTPUser = form.routeByHTTPUser + if (form.httpUser) block.httpUser = form.httpUser + if (form.httpPassword) block.httpPassword = form.httpPassword + if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser if (form.multiplexer && form.multiplexer !== 'httpconnect') { - config.multiplexer = form.multiplexer + block.multiplexer = form.multiplexer } } if (form.type === 'stcp' || form.type === 'sudp' || form.type === 'xtcp') { - // Secure proxy types - if (form.secretKey) config.secretKey = form.secretKey + if (form.secretKey) block.secretKey = form.secretKey if (form.allowUsers) { - config.allowUsers = form.allowUsers + block.allowUsers = form.allowUsers .split(',') .map((s) => s.trim()) .filter(Boolean) } } - if (form.type === 'xtcp') { - // XTCP NAT traversal - if (form.natTraversalDisableAssistedAddrs) { - config.natTraversal = { - disableAssistedAddrs: true, - } + if (form.type === 'xtcp' && form.natTraversalDisableAssistedAddrs) { + block.natTraversal = { + disableAssistedAddrs: true, } } - return config + return withStoreProxyBlock( + { + name: form.name, + type: form.type, + }, + form.type, + block, + ) } -export function formToStoreVisitor(form: VisitorFormData): Record { - const config: Record = { - name: form.name, - type: form.type, - } +export function formToStoreVisitor(form: VisitorFormData): VisitorDefinition { + const block: Record = {} - // Enabled if (!form.enabled) { - config.enabled = false + block.enabled = false } - // Transport if (form.useEncryption || form.useCompression) { - config.transport = {} - if (form.useEncryption) config.transport.useEncryption = true - if (form.useCompression) config.transport.useCompression = true + block.transport = {} + if (form.useEncryption) block.transport.useEncryption = true + if (form.useCompression) block.transport.useCompression = true } - // Base fields - if (form.secretKey) config.secretKey = form.secretKey - if (form.serverUser) config.serverUser = form.serverUser - if (form.serverName) config.serverName = form.serverName + if (form.secretKey) block.secretKey = form.secretKey + if (form.serverUser) block.serverUser = form.serverUser + if (form.serverName) block.serverName = form.serverName if (form.bindAddr && form.bindAddr !== '127.0.0.1') { - config.bindAddr = form.bindAddr + block.bindAddr = form.bindAddr } if (form.bindPort != null) { - config.bindPort = form.bindPort + block.bindPort = form.bindPort } - // XTCP specific if (form.type === 'xtcp') { if (form.protocol && form.protocol !== 'quic') { - config.protocol = form.protocol + block.protocol = form.protocol } if (form.keepTunnelOpen) { - config.keepTunnelOpen = true + block.keepTunnelOpen = true } if (form.maxRetriesAnHour != null) { - config.maxRetriesAnHour = form.maxRetriesAnHour + block.maxRetriesAnHour = form.maxRetriesAnHour } if (form.minRetryInterval != null) { - config.minRetryInterval = form.minRetryInterval + block.minRetryInterval = form.minRetryInterval } if (form.fallbackTo) { - config.fallbackTo = form.fallbackTo + block.fallbackTo = form.fallbackTo } if (form.fallbackTimeoutMs != null) { - config.fallbackTimeoutMs = form.fallbackTimeoutMs + block.fallbackTimeoutMs = form.fallbackTimeoutMs } if (form.natTraversalDisableAssistedAddrs) { - config.natTraversal = { + block.natTraversal = { disableAssistedAddrs: true, } } } - return config + return withStoreVisitorBlock( + { + name: form.name, + type: form.type, + }, + form.type, + block, + ) } // ======================================== // CONVERTERS: Store API -> Form // ======================================== -export function storeProxyToForm(config: StoreProxyConfig): ProxyFormData { - const c = config.config || {} +function getStoreProxyBlock(config: ProxyDefinition): Record { + switch (config.type) { + case 'tcp': + return config.tcp || {} + case 'udp': + return config.udp || {} + case 'http': + return config.http || {} + case 'https': + return config.https || {} + case 'tcpmux': + return config.tcpmux || {} + case 'stcp': + return config.stcp || {} + case 'sudp': + return config.sudp || {} + case 'xtcp': + return config.xtcp || {} + } +} + +function withStoreProxyBlock( + payload: ProxyDefinition, + type: ProxyType, + block: Record, +): ProxyDefinition { + switch (type) { + case 'tcp': + payload.tcp = block + break + case 'udp': + payload.udp = block + break + case 'http': + payload.http = block + break + case 'https': + payload.https = block + break + case 'tcpmux': + payload.tcpmux = block + break + case 'stcp': + payload.stcp = block + break + case 'sudp': + payload.sudp = block + break + case 'xtcp': + payload.xtcp = block + break + } + return payload +} + +function getStoreVisitorBlock(config: VisitorDefinition): Record { + switch (config.type) { + case 'stcp': + return config.stcp || {} + case 'sudp': + return config.sudp || {} + case 'xtcp': + return config.xtcp || {} + } +} + +function withStoreVisitorBlock( + payload: VisitorDefinition, + type: VisitorType, + block: Record, +): VisitorDefinition { + switch (type) { + case 'stcp': + payload.stcp = block + break + case 'sudp': + payload.sudp = block + break + case 'xtcp': + payload.xtcp = block + break + } + return payload +} + +export function storeProxyToForm(config: ProxyDefinition): ProxyFormData { + const c = getStoreProxyBlock(config) const form = createDefaultProxyForm() form.name = config.name || '' - form.type = (config.type as ProxyType) || 'tcp' + form.type = config.type || 'tcp' form.enabled = c.enabled !== false // Backend @@ -608,13 +692,13 @@ export function storeProxyToForm(config: StoreProxyConfig): ProxyFormData { } export function storeVisitorToForm( - config: StoreVisitorConfig, + config: VisitorDefinition, ): VisitorFormData { - const c = config.config || {} + const c = getStoreVisitorBlock(config) const form = createDefaultVisitorForm() form.name = config.name || '' - form.type = (config.type as VisitorType) || 'stcp' + form.type = config.type || 'stcp' form.enabled = c.enabled !== false // Transport diff --git a/web/frpc/src/views/Overview.vue b/web/frpc/src/views/Overview.vue index 6018403f..c8dfcdba 100644 --- a/web/frpc/src/views/Overview.vue +++ b/web/frpc/src/views/Overview.vue @@ -337,17 +337,18 @@
- - Server: {{ visitor.config.serverName }} + + Server: {{ getVisitorBlock(visitor)?.serverName }} - Bind: {{ visitor.config.bindAddr || '127.0.0.1' - }}:{{ getVisitorBlock(visitor)?.bindPort }}
@@ -388,8 +389,8 @@ import { } from '../api/frpc' import type { ProxyStatus, - StoreProxyConfig, - StoreVisitorConfig, + ProxyDefinition, + VisitorDefinition, } from '../types/proxy' import StatCard from '../components/StatCard.vue' import ProxyCard from '../components/ProxyCard.vue' @@ -398,8 +399,8 @@ const router = useRouter() // State const status = ref([]) -const storeProxies = ref([]) -const storeVisitors = ref([]) +const storeProxies = ref([]) +const storeVisitors = ref([]) const storeEnabled = ref(false) const loading = ref(false) const searchText = ref('') @@ -463,9 +464,41 @@ const filteredStatus = computed(() => { }) const disabledStoreProxies = computed(() => { - return storeProxies.value.filter((p) => p.config?.enabled === false) + return storeProxies.value.filter((p) => getProxyBlock(p)?.enabled === false) }) +const getProxyBlock = (proxy: ProxyDefinition) => { + switch (proxy.type) { + case 'tcp': + return proxy.tcp + case 'udp': + return proxy.udp + case 'http': + return proxy.http + case 'https': + return proxy.https + case 'tcpmux': + return proxy.tcpmux + case 'stcp': + return proxy.stcp + case 'sudp': + return proxy.sudp + case 'xtcp': + return proxy.xtcp + } +} + +const getVisitorBlock = (visitor: VisitorDefinition) => { + switch (visitor.type) { + case 'stcp': + return visitor.stcp + case 'sudp': + return visitor.sudp + case 'xtcp': + return visitor.xtcp + } +} + // Methods const toggleTypeFilter = (type: string) => { filterType.value = filterType.value === type ? '' : type @@ -563,11 +596,11 @@ const handleDelete = async (proxy: ProxyStatus) => { confirmAndDeleteProxy(proxy.name) } -const handleEditStoreProxy = (proxy: StoreProxyConfig) => { +const handleEditStoreProxy = (proxy: ProxyDefinition) => { router.push('/proxies/' + encodeURIComponent(proxy.name) + '/edit') } -const handleDeleteStoreProxy = async (proxy: StoreProxyConfig) => { +const handleDeleteStoreProxy = async (proxy: ProxyDefinition) => { confirmAndDeleteProxy(proxy.name) } @@ -575,7 +608,7 @@ const handleCreateVisitor = () => { router.push('/visitors/create') } -const handleEditVisitor = (visitor: StoreVisitorConfig) => { +const handleEditVisitor = (visitor: VisitorDefinition) => { router.push('/visitors/' + encodeURIComponent(visitor.name) + '/edit') } From 774478d0710e2bac7a85128bbdcd334e2d6581be Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 4 Mar 2026 19:27:30 +0800 Subject: [PATCH 07/43] pkg/config: improve error messages with line number details for config parsing (#5194) When frpc verify encounters config errors, the error messages now include line/column information to help users locate problems: - TOML syntax errors: report line and column from the TOML parser (e.g., "toml: line 4, column 11: expected character ]") - Type mismatch errors: report the field name and approximate line number (e.g., "line 3: field \"proxies\": cannot unmarshal string into ...") - File format detection: use file extension to determine format, preventing silent fallthrough from TOML to YAML parser which produced confusing errors Previously, a TOML file with syntax errors would silently fall through to the YAML parser, which would misinterpret the content and produce unhelpful errors like "json: cannot unmarshal string into Go value of type v1.ClientConfig". https://claude.ai/code/session_017HWLfcXS3U2hLoy4dsg8Nv Co-authored-by: Claude --- pkg/config/load.go | 100 ++++++++++++++++++++++++++++++++++--- pkg/config/load_test.go | 106 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 6 deletions(-) diff --git a/pkg/config/load.go b/pkg/config/load.go index 159cd097..dc1de0af 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -16,6 +16,8 @@ package config import ( "bytes" + "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -108,7 +110,21 @@ func LoadConfigureFromFile(path string, c any, strict bool) error { if err != nil { return err } - return LoadConfigure(content, c, strict) + return LoadConfigure(content, c, strict, detectFormatFromPath(path)) +} + +// detectFormatFromPath returns a format hint based on the file extension. +func detectFormatFromPath(path string) string { + switch strings.ToLower(filepath.Ext(path)) { + case ".toml": + return "toml" + case ".yaml", ".yml": + return "yaml" + case ".json": + return "json" + default: + return "" + } } // parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling @@ -155,30 +171,102 @@ func decodeJSONContent(content []byte, target any, strict bool) error { // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. -func LoadConfigure(b []byte, c any, strict bool) error { +// An optional format hint (e.g. "toml", "yaml", "json") can be provided +// to enable better error messages with line number information. +func LoadConfigure(b []byte, c any, strict bool, formats ...string) error { + format := "" + if len(formats) > 0 { + format = formats[0] + } + + originalBytes := b + var tomlObj any - // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). - if err := toml.Unmarshal(b, &tomlObj); err == nil { + tomlErr := toml.Unmarshal(b, &tomlObj) + if tomlErr == nil { var err error b, err = jsonx.Marshal(&tomlObj) if err != nil { return err } + } else if format == "toml" { + // File is known to be TOML but has syntax errors — report with line/column info. + return formatTOMLError(tomlErr) } + // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. if yaml.IsJSONBuffer(b) { - return decodeJSONContent(b, c, strict) + if err := decodeJSONContent(b, c, strict); err != nil { + return enhanceDecodeError(err, originalBytes) + } + return nil } // Handle YAML content if strict { // In strict mode, always use our custom handler to support YAML merge - return parseYAMLWithDotFieldsHandling(b, c) + if err := parseYAMLWithDotFieldsHandling(b, c); err != nil { + return enhanceDecodeError(err, originalBytes) + } + return nil } // Non-strict mode, parse normally return yaml.Unmarshal(b, c) } +// formatTOMLError extracts line/column information from TOML decode errors. +func formatTOMLError(err error) error { + var decErr *toml.DecodeError + if errors.As(err, &decErr) { + row, col := decErr.Position() + return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error()) + } + var strictErr *toml.StrictMissingError + if errors.As(err, &strictErr) { + return fmt.Errorf("toml: %s", strictErr.Error()) + } + return err +} + +// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors. +func enhanceDecodeError(err error, originalContent []byte) error { + var typeErr *json.UnmarshalTypeError + if errors.As(err, &typeErr) && typeErr.Field != "" { + line := findFieldLineInContent(originalContent, typeErr.Field) + if line > 0 { + return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type) + } + return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type) + } + return err +} + +// findFieldLineInContent searches the original config content for a field name +// and returns the 1-indexed line number where it appears, or 0 if not found. +func findFieldLineInContent(content []byte, fieldPath string) int { + // Use the last component of the field path (e.g. "proxies" from "proxies" or + // "protocol" from "transport.protocol"). + parts := strings.Split(fieldPath, ".") + searchKey := parts[len(parts)-1] + + lines := bytes.Split(content, []byte("\n")) + for i, line := range lines { + trimmed := bytes.TrimSpace(line) + // Match TOML key assignments like: key = ... + if bytes.HasPrefix(trimmed, []byte(searchKey)) { + rest := bytes.TrimSpace(trimmed[len(searchKey):]) + if len(rest) > 0 && rest[0] == '=' { + return i + 1 + } + } + // Match TOML table array headers like: [[proxies]] + if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) { + return i + 1 + } + } + return 0 +} + func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP)) diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index b23955b8..9f2f5a9f 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -495,3 +495,109 @@ serverPort: 7000 require.Equal("127.0.0.1", clientCfg.ServerAddr) require.Equal(7000, clientCfg.ServerPort) } + +func TestTOMLSyntaxErrorWithLineNumber(t *testing.T) { + require := require.New(t) + + // TOML with syntax error (unclosed table array header) + content := `serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies] +name = "test" +` + + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.Error(err) + require.Contains(err.Error(), "line") + require.Contains(err.Error(), "toml") +} + +func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) { + require := require.New(t) + + // TOML with wrong type: proxies should be a table array, not a string + content := `serverAddr = "127.0.0.1" +serverPort = 7000 +proxies = "this should be a table array" +` + + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.Error(err) + // The error should contain field info + errMsg := err.Error() + require.Contains(errMsg, "proxies") +} + +func TestFindFieldLineInContent(t *testing.T) { + content := []byte(`serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +`) + + tests := []struct { + fieldPath string + wantLine int + }{ + {"serverAddr", 1}, + {"serverPort", 2}, + {"name", 5}, + {"type", 6}, + {"remotePort", 7}, + {"nonexistent", 0}, + } + + for _, tt := range tests { + t.Run(tt.fieldPath, func(t *testing.T) { + got := findFieldLineInContent(content, tt.fieldPath) + require.Equal(t, tt.wantLine, got) + }) + } +} + +func TestFormatDetection(t *testing.T) { + tests := []struct { + path string + format string + }{ + {"config.toml", "toml"}, + {"config.TOML", "toml"}, + {"config.yaml", "yaml"}, + {"config.yml", "yaml"}, + {"config.json", "json"}, + {"config.ini", ""}, + {"config", ""}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + require.Equal(t, tt.format, detectFormatFromPath(tt.path)) + }) + } +} + +func TestValidTOMLStillWorks(t *testing.T) { + require := require.New(t) + + // Valid TOML with format hint should work fine + content := `serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +` + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) + require.Len(clientCfg.Proxies, 1) +} From b7435967b04bd0d4f9261a91af4c31f91006ab80 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 4 Mar 2026 20:53:22 +0800 Subject: [PATCH 08/43] pkg/config: fix line numbers shown incorrectly for TOML type mismatch errors (#5195) --- pkg/config/load.go | 24 ++++++++++++++++-------- pkg/config/load_test.go | 6 ++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/config/load.go b/pkg/config/load.go index dc1de0af..38634fc5 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -180,24 +180,26 @@ func LoadConfigure(b []byte, c any, strict bool, formats ...string) error { } originalBytes := b + parsedFromTOML := false var tomlObj any tomlErr := toml.Unmarshal(b, &tomlObj) if tomlErr == nil { + parsedFromTOML = true var err error b, err = jsonx.Marshal(&tomlObj) if err != nil { return err } } else if format == "toml" { - // File is known to be TOML but has syntax errors — report with line/column info. + // File is known to be TOML but has syntax errors. return formatTOMLError(tomlErr) } // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. if yaml.IsJSONBuffer(b) { if err := decodeJSONContent(b, c, strict); err != nil { - return enhanceDecodeError(err, originalBytes) + return enhanceDecodeError(err, originalBytes, !parsedFromTOML) } return nil } @@ -206,7 +208,7 @@ func LoadConfigure(b []byte, c any, strict bool, formats ...string) error { if strict { // In strict mode, always use our custom handler to support YAML merge if err := parseYAMLWithDotFieldsHandling(b, c); err != nil { - return enhanceDecodeError(err, originalBytes) + return enhanceDecodeError(err, originalBytes, !parsedFromTOML) } return nil } @@ -223,18 +225,20 @@ func formatTOMLError(err error) error { } var strictErr *toml.StrictMissingError if errors.As(err, &strictErr) { - return fmt.Errorf("toml: %s", strictErr.Error()) + return strictErr } return err } // enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors. -func enhanceDecodeError(err error, originalContent []byte) error { +func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error { var typeErr *json.UnmarshalTypeError if errors.As(err, &typeErr) && typeErr.Field != "" { - line := findFieldLineInContent(originalContent, typeErr.Field) - if line > 0 { - return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type) + if includeLine { + line := findFieldLineInContent(originalContent, typeErr.Field) + if line > 0 { + return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type) + } } return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type) } @@ -244,6 +248,10 @@ func enhanceDecodeError(err error, originalContent []byte) error { // findFieldLineInContent searches the original config content for a field name // and returns the 1-indexed line number where it appears, or 0 if not found. func findFieldLineInContent(content []byte, fieldPath string) int { + if fieldPath == "" { + return 0 + } + // Use the last component of the field path (e.g. "proxies" from "proxies" or // "protocol" from "transport.protocol"). parts := strings.Split(fieldPath, ".") diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 9f2f5a9f..b711a5c1 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -496,7 +496,7 @@ serverPort: 7000 require.Equal(7000, clientCfg.ServerPort) } -func TestTOMLSyntaxErrorWithLineNumber(t *testing.T) { +func TestTOMLSyntaxErrorWithPosition(t *testing.T) { require := require.New(t) // TOML with syntax error (unclosed table array header) @@ -510,8 +510,9 @@ name = "test" clientCfg := v1.ClientConfig{} err := LoadConfigure([]byte(content), &clientCfg, false, "toml") require.Error(err) - require.Contains(err.Error(), "line") require.Contains(err.Error(), "toml") + require.Contains(err.Error(), "line") + require.Contains(err.Error(), "column") } func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) { @@ -529,6 +530,7 @@ proxies = "this should be a table array" // The error should contain field info errMsg := err.Error() require.Contains(errMsg, "proxies") + require.NotContains(errMsg, "line") } func TestFindFieldLineInContent(t *testing.T) { From 541878af4d3950c92b542350b9a4a043080c0980 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 5 Mar 2026 00:00:46 +0800 Subject: [PATCH 09/43] docs: update release notes for config parsing error improvements (#5196) --- Release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Release.md b/Release.md index c4425976..87a2cc95 100644 --- a/Release.md +++ b/Release.md @@ -6,3 +6,4 @@ * Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic. * Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded. +* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers. From 462c987f6debaebffa7baf6530e653c2856cea96 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 01:38:24 +0800 Subject: [PATCH 10/43] pkg/msg: change UDPPacket.Content from string to []byte to avoid redundant base64 encode/decode (#5198) --- client/proxy/udp.go | 4 ++-- client/visitor/sudp.go | 6 +++--- pkg/msg/msg.go | 2 +- pkg/proto/udp/udp.go | 10 +++++----- server/proxy/udp.go | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/proxy/udp.go b/client/proxy/udp.go index 1fca9904..68426dc6 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -129,7 +129,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { return } if errRet := errors.PanicToError(func() { - xl.Tracef("get udp package from workConn: %s", udpMsg.Content) + xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content)) readCh <- &udpMsg }); errRet != nil { xl.Infof("reader goroutine for udp work connection closed: %v", errRet) @@ -145,7 +145,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { for rawMsg := range sendCh { switch m := rawMsg.(type) { case *msg.UDPPacket: - xl.Tracef("send udp package to workConn: %s", m.Content) + xl.Tracef("send udp package to workConn, len: %d", len(m.Content)) case *msg.Ping: xl.Tracef("send ping message to udp workConn") } diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 8c7d793c..67b4c214 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -147,7 +147,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { case *msg.UDPPacket: if errRet := errors.PanicToError(func() { sv.readCh <- m - xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content) + xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content)) }); errRet != nil { xl.Infof("reader goroutine for udp work connection closed") return @@ -169,7 +169,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) return } - xl.Tracef("send udp package to workConn: %s", firstPacket.Content) + xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content)) } for { @@ -184,7 +184,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) return } - xl.Tracef("send udp package to workConn: %s", udpMsg.Content) + xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content)) case <-closeCh: return } diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 7b36c2aa..dda8e9aa 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -184,7 +184,7 @@ type Pong struct { } type UDPPacket struct { - Content string `json:"c,omitempty"` + Content []byte `json:"c,omitempty"` LocalAddr *net.UDPAddr `json:"l,omitempty"` RemoteAddr *net.UDPAddr `json:"r,omitempty"` } diff --git a/pkg/proto/udp/udp.go b/pkg/proto/udp/udp.go index 0ad8aae1..4d3df5af 100644 --- a/pkg/proto/udp/udp.go +++ b/pkg/proto/udp/udp.go @@ -15,7 +15,6 @@ package udp import ( - "encoding/base64" "net" "sync" "time" @@ -28,16 +27,17 @@ import ( ) func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket { + content := make([]byte, len(buf)) + copy(content, buf) return &msg.UDPPacket{ - Content: base64.StdEncoding.EncodeToString(buf), + Content: content, LocalAddr: laddr, RemoteAddr: raddr, } } func GetContent(m *msg.UDPPacket) (buf []byte, err error) { - buf, err = base64.StdEncoding.DecodeString(m.Content) - return + return m.Content, nil } func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) { @@ -60,7 +60,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh if err != nil { return } - // buf[:n] will be encoded to string, so the bytes can be reused + // NewUDPPacket copies buf[:n], so the read buffer can be reused udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr) select { diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 3751dc9b..d362ab9c 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -136,7 +136,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { continue case *msg.UDPPacket: if errRet := errors.PanicToError(func() { - xl.Tracef("get udp message from workConn: %s", m.Content) + xl.Tracef("get udp message from workConn, len: %d", len(m.Content)) pxy.readCh <- m metrics.Server.AddTrafficOut( pxy.GetName(), @@ -167,7 +167,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { conn.Close() return } - xl.Tracef("send message to udp workConn: %s", udpMsg.Content) + xl.Tracef("send message to udp workConn, len: %d", len(udpMsg.Content)) metrics.Server.AddTrafficIn( pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type, From f22f7d539c42071762b8682aa922912c38978801 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 02:25:47 +0800 Subject: [PATCH 11/43] server/group: fix port leak and incorrect Listen port in TCPGroup (#5200) Fix two bugs in TCPGroup.Listen(): - Release acquired port when net.Listen fails to prevent port leak - Use realPort instead of port for net.Listen to ensure consistency between port manager records and actual listening port --- server/group/tcp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/group/tcp.go b/server/group/tcp.go index c0dcd5f7..f52d6407 100644 --- a/server/group/tcp.go +++ b/server/group/tcp.go @@ -100,8 +100,9 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr if err != nil { return } - tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(port))) + tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(realPort))) if errRet != nil { + tg.ctl.portManager.Release(realPort) err = errRet return } From c62a1da1611ed6a52893bc81108c31788b625065 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 15:18:38 +0800 Subject: [PATCH 12/43] fix: close connections on error paths to prevent resource leaks (#5202) Fix connection leaks in multiple error paths across client and server: - server/proxy/http: close tmpConn when WithEncryption fails - client/proxy: close localConn when ProxyProtocol WriteTo fails - client/visitor/sudp: close visitorConn on all error paths in getNewVisitorConn - client/visitor/xtcp: close tunnelConn when WithEncryption fails - client/visitor/xtcp: close lConn when NewKCPConnFromUDP fails - pkg/plugin/client/unix_domain_socket: close localConn and connInfo.Conn when WriteTo fails, close connInfo.Conn when DialUnix fails - pkg/plugin/client/tls2raw: close tlsConn when Handshake or Dial fails --- client/proxy/proxy.go | 1 + client/visitor/sudp.go | 4 ++++ client/visitor/xtcp.go | 2 ++ pkg/plugin/client/tls2raw.go | 2 ++ pkg/plugin/client/unix_domain_socket.go | 3 +++ server/proxy/http.go | 1 + 6 files changed, 13 insertions(+) diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 8faff38d..ca7d905b 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -209,6 +209,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor if connInfo.ProxyProtocolHeader != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { workConn.Close() + localConn.Close() xl.Errorf("write proxy protocol header to local conn error: %v", err) return } diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 67b4c214..a341da8a 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -217,6 +217,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { } err = msg.WriteMsg(visitorConn, newVisitorConnMsg) if err != nil { + visitorConn.Close() return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err) } @@ -224,11 +225,13 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) if err != nil { + visitorConn.Close() return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err) } _ = visitorConn.SetReadDeadline(time.Time{}) if newVisitorConnRespMsg.Error != "" { + visitorConn.Close() return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) } @@ -238,6 +241,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) if err != nil { xl.Errorf("create encryption stream error: %v", err) + visitorConn.Close() return nil, err } } diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index dcfba505..2273a271 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -211,6 +211,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) if err != nil { xl.Errorf("create encryption stream error: %v", err) + tunnelConn.Close() tunnelErr = err return } @@ -373,6 +374,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er } remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) if err != nil { + lConn.Close() return fmt.Errorf("create kcp connection from udp connection error: %v", err) } diff --git a/pkg/plugin/client/tls2raw.go b/pkg/plugin/client/tls2raw.go index 445b6c91..ccb75149 100644 --- a/pkg/plugin/client/tls2raw.go +++ b/pkg/plugin/client/tls2raw.go @@ -62,11 +62,13 @@ func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { if err := tlsConn.Handshake(); err != nil { xl.Warnf("tls handshake error: %v", err) + tlsConn.Close() return } rawConn, err := net.Dial("tcp", p.opts.LocalAddr) if err != nil { xl.Warnf("dial to local addr error: %v", err) + tlsConn.Close() return } diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index 52d9c652..3046aade 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -54,10 +54,13 @@ func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *Connect localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) if err != nil { xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) + connInfo.Conn.Close() return } if connInfo.ProxyProtocolHeader != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { + localConn.Close() + connInfo.Conn.Close() return } } diff --git a/server/proxy/http.go b/server/proxy/http.go index 2c4f1fd4..6f0eb008 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -168,6 +168,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { xl.Errorf("create encryption stream error: %v", err) + tmpConn.Close() return } } From 8f633fe363ae20d244e23b77b8dc5260b3171e8f Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 15:55:22 +0800 Subject: [PATCH 13/43] fix: return buffers to pool on error paths to reduce GC pressure (#5203) - pkg/nathole/nathole.go: add pool.PutBuf(buf) on ReadFromUDP error and DecodeMessageInto error paths in waitDetectMessage - pkg/proto/udp/udp.go: add defer pool.PutBuf(buf) in writerFn to ensure buffer is returned when the goroutine exits --- pkg/nathole/nathole.go | 2 ++ pkg/proto/udp/udp.go | 1 + 2 files changed, 3 insertions(+) diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index 72522fac..7d6ebe89 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -298,11 +298,13 @@ func waitDetectMessage( n, raddr, err := conn.ReadFromUDP(buf) _ = conn.SetReadDeadline(time.Time{}) if err != nil { + pool.PutBuf(buf) return nil, err } xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr) var m msg.NatHoleSid if err := DecodeMessageInto(buf[:n], key, &m); err != nil { + pool.PutBuf(buf) xl.Warnf("decode sid message error: %v", err) continue } diff --git a/pkg/proto/udp/udp.go b/pkg/proto/udp/udp.go index 4d3df5af..21c41680 100644 --- a/pkg/proto/udp/udp.go +++ b/pkg/proto/udp/udp.go @@ -85,6 +85,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- }() buf := pool.GetBuf(bufSize) + defer pool.PutBuf(buf) for { _ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second)) n, _, err := udpConn.ReadFromUDP(buf) From cb459b02b618850300b9c3d1aae9cb716d3487bf Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 16:51:52 +0800 Subject: [PATCH 14/43] fix: WebsocketListener nil panic and OIDC auth data race (#5204) - pkg/util/net/websocket.go: store ln parameter in struct to prevent nil pointer panic when Addr() is called - pkg/auth/oidc.go: replace unsynchronized []string with map + RWMutex for subjectsFromLogin to fix data race across concurrent connections --- pkg/auth/oidc.go | 20 ++++++++++++-------- pkg/util/net/websocket.go | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index d9377f32..9aeaf3c5 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -23,6 +23,7 @@ import ( "net/url" "os" "slices" + "sync" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" @@ -205,7 +206,8 @@ type OidcAuthConsumer struct { additionalAuthScopes []v1.AuthScope verifier TokenVerifier - subjectsFromLogin []string + mu sync.RWMutex + subjectsFromLogin map[string]struct{} } func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { @@ -226,7 +228,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri return &OidcAuthConsumer{ additionalAuthScopes: additionalAuthScopes, verifier: verifier, - subjectsFromLogin: []string{}, + subjectsFromLogin: make(map[string]struct{}), } } @@ -235,9 +237,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { if err != nil { return fmt.Errorf("invalid OIDC token in login: %v", err) } - if !slices.Contains(auth.subjectsFromLogin, token.Subject) { - auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject) - } + auth.mu.Lock() + auth.subjectsFromLogin[token.Subject] = struct{}{} + auth.mu.Unlock() return nil } @@ -246,11 +248,13 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err if err != nil { return fmt.Errorf("invalid OIDC token in ping: %v", err) } - if !slices.Contains(auth.subjectsFromLogin, token.Subject) { + auth.mu.RLock() + _, ok := auth.subjectsFromLogin[token.Subject] + auth.mu.RUnlock() + if !ok { return fmt.Errorf("received different OIDC subject in login and ping. "+ - "original subjects: %s, "+ "new subject: %s", - auth.subjectsFromLogin, token.Subject) + token.Subject) } return nil } diff --git a/pkg/util/net/websocket.go b/pkg/util/net/websocket.go index 3ca8b332..6c2f39c4 100644 --- a/pkg/util/net/websocket.go +++ b/pkg/util/net/websocket.go @@ -26,6 +26,7 @@ type WebsocketListener struct { // ln: tcp listener for websocket connections func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) { wl = &WebsocketListener{ + ln: ln, acceptCh: make(chan net.Conn), } From c23894f156eb6a7d55c6cfd47ae5ea1826e51587 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 17:59:41 +0800 Subject: [PATCH 15/43] fix: validate CA cert parsing and add missing ReadHeaderTimeout (#5205) - pkg/transport/tls.go: check AppendCertsFromPEM return value and return clear error when CA file contains no valid PEM certificates - pkg/plugin/client/http2http.go: set ReadHeaderTimeout to 60s to match other plugins and prevent slow header attacks - pkg/plugin/client/http2https.go: same ReadHeaderTimeout fix --- pkg/plugin/client/http2http.go | 3 ++- pkg/plugin/client/http2https.go | 3 ++- pkg/transport/tls.go | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go index 889a10f6..e50a91c0 100644 --- a/pkg/plugin/client/http2http.go +++ b/pkg/plugin/client/http2http.go @@ -21,6 +21,7 @@ import ( stdlog "log" "net/http" "net/http/httputil" + "time" "github.com/fatedier/golib/pool" @@ -68,7 +69,7 @@ func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin p.s = &http.Server{ Handler: rp, - ReadHeaderTimeout: 0, + ReadHeaderTimeout: 60 * time.Second, } go func() { diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index 538f2850..8119e095 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -22,6 +22,7 @@ import ( stdlog "log" "net/http" "net/http/httputil" + "time" "github.com/fatedier/golib/pool" @@ -77,7 +78,7 @@ func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi p.s = &http.Server{ Handler: rp, - ReadHeaderTimeout: 0, + ReadHeaderTimeout: 60 * time.Second, } go func() { diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index e8d2bf48..19ebca73 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" "math/big" "os" "time" @@ -85,7 +86,9 @@ func newCertPool(caPath string) (*x509.CertPool, error) { return nil, err } - pool.AppendCertsFromPEM(caCrt) + if !pool.AppendCertsFromPEM(caCrt) { + return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath) + } return pool, nil } From f2d1f3739ad06e38e77ab77c162cbc9b871ca2d4 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 20:44:40 +0800 Subject: [PATCH 16/43] pkg/util/xlog: fix AddPrefix not updating existing prefix due to range value copy (#5206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In AddPrefix, the loop `for _, p := range l.prefixes` creates a copy of each element. Assignments to p.Value and p.Priority only modify the local copy, not the original slice element, causing updates to existing prefixes to be silently lost. This affects client/service.go where AddPrefix is called with Name:"runID" on reconnection — the old runID value would persist in log output instead of being updated to the new one. Fix by using index-based access `l.prefixes[i]` to modify the original slice element, and add break since prefix names are unique. --- pkg/util/xlog/xlog.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index a1a58d42..f8f4116a 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -63,11 +63,12 @@ func (l *Logger) AddPrefix(prefix LogPrefix) *Logger { if prefix.Priority <= 0 { prefix.Priority = 10 } - for _, p := range l.prefixes { + for i, p := range l.prefixes { if p.Name == prefix.Name { found = true - p.Value = prefix.Value - p.Priority = prefix.Priority + l.prefixes[i].Value = prefix.Value + l.prefixes[i].Priority = prefix.Priority + break } } if !found { From 427c4ca3ae1fad712af0391be47840f9f6f3e7a5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 21:17:30 +0800 Subject: [PATCH 17/43] server/proxy: simplify HTTP proxy domain registration by removing duplicate loop (#5207) The Run() method had two nearly identical loop blocks for registering custom domains and subdomain, with the same group/non-group registration logic copy-pasted (~30 lines of duplication). Consolidate by collecting all domains into a single slice first, then iterating once with the shared registration logic. Also fixes a minor inconsistency where the custom domain block used routeConfig.Domain in CanonicalAddr but the subdomain block used tmpRouteConfig.Domain. --- server/proxy/http.go | 50 +++++++++----------------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/server/proxy/http.go b/server/proxy/http.go index 6f0eb008..31b00410 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -75,16 +75,21 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { } }() - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue + domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) + for _, d := range pxy.cfg.CustomDomains { + if d != "" { + domains = append(domains, d) } + } + if pxy.cfg.SubDomain != "" { + domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) + } + addrs := make([]string, 0) + for _, domain := range domains { routeConfig.Domain = domain for _, location := range locations { routeConfig.Location = location - tmpRouteConfig := routeConfig // handle group @@ -93,12 +98,10 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { if err != nil { return } - pxy.closeFuncs = append(pxy.closeFuncs, func() { pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig) }) } else { - // no group err = pxy.rc.HTTPReverseProxy.Register(routeConfig) if err != nil { return @@ -112,39 +115,6 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) } } - - if pxy.cfg.SubDomain != "" { - routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost - for _, location := range locations { - routeConfig.Location = location - - tmpRouteConfig := routeConfig - - // handle group - if pxy.cfg.LoadBalancer.Group != "" { - err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig) - if err != nil { - return - } - - pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig) - }) - } else { - err = pxy.rc.HTTPReverseProxy.Register(routeConfig) - if err != nil { - return - } - pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig) - }) - } - addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort)) - - xl.Infof("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", - routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) - } - } remoteAddr = strings.Join(addrs, ",") return } From d6445933427c14efc5abf3452a976dc5baf6423e Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 21:31:29 +0800 Subject: [PATCH 18/43] server/proxy: simplify HTTPS and TCPMux proxy domain registration (#5208) Consolidate the separate custom-domain loop and subdomain block into a single unified loop, matching the pattern already applied to HTTPProxy in PR #5207. No behavioral change. --- server/proxy/https.go | 23 +++++++++-------------- server/proxy/tcpmux.go | 22 ++++++++++------------ 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/server/proxy/https.go b/server/proxy/https.go index f137ea7a..b65cac34 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -53,23 +53,18 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { pxy.Close() } }() - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue + domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) + for _, d := range pxy.cfg.CustomDomains { + if d != "" { + domains = append(domains, d) } - - l, err := pxy.listenForDomain(routeConfig, domain) - if err != nil { - return "", err - } - pxy.listeners = append(pxy.listeners, l) - addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort)) - xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group) + } + if pxy.cfg.SubDomain != "" { + domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) } - if pxy.cfg.SubDomain != "" { - domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost + addrs := make([]string, 0) + for _, domain := range domains { l, err := pxy.listenForDomain(routeConfig, domain) if err != nil { return "", err diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index 6e95b66b..f68ad12d 100644 --- a/server/proxy/tcpmux.go +++ b/server/proxy/tcpmux.go @@ -72,21 +72,19 @@ func (pxy *TCPMuxProxy) httpConnectListen( } func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } - - addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) - if err != nil { - return "", err + domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) + for _, d := range pxy.cfg.CustomDomains { + if d != "" { + domains = append(domains, d) } } - if pxy.cfg.SubDomain != "" { - addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, - pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) + domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) + } + + addrs := make([]string, 0) + for _, domain := range domains { + addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) if err != nil { return "", err } From e9f7a1a9f262930e032380cbd8a38c1f7978c7a6 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 22:14:46 +0800 Subject: [PATCH 19/43] pkg: use modern Go stdlib functions to simplify code (#5209) - strings.CutPrefix instead of HasPrefix+TrimPrefix (naming, legacy) - slices.Contains instead of manual loop (plugin/server) - min/max builtins instead of manual comparisons (nathole) --- pkg/config/legacy/conversion.go | 11 +++++------ pkg/config/legacy/utils.go | 4 ++-- pkg/naming/names.go | 5 ++--- pkg/nathole/classify.go | 8 ++------ pkg/plugin/server/http.go | 8 ++------ 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go index 4ae54f88..ec8d0698 100644 --- a/pkg/config/legacy/conversion.go +++ b/pkg/config/legacy/conversion.go @@ -171,15 +171,14 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations { out := v1.HeaderOperations{} for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { + k, ok := strings.CutPrefix(k, "plugin_header_") + if !ok || k == "" { continue } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - if out.Set == nil { - out.Set = make(map[string]string) - } - out.Set[k] = v + if out.Set == nil { + out.Set = make(map[string]string) } + out.Set[k] = v } return out } diff --git a/pkg/config/legacy/utils.go b/pkg/config/legacy/utils.go index 9366f8c7..ad53f1c5 100644 --- a/pkg/config/legacy/utils.go +++ b/pkg/config/legacy/utils.go @@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string m := make(map[string]string) for key, value := range set { - if strings.HasPrefix(key, prefix) { - m[strings.TrimPrefix(key, prefix)] = value + if trimmed, ok := strings.CutPrefix(key, prefix); ok { + m[trimmed] = value } } diff --git a/pkg/naming/names.go b/pkg/naming/names.go index 0e4d4e6a..3ca4dd9c 100644 --- a/pkg/naming/names.go +++ b/pkg/naming/names.go @@ -16,9 +16,8 @@ func StripUserPrefix(user, name string) string { if user == "" { return name } - prefix := user + "." - if strings.HasPrefix(name, prefix) { - return strings.TrimPrefix(name, prefix) + if trimmed, ok := strings.CutPrefix(name, user+"."); ok { + return trimmed } return name } diff --git a/pkg/nathole/classify.go b/pkg/nathole/classify.go index 5abb0e23..6e00a583 100644 --- a/pkg/nathole/classify.go +++ b/pkg/nathole/classify.go @@ -70,12 +70,8 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err continue } - if portNum > portMax { - portMax = portNum - } - if portNum < portMin { - portMin = portNum - } + portMax = max(portMax, portNum) + portMin = min(portMin, portNum) if baseIP != ip { ipChanged = true } diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 196993ef..b05b909e 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -24,6 +24,7 @@ import ( "net/http" "net/url" "reflect" + "slices" "strings" v1 "github.com/fatedier/frp/pkg/config/v1" @@ -64,12 +65,7 @@ func (p *httpPlugin) Name() string { } func (p *httpPlugin) IsSupport(op string) bool { - for _, v := range p.options.Ops { - if v == op { - return true - } - } - return false + return slices.Contains(p.options.Ops, op) } func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) { From 0b4f83cd04db1e6fc11669f6fa3d86f8be156942 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 6 Mar 2026 23:13:29 +0800 Subject: [PATCH 20/43] pkg/config: use modern Go stdlib for sorting and string operations (#5210) - slices.SortedFunc + maps.Values + cmp.Compare instead of manual map-to-slice collection + sort.Slice (source/aggregator.go) - strings.CutSuffix instead of HasSuffix+TrimSuffix, and deduplicate error handling in BandwidthQuantity.UnmarshalString (types/types.go) --- pkg/config/source/aggregator.go | 22 +++++++--------------- pkg/config/source/aggregator_test.go | 21 +++++++++++++++++++++ pkg/config/types/types.go | 18 ++++++------------ pkg/config/types/types_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/pkg/config/source/aggregator.go b/pkg/config/source/aggregator.go index f3be67bd..58496932 100644 --- a/pkg/config/source/aggregator.go +++ b/pkg/config/source/aggregator.go @@ -15,9 +15,11 @@ package source import ( + "cmp" "errors" "fmt" - "sort" + "maps" + "slices" "sync" v1 "github.com/fatedier/frp/pkg/config/v1" @@ -97,21 +99,11 @@ func (a *Aggregator) mapsToSortedSlices( proxyMap map[string]v1.ProxyConfigurer, visitorMap map[string]v1.VisitorConfigurer, ) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { - proxies := make([]v1.ProxyConfigurer, 0, len(proxyMap)) - for _, p := range proxyMap { - proxies = append(proxies, p) - } - sort.Slice(proxies, func(i, j int) bool { - return proxies[i].GetBaseConfig().Name < proxies[j].GetBaseConfig().Name + proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int { + return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name) }) - - visitors := make([]v1.VisitorConfigurer, 0, len(visitorMap)) - for _, v := range visitorMap { - visitors = append(visitors, v) - } - sort.Slice(visitors, func(i, j int) bool { - return visitors[i].GetBaseConfig().Name < visitors[j].GetBaseConfig().Name + visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int { + return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name) }) - return proxies, visitors } diff --git a/pkg/config/source/aggregator_test.go b/pkg/config/source/aggregator_test.go index 5fc9636a..380c05cf 100644 --- a/pkg/config/source/aggregator_test.go +++ b/pkg/config/source/aggregator_test.go @@ -196,6 +196,27 @@ func TestAggregator_VisitorMerge(t *testing.T) { require.Len(visitors, 2) } +func TestAggregator_Load_ReturnsSortedByName(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + err := agg.ConfigSource().ReplaceAll( + []v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")}, + []v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")}, + ) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(proxies, 3) + require.Equal("alice", proxies[0].GetBaseConfig().Name) + require.Equal("bob", proxies[1].GetBaseConfig().Name) + require.Equal("charlie", proxies[2].GetBaseConfig().Name) + require.Len(visitors, 2) + require.Equal("alpha", visitors[0].GetBaseConfig().Name) + require.Equal("zulu", visitors[1].GetBaseConfig().Name) +} + func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) { require := require.New(t) diff --git a/pkg/config/types/types.go b/pkg/config/types/types.go index 8fa3105a..5b2b6930 100644 --- a/pkg/config/types/types.go +++ b/pkg/config/types/types.go @@ -70,24 +70,18 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error { f float64 err error ) - switch { - case strings.HasSuffix(s, "MB"): + if fstr, ok := strings.CutSuffix(s, "MB"); ok { base = MB - fstr := strings.TrimSuffix(s, "MB") f, err = strconv.ParseFloat(fstr, 64) - if err != nil { - return err - } - case strings.HasSuffix(s, "KB"): + } else if fstr, ok := strings.CutSuffix(s, "KB"); ok { base = KB - fstr := strings.TrimSuffix(s, "KB") f, err = strconv.ParseFloat(fstr, 64) - if err != nil { - return err - } - default: + } else { return errors.New("unit not support") } + if err != nil { + return err + } q.s = s q.i = int64(f * float64(base)) diff --git a/pkg/config/types/types_test.go b/pkg/config/types/types_test.go index 8843de5a..c05ac9ee 100644 --- a/pkg/config/types/types_test.go +++ b/pkg/config/types/types_test.go @@ -39,6 +39,31 @@ func TestBandwidthQuantity(t *testing.T) { require.Equal(`{"b":"1KB","int":5}`, string(buf)) } +func TestBandwidthQuantity_MB(t *testing.T) { + require := require.New(t) + + var w Wrap + err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w) + require.NoError(err) + require.EqualValues(2*MB, w.B.Bytes()) + + buf, err := json.Marshal(&w) + require.NoError(err) + require.Equal(`{"b":"2MB","int":1}`, string(buf)) +} + +func TestBandwidthQuantity_InvalidUnit(t *testing.T) { + var w Wrap + err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w) + require.Error(t, err) +} + +func TestBandwidthQuantity_InvalidNumber(t *testing.T) { + var w Wrap + err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w) + require.Error(t, err) +} + func TestPortsRangeSlice2String(t *testing.T) { require := require.New(t) From cf396563f8ebf86d74ae849b0f2a5c33269203e8 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sat, 7 Mar 2026 01:33:37 +0800 Subject: [PATCH 21/43] client/proxy: unify work conn wrapping across all proxy types (#5212) * client/proxy: extract wrapWorkConn to deduplicate UDP/SUDP connection wrapping Move the repeated rate-limiting, encryption, and compression wrapping logic from UDPProxy and SUDPProxy into a shared BaseProxy.wrapWorkConn method, reducing ~18 lines of duplication in each proxy type. * client/proxy: unify work conn wrapping with pooled compression for all proxy types Refactor wrapWorkConn to accept encKey parameter and return (io.ReadWriteCloser, recycleFn, error), enabling HandleTCPWorkConnection to reuse the same limiter/encryption/compression pipeline. Switch all proxy types from WithCompression to WithCompressionFromPool. TCP non-plugin path calls recycleFn via defer after Join; plugin and UDP/SUDP paths skip recycle (objects are GC'd safely, per golib contract). --- client/proxy/proxy.go | 64 ++++++++++++++++++++++++++----------------- client/proxy/sudp.go | 27 ++++-------------- client/proxy/udp.go | 27 ++++-------------- 3 files changed, 49 insertions(+), 69 deletions(-) diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index ca7d905b..84ff49a1 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -16,6 +16,7 @@ package proxy import ( "context" + "fmt" "io" "net" "reflect" @@ -122,6 +123,33 @@ func (pxy *BaseProxy) Close() { } } +// wrapWorkConn applies rate limiting, encryption, and compression +// to a work connection based on the proxy's transport configuration. +// The returned recycle function should be called when the stream is no longer in use +// to return compression resources to the pool. It is safe to not call recycle, +// in which case resources will be garbage collected normally. +func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) { + var rwc io.ReadWriteCloser = conn + if pxy.limiter != nil { + rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { + return conn.Close() + }) + } + if pxy.baseCfg.Transport.UseEncryption { + var err error + rwc, err = libio.WithEncryption(rwc, encKey) + if err != nil { + conn.Close() + return nil, nil, fmt.Errorf("create encryption stream error: %w", err) + } + } + var recycleFn func() + if pxy.baseCfg.Transport.UseCompression { + rwc, recycleFn = libio.WithCompressionFromPool(rwc) + } + return rwc, recycleFn, nil +} + func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { pxy.inWorkConnCallback = cb } @@ -139,30 +167,14 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) { xl := pxy.xl baseCfg := pxy.baseCfg - var ( - remote io.ReadWriteCloser - err error - ) - remote = workConn - if pxy.limiter != nil { - remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error { - return workConn.Close() - }) - } xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t", baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) - if baseCfg.Transport.UseEncryption { - remote, err = libio.WithEncryption(remote, encKey) - if err != nil { - workConn.Close() - xl.Errorf("create encryption stream error: %v", err) - return - } - } - var compressionResourceRecycleFn func() - if baseCfg.Transport.UseCompression { - remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote) + + remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey) + if err != nil { + xl.Errorf("wrap work connection: %v", err) + return } // check if we need to send proxy protocol info @@ -178,7 +190,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { - // Use the common proxy protocol builder function header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion) connInfo.ProxyProtocolHeader = header } @@ -187,12 +198,18 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor if pxy.proxyPlugin != nil { // if plugin is set, let plugin handle connection first + // Don't recycle compression resources here because plugins may + // retain the connection after Handle returns. xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name()) pxy.proxyPlugin.Handle(pxy.ctx, &connInfo) xl.Debugf("handle by plugin finished") return } + if recycleFn != nil { + defer recycleFn() + } + localConn, err := libnet.Dial( net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)), libnet.WithTimeout(10*time.Second), @@ -220,7 +237,4 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor if len(errs) > 0 { xl.Tracef("join connections errors: %v", errs) } - if compressionResourceRecycleFn != nil { - compressionResourceRecycleFn() - } } diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index 3a7af19c..da941446 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -17,7 +17,6 @@ package proxy import ( - "io" "net" "reflect" "strconv" @@ -25,12 +24,10 @@ import ( "time" "github.com/fatedier/golib/errors" - libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" - "github.com/fatedier/frp/pkg/util/limit" netpkg "github.com/fatedier/frp/pkg/util/net" ) @@ -83,27 +80,13 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { xl := pxy.xl xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String()) - var rwc io.ReadWriteCloser = conn - var err error - if pxy.limiter != nil { - rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { - return conn.Close() - }) + remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey) + if err != nil { + xl.Errorf("wrap work connection: %v", err) + return } - if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) - if err != nil { - conn.Close() - xl.Errorf("create encryption stream error: %v", err) - return - } - } - if pxy.cfg.Transport.UseCompression { - rwc = libio.WithCompression(rwc) - } - conn = netpkg.WrapReadWriteCloserToConn(rwc, conn) - workConn := conn + workConn := netpkg.WrapReadWriteCloserToConn(remote, conn) readCh := make(chan *msg.UDPPacket, 1024) sendCh := make(chan msg.Message, 1024) isClose := false diff --git a/client/proxy/udp.go b/client/proxy/udp.go index 68426dc6..570da476 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -17,19 +17,16 @@ package proxy import ( - "io" "net" "reflect" "strconv" "time" "github.com/fatedier/golib/errors" - libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" - "github.com/fatedier/frp/pkg/util/limit" netpkg "github.com/fatedier/frp/pkg/util/net" ) @@ -94,28 +91,14 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { // close resources related with old workConn pxy.Close() - var rwc io.ReadWriteCloser = conn - var err error - if pxy.limiter != nil { - rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { - return conn.Close() - }) + remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey) + if err != nil { + xl.Errorf("wrap work connection: %v", err) + return } - if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) - if err != nil { - conn.Close() - xl.Errorf("create encryption stream error: %v", err) - return - } - } - if pxy.cfg.Transport.UseCompression { - rwc = libio.WithCompression(rwc) - } - conn = netpkg.WrapReadWriteCloserToConn(rwc, conn) pxy.mu.Lock() - pxy.workConn = conn + pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn) pxy.readCh = make(chan *msg.UDPPacket, 1024) pxy.sendCh = make(chan msg.Message, 1024) pxy.closed = false From bb3d0e71401b94433753398272f35fdea27097b1 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sat, 7 Mar 2026 12:00:27 +0800 Subject: [PATCH 22/43] deduplicate common logic across proxy, visitor, and metrics modules (#5213) - Replace duplicate parseBasicAuth with existing httppkg.ParseBasicAuth - Extract buildDomains helper in BaseProxy for HTTP/HTTPS/TCPMux proxies - Extract toProxyStats helper to deduplicate ProxyStats construction - Extract startVisitorListener helper in BaseProxy for STCP/SUDP proxies - Extract acceptLoop helper in BaseVisitor for STCP/XTCP visitors --- client/visitor/stcp.go | 28 ++------------- client/visitor/visitor.go | 12 +++++++ client/visitor/xtcp.go | 28 ++------------- pkg/metrics/mem/server.go | 71 ++++++++++++--------------------------- pkg/util/vhost/http.go | 20 +---------- server/proxy/http.go | 10 +----- server/proxy/https.go | 10 +----- server/proxy/proxy.go | 30 +++++++++++++++++ server/proxy/stcp.go | 16 +-------- server/proxy/sudp.go | 16 +-------- server/proxy/tcpmux.go | 10 +----- 11 files changed, 74 insertions(+), 177 deletions(-) diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index 870e48d9..6529142c 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -42,10 +42,10 @@ func (sv *STCPVisitor) Run() (err error) { if err != nil { return } - go sv.worker() + go sv.acceptLoop(sv.l, "stcp local", sv.handleConn) } - go sv.internalConnWorker() + go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn) if sv.plugin != nil { sv.plugin.Start() @@ -57,30 +57,6 @@ func (sv *STCPVisitor) Close() { sv.BaseVisitor.Close() } -func (sv *STCPVisitor) worker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.l.Accept() - if err != nil { - xl.Warnf("stcp local listener closed") - return - } - go sv.handleConn(conn) - } -} - -func (sv *STCPVisitor) internalConnWorker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.internalLn.Accept() - if err != nil { - xl.Warnf("stcp internal listener closed") - return - } - go sv.handleConn(conn) - } -} - func (sv *STCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) var tunnelErr error diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index 87e4f29f..51999499 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -119,6 +119,18 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error { return v.internalLn.PutConn(conn) } +func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) { + xl := xlog.FromContextSafe(v.ctx) + for { + conn, err := l.Accept() + if err != nil { + xl.Warnf("%s listener closed", name) + return + } + go handleConn(conn) + } +} + func (v *BaseVisitor) Close() { if v.l != nil { v.l.Close() diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 2273a271..b2f4ef37 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -65,10 +65,10 @@ func (sv *XTCPVisitor) Run() (err error) { if err != nil { return } - go sv.worker() + go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn) } - go sv.internalConnWorker() + go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn) go sv.processTunnelStartEvents() if sv.cfg.KeepTunnelOpen { sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour) @@ -93,30 +93,6 @@ func (sv *XTCPVisitor) Close() { } } -func (sv *XTCPVisitor) worker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.l.Accept() - if err != nil { - xl.Warnf("xtcp local listener closed") - return - } - go sv.handleConn(conn) - } -} - -func (sv *XTCPVisitor) internalConnWorker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.internalLn.Accept() - if err != nil { - xl.Warnf("xtcp internal listener closed") - return - } - go sv.handleConn(conn) - } -} - func (sv *XTCPVisitor) processTunnelStartEvents() { for { select { diff --git a/pkg/metrics/mem/server.go b/pkg/metrics/mem/server.go index 677788d3..16a6491e 100644 --- a/pkg/metrics/mem/server.go +++ b/pkg/metrics/mem/server.go @@ -203,6 +203,25 @@ func (m *serverMetrics) GetServer() *ServerStats { return s } +func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats { + ps := &ProxyStats{ + Name: name, + Type: proxyStats.ProxyType, + User: proxyStats.User, + ClientID: proxyStats.ClientID, + TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), + TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), + CurConns: int64(proxyStats.CurConns.Count()), + } + if !proxyStats.LastStartTime.IsZero() { + ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") + } + if !proxyStats.LastCloseTime.IsZero() { + ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") + } + return ps +} + func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats { res := make([]*ProxyStats, 0) m.mu.Lock() @@ -212,23 +231,7 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats { if proxyStats.ProxyType != proxyType { continue } - - ps := &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - User: proxyStats.User, - ClientID: proxyStats.ClientID, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: int64(proxyStats.CurConns.Count()), - } - if !proxyStats.LastStartTime.IsZero() { - ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } - res = append(res, ps) + res = append(res, toProxyStats(name, proxyStats)) } return res } @@ -241,26 +244,10 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri if proxyStats.ProxyType != proxyType { continue } - if name != proxyName { continue } - - res = &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - User: proxyStats.User, - ClientID: proxyStats.ClientID, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: int64(proxyStats.CurConns.Count()), - } - if !proxyStats.LastStartTime.IsZero() { - res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } + res = toProxyStats(name, proxyStats) break } return @@ -272,21 +259,7 @@ func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) { proxyStats, ok := m.info.ProxyStatistics[proxyName] if ok { - res = &ProxyStats{ - Name: proxyName, - Type: proxyStats.ProxyType, - User: proxyStats.User, - ClientID: proxyStats.ClientID, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: int64(proxyStats.CurConns.Count()), - } - if !proxyStats.LastStartTime.IsZero() { - res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } + res = toProxyStats(proxyName, proxyStats) } return } diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 05ec174b..d12e7916 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -266,31 +266,13 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req go libio.Join(remote, client) } -func parseBasicAuth(auth string) (username, password string, ok bool) { - const prefix = "Basic " - // Case insensitive prefix match. See Issue 22736. - if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { - return - } - c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) - if err != nil { - return - } - cs := string(c) - s := strings.IndexByte(cs, ':') - if s < 0 { - return - } - return cs[:s], cs[s+1:], true -} - func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request { user := "" // If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header. if req.URL.Host != "" { proxyAuth := req.Header.Get("Proxy-Authorization") if proxyAuth != "" { - user, _, _ = parseBasicAuth(proxyAuth) + user, _, _ = httppkg.ParseBasicAuth(proxyAuth) } } if user == "" { diff --git a/server/proxy/http.go b/server/proxy/http.go index 31b00410..e5df06c5 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -75,15 +75,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { } }() - domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) - for _, d := range pxy.cfg.CustomDomains { - if d != "" { - domains = append(domains, d) - } - } - if pxy.cfg.SubDomain != "" { - domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) addrs := make([]string, 0) for _, domain := range domains { diff --git a/server/proxy/https.go b/server/proxy/https.go index b65cac34..ec720242 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -53,15 +53,7 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { pxy.Close() } }() - domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) - for _, d := range pxy.cfg.CustomDomains { - if d != "" { - domains = append(domains, d) - } - } - if pxy.cfg.SubDomain != "" { - domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) addrs := make([]string, 0) for _, domain := range domains { diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 564eca28..dcc652a0 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -173,6 +173,36 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, return } +// startVisitorListener sets up a VisitorManager listener for visitor-based proxies (STCP, SUDP). +func (pxy *BaseProxy) startVisitorListener(secretKey string, allowUsers []string, proxyType string) error { + // if allowUsers is empty, only allow same user from proxy + if len(allowUsers) == 0 { + allowUsers = []string{pxy.GetUserInfo().User} + } + listener, err := pxy.rc.VisitorManager.Listen(pxy.GetName(), secretKey, allowUsers) + if err != nil { + return err + } + pxy.listeners = append(pxy.listeners, listener) + pxy.xl.Infof("%s proxy custom listen success", proxyType) + pxy.startCommonTCPListenersHandler() + return nil +} + +// buildDomains constructs a list of domains from custom domains and subdomain configuration. +func (pxy *BaseProxy) buildDomains(customDomains []string, subDomain string) []string { + domains := make([]string, 0, len(customDomains)+1) + for _, d := range customDomains { + if d != "" { + domains = append(domains, d) + } + } + if subDomain != "" { + domains = append(domains, subDomain+"."+pxy.serverCfg.SubDomainHost) + } + return domains +} + // startCommonTCPListenersHandler start a goroutine handler for each listener. func (pxy *BaseProxy) startCommonTCPListenersHandler() { xl := xlog.FromContextSafe(pxy.ctx) diff --git a/server/proxy/stcp.go b/server/proxy/stcp.go index 06b1b17f..113fd13b 100644 --- a/server/proxy/stcp.go +++ b/server/proxy/stcp.go @@ -41,21 +41,7 @@ func NewSTCPProxy(baseProxy *BaseProxy) Proxy { } func (pxy *STCPProxy) Run() (remoteAddr string, err error) { - xl := pxy.xl - allowUsers := pxy.cfg.AllowUsers - // if allowUsers is empty, only allow same user from proxy - if len(allowUsers) == 0 { - allowUsers = []string{pxy.GetUserInfo().User} - } - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) - if errRet != nil { - err = errRet - return - } - pxy.listeners = append(pxy.listeners, listener) - xl.Infof("stcp proxy custom listen success") - - pxy.startCommonTCPListenersHandler() + err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "stcp") return } diff --git a/server/proxy/sudp.go b/server/proxy/sudp.go index f37fb423..00438882 100644 --- a/server/proxy/sudp.go +++ b/server/proxy/sudp.go @@ -41,21 +41,7 @@ func NewSUDPProxy(baseProxy *BaseProxy) Proxy { } func (pxy *SUDPProxy) Run() (remoteAddr string, err error) { - xl := pxy.xl - allowUsers := pxy.cfg.AllowUsers - // if allowUsers is empty, only allow same user from proxy - if len(allowUsers) == 0 { - allowUsers = []string{pxy.GetUserInfo().User} - } - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) - if errRet != nil { - err = errRet - return - } - pxy.listeners = append(pxy.listeners, listener) - xl.Infof("sudp proxy custom listen success") - - pxy.startCommonTCPListenersHandler() + err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "sudp") return } diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index f68ad12d..2a0c8512 100644 --- a/server/proxy/tcpmux.go +++ b/server/proxy/tcpmux.go @@ -72,15 +72,7 @@ func (pxy *TCPMuxProxy) httpConnectListen( } func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { - domains := make([]string, 0, len(pxy.cfg.CustomDomains)+1) - for _, d := range pxy.cfg.CustomDomains { - if d != "" { - domains = append(domains, d) - } - } - if pxy.cfg.SubDomain != "" { - domains = append(domains, pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost) - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) addrs := make([]string, 0) for _, domain := range domains { From c70ceff370986a83ad3de5d62455e8cac7d5e0f2 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sat, 7 Mar 2026 13:36:02 +0800 Subject: [PATCH 23/43] fix: three high-severity bugs across nathole, proxy, and udp modules (#5214) - pkg/nathole: add RLock when reading clientCfgs map in PreCheck path to prevent concurrent map read/write crash - server/proxy: fix error variable shadowing in GetWorkConnFromPool that could return a closed connection with nil error - pkg/util/net: check ListenUDP error before spawning goroutines and assign readConn to struct field so Close() works correctly --- pkg/nathole/controller.go | 2 ++ pkg/util/net/udp.go | 4 ++++ server/proxy/proxy.go | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/nathole/controller.go b/pkg/nathole/controller.go index 88d788d6..2562bfd2 100644 --- a/pkg/nathole/controller.go +++ b/pkg/nathole/controller.go @@ -152,7 +152,9 @@ func (c *Controller) GenSid() string { func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) { if m.PreCheck { + c.mu.RLock() cfg, ok := c.clientCfgs[m.ProxyName] + c.mu.RUnlock() if !ok { _ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName))) return diff --git a/pkg/util/net/udp.go b/pkg/util/net/udp.go index 79ebbb14..4c10fc02 100644 --- a/pkg/util/net/udp.go +++ b/pkg/util/net/udp.go @@ -168,11 +168,15 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) { return l, err } readConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return l, err + } l = &UDPListener{ addr: udpAddr, acceptCh: make(chan net.Conn), writeCh: make(chan *UDPPacket, 1000), + readConn: readConn, fakeConns: make(map[string]*FakeUDPConn), } diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index dcc652a0..5b7898eb 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -150,7 +150,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String()) dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16) } - err := msg.WriteMsg(workConn, &msg.StartWorkConn{ + err = msg.WriteMsg(workConn, &msg.StartWorkConn{ ProxyName: pxy.GetName(), SrcAddr: srcAddr, SrcPort: uint16(srcPort), @@ -161,6 +161,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, if err != nil { xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i) workConn.Close() + workConn = nil } else { break } From bd200b1a3b64845c23ca1c8fa371acfe1cc189f8 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Sat, 7 Mar 2026 12:43:04 +0200 Subject: [PATCH 24/43] fix: typos in comments, tests, functions (#5216) --- cmd/frpc/sub/nathole.go | 2 +- server/service.go | 2 +- test/e2e/legacy/basic/server.go | 2 +- test/e2e/pkg/ssh/client.go | 4 ++-- test/e2e/v1/basic/server.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index a07d6852..b76c8ab7 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -47,7 +47,7 @@ var natholeDiscoveryCmd = &cobra.Command{ Use: "discover", Short: "Discover nathole information from stun server", RunE: func(cmd *cobra.Command, args []string) error { - // ignore error here, because we can use command line pameters + // ignore error here, because we can use command line parameters cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { cfg = &v1.ClientCommonConfig{} diff --git a/server/service.go b/server/service.go index b0db0327..4eddbf28 100644 --- a/server/service.go +++ b/server/service.go @@ -193,7 +193,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { if err != nil { return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) } - log.Infof("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough) + log.Infof("tcpmux httpconnect multiplexer listen on %s, passthrough: %v", address, cfg.TCPMuxPassthrough) } // Init all plugins diff --git a/test/e2e/legacy/basic/server.go b/test/e2e/legacy/basic/server.go index 4399439d..418910aa 100644 --- a/test/e2e/legacy/basic/server.go +++ b/test/e2e/legacy/basic/server.go @@ -28,7 +28,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000)) udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000)) clientConf += fmt.Sprintf(` - [tcp-allowded-in-range] + [tcp-allowed-in-range] type = tcp local_port = {{ .%s }} remote_port = {{ .%s }} diff --git a/test/e2e/pkg/ssh/client.go b/test/e2e/pkg/ssh/client.go index b45e39da..d2525871 100644 --- a/test/e2e/pkg/ssh/client.go +++ b/test/e2e/pkg/ssh/client.go @@ -75,11 +75,11 @@ func (c *TunnelClient) serveListener() { if err != nil { return } - go c.hanldeConn(conn) + go c.handleConn(conn) } } -func (c *TunnelClient) hanldeConn(conn net.Conn) { +func (c *TunnelClient) handleConn(conn net.Conn) { defer conn.Close() local, err := net.Dial("tcp", c.localAddr) if err != nil { diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go index a3fe5992..274102a2 100644 --- a/test/e2e/v1/basic/server.go +++ b/test/e2e/v1/basic/server.go @@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000)) clientConf += fmt.Sprintf(` [[proxies]] - name = "tcp-allowded-in-range" + name = "tcp-allowed-in-range" type = "tcp" localPort = {{ .%s }} remotePort = {{ .%s }} From 017d71717f8a64d6ab7d3798b7258f429a7baaed Mon Sep 17 00:00:00 2001 From: fatedier Date: Sat, 7 Mar 2026 20:17:00 +0800 Subject: [PATCH 25/43] server: introduce SessionContext to encapsulate NewControl parameters (#5217) Replace 10 positional parameters in NewControl() with a single SessionContext struct, matching the client-side pattern. This also eliminates the post-construction mutation of clientRegistry and removes two TODO comments. --- server/control.go | 189 +++++++++++++++++++++------------------------- server/service.go | 25 ++++-- 2 files changed, 103 insertions(+), 111 deletions(-) diff --git a/server/control.go b/server/control.go index 863104f3..219d6368 100644 --- a/server/control.go +++ b/server/control.go @@ -95,20 +95,33 @@ func (cm *ControlManager) Close() error { return nil } -type Control struct { +// SessionContext encapsulates the input parameters for creating a new Control. +type SessionContext struct { // all resource managers and controllers - rc *controller.ResourceController - + RC *controller.ResourceController // proxy manager - pxyManager *proxy.Manager - + PxyManager *proxy.Manager // plugin manager - pluginManager *plugin.Manager - + PluginManager *plugin.Manager // verifies authentication based on selected method - authVerifier auth.Verifier + AuthVerifier auth.Verifier // key used for connection encryption - encryptionKey []byte + EncryptionKey []byte + // control connection + Conn net.Conn + // indicates whether the connection is encrypted + ConnEncrypted bool + // login message + LoginMsg *msg.Login + // server configuration + ServerCfg *v1.ServerConfig + // client registry + ClientRegistry *registry.ClientRegistry +} + +type Control struct { + // session context + sessionCtx *SessionContext // other components can use this to communicate with client msgTransporter transport.MessageTransporter @@ -117,12 +130,6 @@ type Control struct { // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types. msgDispatcher *msg.Dispatcher - // login message - loginMsg *msg.Login - - // control connection - conn net.Conn - // work connections workConnCh chan net.Conn @@ -145,61 +152,37 @@ type Control struct { mu sync.RWMutex - // Server configuration information - serverCfg *v1.ServerConfig - - clientRegistry *registry.ClientRegistry - xl *xlog.Logger ctx context.Context doneCh chan struct{} } -// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext. -func NewControl( - ctx context.Context, - rc *controller.ResourceController, - pxyManager *proxy.Manager, - pluginManager *plugin.Manager, - authVerifier auth.Verifier, - encryptionKey []byte, - ctlConn net.Conn, - ctlConnEncrypted bool, - loginMsg *msg.Login, - serverCfg *v1.ServerConfig, -) (*Control, error) { - poolCount := loginMsg.PoolCount - if poolCount > int(serverCfg.Transport.MaxPoolCount) { - poolCount = int(serverCfg.Transport.MaxPoolCount) +func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { + poolCount := sessionCtx.LoginMsg.PoolCount + if poolCount > int(sessionCtx.ServerCfg.Transport.MaxPoolCount) { + poolCount = int(sessionCtx.ServerCfg.Transport.MaxPoolCount) } ctl := &Control{ - rc: rc, - pxyManager: pxyManager, - pluginManager: pluginManager, - authVerifier: authVerifier, - encryptionKey: encryptionKey, - conn: ctlConn, - loginMsg: loginMsg, - workConnCh: make(chan net.Conn, poolCount+10), - proxies: make(map[string]proxy.Proxy), - poolCount: poolCount, - portsUsedNum: 0, - runID: loginMsg.RunID, - serverCfg: serverCfg, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, - doneCh: make(chan struct{}), + sessionCtx: sessionCtx, + workConnCh: make(chan net.Conn, poolCount+10), + proxies: make(map[string]proxy.Proxy), + poolCount: poolCount, + portsUsedNum: 0, + runID: sessionCtx.LoginMsg.RunID, + xl: xlog.FromContextSafe(ctx), + ctx: ctx, + doneCh: make(chan struct{}), } ctl.lastPing.Store(time.Now()) - if ctlConnEncrypted { - cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey) + if sessionCtx.ConnEncrypted { + cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.EncryptionKey) if err != nil { return nil, err } ctl.msgDispatcher = msg.NewDispatcher(cryptoRW) } else { - ctl.msgDispatcher = msg.NewDispatcher(ctl.conn) + ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) } ctl.registerMsgHandlers() ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) @@ -213,7 +196,7 @@ func (ctl *Control) Start() { RunID: ctl.runID, Error: "", } - _ = msg.WriteMsg(ctl.conn, loginRespMsg) + _ = msg.WriteMsg(ctl.sessionCtx.Conn, loginRespMsg) go func() { for i := 0; i < ctl.poolCount; i++ { @@ -225,7 +208,7 @@ func (ctl *Control) Start() { } func (ctl *Control) Close() error { - ctl.conn.Close() + ctl.sessionCtx.Conn.Close() return nil } @@ -233,7 +216,7 @@ func (ctl *Control) Replaced(newCtl *Control) { xl := ctl.xl xl.Infof("replaced by client [%s]", newCtl.runID) ctl.runID = "" - ctl.conn.Close() + ctl.sessionCtx.Conn.Close() } func (ctl *Control) RegisterWorkConn(conn net.Conn) error { @@ -291,7 +274,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { return } - case <-time.After(time.Duration(ctl.serverCfg.UserConnTimeout) * time.Second): + case <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second): err = fmt.Errorf("timeout trying to get work connection") xl.Warnf("%v", err) return @@ -304,15 +287,15 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { } func (ctl *Control) heartbeatWorker() { - if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 { + if ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout <= 0 { return } xl := ctl.xl go wait.Until(func() { - if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second { + if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout)*time.Second { xl.Warnf("heartbeat timeout") - ctl.conn.Close() + ctl.sessionCtx.Conn.Close() return } }, time.Second, ctl.doneCh) @@ -330,7 +313,7 @@ func (ctl *Control) worker() { go ctl.msgDispatcher.Run() <-ctl.msgDispatcher.Done() - ctl.conn.Close() + ctl.sessionCtx.Conn.Close() ctl.mu.Lock() defer ctl.mu.Unlock() @@ -342,26 +325,26 @@ func (ctl *Control) worker() { for _, pxy := range ctl.proxies { pxy.Close() - ctl.pxyManager.Del(pxy.GetName()) + ctl.sessionCtx.PxyManager.Del(pxy.GetName()) metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) notifyContent := &plugin.CloseProxyContent{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, CloseProxy: msg.CloseProxy{ ProxyName: pxy.GetName(), }, } go func() { - _ = ctl.pluginManager.CloseProxy(notifyContent) + _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) }() } metrics.Server.CloseClient() - ctl.clientRegistry.MarkOfflineByRunID(ctl.runID) + ctl.sessionCtx.ClientRegistry.MarkOfflineByRunID(ctl.runID) xl.Infof("client exit success") close(ctl.doneCh) } @@ -381,14 +364,14 @@ func (ctl *Control) handleNewProxy(m msg.Message) { content := &plugin.NewProxyContent{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, NewProxy: *inMsg, } var remoteAddr string - retContent, err := ctl.pluginManager.NewProxy(content) + retContent, err := ctl.sessionCtx.PluginManager.NewProxy(content) if err == nil { inMsg = &retContent.NewProxy remoteAddr, err = ctl.RegisterProxy(inMsg) @@ -401,15 +384,15 @@ func (ctl *Control) handleNewProxy(m msg.Message) { if err != nil { xl.Warnf("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err) resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName), - err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)) + err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)) } else { resp.RemoteAddr = remoteAddr xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType) - clientID := ctl.loginMsg.ClientID + clientID := ctl.sessionCtx.LoginMsg.ClientID if clientID == "" { - clientID = ctl.loginMsg.RunID + clientID = ctl.sessionCtx.LoginMsg.RunID } - metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.loginMsg.User, clientID) + metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.sessionCtx.LoginMsg.User, clientID) } _ = ctl.msgDispatcher.Send(resp) } @@ -420,21 +403,21 @@ func (ctl *Control) handlePing(m msg.Message) { content := &plugin.PingContent{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, Ping: *inMsg, } - retContent, err := ctl.pluginManager.Ping(content) + retContent, err := ctl.sessionCtx.PluginManager.Ping(content) if err == nil { inMsg = &retContent.Ping - err = ctl.authVerifier.VerifyPing(inMsg) + err = ctl.sessionCtx.AuthVerifier.VerifyPing(inMsg) } if err != nil { xl.Warnf("received invalid ping: %v", err) _ = ctl.msgDispatcher.Send(&msg.Pong{ - Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), + Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)), }) return } @@ -445,17 +428,17 @@ func (ctl *Control) handlePing(m msg.Message) { func (ctl *Control) handleNatHoleVisitor(m msg.Message) { inMsg := m.(*msg.NatHoleVisitor) - ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User) + ctl.sessionCtx.RC.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.sessionCtx.LoginMsg.User) } func (ctl *Control) handleNatHoleClient(m msg.Message) { inMsg := m.(*msg.NatHoleClient) - ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter) + ctl.sessionCtx.RC.NatHoleController.HandleClient(inMsg, ctl.msgTransporter) } func (ctl *Control) handleNatHoleReport(m msg.Message) { inMsg := m.(*msg.NatHoleReport) - ctl.rc.NatHoleController.HandleReport(inMsg) + ctl.sessionCtx.RC.NatHoleController.HandleReport(inMsg) } func (ctl *Control) handleCloseProxy(m msg.Message) { @@ -468,15 +451,15 @@ func (ctl *Control) handleCloseProxy(m msg.Message) { func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) { var pxyConf v1.ProxyConfigurer // Load configures from NewProxy message and validate. - pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.serverCfg) + pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.sessionCtx.ServerCfg) if err != nil { return } // User info userInfo := plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, RunID: ctl.runID, } @@ -484,22 +467,22 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err // In fact, it creates different proxies based on the proxy type. We just call run() here. pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{ UserInfo: userInfo, - LoginMsg: ctl.loginMsg, + LoginMsg: ctl.sessionCtx.LoginMsg, PoolCount: ctl.poolCount, - ResourceController: ctl.rc, + ResourceController: ctl.sessionCtx.RC, GetWorkConnFn: ctl.GetWorkConn, Configurer: pxyConf, - ServerCfg: ctl.serverCfg, - EncryptionKey: ctl.encryptionKey, + ServerCfg: ctl.sessionCtx.ServerCfg, + EncryptionKey: ctl.sessionCtx.EncryptionKey, }) if err != nil { return remoteAddr, err } // Check ports used number in each client - if ctl.serverCfg.MaxPortsPerClient > 0 { + if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { ctl.mu.Lock() - if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.serverCfg.MaxPortsPerClient) { + if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.sessionCtx.ServerCfg.MaxPortsPerClient) { ctl.mu.Unlock() err = fmt.Errorf("exceed the max_ports_per_client") return @@ -516,7 +499,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err }() } - if ctl.pxyManager.Exist(pxyMsg.ProxyName) { + if ctl.sessionCtx.PxyManager.Exist(pxyMsg.ProxyName) { err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName) return } @@ -531,7 +514,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err } }() - err = ctl.pxyManager.Add(pxyMsg.ProxyName, pxy) + err = ctl.sessionCtx.PxyManager.Add(pxyMsg.ProxyName, pxy) if err != nil { return } @@ -550,11 +533,11 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { return } - if ctl.serverCfg.MaxPortsPerClient > 0 { + if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { ctl.portsUsedNum -= pxy.GetUsedPortsNum() } pxy.Close() - ctl.pxyManager.Del(pxy.GetName()) + ctl.sessionCtx.PxyManager.Del(pxy.GetName()) delete(ctl.proxies, closeMsg.ProxyName) ctl.mu.Unlock() @@ -562,16 +545,16 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { notifyContent := &plugin.CloseProxyContent{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, CloseProxy: msg.CloseProxy{ ProxyName: pxy.GetName(), }, } go func() { - _ = ctl.pluginManager.CloseProxy(notifyContent) + _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) }() return } diff --git a/server/service.go b/server/service.go index 4eddbf28..28ccb451 100644 --- a/server/service.go +++ b/server/service.go @@ -604,8 +604,18 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter return err } - // TODO(fatedier): use SessionContext - ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg) + ctl, err := NewControl(ctx, &SessionContext{ + RC: svr.rc, + PxyManager: svr.pxyManager, + PluginManager: svr.pluginManager, + AuthVerifier: authVerifier, + EncryptionKey: svr.auth.EncryptionKey(), + Conn: ctlConn, + ConnEncrypted: !internal, + LoginMsg: loginMsg, + ServerCfg: svr.cfg, + ClientRegistry: svr.clientRegistry, + }) if err != nil { xl.Warnf("create new controller error: %v", err) // don't return detailed errors to client @@ -626,7 +636,6 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter ctl.Close() return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User) } - ctl.clientRegistry = svr.clientRegistry ctl.Start() @@ -652,9 +661,9 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) // server plugin hook content := &plugin.NewWorkConnContent{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, NewWorkConn: *newMsg, } @@ -662,7 +671,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) if err == nil { newMsg = &retContent.NewWorkConn // Check auth. - err = ctl.authVerifier.VerifyNewWorkConn(newMsg) + err = ctl.sessionCtx.AuthVerifier.VerifyNewWorkConn(newMsg) } if err != nil { xl.Warnf("invalid NewWorkConn with run id [%s]", newMsg.RunID) @@ -683,7 +692,7 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis if !exist { return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID) } - visitorUser = ctl.loginMsg.User + visitorUser = ctl.sessionCtx.LoginMsg.User } return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey, newMsg.UseEncryption, newMsg.UseCompression, visitorUser) From c2454e711465acc16e5aa92018ef491052949b5b Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Sat, 7 Mar 2026 17:10:19 +0200 Subject: [PATCH 26/43] refactor: fix modernize lint issues (#5215) --- .golangci.yml | 4 +++ client/proxy/sudp.go | 2 +- client/proxy/udp.go | 2 +- client/proxy/xtcp.go | 2 +- pkg/config/legacy/proxy.go | 16 ++++----- pkg/config/legacy/visitor.go | 6 ++-- pkg/config/template.go | 2 +- pkg/config/types/types.go | 4 +-- pkg/config/v1/proxy.go | 16 ++++----- pkg/config/v1/proxy_plugin.go | 20 +++++------ pkg/config/v1/visitor.go | 6 ++-- pkg/config/v1/visitor_plugin.go | 2 +- pkg/msg/msg.go | 2 +- pkg/nathole/analysis.go | 2 +- pkg/nathole/nathole.go | 4 +-- pkg/plugin/visitor/virtual_net.go | 5 +-- pkg/policy/featuregate/feature_gate.go | 17 +++------ pkg/util/http/http.go | 6 ++-- pkg/util/net/udp.go | 6 +--- pkg/util/util/util.go | 4 +-- server/control.go | 5 +-- server/proxy/http.go | 2 +- server/proxy/https.go | 2 +- server/proxy/stcp.go | 2 +- server/proxy/sudp.go | 2 +- server/proxy/tcp.go | 2 +- server/proxy/tcpmux.go | 2 +- server/proxy/udp.go | 2 +- server/proxy/xtcp.go | 2 +- test/e2e/framework/process.go | 5 ++- test/e2e/legacy/basic/basic.go | 49 +++++++++++++++----------- test/e2e/legacy/features/group.go | 16 ++++----- test/e2e/legacy/plugin/client.go | 8 +++-- test/e2e/pkg/port/port.go | 2 +- test/e2e/v1/basic/basic.go | 49 +++++++++++++++----------- test/e2e/v1/features/group.go | 18 +++++----- test/e2e/v1/plugin/client.go | 8 +++-- 37 files changed, 152 insertions(+), 152 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index cbdb4510..4a8c92ec 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,7 @@ linters: - lll - makezero - misspell + - modernize - prealloc - predeclared - revive @@ -47,6 +48,9 @@ linters: ignore-rules: - cancelled - marshalled + modernize: + disable: + - omitzero unparam: check-exported: false exclusions: diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index da941446..21e34a41 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -32,7 +32,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) } type SUDPProxy struct { diff --git a/client/proxy/udp.go b/client/proxy/udp.go index 570da476..8547ef72 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -31,7 +31,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) } type UDPProxy struct { diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 808e0ebe..41dc5229 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -34,7 +34,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) } type XTCPProxy struct { diff --git a/pkg/config/legacy/proxy.go b/pkg/config/legacy/proxy.go index 0c461a1a..8d7cf9a0 100644 --- a/pkg/config/legacy/proxy.go +++ b/pkg/config/legacy/proxy.go @@ -39,14 +39,14 @@ const ( // Proxy var ( proxyConfTypeMap = map[ProxyType]reflect.Type{ - ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}), - ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}), - ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}), - ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}), - ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}), - ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}), - ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}), - ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}), + ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](), + ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](), + ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](), + ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](), + ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](), + ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](), + ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](), + ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](), } ) diff --git a/pkg/config/legacy/visitor.go b/pkg/config/legacy/visitor.go index 031c29bf..110a214d 100644 --- a/pkg/config/legacy/visitor.go +++ b/pkg/config/legacy/visitor.go @@ -32,9 +32,9 @@ const ( // Visitor var ( visitorConfTypeMap = map[VisitorType]reflect.Type{ - VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}), - VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}), - VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}), + VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](), + VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](), + VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](), } ) diff --git a/pkg/config/template.go b/pkg/config/template.go index 44bc456d..16fe069f 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -38,7 +38,7 @@ func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, e return nil, fmt.Errorf("first and second range numbers are not in pairs") } pairs := make([]NumberPair, 0, len(firstRangeNumbers)) - for i := 0; i < len(firstRangeNumbers); i++ { + for i := range firstRangeNumbers { pairs = append(pairs, NumberPair{ First: firstRangeNumbers[i], Second: secondRangeNumbers[i], diff --git a/pkg/config/types/types.go b/pkg/config/types/types.go index 5b2b6930..972908ed 100644 --- a/pkg/config/types/types.go +++ b/pkg/config/types/types.go @@ -137,8 +137,8 @@ func (p PortsRangeSlice) String() string { func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) { str = strings.TrimSpace(str) out := []PortsRange{} - numRanges := strings.Split(str, ",") - for _, numRangeStr := range numRanges { + numRanges := strings.SplitSeq(str, ",") + for numRangeStr := range numRanges { // 1000-2000 or 2001 numArray := strings.Split(numRangeStr, "-") // length: only 1 or 2 is correct diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 97bfab22..e7d1a6b7 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -239,14 +239,14 @@ const ( ) var proxyConfigTypeMap = map[ProxyType]reflect.Type{ - ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}), - ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}), - ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}), - ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}), - ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}), - ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}), - ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}), - ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}), + ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](), + ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](), + ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](), + ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](), + ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](), + ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](), + ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](), + ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](), } func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer { diff --git a/pkg/config/v1/proxy_plugin.go b/pkg/config/v1/proxy_plugin.go index 003f825f..0d6ca3d0 100644 --- a/pkg/config/v1/proxy_plugin.go +++ b/pkg/config/v1/proxy_plugin.go @@ -37,16 +37,16 @@ const ( ) var clientPluginOptionsTypeMap = map[string]reflect.Type{ - PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), - PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), - PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), - PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), - PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), - PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), - PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), - PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), - PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), - PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}), + PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](), + PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](), + PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](), + PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](), + PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](), + PluginSocks5: reflect.TypeFor[Socks5PluginOptions](), + PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](), + PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](), + PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](), + PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](), } type ClientPluginOptions interface { diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 8f94f672..d1035d4f 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -79,9 +79,9 @@ const ( ) var visitorConfigTypeMap = map[VisitorType]reflect.Type{ - VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}), - VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}), - VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}), + VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](), + VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](), + VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](), } type TypedVisitorConfig struct { diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go index 2e2d0494..6541e3de 100644 --- a/pkg/config/v1/visitor_plugin.go +++ b/pkg/config/v1/visitor_plugin.go @@ -25,7 +25,7 @@ const ( ) var visitorPluginOptionsTypeMap = map[string]reflect.Type{ - VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}), + VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](), } type VisitorPluginOptions interface { diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index dda8e9aa..e8bcbc35 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{ TypeNatHoleReport: NatHoleReport{}, } -var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name() +var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name() type ClientSpec struct { // Due to the support of VirtualClient, frps needs to know the client type in order to diff --git a/pkg/nathole/analysis.go b/pkg/nathole/analysis.go index 6be0be55..ed9eb6ce 100644 --- a/pkg/nathole/analysis.go +++ b/pkg/nathole/analysis.go @@ -151,7 +151,7 @@ func getBehaviorScoresByMode(mode int, defaultScore int) []*BehaviorScore { func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore { behaviors := getBehaviorByMode(mode) scores := make([]*BehaviorScore, 0, len(behaviors)) - for i := 0; i < len(behaviors); i++ { + for i := range behaviors { score := receiverScore if behaviors[i].A.Role == DetectRoleSender { score = senderScore diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index 7d6ebe89..bea3d371 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -410,7 +410,7 @@ func sendSidMessageToRandomPorts( xl := xlog.FromContextSafe(ctx) used := sets.New[int]() getUnusedPort := func() int { - for i := 0; i < 10; i++ { + for range 10 { port := rand.IntN(65535-1024) + 1024 if !used.Has(port) { used.Insert(port) @@ -420,7 +420,7 @@ func sendSidMessageToRandomPorts( return 0 } - for i := 0; i < count; i++ { + for range count { select { case <-ctx.Done(): return diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go index 8193ce03..fa8c700e 100644 --- a/pkg/plugin/visitor/virtual_net.go +++ b/pkg/plugin/visitor/virtual_net.go @@ -153,10 +153,7 @@ func (p *VirtualNetPlugin) run() { // Exponential backoff: 60s, 120s, 240s, 300s (capped) baseDelay := 60 * time.Second - reconnectDelay = baseDelay * time.Duration(1< 300*time.Second { - reconnectDelay = 300 * time.Second - } + reconnectDelay = min(baseDelay*time.Duration(1< 0 { diff --git a/pkg/policy/featuregate/feature_gate.go b/pkg/policy/featuregate/feature_gate.go index 81a392b7..e831ac0a 100644 --- a/pkg/policy/featuregate/feature_gate.go +++ b/pkg/policy/featuregate/feature_gate.go @@ -16,6 +16,7 @@ package featuregate import ( "fmt" + "maps" "sort" "strings" "sync" @@ -93,9 +94,7 @@ type featureGate struct { // NewFeatureGate creates a new feature gate with the default features func NewFeatureGate() MutableFeatureGate { known := map[Feature]FeatureSpec{} - for k, v := range defaultFeatures { - known[k] = v - } + maps.Copy(known, defaultFeatures) f := &featureGate{} f.known.Store(known) @@ -110,13 +109,9 @@ func (f *featureGate) SetFromMap(m map[string]bool) error { // Copy existing state known := map[Feature]FeatureSpec{} - for k, v := range f.known.Load().(map[Feature]FeatureSpec) { - known[k] = v - } + maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec)) enabled := map[Feature]bool{} - for k, v := range f.enabled.Load().(map[Feature]bool) { - enabled[k] = v - } + maps.Copy(enabled, f.enabled.Load().(map[Feature]bool)) // Apply the new settings for k, v := range m { @@ -148,9 +143,7 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error { // Copy existing state known := map[Feature]FeatureSpec{} - for k, v := range f.known.Load().(map[Feature]FeatureSpec) { - known[k] = v - } + maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec)) // Add new features for name, spec := range features { diff --git a/pkg/util/http/http.go b/pkg/util/http/http.go index b85a46a3..8a763727 100644 --- a/pkg/util/http/http.go +++ b/pkg/util/http/http.go @@ -89,11 +89,11 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) { return } cs := string(c) - s := strings.IndexByte(cs, ':') - if s < 0 { + before, after, found := strings.Cut(cs, ":") + if !found { return } - return cs[:s], cs[s+1:], true + return before, after, true } func BasicAuth(username, passwd string) string { diff --git a/pkg/util/net/udp.go b/pkg/util/net/udp.go index 4c10fc02..aa4d9834 100644 --- a/pkg/util/net/udp.go +++ b/pkg/util/net/udp.go @@ -86,11 +86,7 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) { c.lastActive = time.Now() c.mu.Unlock() - if len(b) < len(content) { - n = len(b) - } else { - n = len(content) - } + n = min(len(b), len(content)) copy(b, content) return n, nil } diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 84408385..31997485 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -68,8 +68,8 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { rangeStr = strings.TrimSpace(rangeStr) numbers = make([]int64, 0) // e.g. 1000-2000,2001,2002,3000-4000 - numRanges := strings.Split(rangeStr, ",") - for _, numRangeStr := range numRanges { + numRanges := strings.SplitSeq(rangeStr, ",") + for numRangeStr := range numRanges { // 1000-2000 or 2001 numArray := strings.Split(numRangeStr, "-") // length: only 1 or 2 is correct diff --git a/server/control.go b/server/control.go index 219d6368..1e668ec8 100644 --- a/server/control.go +++ b/server/control.go @@ -158,10 +158,7 @@ type Control struct { } func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { - poolCount := sessionCtx.LoginMsg.PoolCount - if poolCount > int(sessionCtx.ServerCfg.Transport.MaxPoolCount) { - poolCount = int(sessionCtx.ServerCfg.Transport.MaxPoolCount) - } + poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount)) ctl := &Control{ sessionCtx: sessionCtx, workConnCh: make(chan net.Conn, poolCount+10), diff --git a/server/proxy/http.go b/server/proxy/http.go index e5df06c5..05afc2e9 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -31,7 +31,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.HTTPProxyConfig{}), NewHTTPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.HTTPProxyConfig](), NewHTTPProxy) } type HTTPProxy struct { diff --git a/server/proxy/https.go b/server/proxy/https.go index ec720242..e2efc736 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -25,7 +25,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.HTTPSProxyConfig{}), NewHTTPSProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.HTTPSProxyConfig](), NewHTTPSProxy) } type HTTPSProxy struct { diff --git a/server/proxy/stcp.go b/server/proxy/stcp.go index 113fd13b..f8c17a46 100644 --- a/server/proxy/stcp.go +++ b/server/proxy/stcp.go @@ -21,7 +21,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.STCPProxyConfig{}), NewSTCPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.STCPProxyConfig](), NewSTCPProxy) } type STCPProxy struct { diff --git a/server/proxy/sudp.go b/server/proxy/sudp.go index 00438882..d409fed6 100644 --- a/server/proxy/sudp.go +++ b/server/proxy/sudp.go @@ -21,7 +21,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) } type SUDPProxy struct { diff --git a/server/proxy/tcp.go b/server/proxy/tcp.go index a6eae3a9..c305e779 100644 --- a/server/proxy/tcp.go +++ b/server/proxy/tcp.go @@ -24,7 +24,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.TCPProxyConfig{}), NewTCPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.TCPProxyConfig](), NewTCPProxy) } type TCPProxy struct { diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index 2a0c8512..0c421c8e 100644 --- a/server/proxy/tcpmux.go +++ b/server/proxy/tcpmux.go @@ -26,7 +26,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.TCPMuxProxyConfig{}), NewTCPMuxProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.TCPMuxProxyConfig](), NewTCPMuxProxy) } type TCPMuxProxy struct { diff --git a/server/proxy/udp.go b/server/proxy/udp.go index d362ab9c..6c5b854a 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -35,7 +35,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) } type UDPProxy struct { diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index 1ccf331c..bef7320e 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -24,7 +24,7 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) } type XTCPProxy struct { diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 0b837e39..7fce0caf 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -2,6 +2,7 @@ package framework import ( "fmt" + "maps" "os" "path/filepath" "time" @@ -20,9 +21,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str ExpectNoError(err) ExpectTrue(len(templates) > 0) - for name, port := range ports { - f.usedPorts[name] = port - } + maps.Copy(f.usedPorts, ports) currentServerProcesses := make([]*process.Process, 0, len(serverTemplates)) for i := range serverTemplates { diff --git a/test/e2e/legacy/basic/basic.go b/test/e2e/legacy/basic/basic.go index 763d3353..d3cd61b1 100644 --- a/test/e2e/legacy/basic/basic.go +++ b/test/e2e/legacy/basic/basic.go @@ -26,7 +26,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { proxyType := t ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { serverConf := consts.LegacyDefaultServerConfig - clientConf := consts.LegacyDefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) localPortName := "" protocol := "tcp" @@ -78,10 +79,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f). @@ -102,7 +103,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { vhost_http_port = %d `, vhostHTTPPort) - clientConf := consts.LegacyDefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` @@ -147,13 +149,13 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = test.proxyName + ".example.com" } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) framework.NewRequestExpect(f). Explain(test.proxyName + "-" + domain). @@ -185,7 +187,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.LegacyDefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` [%s] @@ -229,10 +232,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = test.proxyName + ".example.com" } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -244,7 +247,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { f.RunServer("", localServer) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) framework.NewRequestExpect(f). Explain(test.proxyName + "-" + domain). @@ -282,9 +285,12 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { proxyType := t ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { serverConf := consts.LegacyDefaultServerConfig - clientServerConf := consts.LegacyDefaultClientConfig + "\nuser = user1" - clientVisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user1" - clientUser2VisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user2" + var clientServerConf strings.Builder + clientServerConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1") + var clientVisitorConf strings.Builder + clientVisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1") + var clientUser2VisitorConf strings.Builder + clientUser2VisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user2") localPortName := "" protocol := "tcp" @@ -400,20 +406,20 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n" + clientServerConf.WriteString(getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n") } for _, test := range tests { config := getProxyVisitorConf( test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, ) + "\n" if test.deployUser2Client { - clientUser2VisitorConf += config + clientUser2VisitorConf.WriteString(config) } else { - clientVisitorConf += config + clientVisitorConf.WriteString(config) } } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf}) + f.RunProcesses([]string{serverConf}, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) for _, test := range tests { timeout := time.Second @@ -440,7 +446,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { ginkgo.Describe("TCPMUX", func() { ginkgo.It("Type tcpmux", func() { serverConf := consts.LegacyDefaultServerConfig - clientConf := consts.LegacyDefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") serverConf += fmt.Sprintf(` @@ -483,14 +490,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.extraConfig) + "\n") localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) f.RunServer(port.GenName(test.proxyName), localServer) } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) // Request without HTTP connect should get error framework.NewRequestExpect(f). diff --git a/test/e2e/legacy/features/group.go b/test/e2e/legacy/features/group.go index dc15f75c..28e5eeed 100644 --- a/test/e2e/legacy/features/group.go +++ b/test/e2e/legacy/features/group.go @@ -48,12 +48,10 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { return true }) } - for i := 0; i < 10; i++ { - wait.Add(1) - go func() { - defer wait.Done() + for range 10 { + wait.Go(func() { expectFn() - }() + }) } wait.Wait() @@ -94,7 +92,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { fooCount := 0 barCount := 0 - for i := 0; i < 10; i++ { + for i := range 10 { framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { switch string(resp.Content) { case "foo": @@ -150,7 +148,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { // check foo and bar is ok results := []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true @@ -161,7 +159,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { // close bar server, check foo is ok barServer.Close() time.Sleep(2 * time.Second) - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() } @@ -169,7 +167,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { f.RunServer("", barServer) time.Sleep(2 * time.Second) results = []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true diff --git a/test/e2e/legacy/plugin/client.go b/test/e2e/legacy/plugin/client.go index b69c6aed..c38cbaf7 100644 --- a/test/e2e/legacy/plugin/client.go +++ b/test/e2e/legacy/plugin/client.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "strconv" + "strings" "github.com/onsi/ginkgo/v2" @@ -22,7 +23,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { ginkgo.Describe("UnixDomainSocket", func() { ginkgo.It("Expose a unix domain socket echo server", func() { serverConf := consts.LegacyDefaultServerConfig - clientConf := consts.LegacyDefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, portName string, extra string) string { return fmt.Sprintf(` @@ -65,10 +67,10 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() diff --git a/test/e2e/pkg/port/port.go b/test/e2e/pkg/port/port.go index 49cc9a68..82ac8586 100644 --- a/test/e2e/pkg/port/port.go +++ b/test/e2e/pkg/port/port.go @@ -52,7 +52,7 @@ func (pa *Allocator) GetByName(portName string) int { pa.mu.Lock() defer pa.mu.Unlock() - for i := 0; i < 20; i++ { + for range 20 { port := pa.getByRange(builder.rangePortFrom, builder.rangePortTo) if port == 0 { return 0 diff --git a/test/e2e/v1/basic/basic.go b/test/e2e/v1/basic/basic.go index 9d9d34cb..b9f4eded 100644 --- a/test/e2e/v1/basic/basic.go +++ b/test/e2e/v1/basic/basic.go @@ -26,7 +26,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { proxyType := t ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) localPortName := "" protocol := "tcp" @@ -79,10 +80,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f). @@ -103,7 +104,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { vhostHTTPPort = %d `, vhostHTTPPort) - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` @@ -149,13 +151,13 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) domain = strings.TrimLeft(domain, "[\"") domain = strings.TrimRight(domain, "]\"") @@ -189,7 +191,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` [[proxies]] @@ -234,10 +237,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -249,7 +252,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { f.RunServer("", localServer) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) domain = strings.TrimLeft(domain, "[\"") domain = strings.TrimRight(domain, "]\"") @@ -289,9 +292,12 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { proxyType := t ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { serverConf := consts.DefaultServerConfig - clientServerConf := consts.DefaultClientConfig + "\nuser = \"user1\"" - clientVisitorConf := consts.DefaultClientConfig + "\nuser = \"user1\"" - clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = \"user2\"" + var clientServerConf strings.Builder + clientServerConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user1\"") + var clientVisitorConf strings.Builder + clientVisitorConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user1\"") + var clientUser2VisitorConf strings.Builder + clientUser2VisitorConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user2\"") localPortName := "" protocol := "tcp" @@ -407,20 +413,20 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n" + clientServerConf.WriteString(getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n") } for _, test := range tests { config := getProxyVisitorConf( test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, ) + "\n" if test.deployUser2Client { - clientUser2VisitorConf += config + clientUser2VisitorConf.WriteString(config) } else { - clientVisitorConf += config + clientVisitorConf.WriteString(config) } } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf}) + f.RunProcesses([]string{serverConf}, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) for _, test := range tests { timeout := time.Second @@ -447,7 +453,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { ginkgo.Describe("TCPMUX", func() { ginkgo.It("Type tcpmux", func() { serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") serverConf += fmt.Sprintf(` @@ -491,14 +498,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.extraConfig) + "\n") localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) f.RunServer(port.GenName(test.proxyName), localServer) } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) // Request without HTTP connect should get error framework.NewRequestExpect(f). diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go index f6bb1856..850a932c 100644 --- a/test/e2e/v1/features/group.go +++ b/test/e2e/v1/features/group.go @@ -50,12 +50,10 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { return true }) } - for i := 0; i < 10; i++ { - wait.Add(1) - go func() { - defer wait.Done() + for range 10 { + wait.Go(func() { expectFn() - }() + }) } wait.Wait() @@ -98,7 +96,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { fooCount := 0 barCount := 0 - for i := 0; i < 10; i++ { + for i := range 10 { framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { switch string(resp.Content) { case "foo": @@ -163,7 +161,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { fooCount := 0 barCount := 0 - for i := 0; i < 10; i++ { + for i := range 10 { framework.NewRequestExpect(f). Explain("times " + strconv.Itoa(i)). Port(vhostHTTPSPort). @@ -230,7 +228,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { // check foo and bar is ok results := []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true @@ -241,7 +239,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { // close bar server, check foo is ok barServer.Close() time.Sleep(2 * time.Second) - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() } @@ -249,7 +247,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { f.RunServer("", barServer) time.Sleep(2 * time.Second) results = []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go index 73e2d863..4cb9d7d4 100644 --- a/test/e2e/v1/plugin/client.go +++ b/test/e2e/v1/plugin/client.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/onsi/ginkgo/v2" @@ -23,7 +24,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { ginkgo.Describe("UnixDomainSocket", func() { ginkgo.It("Expose a unix domain socket echo server", func() { serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) getProxyConf := func(proxyName string, portName string, extra string) string { return fmt.Sprintf(` @@ -69,10 +71,10 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() From 764a626b6ee6ba0cc8024148a19e7c02f181647d Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Mar 2026 00:02:14 +0800 Subject: [PATCH 27/43] server/control: deduplicate close-proxy logic and UserInfo construction (#5218) Extract closeProxy() helper to eliminate duplicated 4-step cleanup sequence (Close, PxyManager.Del, metrics, plugin notify) between worker() and CloseProxy(). Extract loginUserInfo() helper to eliminate 4 repeated plugin.UserInfo constructions using LoginMsg fields. Optimize worker() to snapshot and clear the proxies map under lock, then perform cleanup outside the lock to reduce lock hold time. --- server/control.go | 79 +++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/server/control.go b/server/control.go index 1e668ec8..314669ff 100644 --- a/server/control.go +++ b/server/control.go @@ -303,6 +303,30 @@ func (ctl *Control) WaitClosed() { <-ctl.doneCh } +func (ctl *Control) loginUserInfo() plugin.UserInfo { + return plugin.UserInfo{ + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, + } +} + +func (ctl *Control) closeProxy(pxy proxy.Proxy) { + pxy.Close() + ctl.sessionCtx.PxyManager.Del(pxy.GetName()) + metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) + + notifyContent := &plugin.CloseProxyContent{ + User: ctl.loginUserInfo(), + CloseProxy: msg.CloseProxy{ + ProxyName: pxy.GetName(), + }, + } + go func() { + _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) + }() +} + func (ctl *Control) worker() { xl := ctl.xl @@ -313,31 +337,16 @@ func (ctl *Control) worker() { ctl.sessionCtx.Conn.Close() ctl.mu.Lock() - defer ctl.mu.Unlock() - close(ctl.workConnCh) for workConn := range ctl.workConnCh { workConn.Close() } + proxies := ctl.proxies + ctl.proxies = make(map[string]proxy.Proxy) + ctl.mu.Unlock() - for _, pxy := range ctl.proxies { - pxy.Close() - ctl.sessionCtx.PxyManager.Del(pxy.GetName()) - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) - - notifyContent := &plugin.CloseProxyContent{ - User: plugin.UserInfo{ - User: ctl.sessionCtx.LoginMsg.User, - Metas: ctl.sessionCtx.LoginMsg.Metas, - RunID: ctl.sessionCtx.LoginMsg.RunID, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) - }() + for _, pxy := range proxies { + ctl.closeProxy(pxy) } metrics.Server.CloseClient() @@ -360,11 +369,7 @@ func (ctl *Control) handleNewProxy(m msg.Message) { inMsg := m.(*msg.NewProxy) content := &plugin.NewProxyContent{ - User: plugin.UserInfo{ - User: ctl.sessionCtx.LoginMsg.User, - Metas: ctl.sessionCtx.LoginMsg.Metas, - RunID: ctl.sessionCtx.LoginMsg.RunID, - }, + User: ctl.loginUserInfo(), NewProxy: *inMsg, } var remoteAddr string @@ -399,11 +404,7 @@ func (ctl *Control) handlePing(m msg.Message) { inMsg := m.(*msg.Ping) content := &plugin.PingContent{ - User: plugin.UserInfo{ - User: ctl.sessionCtx.LoginMsg.User, - Metas: ctl.sessionCtx.LoginMsg.Metas, - RunID: ctl.sessionCtx.LoginMsg.RunID, - }, + User: ctl.loginUserInfo(), Ping: *inMsg, } retContent, err := ctl.sessionCtx.PluginManager.Ping(content) @@ -533,25 +534,9 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { ctl.portsUsedNum -= pxy.GetUsedPortsNum() } - pxy.Close() - ctl.sessionCtx.PxyManager.Del(pxy.GetName()) delete(ctl.proxies, closeMsg.ProxyName) ctl.mu.Unlock() - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) - - notifyContent := &plugin.CloseProxyContent{ - User: plugin.UserInfo{ - User: ctl.sessionCtx.LoginMsg.User, - Metas: ctl.sessionCtx.LoginMsg.Metas, - RunID: ctl.sessionCtx.LoginMsg.RunID, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) - }() + ctl.closeProxy(pxy) return } From 605f3bdece225135253c4a0a53e892e5166b7e50 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Mar 2026 01:03:40 +0800 Subject: [PATCH 28/43] client/visitor: deduplicate visitor connection handshake and wrapping (#5219) Extract two shared helpers to eliminate duplicated code across STCP, SUDP, and XTCP visitors: - dialRawVisitorConn: handles ConnectServer + NewVisitorConn handshake (auth, sign key, 10s read deadline, error check) - wrapVisitorConn: handles encryption + pooled compression wrapping, returning a recycleFn for pool resource cleanup SUDP is upgraded from WithCompression to WithCompressionFromPool, aligning with the pooled compression used by STCP and XTCP. --- client/visitor/stcp.go | 58 +++------------------------------- client/visitor/sudp.go | 66 ++++++++------------------------------- client/visitor/visitor.go | 62 ++++++++++++++++++++++++++++++++++++ client/visitor/xtcp.go | 21 +++++-------- 4 files changed, 87 insertions(+), 120 deletions(-) diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index 6529142c..03ec51fe 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -15,18 +15,12 @@ package visitor import ( - "fmt" - "io" "net" "strconv" - "time" libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" - "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/frp/pkg/naming" - "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -61,7 +55,6 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) var tunnelErr error defer func() { - // If there was an error and connection supports CloseWithError, use it if tunnelErr != nil { if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok { _ = eConn.CloseWithError(tunnelErr) @@ -72,62 +65,21 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { }() xl.Debugf("get a new stcp user connection") - visitorConn, err := sv.helper.ConnectServer() + visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig()) if err != nil { + xl.Warnf("dialRawVisitorConn error: %v", err) tunnelErr = err return } defer visitorConn.Close() - now := time.Now().Unix() - targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) - newVisitorConnMsg := &msg.NewVisitorConn{ - RunID: sv.helper.RunID(), - ProxyName: targetProxyName, - SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), - Timestamp: now, - UseEncryption: sv.cfg.Transport.UseEncryption, - UseCompression: sv.cfg.Transport.UseCompression, - } - err = msg.WriteMsg(visitorConn, newVisitorConnMsg) + remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig()) if err != nil { - xl.Warnf("send newVisitorConnMsg to server error: %v", err) + xl.Warnf("wrapVisitorConn error: %v", err) tunnelErr = err return } - - var newVisitorConnRespMsg msg.NewVisitorConnResp - _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) - if err != nil { - xl.Warnf("get newVisitorConnRespMsg error: %v", err) - tunnelErr = err - return - } - _ = visitorConn.SetReadDeadline(time.Time{}) - - if newVisitorConnRespMsg.Error != "" { - xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) - tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error) - return - } - - var remote io.ReadWriteCloser - remote = visitorConn - if sv.cfg.Transport.UseEncryption { - remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) - if err != nil { - xl.Errorf("create encryption stream error: %v", err) - tunnelErr = err - return - } - } - - if sv.cfg.Transport.UseCompression { - var recycleFn func() - remote, recycleFn = libio.WithCompressionFromPool(remote) - defer recycleFn() - } + defer recycleFn() libio.Join(userConn, remote) } diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index a341da8a..6014161c 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -16,21 +16,17 @@ package visitor import ( "fmt" - "io" "net" "strconv" "sync" "time" "github.com/fatedier/golib/errors" - libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/proto/udp" netpkg "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -76,6 +72,7 @@ func (sv *SUDPVisitor) dispatcher() { var ( visitorConn net.Conn + recycleFn func() err error firstPacket *msg.UDPPacket @@ -93,14 +90,17 @@ func (sv *SUDPVisitor) dispatcher() { return } - visitorConn, err = sv.getNewVisitorConn() + visitorConn, recycleFn, err = sv.getNewVisitorConn() if err != nil { xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err) continue } // visitorConn always be closed when worker done. - sv.worker(visitorConn, firstPacket) + func() { + defer recycleFn() + sv.worker(visitorConn, firstPacket) + }() select { case <-sv.checkCloseCh: @@ -198,57 +198,17 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { xl.Infof("sudp worker is closed") } -func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { - xl := xlog.FromContextSafe(sv.ctx) - visitorConn, err := sv.helper.ConnectServer() +func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) { + rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig()) if err != nil { - return nil, fmt.Errorf("frpc connect frps error: %v", err) + return nil, func() {}, err } - - now := time.Now().Unix() - targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) - newVisitorConnMsg := &msg.NewVisitorConn{ - RunID: sv.helper.RunID(), - ProxyName: targetProxyName, - SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), - Timestamp: now, - UseEncryption: sv.cfg.Transport.UseEncryption, - UseCompression: sv.cfg.Transport.UseCompression, - } - err = msg.WriteMsg(visitorConn, newVisitorConnMsg) + rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig()) if err != nil { - visitorConn.Close() - return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err) + rawConn.Close() + return nil, func() {}, err } - - var newVisitorConnRespMsg msg.NewVisitorConnResp - _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) - if err != nil { - visitorConn.Close() - return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err) - } - _ = visitorConn.SetReadDeadline(time.Time{}) - - if newVisitorConnRespMsg.Error != "" { - visitorConn.Close() - return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) - } - - var remote io.ReadWriteCloser - remote = visitorConn - if sv.cfg.Transport.UseEncryption { - remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) - if err != nil { - xl.Errorf("create encryption stream error: %v", err) - visitorConn.Close() - return nil, err - } - } - if sv.cfg.Transport.UseCompression { - remote = libio.WithCompression(remote) - } - return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil + return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil } func (sv *SUDPVisitor) Close() { diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index 51999499..14a7aa37 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -16,13 +16,21 @@ package visitor import ( "context" + "fmt" + "io" "net" "sync" + "time" + + libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" plugin "github.com/fatedier/frp/pkg/plugin/visitor" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/vnet" ) @@ -142,3 +150,57 @@ func (v *BaseVisitor) Close() { v.plugin.Close() } } + +func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) { + visitorConn, err := v.helper.ConnectServer() + if err != nil { + return nil, fmt.Errorf("connect to server error: %v", err) + } + + now := time.Now().Unix() + targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName) + newVisitorConnMsg := &msg.NewVisitorConn{ + RunID: v.helper.RunID(), + ProxyName: targetProxyName, + SignKey: util.GetAuthKey(cfg.SecretKey, now), + Timestamp: now, + UseEncryption: cfg.Transport.UseEncryption, + UseCompression: cfg.Transport.UseCompression, + } + err = msg.WriteMsg(visitorConn, newVisitorConnMsg) + if err != nil { + visitorConn.Close() + return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err) + } + + var newVisitorConnRespMsg msg.NewVisitorConnResp + _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) + if err != nil { + visitorConn.Close() + return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err) + } + _ = visitorConn.SetReadDeadline(time.Time{}) + + if newVisitorConnRespMsg.Error != "" { + visitorConn.Close() + return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) + } + return visitorConn, nil +} + +func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) { + rwc := conn + if cfg.Transport.UseEncryption { + var err error + rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey)) + if err != nil { + return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err) + } + } + recycleFn := func() {} + if cfg.Transport.UseCompression { + rwc, recycleFn = libio.WithCompressionFromPool(rwc) + } + return rwc, recycleFn, nil +} diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index b2f4ef37..e7a60895 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -182,21 +182,14 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { return } - var muxConnRWCloser io.ReadWriteCloser = tunnelConn - if sv.cfg.Transport.UseEncryption { - muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) - if err != nil { - xl.Errorf("create encryption stream error: %v", err) - tunnelConn.Close() - tunnelErr = err - return - } - } - if sv.cfg.Transport.UseCompression { - var recycleFn func() - muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser) - defer recycleFn() + muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig()) + if err != nil { + xl.Errorf("%v", err) + tunnelConn.Close() + tunnelErr = err + return } + defer recycleFn() _, _, errs := libio.Join(userConn, muxConnRWCloser) xl.Debugf("join connections closed") From 535eb3db3525fac4b173776ee6c77c1aeb96162a Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Sun, 8 Mar 2026 04:38:16 +0200 Subject: [PATCH 29/43] refactor: use maps.Clone and slices.Concat (#5220) --- pkg/policy/featuregate/feature_gate.go | 12 ++++-------- test/e2e/framework/process.go | 5 ++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/pkg/policy/featuregate/feature_gate.go b/pkg/policy/featuregate/feature_gate.go index e831ac0a..f27aecf3 100644 --- a/pkg/policy/featuregate/feature_gate.go +++ b/pkg/policy/featuregate/feature_gate.go @@ -93,8 +93,7 @@ type featureGate struct { // NewFeatureGate creates a new feature gate with the default features func NewFeatureGate() MutableFeatureGate { - known := map[Feature]FeatureSpec{} - maps.Copy(known, defaultFeatures) + known := maps.Clone(defaultFeatures) f := &featureGate{} f.known.Store(known) @@ -108,10 +107,8 @@ func (f *featureGate) SetFromMap(m map[string]bool) error { defer f.lock.Unlock() // Copy existing state - known := map[Feature]FeatureSpec{} - maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec)) - enabled := map[Feature]bool{} - maps.Copy(enabled, f.enabled.Load().(map[Feature]bool)) + known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) + enabled := maps.Clone(f.enabled.Load().(map[Feature]bool)) // Apply the new settings for k, v := range m { @@ -142,8 +139,7 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error { } // Copy existing state - known := map[Feature]FeatureSpec{} - maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec)) + known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) // Add new features for name, spec := range features { diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 7fce0caf..702a9b87 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -5,6 +5,7 @@ import ( "maps" "os" "path/filepath" + "slices" "time" flog "github.com/fatedier/frp/pkg/util/log" @@ -14,9 +15,7 @@ import ( // RunProcesses run multiple processes from templates. // The first template should always be frps. func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) { - templates := make([]string, 0, len(serverTemplates)+len(clientTemplates)) - templates = append(templates, serverTemplates...) - templates = append(templates, clientTemplates...) + templates := slices.Concat(serverTemplates, clientTemplates) outs, ports, err := f.RenderTemplates(templates) ExpectNoError(err) ExpectTrue(len(templates) > 0) From eeb0dacfc1908a21ecdf629812cbc2244630fab6 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Mar 2026 10:40:39 +0800 Subject: [PATCH 30/43] pkg/metrics/mem: remove redundant map write-backs and optimize proxy lookup (#5221) Remove 4 redundant pointer map write-backs in OpenConnection, CloseConnection, AddTrafficIn, and AddTrafficOut since the map stores pointers and mutations are already visible without reassignment. Optimize GetProxiesByTypeAndName from O(n) full map scan to O(1) direct map lookup by proxy name. --- pkg/metrics/mem/server.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/metrics/mem/server.go b/pkg/metrics/mem/server.go index 16a6491e..add90a5a 100644 --- a/pkg/metrics/mem/server.go +++ b/pkg/metrics/mem/server.go @@ -143,7 +143,6 @@ func (m *serverMetrics) OpenConnection(name string, _ string) { proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.CurConns.Inc(1) - m.info.ProxyStatistics[name] = proxyStats } } @@ -155,7 +154,6 @@ func (m *serverMetrics) CloseConnection(name string, _ string) { proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.CurConns.Dec(1) - m.info.ProxyStatistics[name] = proxyStats } } @@ -168,7 +166,6 @@ func (m *serverMetrics) AddTrafficIn(name string, _ string, trafficBytes int64) proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.TrafficIn.Inc(trafficBytes) - m.info.ProxyStatistics[name] = proxyStats } } @@ -181,7 +178,6 @@ func (m *serverMetrics) AddTrafficOut(name string, _ string, trafficBytes int64) proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.TrafficOut.Inc(trafficBytes) - m.info.ProxyStatistics[name] = proxyStats } } @@ -240,15 +236,9 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri m.mu.Lock() defer m.mu.Unlock() - for name, proxyStats := range m.info.ProxyStatistics { - if proxyStats.ProxyType != proxyType { - continue - } - if name != proxyName { - continue - } - res = toProxyStats(name, proxyStats) - break + proxyStats, ok := m.info.ProxyStatistics[proxyName] + if ok && proxyStats.ProxyType == proxyType { + res = toProxyStats(proxyName, proxyStats) } return } From c7ac12ea0f1fa2b7d244b96e56de0a2500cc27d5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Mar 2026 18:57:21 +0800 Subject: [PATCH 31/43] server/group: refactor with shared abstractions and fix concurrency issues (#5222) * server/group: refactor group package with shared abstractions and fix concurrency issues Extract common patterns into reusable components: - groupRegistry[G]: generic concurrent map for group lifecycle management - baseGroup: shared plumbing for listener-based groups (TCP, HTTPS, TCPMux) - Listener: unified virtual listener replacing 3 identical implementations Fix concurrency issues: - Stale-pointer race: isCurrent check + errGroupStale + controller retry loops - Worker generation safety: pass realLn and acceptCh as params instead of reading mutable fields - Connection leak: close conn on worker panic recovery path - ABBA deadlock in HTTP UnRegister: consistent lock ordering (group.mu -> registry.mu) - Round-robin overflow in HTTPGroup: use unsigned modulo Add unit tests (17 tests) for registry, listener, and baseGroup. Add TCPMux group load balancing e2e test. * server/group: replace tautological assertion with require.NotPanics * server/group: remove blank line between doc comment and type declaration --- server/group/base.go | 77 ++++++++++++++ server/group/base_test.go | 169 +++++++++++++++++++++++++++++++ server/group/group.go | 2 + server/group/http.go | 59 +++++------ server/group/https.go | 151 ++++++---------------------- server/group/listener.go | 49 +++++++++ server/group/listener_test.go | 68 +++++++++++++ server/group/registry.go | 59 +++++++++++ server/group/registry_test.go | 102 +++++++++++++++++++ server/group/tcp.go | 175 +++++++------------------------- server/group/tcpmux.go | 183 ++++++++-------------------------- test/e2e/v1/features/group.go | 62 ++++++++++++ 12 files changed, 720 insertions(+), 436 deletions(-) create mode 100644 server/group/base.go create mode 100644 server/group/base_test.go create mode 100644 server/group/listener.go create mode 100644 server/group/listener_test.go create mode 100644 server/group/registry.go create mode 100644 server/group/registry_test.go diff --git a/server/group/base.go b/server/group/base.go new file mode 100644 index 00000000..5684d8ef --- /dev/null +++ b/server/group/base.go @@ -0,0 +1,77 @@ +package group + +import ( + "net" + "sync" + + gerr "github.com/fatedier/golib/errors" +) + +// baseGroup contains the shared plumbing for listener-based groups +// (TCP, HTTPS, TCPMux). Each concrete group embeds this and provides +// its own Listen method with protocol-specific validation. +type baseGroup struct { + group string + groupKey string + + acceptCh chan net.Conn + realLn net.Listener + lns []*Listener + mu sync.Mutex + cleanupFn func() +} + +// initBase resets the baseGroup for a fresh listen cycle. +// Must be called under mu when len(lns) == 0. +func (bg *baseGroup) initBase(group, groupKey string, realLn net.Listener, cleanupFn func()) { + bg.group = group + bg.groupKey = groupKey + bg.realLn = realLn + bg.acceptCh = make(chan net.Conn) + bg.cleanupFn = cleanupFn +} + +// worker reads from the real listener and fans out to acceptCh. +// The parameters are captured at creation time so that the worker is +// bound to a specific listen cycle and cannot observe a later initBase. +func (bg *baseGroup) worker(realLn net.Listener, acceptCh chan<- net.Conn) { + for { + c, err := realLn.Accept() + if err != nil { + return + } + err = gerr.PanicToError(func() { + acceptCh <- c + }) + if err != nil { + c.Close() + return + } + } +} + +// newListener creates a new Listener wired to this baseGroup. +// Must be called under mu. +func (bg *baseGroup) newListener(addr net.Addr) *Listener { + ln := newListener(bg.acceptCh, addr, bg.closeListener) + bg.lns = append(bg.lns, ln) + return ln +} + +// closeListener removes ln from the list. When the last listener is removed, +// it closes acceptCh, closes the real listener, and calls cleanupFn. +func (bg *baseGroup) closeListener(ln *Listener) { + bg.mu.Lock() + defer bg.mu.Unlock() + for i, l := range bg.lns { + if l == ln { + bg.lns = append(bg.lns[:i], bg.lns[i+1:]...) + break + } + } + if len(bg.lns) == 0 { + close(bg.acceptCh) + bg.realLn.Close() + bg.cleanupFn() + } +} diff --git a/server/group/base_test.go b/server/group/base_test.go new file mode 100644 index 00000000..1b470841 --- /dev/null +++ b/server/group/base_test.go @@ -0,0 +1,169 @@ +package group + +import ( + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeLn is a controllable net.Listener for tests. +type fakeLn struct { + connCh chan net.Conn + closed chan struct{} + once sync.Once +} + +func newFakeLn() *fakeLn { + return &fakeLn{ + connCh: make(chan net.Conn, 8), + closed: make(chan struct{}), + } +} + +func (f *fakeLn) Accept() (net.Conn, error) { + select { + case c := <-f.connCh: + return c, nil + case <-f.closed: + return nil, net.ErrClosed + } +} + +func (f *fakeLn) Close() error { + f.once.Do(func() { close(f.closed) }) + return nil +} + +func (f *fakeLn) Addr() net.Addr { return fakeAddr("127.0.0.1:9999") } + +func (f *fakeLn) inject(c net.Conn) { + select { + case f.connCh <- c: + case <-f.closed: + } +} + +func TestBaseGroup_WorkerFanOut(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + go bg.worker(fl, bg.acceptCh) + + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + select { + case got := <-bg.acceptCh: + assert.Equal(t, c1, got) + got.Close() + case <-time.After(time.Second): + t.Fatal("timed out waiting for connection on acceptCh") + } + + fl.Close() +} + +func TestBaseGroup_WorkerStopsOnListenerClose(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + done := make(chan struct{}) + go func() { + bg.worker(fl, bg.acceptCh) + close(done) + }() + + fl.Close() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("worker did not stop after listener close") + } +} + +func TestBaseGroup_WorkerClosesConnOnClosedChannel(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + // Close acceptCh before worker sends. + close(bg.acceptCh) + + done := make(chan struct{}) + go func() { + bg.worker(fl, bg.acceptCh) + close(done) + }() + + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("worker did not stop after panic recovery") + } + + // c1 should have been closed by worker's panic recovery path. + buf := make([]byte, 1) + _, err := c1.Read(buf) + assert.Error(t, err, "connection should be closed by worker") +} + +func TestBaseGroup_CloseLastListenerTriggersCleanup(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + cleanupCalled := 0 + bg.initBase("g", "key", fl, func() { cleanupCalled++ }) + + bg.mu.Lock() + ln1 := bg.newListener(fl.Addr()) + ln2 := bg.newListener(fl.Addr()) + bg.mu.Unlock() + + go bg.worker(fl, bg.acceptCh) + + ln1.Close() + assert.Equal(t, 0, cleanupCalled, "cleanup should not run while listeners remain") + + ln2.Close() + assert.Equal(t, 1, cleanupCalled, "cleanup should run after last listener closed") +} + +func TestBaseGroup_CloseOneOfTwoListeners(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + cleanupCalled := 0 + bg.initBase("g", "key", fl, func() { cleanupCalled++ }) + + bg.mu.Lock() + ln1 := bg.newListener(fl.Addr()) + ln2 := bg.newListener(fl.Addr()) + bg.mu.Unlock() + + go bg.worker(fl, bg.acceptCh) + + ln1.Close() + assert.Equal(t, 0, cleanupCalled) + + // ln2 should still receive connections. + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + got, err := ln2.Accept() + require.NoError(t, err) + assert.Equal(t, c1, got) + got.Close() + + ln2.Close() + assert.Equal(t, 1, cleanupCalled) +} diff --git a/server/group/group.go b/server/group/group.go index ab38cf45..1fbedf5c 100644 --- a/server/group/group.go +++ b/server/group/group.go @@ -24,4 +24,6 @@ var ( ErrListenerClosed = errors.New("group listener closed") ErrGroupDifferentPort = errors.New("group should have same remote port") ErrProxyRepeated = errors.New("group proxy repeated") + + errGroupStale = errors.New("stale group reference") ) diff --git a/server/group/http.go b/server/group/http.go index 26af595e..dd905581 100644 --- a/server/group/http.go +++ b/server/group/http.go @@ -9,53 +9,42 @@ import ( "github.com/fatedier/frp/pkg/util/vhost" ) +// HTTPGroupController manages HTTP groups that use round-robin +// callback routing (fundamentally different from listener-based groups). type HTTPGroupController struct { - // groups indexed by group name - groups map[string]*HTTPGroup - - // register createConn for each group to vhostRouter. - // createConn will get a connection from one proxy of the group + groupRegistry[*HTTPGroup] vhostRouter *vhost.Routers - - mu sync.Mutex } func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController { return &HTTPGroupController{ - groups: make(map[string]*HTTPGroup), - vhostRouter: vhostRouter, + groupRegistry: newGroupRegistry[*HTTPGroup](), + vhostRouter: vhostRouter, } } func (ctl *HTTPGroupController) Register( proxyName, group, groupKey string, routeConfig vhost.RouteConfig, -) (err error) { - indexKey := group - ctl.mu.Lock() - g, ok := ctl.groups[indexKey] - if !ok { - g = NewHTTPGroup(ctl) - ctl.groups[indexKey] = g +) error { + for { + g := ctl.getOrCreate(group, func() *HTTPGroup { + return NewHTTPGroup(ctl) + }) + err := g.Register(proxyName, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return err } - ctl.mu.Unlock() - - return g.Register(proxyName, group, groupKey, routeConfig) } func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) { - indexKey := group - ctl.mu.Lock() - defer ctl.mu.Unlock() - g, ok := ctl.groups[indexKey] + g, ok := ctl.get(group) if !ok { return } - - isEmpty := g.UnRegister(proxyName) - if isEmpty { - delete(ctl.groups, indexKey) - } + g.UnRegister(proxyName) } type HTTPGroup struct { @@ -87,6 +76,9 @@ func (g *HTTPGroup) Register( ) (err error) { g.mu.Lock() defer g.mu.Unlock() + if !g.ctl.isCurrent(group, func(cur *HTTPGroup) bool { return cur == g }) { + return errGroupStale + } if len(g.createFuncs) == 0 { // the first proxy in this group tmp := routeConfig // copy object @@ -123,7 +115,7 @@ func (g *HTTPGroup) Register( return nil } -func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) { +func (g *HTTPGroup) UnRegister(proxyName string) { g.mu.Lock() defer g.mu.Unlock() delete(g.createFuncs, proxyName) @@ -135,10 +127,11 @@ func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) { } if len(g.createFuncs) == 0 { - isEmpty = true g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser) + g.ctl.removeIf(g.group, func(cur *HTTPGroup) bool { + return cur == g + }) } - return } func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { @@ -151,7 +144,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { location := g.location routeByHTTPUser := g.routeByHTTPUser if len(g.pxyNames) > 0 { - name := g.pxyNames[int(newIndex)%len(g.pxyNames)] + name := g.pxyNames[newIndex%uint64(len(g.pxyNames))] f = g.createFuncs[name] } g.mu.RUnlock() @@ -174,7 +167,7 @@ func (g *HTTPGroup) chooseEndpoint() (string, error) { location := g.location routeByHTTPUser := g.routeByHTTPUser if len(g.pxyNames) > 0 { - name = g.pxyNames[int(newIndex)%len(g.pxyNames)] + name = g.pxyNames[newIndex%uint64(len(g.pxyNames))] } g.mu.RUnlock() diff --git a/server/group/https.go b/server/group/https.go index 4089b0cb..1ab97578 100644 --- a/server/group/https.go +++ b/server/group/https.go @@ -17,25 +17,19 @@ package group import ( "context" "net" - "sync" - - gerr "github.com/fatedier/golib/errors" "github.com/fatedier/frp/pkg/util/vhost" ) type HTTPSGroupController struct { - groups map[string]*HTTPSGroup - + groupRegistry[*HTTPSGroup] httpsMuxer *vhost.HTTPSMuxer - - mu sync.Mutex } func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController { return &HTTPSGroupController{ - groups: make(map[string]*HTTPSGroup), - httpsMuxer: httpsMuxer, + groupRegistry: newGroupRegistry[*HTTPSGroup](), + httpsMuxer: httpsMuxer, } } @@ -44,41 +38,28 @@ func (ctl *HTTPSGroupController) Listen( group, groupKey string, routeConfig vhost.RouteConfig, ) (l net.Listener, err error) { - indexKey := group - ctl.mu.Lock() - g, ok := ctl.groups[indexKey] - if !ok { - g = NewHTTPSGroup(ctl) - ctl.groups[indexKey] = g + for { + g := ctl.getOrCreate(group, func() *HTTPSGroup { + return NewHTTPSGroup(ctl) + }) + l, err = g.Listen(ctx, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return } - ctl.mu.Unlock() - - return g.Listen(ctx, group, groupKey, routeConfig) -} - -func (ctl *HTTPSGroupController) RemoveGroup(group string) { - ctl.mu.Lock() - defer ctl.mu.Unlock() - delete(ctl.groups, group) } type HTTPSGroup struct { - group string - groupKey string - domain string + baseGroup - acceptCh chan net.Conn - httpsLn *vhost.Listener - lns []*HTTPSGroupListener - ctl *HTTPSGroupController - mu sync.Mutex + domain string + ctl *HTTPSGroupController } func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup { return &HTTPSGroup{ - lns: make([]*HTTPSGroupListener, 0), - ctl: ctl, - acceptCh: make(chan net.Conn), + ctl: ctl, } } @@ -86,23 +67,27 @@ func (g *HTTPSGroup) Listen( ctx context.Context, group, groupKey string, routeConfig vhost.RouteConfig, -) (ln *HTTPSGroupListener, err error) { +) (ln *Listener, err error) { g.mu.Lock() defer g.mu.Unlock() + if !g.ctl.isCurrent(group, func(cur *HTTPSGroup) bool { return cur == g }) { + return nil, errGroupStale + } if len(g.lns) == 0 { // the first listener, listen on the real address httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig) if errRet != nil { return nil, errRet } - ln = newHTTPSGroupListener(group, g, httpsLn.Addr()) - g.group = group - g.groupKey = groupKey g.domain = routeConfig.Domain - g.httpsLn = httpsLn - g.lns = append(g.lns, ln) - go g.worker() + g.initBase(group, groupKey, httpsLn, func() { + g.ctl.removeIf(g.group, func(cur *HTTPSGroup) bool { + return cur == g + }) + }) + ln = g.newListener(httpsLn.Addr()) + go g.worker(httpsLn, g.acceptCh) } else { // route config in the same group must be equal if g.group != group || g.domain != routeConfig.Domain { @@ -111,87 +96,7 @@ func (g *HTTPSGroup) Listen( if g.groupKey != groupKey { return nil, ErrGroupAuthFailed } - ln = newHTTPSGroupListener(group, g, g.lns[0].Addr()) - g.lns = append(g.lns, ln) + ln = g.newListener(g.lns[0].Addr()) } return } - -func (g *HTTPSGroup) worker() { - for { - c, err := g.httpsLn.Accept() - if err != nil { - return - } - err = gerr.PanicToError(func() { - g.acceptCh <- c - }) - if err != nil { - return - } - } -} - -func (g *HTTPSGroup) Accept() <-chan net.Conn { - return g.acceptCh -} - -func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) { - g.mu.Lock() - defer g.mu.Unlock() - for i, tmpLn := range g.lns { - if tmpLn == ln { - g.lns = append(g.lns[:i], g.lns[i+1:]...) - break - } - } - if len(g.lns) == 0 { - close(g.acceptCh) - if g.httpsLn != nil { - g.httpsLn.Close() - } - g.ctl.RemoveGroup(g.group) - } -} - -type HTTPSGroupListener struct { - groupName string - group *HTTPSGroup - - addr net.Addr - closeCh chan struct{} -} - -func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener { - return &HTTPSGroupListener{ - groupName: name, - group: group, - addr: addr, - closeCh: make(chan struct{}), - } -} - -func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) { - var ok bool - select { - case <-ln.closeCh: - return nil, ErrListenerClosed - case c, ok = <-ln.group.Accept(): - if !ok { - return nil, ErrListenerClosed - } - return c, nil - } -} - -func (ln *HTTPSGroupListener) Addr() net.Addr { - return ln.addr -} - -func (ln *HTTPSGroupListener) Close() (err error) { - close(ln.closeCh) - - // remove self from HTTPSGroup - ln.group.CloseListener(ln) - return -} diff --git a/server/group/listener.go b/server/group/listener.go new file mode 100644 index 00000000..33c5c0df --- /dev/null +++ b/server/group/listener.go @@ -0,0 +1,49 @@ +package group + +import ( + "net" + "sync" +) + +// Listener is a per-proxy virtual listener that receives connections +// from a shared group. It implements net.Listener. +type Listener struct { + acceptCh <-chan net.Conn + addr net.Addr + closeCh chan struct{} + onClose func(*Listener) + once sync.Once +} + +func newListener(acceptCh <-chan net.Conn, addr net.Addr, onClose func(*Listener)) *Listener { + return &Listener{ + acceptCh: acceptCh, + addr: addr, + closeCh: make(chan struct{}), + onClose: onClose, + } +} + +func (ln *Listener) Accept() (net.Conn, error) { + select { + case <-ln.closeCh: + return nil, ErrListenerClosed + case c, ok := <-ln.acceptCh: + if !ok { + return nil, ErrListenerClosed + } + return c, nil + } +} + +func (ln *Listener) Addr() net.Addr { + return ln.addr +} + +func (ln *Listener) Close() error { + ln.once.Do(func() { + close(ln.closeCh) + ln.onClose(ln) + }) + return nil +} diff --git a/server/group/listener_test.go b/server/group/listener_test.go new file mode 100644 index 00000000..4e3e30e6 --- /dev/null +++ b/server/group/listener_test.go @@ -0,0 +1,68 @@ +package group + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListener_Accept(t *testing.T) { + acceptCh := make(chan net.Conn, 1) + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + c1, c2 := net.Pipe() + defer c1.Close() + defer c2.Close() + + acceptCh <- c1 + got, err := ln.Accept() + require.NoError(t, err) + assert.Equal(t, c1, got) +} + +func TestListener_AcceptAfterChannelClose(t *testing.T) { + acceptCh := make(chan net.Conn) + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + close(acceptCh) + _, err := ln.Accept() + assert.ErrorIs(t, err, ErrListenerClosed) +} + +func TestListener_AcceptAfterListenerClose(t *testing.T) { + acceptCh := make(chan net.Conn) // open, not closed + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + ln.Close() + _, err := ln.Accept() + assert.ErrorIs(t, err, ErrListenerClosed) +} + +func TestListener_DoubleClose(t *testing.T) { + closeCalls := 0 + ln := newListener( + make(chan net.Conn), + fakeAddr("127.0.0.1:1234"), + func(*Listener) { closeCalls++ }, + ) + + assert.NotPanics(t, func() { + ln.Close() + ln.Close() + }) + assert.Equal(t, 1, closeCalls, "onClose should be called exactly once") +} + +func TestListener_Addr(t *testing.T) { + addr := fakeAddr("10.0.0.1:5555") + ln := newListener(make(chan net.Conn), addr, func(*Listener) {}) + assert.Equal(t, addr, ln.Addr()) +} + +// fakeAddr implements net.Addr for testing. +type fakeAddr string + +func (a fakeAddr) Network() string { return "tcp" } +func (a fakeAddr) String() string { return string(a) } diff --git a/server/group/registry.go b/server/group/registry.go new file mode 100644 index 00000000..4064c535 --- /dev/null +++ b/server/group/registry.go @@ -0,0 +1,59 @@ +package group + +import ( + "sync" +) + +// groupRegistry is a concurrent map of named groups with +// automatic creation on first access. +type groupRegistry[G any] struct { + groups map[string]G + mu sync.Mutex +} + +func newGroupRegistry[G any]() groupRegistry[G] { + return groupRegistry[G]{ + groups: make(map[string]G), + } +} + +func (r *groupRegistry[G]) getOrCreate(key string, newFn func() G) G { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + if !ok { + g = newFn() + r.groups[key] = g + } + return g +} + +func (r *groupRegistry[G]) get(key string) (G, bool) { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + return g, ok +} + +// isCurrent returns true if key exists in the registry and matchFn +// returns true for the stored value. +func (r *groupRegistry[G]) isCurrent(key string, matchFn func(G) bool) bool { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + return ok && matchFn(g) +} + +// removeIf atomically looks up the group for key, calls fn on it, +// and removes the entry if fn returns true. +func (r *groupRegistry[G]) removeIf(key string, fn func(G) bool) { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + if !ok { + return + } + if fn(g) { + delete(r.groups, key) + } +} diff --git a/server/group/registry_test.go b/server/group/registry_test.go new file mode 100644 index 00000000..106d3998 --- /dev/null +++ b/server/group/registry_test.go @@ -0,0 +1,102 @@ +package group + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetOrCreate_New(t *testing.T) { + r := newGroupRegistry[*int]() + called := 0 + v := 42 + got := r.getOrCreate("k", func() *int { called++; return &v }) + assert.Equal(t, 1, called) + assert.Equal(t, &v, got) +} + +func TestGetOrCreate_Existing(t *testing.T) { + r := newGroupRegistry[*int]() + v := 42 + r.getOrCreate("k", func() *int { return &v }) + + called := 0 + got := r.getOrCreate("k", func() *int { called++; return nil }) + assert.Equal(t, 0, called) + assert.Equal(t, &v, got) +} + +func TestGet_ExistingAndMissing(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + + got, ok := r.get("k") + assert.True(t, ok) + assert.Equal(t, &v, got) + + _, ok = r.get("missing") + assert.False(t, ok) +} + +func TestIsCurrent(t *testing.T) { + r := newGroupRegistry[*int]() + v1 := 1 + v2 := 2 + r.getOrCreate("k", func() *int { return &v1 }) + + assert.True(t, r.isCurrent("k", func(g *int) bool { return g == &v1 })) + assert.False(t, r.isCurrent("k", func(g *int) bool { return g == &v2 })) + assert.False(t, r.isCurrent("missing", func(g *int) bool { return true })) +} + +func TestRemoveIf(t *testing.T) { + t.Run("removes when fn returns true", func(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + r.removeIf("k", func(g *int) bool { return g == &v }) + _, ok := r.get("k") + assert.False(t, ok) + }) + + t.Run("keeps when fn returns false", func(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + r.removeIf("k", func(g *int) bool { return false }) + _, ok := r.get("k") + assert.True(t, ok) + }) + + t.Run("noop on missing key", func(t *testing.T) { + r := newGroupRegistry[*int]() + r.removeIf("missing", func(g *int) bool { return true }) // should not panic + }) +} + +func TestConcurrentGetOrCreateAndRemoveIf(t *testing.T) { + r := newGroupRegistry[*int]() + const n = 100 + var wg sync.WaitGroup + wg.Add(n * 2) + for i := range n { + v := i + go func() { + defer wg.Done() + r.getOrCreate("k", func() *int { return &v }) + }() + go func() { + defer wg.Done() + r.removeIf("k", func(*int) bool { return true }) + }() + } + wg.Wait() + + // After all goroutines finish, accessing the key must not panic. + require.NotPanics(t, func() { + _, _ = r.get("k") + }) +} diff --git a/server/group/tcp.go b/server/group/tcp.go index f52d6407..d6bfbcff 100644 --- a/server/group/tcp.go +++ b/server/group/tcp.go @@ -17,83 +17,67 @@ package group import ( "net" "strconv" - "sync" - - gerr "github.com/fatedier/golib/errors" "github.com/fatedier/frp/server/ports" ) -// TCPGroupCtl manage all TCPGroups +// TCPGroupCtl manages all TCPGroups. type TCPGroupCtl struct { - groups map[string]*TCPGroup - - // portManager is used to manage port + groupRegistry[*TCPGroup] portManager *ports.Manager - mu sync.Mutex } -// NewTCPGroupCtl return a new TcpGroupCtl +// NewTCPGroupCtl returns a new TCPGroupCtl. func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl { return &TCPGroupCtl{ - groups: make(map[string]*TCPGroup), - portManager: portManager, + groupRegistry: newGroupRegistry[*TCPGroup](), + portManager: portManager, } } -// Listen is the wrapper for TCPGroup's Listen -// If there are no group, we will create one here +// Listen is the wrapper for TCPGroup's Listen. +// If there is no group, one will be created. func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string, addr string, port int, ) (l net.Listener, realPort int, err error) { - tgc.mu.Lock() - tcpGroup, ok := tgc.groups[group] - if !ok { - tcpGroup = NewTCPGroup(tgc) - tgc.groups[group] = tcpGroup + for { + tcpGroup := tgc.getOrCreate(group, func() *TCPGroup { + return NewTCPGroup(tgc) + }) + l, realPort, err = tcpGroup.Listen(proxyName, group, groupKey, addr, port) + if err == errGroupStale { + continue + } + return } - tgc.mu.Unlock() - - return tcpGroup.Listen(proxyName, group, groupKey, addr, port) } -// RemoveGroup remove TCPGroup from controller -func (tgc *TCPGroupCtl) RemoveGroup(group string) { - tgc.mu.Lock() - defer tgc.mu.Unlock() - delete(tgc.groups, group) -} - -// TCPGroup route connections to different proxies +// TCPGroup routes connections to different proxies. type TCPGroup struct { - group string - groupKey string + baseGroup + addr string port int realPort int - - acceptCh chan net.Conn - tcpLn net.Listener - lns []*TCPGroupListener ctl *TCPGroupCtl - mu sync.Mutex } -// NewTCPGroup return a new TCPGroup +// NewTCPGroup returns a new TCPGroup. func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup { return &TCPGroup{ - lns: make([]*TCPGroupListener, 0), - ctl: ctl, - acceptCh: make(chan net.Conn), + ctl: ctl, } } -// Listen will return a new TCPGroupListener -// if TCPGroup already has a listener, just add a new TCPGroupListener to the queues -// otherwise, listen on the real address -func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *TCPGroupListener, realPort int, err error) { +// Listen will return a new Listener. +// If TCPGroup already has a listener, just add a new Listener to the queues, +// otherwise listen on the real address. +func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *Listener, realPort int, err error) { tg.mu.Lock() defer tg.mu.Unlock() + if !tg.ctl.isCurrent(group, func(cur *TCPGroup) bool { return cur == tg }) { + return nil, 0, errGroupStale + } if len(tg.lns) == 0 { // the first listener, listen on the real address realPort, err = tg.ctl.portManager.Acquire(proxyName, port) @@ -106,19 +90,18 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr err = errRet return } - ln = newTCPGroupListener(group, tg, tcpLn.Addr()) - tg.group = group - tg.groupKey = groupKey tg.addr = addr tg.port = port tg.realPort = realPort - tg.tcpLn = tcpLn - tg.lns = append(tg.lns, ln) - if tg.acceptCh == nil { - tg.acceptCh = make(chan net.Conn) - } - go tg.worker() + tg.initBase(group, groupKey, tcpLn, func() { + tg.ctl.portManager.Release(tg.realPort) + tg.ctl.removeIf(tg.group, func(cur *TCPGroup) bool { + return cur == tg + }) + }) + ln = tg.newListener(tcpLn.Addr()) + go tg.worker(tcpLn, tg.acceptCh) } else { // address and port in the same group must be equal if tg.group != group || tg.addr != addr { @@ -133,92 +116,8 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr err = ErrGroupAuthFailed return } - ln = newTCPGroupListener(group, tg, tg.lns[0].Addr()) + ln = tg.newListener(tg.lns[0].Addr()) realPort = tg.realPort - tg.lns = append(tg.lns, ln) } return } - -// worker is called when the real tcp listener has been created -func (tg *TCPGroup) worker() { - for { - c, err := tg.tcpLn.Accept() - if err != nil { - return - } - err = gerr.PanicToError(func() { - tg.acceptCh <- c - }) - if err != nil { - return - } - } -} - -func (tg *TCPGroup) Accept() <-chan net.Conn { - return tg.acceptCh -} - -// CloseListener remove the TCPGroupListener from the TCPGroup -func (tg *TCPGroup) CloseListener(ln *TCPGroupListener) { - tg.mu.Lock() - defer tg.mu.Unlock() - for i, tmpLn := range tg.lns { - if tmpLn == ln { - tg.lns = append(tg.lns[:i], tg.lns[i+1:]...) - break - } - } - if len(tg.lns) == 0 { - close(tg.acceptCh) - tg.tcpLn.Close() - tg.ctl.portManager.Release(tg.realPort) - tg.ctl.RemoveGroup(tg.group) - } -} - -// TCPGroupListener -type TCPGroupListener struct { - groupName string - group *TCPGroup - - addr net.Addr - closeCh chan struct{} -} - -func newTCPGroupListener(name string, group *TCPGroup, addr net.Addr) *TCPGroupListener { - return &TCPGroupListener{ - groupName: name, - group: group, - addr: addr, - closeCh: make(chan struct{}), - } -} - -// Accept will accept connections from TCPGroup -func (ln *TCPGroupListener) Accept() (c net.Conn, err error) { - var ok bool - select { - case <-ln.closeCh: - return nil, ErrListenerClosed - case c, ok = <-ln.group.Accept(): - if !ok { - return nil, ErrListenerClosed - } - return c, nil - } -} - -func (ln *TCPGroupListener) Addr() net.Addr { - return ln.addr -} - -// Close close the listener -func (ln *TCPGroupListener) Close() (err error) { - close(ln.closeCh) - - // remove self from TcpGroup - ln.group.CloseListener(ln) - return -} diff --git a/server/group/tcpmux.go b/server/group/tcpmux.go index 1712bc74..e17a152a 100644 --- a/server/group/tcpmux.go +++ b/server/group/tcpmux.go @@ -18,118 +18,100 @@ import ( "context" "fmt" "net" - "sync" - - gerr "github.com/fatedier/golib/errors" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/vhost" ) -// TCPMuxGroupCtl manage all TCPMuxGroups +// TCPMuxGroupCtl manages all TCPMuxGroups. type TCPMuxGroupCtl struct { - groups map[string]*TCPMuxGroup - - // portManager is used to manage port + groupRegistry[*TCPMuxGroup] tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer - mu sync.Mutex } -// NewTCPMuxGroupCtl return a new TCPMuxGroupCtl +// NewTCPMuxGroupCtl returns a new TCPMuxGroupCtl. func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl { return &TCPMuxGroupCtl{ - groups: make(map[string]*TCPMuxGroup), + groupRegistry: newGroupRegistry[*TCPMuxGroup](), tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer, } } -// Listen is the wrapper for TCPMuxGroup's Listen -// If there are no group, we will create one here +// Listen is the wrapper for TCPMuxGroup's Listen. +// If there is no group, one will be created. func (tmgc *TCPMuxGroupCtl) Listen( ctx context.Context, multiplexer, group, groupKey string, routeConfig vhost.RouteConfig, ) (l net.Listener, err error) { - tmgc.mu.Lock() - tcpMuxGroup, ok := tmgc.groups[group] - if !ok { - tcpMuxGroup = NewTCPMuxGroup(tmgc) - tmgc.groups[group] = tcpMuxGroup - } - tmgc.mu.Unlock() + for { + tcpMuxGroup := tmgc.getOrCreate(group, func() *TCPMuxGroup { + return NewTCPMuxGroup(tmgc) + }) - switch v1.TCPMultiplexerType(multiplexer) { - case v1.TCPMultiplexerHTTPConnect: - return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) - default: - err = fmt.Errorf("unknown multiplexer [%s]", multiplexer) - return + switch v1.TCPMultiplexerType(multiplexer) { + case v1.TCPMultiplexerHTTPConnect: + l, err = tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return + default: + return nil, fmt.Errorf("unknown multiplexer [%s]", multiplexer) + } } } -// RemoveGroup remove TCPMuxGroup from controller -func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) { - tmgc.mu.Lock() - defer tmgc.mu.Unlock() - delete(tmgc.groups, group) -} - -// TCPMuxGroup route connections to different proxies +// TCPMuxGroup routes connections to different proxies. type TCPMuxGroup struct { - group string - groupKey string + baseGroup + domain string routeByHTTPUser string username string password string - - acceptCh chan net.Conn - tcpMuxLn net.Listener - lns []*TCPMuxGroupListener - ctl *TCPMuxGroupCtl - mu sync.Mutex + ctl *TCPMuxGroupCtl } -// NewTCPMuxGroup return a new TCPMuxGroup +// NewTCPMuxGroup returns a new TCPMuxGroup. func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup { return &TCPMuxGroup{ - lns: make([]*TCPMuxGroupListener, 0), - ctl: ctl, - acceptCh: make(chan net.Conn), + ctl: ctl, } } -// Listen will return a new TCPMuxGroupListener -// if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues -// otherwise, listen on the real address +// HTTPConnectListen will return a new Listener. +// If TCPMuxGroup already has a listener, just add a new Listener to the queues, +// otherwise listen on the real address. func (tmg *TCPMuxGroup) HTTPConnectListen( ctx context.Context, group, groupKey string, routeConfig vhost.RouteConfig, -) (ln *TCPMuxGroupListener, err error) { +) (ln *Listener, err error) { tmg.mu.Lock() defer tmg.mu.Unlock() + if !tmg.ctl.isCurrent(group, func(cur *TCPMuxGroup) bool { return cur == tmg }) { + return nil, errGroupStale + } if len(tmg.lns) == 0 { // the first listener, listen on the real address tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig) if errRet != nil { return nil, errRet } - ln = newTCPMuxGroupListener(group, tmg, tcpMuxLn.Addr()) - tmg.group = group - tmg.groupKey = groupKey tmg.domain = routeConfig.Domain tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser tmg.username = routeConfig.Username tmg.password = routeConfig.Password - tmg.tcpMuxLn = tcpMuxLn - tmg.lns = append(tmg.lns, ln) - if tmg.acceptCh == nil { - tmg.acceptCh = make(chan net.Conn) - } - go tmg.worker() + tmg.initBase(group, groupKey, tcpMuxLn, func() { + tmg.ctl.removeIf(tmg.group, func(cur *TCPMuxGroup) bool { + return cur == tmg + }) + }) + ln = tmg.newListener(tcpMuxLn.Addr()) + go tmg.worker(tcpMuxLn, tmg.acceptCh) } else { // route config in the same group must be equal if tmg.group != group || tmg.domain != routeConfig.Domain || @@ -141,90 +123,7 @@ func (tmg *TCPMuxGroup) HTTPConnectListen( if tmg.groupKey != groupKey { return nil, ErrGroupAuthFailed } - ln = newTCPMuxGroupListener(group, tmg, tmg.lns[0].Addr()) - tmg.lns = append(tmg.lns, ln) + ln = tmg.newListener(tmg.lns[0].Addr()) } return } - -// worker is called when the real TCP listener has been created -func (tmg *TCPMuxGroup) worker() { - for { - c, err := tmg.tcpMuxLn.Accept() - if err != nil { - return - } - err = gerr.PanicToError(func() { - tmg.acceptCh <- c - }) - if err != nil { - return - } - } -} - -func (tmg *TCPMuxGroup) Accept() <-chan net.Conn { - return tmg.acceptCh -} - -// CloseListener remove the TCPMuxGroupListener from the TCPMuxGroup -func (tmg *TCPMuxGroup) CloseListener(ln *TCPMuxGroupListener) { - tmg.mu.Lock() - defer tmg.mu.Unlock() - for i, tmpLn := range tmg.lns { - if tmpLn == ln { - tmg.lns = append(tmg.lns[:i], tmg.lns[i+1:]...) - break - } - } - if len(tmg.lns) == 0 { - close(tmg.acceptCh) - tmg.tcpMuxLn.Close() - tmg.ctl.RemoveGroup(tmg.group) - } -} - -// TCPMuxGroupListener -type TCPMuxGroupListener struct { - groupName string - group *TCPMuxGroup - - addr net.Addr - closeCh chan struct{} -} - -func newTCPMuxGroupListener(name string, group *TCPMuxGroup, addr net.Addr) *TCPMuxGroupListener { - return &TCPMuxGroupListener{ - groupName: name, - group: group, - addr: addr, - closeCh: make(chan struct{}), - } -} - -// Accept will accept connections from TCPMuxGroup -func (ln *TCPMuxGroupListener) Accept() (c net.Conn, err error) { - var ok bool - select { - case <-ln.closeCh: - return nil, ErrListenerClosed - case c, ok = <-ln.group.Accept(): - if !ok { - return nil, ErrListenerClosed - } - return c, nil - } -} - -func (ln *TCPMuxGroupListener) Addr() net.Addr { - return ln.addr -} - -// Close close the listener -func (ln *TCPMuxGroupListener) Close() (err error) { - close(ln.closeCh) - - // remove self from TcpMuxGroup - ln.group.CloseListener(ln) - return -} diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go index 850a932c..2a7891e7 100644 --- a/test/e2e/v1/features/group.go +++ b/test/e2e/v1/features/group.go @@ -186,6 +186,68 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) }) + + ginkgo.It("TCPMux httpconnect", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + tcpmuxHTTPConnectPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["tcpmux-group.example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["tcpmux-group.example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, barPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", vhostPort) + fooCount := 0 + barCount := 0 + for i := range 10 { + framework.NewRequestExpect(f). + Explain("times " + strconv.Itoa(i)). + RequestModify(func(r *request.Request) { + r.Addr("tcpmux-group.example.com").Proxy(proxyURL) + }). + Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) }) ginkgo.Describe("Health Check", func() { From bcd2424c24cd1e3c3d0641aaee98679ae7f2a6b3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Mar 2026 23:41:33 +0800 Subject: [PATCH 32/43] test/e2e: optimize e2e test time by replacing sleeps with TCP readiness checks (#5223) Replace the fixed 500ms sleep after each frps startup in RunProcesses with a TCP dial-based readiness check that polls the server bind port. This reduces the e2e suite wall time from ~97s to ~43s. Also simplify the RunProcesses API to accept a single server template string instead of a slice, matching how every call site uses it. --- test/e2e/examples.go | 2 +- test/e2e/framework/process.go | 100 ++++++++++++-------- test/e2e/legacy/basic/basic.go | 10 +- test/e2e/legacy/basic/client.go | 6 +- test/e2e/legacy/basic/client_server.go | 2 +- test/e2e/legacy/basic/config.go | 2 +- test/e2e/legacy/basic/http.go | 16 ++-- test/e2e/legacy/basic/server.go | 8 +- test/e2e/legacy/basic/tcpmux.go | 6 +- test/e2e/legacy/basic/xtcp.go | 2 +- test/e2e/legacy/features/bandwidth_limit.go | 4 +- test/e2e/legacy/features/group.go | 6 +- test/e2e/legacy/features/heartbeat.go | 2 +- test/e2e/legacy/features/monitor.go | 2 +- test/e2e/legacy/features/real_ip.go | 6 +- test/e2e/legacy/plugin/client.go | 14 +-- test/e2e/legacy/plugin/server.go | 16 ++-- test/e2e/pkg/process/process.go | 4 + test/e2e/v1/basic/annotations.go | 2 +- test/e2e/v1/basic/basic.go | 10 +- test/e2e/v1/basic/client.go | 6 +- test/e2e/v1/basic/client_server.go | 2 +- test/e2e/v1/basic/config.go | 8 +- test/e2e/v1/basic/http.go | 20 ++-- test/e2e/v1/basic/server.go | 8 +- test/e2e/v1/basic/tcpmux.go | 6 +- test/e2e/v1/basic/token_source.go | 44 ++++----- test/e2e/v1/basic/xtcp.go | 2 +- test/e2e/v1/features/bandwidth_limit.go | 4 +- test/e2e/v1/features/group.go | 10 +- test/e2e/v1/features/heartbeat.go | 2 +- test/e2e/v1/features/monitor.go | 2 +- test/e2e/v1/features/real_ip.go | 14 +-- test/e2e/v1/features/ssh_tunnel.go | 17 +++- test/e2e/v1/features/store.go | 14 +-- test/e2e/v1/plugin/client.go | 20 ++-- test/e2e/v1/plugin/server.go | 16 ++-- 37 files changed, 224 insertions(+), 191 deletions(-) diff --git a/test/e2e/examples.go b/test/e2e/examples.go index 135ce706..9f0bc335 100644 --- a/test/e2e/examples.go +++ b/test/e2e/examples.go @@ -26,7 +26,7 @@ var _ = ginkgo.Describe("[Feature: Example]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 702a9b87..6d21e3a2 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -3,67 +3,70 @@ package framework import ( "fmt" "maps" + "net" "os" "path/filepath" - "slices" + "strconv" "time" flog "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/process" ) -// RunProcesses run multiple processes from templates. -// The first template should always be frps. -func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) { - templates := slices.Concat(serverTemplates, clientTemplates) +// RunProcesses starts one frps and zero or more frpc processes from templates. +func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string) (*process.Process, []*process.Process) { + templates := append([]string{serverTemplate}, clientTemplates...) outs, ports, err := f.RenderTemplates(templates) ExpectNoError(err) - ExpectTrue(len(templates) > 0) maps.Copy(f.usedPorts, ports) - currentServerProcesses := make([]*process.Process, 0, len(serverTemplates)) - for i := range serverTemplates { - path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i)) - err = os.WriteFile(path, []byte(outs[i]), 0o600) - ExpectNoError(err) + // Start frps. + serverPath := filepath.Join(f.TempDirectory, "frp-e2e-server-0") + err = os.WriteFile(serverPath, []byte(outs[0]), 0o600) + ExpectNoError(err) - if TestContext.Debug { - flog.Debugf("[%s] %s", path, outs[i]) - } - - p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs) - f.serverConfPaths = append(f.serverConfPaths, path) - f.serverProcesses = append(f.serverProcesses, p) - currentServerProcesses = append(currentServerProcesses, p) - err = p.Start() - ExpectNoError(err) - time.Sleep(500 * time.Millisecond) + if TestContext.Debug { + flog.Debugf("[%s] %s", serverPath, outs[0]) } - time.Sleep(2 * time.Second) - currentClientProcesses := make([]*process.Process, 0, len(clientTemplates)) + serverProcess := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", serverPath}, f.osEnvs) + f.serverConfPaths = append(f.serverConfPaths, serverPath) + f.serverProcesses = append(f.serverProcesses, serverProcess) + err = serverProcess.Start() + ExpectNoError(err) + + if port, ok := ports[consts.PortServerName]; ok { + ExpectNoError(WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 5*time.Second)) + } else { + time.Sleep(2 * time.Second) + } + + // Start frpc(s). + clientProcesses := make([]*process.Process, 0, len(clientTemplates)) for i := range clientTemplates { - index := i + len(serverTemplates) path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i)) - err = os.WriteFile(path, []byte(outs[index]), 0o600) + err = os.WriteFile(path, []byte(outs[1+i]), 0o600) ExpectNoError(err) if TestContext.Debug { - flog.Debugf("[%s] %s", path, outs[index]) + flog.Debugf("[%s] %s", path, outs[1+i]) } p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs) f.clientConfPaths = append(f.clientConfPaths, path) f.clientProcesses = append(f.clientProcesses, p) - currentClientProcesses = append(currentClientProcesses, p) + clientProcesses = append(clientProcesses, p) err = p.Start() ExpectNoError(err) - time.Sleep(500 * time.Millisecond) } - time.Sleep(3 * time.Second) + // frpc needs time to connect and register proxies with frps. + if len(clientProcesses) > 0 { + time.Sleep(1500 * time.Millisecond) + } - return currentServerProcesses, currentClientProcesses + return serverProcess, clientProcesses } func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { @@ -71,11 +74,10 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { f.serverProcesses = append(f.serverProcesses, p) err := p.Start() if err != nil { - return p, p.StdOutput(), err + return p, p.Output(), err } - // Give frps extra time to finish binding ports before proceeding. - time.Sleep(4 * time.Second) - return p, p.StdOutput(), nil + time.Sleep(2 * time.Second) + return p, p.Output(), nil } func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { @@ -83,10 +85,10 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { f.clientProcesses = append(f.clientProcesses, p) err := p.Start() if err != nil { - return p, p.StdOutput(), err + return p, p.Output(), err } - time.Sleep(2 * time.Second) - return p, p.StdOutput(), nil + time.Sleep(1500 * time.Millisecond) + return p, p.Output(), nil } func (f *Framework) GenerateConfigFile(content string) string { @@ -96,3 +98,25 @@ func (f *Framework) GenerateConfigFile(content string) string { ExpectNoError(err) return path } + +// WaitForTCPReady polls a TCP address until a connection succeeds or timeout. +func WaitForTCPReady(addr string, timeout time.Duration) error { + if timeout <= 0 { + return fmt.Errorf("invalid timeout for TCP readiness on %s: timeout must be positive", addr) + } + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", addr, 100*time.Millisecond) + if err == nil { + conn.Close() + return nil + } + lastErr = err + time.Sleep(50 * time.Millisecond) + } + if lastErr == nil { + return fmt.Errorf("timeout waiting for TCP readiness on %s before any dial attempt", addr) + } + return fmt.Errorf("timeout waiting for TCP readiness on %s: %w", addr, lastErr) +} diff --git a/test/e2e/legacy/basic/basic.go b/test/e2e/legacy/basic/basic.go index d3cd61b1..0115c6cb 100644 --- a/test/e2e/legacy/basic/basic.go +++ b/test/e2e/legacy/basic/basic.go @@ -82,7 +82,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f). @@ -152,7 +152,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { for domain := range strings.SplitSeq(test.customDomains, ",") { @@ -235,7 +235,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -419,7 +419,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { } } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) + f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) for _, test := range tests { timeout := time.Second @@ -497,7 +497,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) // Request without HTTP connect should get error framework.NewRequestExpect(f). diff --git a/test/e2e/legacy/basic/client.go b/test/e2e/legacy/basic/client.go index daed8b22..d9b9b6da 100644 --- a/test/e2e/legacy/basic/client.go +++ b/test/e2e/legacy/basic/client.go @@ -48,7 +48,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.TCPEchoServerPort, p2Port, framework.TCPEchoServerPort, p3Port) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(p1Port).Ensure() framework.NewRequestExpect(f).Port(p2Port).Ensure() @@ -90,7 +90,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { admin_pwd = admin `, dashboardPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") @@ -116,7 +116,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { remote_port = %d `, adminPort, framework.TCPEchoServerPort, testPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(testPort).Ensure() diff --git a/test/e2e/legacy/basic/client_server.go b/test/e2e/legacy/basic/client_server.go index d85b5acc..330b9cfc 100644 --- a/test/e2e/legacy/basic/client_server.go +++ b/test/e2e/legacy/basic/client_server.go @@ -76,7 +76,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur clientConfs = append(clientConfs, client2Conf) } - f.RunProcesses([]string{serverConf}, clientConfs) + f.RunProcesses(serverConf, clientConfs) if configures.testDelay > 0 { time.Sleep(configures.testDelay) diff --git a/test/e2e/legacy/basic/config.go b/test/e2e/legacy/basic/config.go index 6e9f3564..334cc062 100644 --- a/test/e2e/legacy/basic/config.go +++ b/test/e2e/legacy/basic/config.go @@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() { `, "`", "`", framework.TCPEchoServerPort, portName) f.SetEnvs([]string{"FRP_TOKEN=123"}) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) diff --git a/test/e2e/legacy/basic/http.go b/test/e2e/legacy/basic/http.go index 77ba1052..f1807ecd 100644 --- a/test/e2e/legacy/basic/http.go +++ b/test/e2e/legacy/basic/http.go @@ -56,7 +56,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { locations = /bar `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tests := []struct { path string @@ -111,7 +111,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { custom_domains = normal.example.com `, fooPort, barPort, otherPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // user1 framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). @@ -152,7 +152,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { http_pwd = test `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -188,7 +188,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { custom_domains = *.example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not match host framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -238,7 +238,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { subdomain = bar `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // foo framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). @@ -279,7 +279,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { header_X-From-Where = frp `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -312,7 +312,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { host_header_rewrite = rewrite.example.com `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -360,7 +360,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { custom_domains = 127.0.0.1 `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) diff --git a/test/e2e/legacy/basic/server.go b/test/e2e/legacy/basic/server.go index 418910aa..a01f44aa 100644 --- a/test/e2e/legacy/basic/server.go +++ b/test/e2e/legacy/basic/server.go @@ -58,7 +58,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { remote_port = 11003 `, framework.UDPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // TCP // Allowed in range @@ -97,7 +97,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { local_port = {{ .%s }} `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) client := f.APIClientForFrpc(adminPort) @@ -138,7 +138,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { custom_domains = example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("example.com") @@ -165,7 +165,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { custom_domains = example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") diff --git a/test/e2e/legacy/basic/tcpmux.go b/test/e2e/legacy/basic/tcpmux.go index 15477837..3872ea04 100644 --- a/test/e2e/legacy/basic/tcpmux.go +++ b/test/e2e/legacy/basic/tcpmux.go @@ -76,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { custom_domains = normal.example.com `, fooPort, barPort, otherPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // user1 framework.NewRequestExpect(f).Explain("user1"). @@ -121,7 +121,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { http_pwd = test `, fooPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Explain("no auth"). @@ -204,7 +204,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { custom_domains = normal.example.com `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f). RequestModify(func(r *request.Request) { diff --git a/test/e2e/legacy/basic/xtcp.go b/test/e2e/legacy/basic/xtcp.go index 3c47f577..6e72f2e1 100644 --- a/test/e2e/legacy/basic/xtcp.go +++ b/test/e2e/legacy/basic/xtcp.go @@ -41,7 +41,7 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() { fallback_timeout_ms = 200 `, framework.TCPEchoServerPort, bindPortName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f). RequestModify(func(r *request.Request) { r.Timeout(time.Second) diff --git a/test/e2e/legacy/features/bandwidth_limit.go b/test/e2e/legacy/features/bandwidth_limit.go index c94e473f..a54866b8 100644 --- a/test/e2e/legacy/features/bandwidth_limit.go +++ b/test/e2e/legacy/features/bandwidth_limit.go @@ -35,7 +35,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { bandwidth_limit = 10KB `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) content := strings.Repeat("a", 50*1024) // 5KB start := time.Now() @@ -89,7 +89,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { remote_port = %d `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) content := strings.Repeat("a", 50*1024) // 5KB start := time.Now() diff --git a/test/e2e/legacy/features/group.go b/test/e2e/legacy/features/group.go index 28e5eeed..57c143c5 100644 --- a/test/e2e/legacy/features/group.go +++ b/test/e2e/legacy/features/group.go @@ -88,7 +88,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { group_key = 123 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) fooCount := 0 barCount := 0 @@ -144,7 +144,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { health_check_interval_s = 1 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // check foo and bar is ok results := []string{} @@ -213,7 +213,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { health_check_url = /healthz `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // send first HTTP request var contents []string diff --git a/test/e2e/legacy/features/heartbeat.go b/test/e2e/legacy/features/heartbeat.go index 09bf0e6a..3c1758f2 100644 --- a/test/e2e/legacy/features/heartbeat.go +++ b/test/e2e/legacy/features/heartbeat.go @@ -38,7 +38,7 @@ var _ = ginkgo.Describe("[Feature: Heartbeat]", func() { `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() diff --git a/test/e2e/legacy/features/monitor.go b/test/e2e/legacy/features/monitor.go index 75aa183b..0b19a85e 100644 --- a/test/e2e/legacy/features/monitor.go +++ b/test/e2e/legacy/features/monitor.go @@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() time.Sleep(500 * time.Millisecond) diff --git a/test/e2e/legacy/features/real_ip.go b/test/e2e/legacy/features/real_ip.go index a79afb45..dae74e56 100644 --- a/test/e2e/legacy/features/real_ip.go +++ b/test/e2e/legacy/features/real_ip.go @@ -44,7 +44,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { custom_domains = normal.example.com `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -90,7 +90,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { proxy_protocol_version = v2 `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) @@ -136,7 +136,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { proxy_protocol_version = v2 `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("normal.example.com") diff --git a/test/e2e/legacy/plugin/client.go b/test/e2e/legacy/plugin/client.go index c38cbaf7..ca294280 100644 --- a/test/e2e/legacy/plugin/client.go +++ b/test/e2e/legacy/plugin/client.go @@ -70,7 +70,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() @@ -92,7 +92,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_http_passwd = 123 `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { @@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_passwd = 123 `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { @@ -168,7 +168,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_http_passwd = 123 `, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // from tcp proxy framework.NewRequestExpect(f).Request( @@ -202,7 +202,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_local_addr = 127.0.0.1:%d `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -246,7 +246,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_key_path = %s `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -290,7 +290,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { plugin_key_path = %s `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) diff --git a/test/e2e/legacy/plugin/server.go b/test/e2e/legacy/plugin/server.go index 9120b7d7..b00b1bac 100644 --- a/test/e2e/legacy/plugin/server.go +++ b/test/e2e/legacy/plugin/server.go @@ -71,7 +71,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort2) - f.RunProcesses([]string{serverConf}, []string{clientConf, invalidTokenClientConf}) + f.RunProcesses(serverConf, []string{clientConf, invalidTokenClientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure() @@ -119,7 +119,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -153,7 +153,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = 0 `, framework.TCPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -195,7 +195,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - _, clients := f.RunProcesses([]string{serverConf}, []string{clientConf}) + _, clients := f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -250,7 +250,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -297,7 +297,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -342,7 +342,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -389,7 +389,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() diff --git a/test/e2e/pkg/process/process.go b/test/e2e/pkg/process/process.go index e6721e1d..04796225 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -61,6 +61,10 @@ func (p *Process) StdOutput() string { return p.stdOutput.String() } +func (p *Process) Output() string { + return p.stdOutput.String() + p.errorOutput.String() +} + func (p *Process) SetBeforeStopHandler(fn func()) { p.beforeStopHandler = fn } diff --git a/test/e2e/v1/basic/annotations.go b/test/e2e/v1/basic/annotations.go index 4f4a8a91..78e94a16 100644 --- a/test/e2e/v1/basic/annotations.go +++ b/test/e2e/v1/basic/annotations.go @@ -35,7 +35,7 @@ var _ = ginkgo.Describe("[Feature: Annotations]", func() { "frp.e2e.test/bar" = "value2" `, framework.TCPEchoServerPort, p1Port) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(p1Port).Ensure() diff --git a/test/e2e/v1/basic/basic.go b/test/e2e/v1/basic/basic.go index b9f4eded..8994ba73 100644 --- a/test/e2e/v1/basic/basic.go +++ b/test/e2e/v1/basic/basic.go @@ -83,7 +83,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f). @@ -154,7 +154,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { for domain := range strings.SplitSeq(test.customDomains, ",") { @@ -240,7 +240,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -426,7 +426,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { } } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) + f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) for _, test := range tests { timeout := time.Second @@ -505,7 +505,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) // Request without HTTP connect should get error framework.NewRequestExpect(f). diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go index fd269d75..ec2011ea 100644 --- a/test/e2e/v1/basic/client.go +++ b/test/e2e/v1/basic/client.go @@ -51,7 +51,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.TCPEchoServerPort, p2Port, framework.TCPEchoServerPort, p3Port) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(p1Port).Ensure() framework.NewRequestExpect(f).Port(p2Port).Ensure() @@ -93,7 +93,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { webServer.password = "admin" `, dashboardPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") @@ -120,7 +120,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { remotePort = %d `, adminPort, framework.TCPEchoServerPort, testPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(testPort).Ensure() diff --git a/test/e2e/v1/basic/client_server.go b/test/e2e/v1/basic/client_server.go index 85dd1227..3a2fa28d 100644 --- a/test/e2e/v1/basic/client_server.go +++ b/test/e2e/v1/basic/client_server.go @@ -78,7 +78,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur clientConfs = append(clientConfs, client2Conf) } - f.RunProcesses([]string{serverConf}, clientConfs) + f.RunProcesses(serverConf, clientConfs) if configures.testDelay > 0 { time.Sleep(configures.testDelay) diff --git a/test/e2e/v1/basic/config.go b/test/e2e/v1/basic/config.go index 314e7fc4..e290109e 100644 --- a/test/e2e/v1/basic/config.go +++ b/test/e2e/v1/basic/config.go @@ -35,7 +35,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() { `, "`", "`", framework.TCPEchoServerPort, portName) f.SetEnvs([]string{"FRP_TOKEN=123"}) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) @@ -69,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() { escapeTemplate("{{- end }}"), ) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) client := f.APIClientForFrpc(adminPort) checkProxyFn := func(name string, localPort, remotePort int) { @@ -149,7 +149,7 @@ proxies: remotePort: %d `, port.GenName("Server"), framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -161,7 +161,7 @@ proxies: "proxies": [{"name": "tcp", "type": "tcp", "localPort": {{ .%s }}, "remotePort": %d}]}`, port.GenName("Server"), framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) }) diff --git a/test/e2e/v1/basic/http.go b/test/e2e/v1/basic/http.go index c37e84e7..b594f1ea 100644 --- a/test/e2e/v1/basic/http.go +++ b/test/e2e/v1/basic/http.go @@ -59,7 +59,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { locations = ["/bar"] `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tests := []struct { path string @@ -117,7 +117,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { customDomains = ["normal.example.com"] `, fooPort, barPort, otherPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // user1 framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). @@ -159,7 +159,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { httpPassword = "test" `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -196,7 +196,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { customDomains = ["*.example.com"] `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not match host framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -248,7 +248,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { subdomain = "bar" `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // foo framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). @@ -290,7 +290,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { requestHeaders.set.x-from-where = "frp" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -323,7 +323,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { responseHeaders.set.x-from-where = "frp" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -357,7 +357,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { hostHeaderRewrite = "rewrite.example.com" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -406,7 +406,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { customDomains = ["127.0.0.1"] `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) @@ -447,7 +447,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { customDomains = ["normal.example.com"] `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go index 274102a2..f2714abb 100644 --- a/test/e2e/v1/basic/server.go +++ b/test/e2e/v1/basic/server.go @@ -67,7 +67,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { remotePort = 11003 `, framework.UDPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // TCP // Allowed in range @@ -108,7 +108,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { localPort = {{ .%s }} `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) client := f.APIClientForFrpc(adminPort) @@ -150,7 +150,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { customDomains = ["example.com"] `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("example.com") @@ -178,7 +178,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { customDomains = ["example.com"] `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") diff --git a/test/e2e/v1/basic/tcpmux.go b/test/e2e/v1/basic/tcpmux.go index 7ee58a79..9b839faa 100644 --- a/test/e2e/v1/basic/tcpmux.go +++ b/test/e2e/v1/basic/tcpmux.go @@ -79,7 +79,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { customDomains = ["normal.example.com"] `, fooPort, barPort, otherPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // user1 framework.NewRequestExpect(f).Explain("user1"). @@ -125,7 +125,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { httpPassword = "test" `, fooPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Explain("no auth"). @@ -209,7 +209,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { customDomains = ["normal.example.com"] `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f). RequestModify(func(r *request.Request) { diff --git a/test/e2e/v1/basic/token_source.go b/test/e2e/v1/basic/token_source.go index 4ed7051c..d12dd4de 100644 --- a/test/e2e/v1/basic/token_source.go +++ b/test/e2e/v1/basic/token_source.go @@ -16,8 +16,11 @@ package basic import ( "fmt" + "net" "os" "path/filepath" + "strconv" + "time" "github.com/onsi/ginkgo/v2" @@ -73,7 +76,7 @@ localPort = {{ .%s }} remotePort = {{ .%s }} `, tokenContent, framework.TCPEchoServerPort, portName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) @@ -109,7 +112,7 @@ localPort = {{ .%s }} remotePort = {{ .%s }} `, tokenFile, framework.TCPEchoServerPort, portName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) @@ -150,7 +153,7 @@ localPort = {{ .%s }} remotePort = {{ .%s }} `, clientTokenFile, framework.TCPEchoServerPort, portName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) @@ -190,7 +193,7 @@ localPort = {{ .%s }} remotePort = {{ .%s }} `, clientTokenFile, framework.TCPEchoServerPort, portName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // This should fail due to token mismatch - the client should not be able to connect // We expect the request to fail because the proxy tunnel is not established @@ -198,32 +201,27 @@ remotePort = {{ .%s }} }) ginkgo.It("should fail with non-existent token file", func() { - // This test verifies that server fails to start when tokenSource points to non-existent file - // We'll verify this by checking that the configuration loading itself fails - - // Create a config that references a non-existent file tmpDir := f.TempDirectory nonExistentFile := filepath.Join(tmpDir, "non_existent_token") - serverConf := consts.DefaultServerConfig - - // Server config with non-existent tokenSource file - serverConf += fmt.Sprintf(` + serverPort := f.AllocPort() + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d auth.tokenSource.type = "file" auth.tokenSource.file.path = "%s" -`, nonExistentFile) +`, serverPort, nonExistentFile) - // The test expectation is that this will fail during the RunProcesses call - // because the server cannot load the configuration due to missing token file - defer func() { - if r := recover(); r != nil { - // Expected: server should fail to start due to missing file - ginkgo.By(fmt.Sprintf("Server correctly failed to start: %v", r)) - } - }() + serverConfigPath := f.GenerateConfigFile(serverConf) - // This should cause a panic or error during server startup - f.RunProcesses([]string{serverConf}, []string{}) + _, _, _ = f.RunFrps("-c", serverConfigPath) + + // Server should have failed to start, so the port should not be listening. + conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(serverPort)), 1*time.Second) + if err == nil { + conn.Close() + } + framework.ExpectTrue(err != nil, "server should not be listening on port %d", serverPort) }) }) diff --git a/test/e2e/v1/basic/xtcp.go b/test/e2e/v1/basic/xtcp.go index a5aaf67b..57905f0d 100644 --- a/test/e2e/v1/basic/xtcp.go +++ b/test/e2e/v1/basic/xtcp.go @@ -42,7 +42,7 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() { fallbackTimeoutMs = 200 `, framework.TCPEchoServerPort, bindPortName) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f). RequestModify(func(r *request.Request) { r.Timeout(time.Second) diff --git a/test/e2e/v1/features/bandwidth_limit.go b/test/e2e/v1/features/bandwidth_limit.go index efcf38ed..11503adb 100644 --- a/test/e2e/v1/features/bandwidth_limit.go +++ b/test/e2e/v1/features/bandwidth_limit.go @@ -36,7 +36,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { transport.bandwidthLimit = "10KB" `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) content := strings.Repeat("a", 50*1024) // 5KB start := time.Now() @@ -92,7 +92,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { remotePort = %d `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) content := strings.Repeat("a", 50*1024) // 5KB start := time.Now() diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go index 2a7891e7..610903d0 100644 --- a/test/e2e/v1/features/group.go +++ b/test/e2e/v1/features/group.go @@ -92,7 +92,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { loadBalancer.groupKey = "123" `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) fooCount := 0 barCount := 0 @@ -157,7 +157,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { loadBalancer.groupKey = "123" `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) fooCount := 0 barCount := 0 @@ -222,7 +222,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { loadBalancer.groupKey = "123" `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) proxyURL := fmt.Sprintf("http://127.0.0.1:%d", vhostPort) fooCount := 0 @@ -286,7 +286,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.intervalSeconds = 1 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // check foo and bar is ok results := []string{} @@ -357,7 +357,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.path = "/healthz" `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // send first HTTP request var contents []string diff --git a/test/e2e/v1/features/heartbeat.go b/test/e2e/v1/features/heartbeat.go index 4f5409fe..08a1b5d9 100644 --- a/test/e2e/v1/features/heartbeat.go +++ b/test/e2e/v1/features/heartbeat.go @@ -37,7 +37,7 @@ var _ = ginkgo.Describe("[Feature: Heartbeat]", func() { `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() diff --git a/test/e2e/v1/features/monitor.go b/test/e2e/v1/features/monitor.go index fe92203a..f70dce16 100644 --- a/test/e2e/v1/features/monitor.go +++ b/test/e2e/v1/features/monitor.go @@ -34,7 +34,7 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() time.Sleep(500 * time.Millisecond) diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go index a52cf0a2..6bebfc2b 100644 --- a/test/e2e/v1/features/real_ip.go +++ b/test/e2e/v1/features/real_ip.go @@ -48,7 +48,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { customDomains = ["normal.example.com"] `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -82,7 +82,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { customDomains = ["normal.example.com"] `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -112,7 +112,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { localAddr = "127.0.0.1:%d" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -154,7 +154,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { localAddr = "127.0.0.1:%d" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -212,7 +212,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { transport.proxyProtocolVersion = "v2" `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) @@ -262,7 +262,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { transport.proxyProtocolVersion = "v2" `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Protocol("udp").Port(remotePort).Ensure(func(resp *request.Response) bool { log.Tracef("udp proxy protocol get SourceAddr: %s", string(resp.Content)) @@ -309,7 +309,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { transport.proxyProtocolVersion = "v2" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("normal.example.com") diff --git a/test/e2e/v1/features/ssh_tunnel.go b/test/e2e/v1/features/ssh_tunnel.go index 0b8725f6..c120eab0 100644 --- a/test/e2e/v1/features/ssh_tunnel.go +++ b/test/e2e/v1/features/ssh_tunnel.go @@ -3,6 +3,8 @@ package features import ( "crypto/tls" "fmt" + "net" + "strconv" "time" "github.com/onsi/ginkgo/v2" @@ -25,7 +27,8 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { sshTunnelGateway.bindPort = %d `, sshPort) - f.RunProcesses([]string{serverConf}, nil) + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) localPort := f.PortByName(framework.TCPEchoServerPort) remotePort := f.AllocPort() @@ -49,7 +52,8 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { sshTunnelGateway.bindPort = %d `, vhostPort, sshPort) - f.RunProcesses([]string{serverConf}, nil) + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) localPort := f.PortByName(framework.HTTPSimpleServerPort) tc := ssh.NewTunnelClient( @@ -76,7 +80,8 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { sshTunnelGateway.bindPort = %d `, vhostPort, sshPort) - f.RunProcesses([]string{serverConf}, nil) + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) localPort := f.AllocPort() testDomain := "test.example.com" @@ -118,7 +123,8 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { sshTunnelGateway.bindPort = %d `, tcpmuxPort, sshPort) - f.RunProcesses([]string{serverConf}, nil) + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) localPort := f.AllocPort() testDomain := "test.example.com" @@ -173,7 +179,8 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { bindPort = %d `, bindPort) - f.RunProcesses([]string{serverConf}, []string{visitorConf}) + f.RunProcesses(serverConf, []string{visitorConf}) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) localPort := f.PortByName(framework.TCPEchoServerPort) tc := ssh.NewTunnelClient( diff --git a/test/e2e/v1/features/store.go b/test/e2e/v1/features/store.go index 284c85f4..3fc6bb14 100644 --- a/test/e2e/v1/features/store.go +++ b/test/e2e/v1/features/store.go @@ -30,7 +30,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ @@ -71,7 +71,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ @@ -125,7 +125,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ @@ -173,7 +173,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) proxyConfig := map[string]any{ @@ -225,7 +225,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { webServer.port = %d `, adminPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { @@ -247,7 +247,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) invalidBody, _ := json.Marshal(map[string]any{ @@ -280,7 +280,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { path = "%s/store.json" `, adminPort, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) time.Sleep(500 * time.Millisecond) createBody, _ := json.Marshal(map[string]any{ diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go index 4cb9d7d4..1ea25a6e 100644 --- a/test/e2e/v1/plugin/client.go +++ b/test/e2e/v1/plugin/client.go @@ -74,7 +74,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf.String()}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() @@ -98,7 +98,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { httpPassword = "123" `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { @@ -132,7 +132,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { password = "123" `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { @@ -182,7 +182,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { httpPassword = "123" `, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // from tcp proxy framework.NewRequestExpect(f).Request( @@ -218,7 +218,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { localAddr = "127.0.0.1:%d" `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -264,7 +264,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { keyPath = "%s" `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -310,7 +310,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { keyPath = "%s" `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) @@ -350,7 +350,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { hostHeaderRewrite = "rewrite.test.com" `, remotePort, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -385,7 +385,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { requestHeaders.set.x-from-where = "frp" `, remotePort, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -431,7 +431,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { keyPath = "%s" `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go index 6637650d..fd684ef5 100644 --- a/test/e2e/v1/plugin/server.go +++ b/test/e2e/v1/plugin/server.go @@ -74,7 +74,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort2) - f.RunProcesses([]string{serverConf}, []string{clientConf, invalidTokenClientConf}) + f.RunProcesses(serverConf, []string{clientConf, invalidTokenClientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure() @@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -160,7 +160,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = 0 `, framework.TCPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -204,7 +204,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - _, clients := f.RunProcesses([]string{serverConf}, []string{clientConf}) + _, clients := f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -261,7 +261,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -310,7 +310,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -357,7 +357,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -406,7 +406,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() From 48e890146698fcd3c9e0dd680202ec954032ed07 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 9 Mar 2026 10:28:47 +0800 Subject: [PATCH 33/43] test/e2e: optimize RunFrps/RunFrpc with process exit detection (#5225) * test/e2e: optimize RunFrps/RunFrpc with process exit detection Refactor Process to track subprocess lifecycle via a done channel, replacing direct cmd.Wait() in Stop() to avoid double-Wait races. RunFrps/RunFrpc now use select on the done channel instead of fixed sleeps, allowing short-lived processes (verify, startup failures) to return immediately while preserving existing timeout behavior for long-running daemons. * test/e2e: guard Process against double-Start and Stop-before-Start Add started flag to prevent double-Start panics and allow Stop to return immediately when the process was never started. Use sync.Once for closing the done channel as defense-in-depth against double close. --- test/e2e/framework/process.go | 10 +++++++-- test/e2e/pkg/process/process.go | 39 ++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 6d21e3a2..e2c48427 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -76,7 +76,10 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { if err != nil { return p, p.Output(), err } - time.Sleep(2 * time.Second) + select { + case <-p.Done(): + case <-time.After(2 * time.Second): + } return p, p.Output(), nil } @@ -87,7 +90,10 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { if err != nil { return p, p.Output(), err } - time.Sleep(1500 * time.Millisecond) + select { + case <-p.Done(): + case <-time.After(1500 * time.Millisecond): + } return p, p.Output(), nil } diff --git a/test/e2e/pkg/process/process.go b/test/e2e/pkg/process/process.go index 04796225..dba006c1 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -3,7 +3,9 @@ package process import ( "bytes" "context" + "errors" "os/exec" + "sync" ) type Process struct { @@ -12,6 +14,11 @@ type Process struct { errorOutput *bytes.Buffer stdOutput *bytes.Buffer + done chan struct{} + closeOne sync.Once + waitErr error + + started bool beforeStopHandler func() stopped bool } @@ -27,6 +34,7 @@ func NewWithEnvs(path string, params []string, envs []string) *Process { p := &Process{ cmd: cmd, cancel: cancel, + done: make(chan struct{}), } p.errorOutput = bytes.NewBufferString("") p.stdOutput = bytes.NewBufferString("") @@ -36,11 +44,35 @@ func NewWithEnvs(path string, params []string, envs []string) *Process { } func (p *Process) Start() error { - return p.cmd.Start() + if p.started { + return errors.New("process already started") + } + p.started = true + + err := p.cmd.Start() + if err != nil { + p.waitErr = err + p.closeDone() + return err + } + go func() { + p.waitErr = p.cmd.Wait() + p.closeDone() + }() + return nil +} + +func (p *Process) closeDone() { + p.closeOne.Do(func() { close(p.done) }) +} + +// Done returns a channel that is closed when the process exits. +func (p *Process) Done() <-chan struct{} { + return p.done } func (p *Process) Stop() error { - if p.stopped { + if p.stopped || !p.started { return nil } defer func() { @@ -50,7 +82,8 @@ func (p *Process) Stop() error { p.beforeStopHandler() } p.cancel() - return p.cmd.Wait() + <-p.done + return p.waitErr } func (p *Process) ErrorOutput() string { From 9669e1ca0c2ea20030cc2b2604fd73dc397ba406 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 9 Mar 2026 22:28:23 +0800 Subject: [PATCH 34/43] test/e2e: replace RunProcesses client sleep with log-based proxy readiness detection (#5226) * test/e2e: replace RunProcesses client sleep with log-based proxy readiness detection Replace the fixed 1500ms sleep in RunProcesses with event-driven proxy registration detection by monitoring frpc log output for "start proxy success" messages. Key changes: - Add thread-safe SafeBuffer to replace bytes.Buffer in Process, enabling concurrent read/write of process output during execution - Add Process.WaitForOutput() to poll process output for pattern matches with timeout and early exit on process termination - Add waitForClientProxyReady() that uses config.LoadClientConfig() to extract proxy names, then waits for each proxy's success log - For visitor-only clients (no deterministic readiness signal), fall back to the original sleep with elapsed time deducted * test/e2e: use shared deadline for proxy readiness and fix doc comment - Use a single deadline in waitForClientProxyReady so total wait across all proxies does not exceed the given timeout - Fix WaitForOutput doc comment to accurately describe single pattern with count semantics --- test/e2e/framework/process.go | 45 ++++++++++++++++++++++++++-- test/e2e/pkg/process/process.go | 53 ++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index e2c48427..cca93b1d 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/fatedier/frp/pkg/config" flog "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/process" @@ -61,9 +62,22 @@ func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string err = p.Start() ExpectNoError(err) } - // frpc needs time to connect and register proxies with frps. - if len(clientProcesses) > 0 { - time.Sleep(1500 * time.Millisecond) + // Wait for each client's proxies to register with frps. + // If any client has no proxies (e.g. visitor-only), fall back to sleep + // for the remaining time since visitors have no deterministic readiness signal. + allConfirmed := len(clientProcesses) > 0 + start := time.Now() + for i, p := range clientProcesses { + configPath := f.clientConfPaths[len(f.clientConfPaths)-len(clientProcesses)+i] + if !waitForClientProxyReady(configPath, p, 5*time.Second) { + allConfirmed = false + } + } + if len(clientProcesses) > 0 && !allConfirmed { + remaining := 1500*time.Millisecond - time.Since(start) + if remaining > 0 { + time.Sleep(remaining) + } } return serverProcess, clientProcesses @@ -105,6 +119,31 @@ func (f *Framework) GenerateConfigFile(content string) string { return path } +// waitForClientProxyReady parses the client config to extract proxy names, +// then waits for each proxy's "start proxy success" log in the process output. +// Returns true only if proxies were expected and all registered successfully. +func waitForClientProxyReady(configPath string, p *process.Process, timeout time.Duration) bool { + _, proxyCfgs, _, _, err := config.LoadClientConfig(configPath, false) + if err != nil || len(proxyCfgs) == 0 { + return false + } + + // Use a single deadline so the total wait across all proxies does not exceed timeout. + deadline := time.Now().Add(timeout) + for _, cfg := range proxyCfgs { + remaining := time.Until(deadline) + if remaining <= 0 { + return false + } + name := cfg.GetBaseConfig().Name + pattern := fmt.Sprintf("[%s] start proxy success", name) + if err := p.WaitForOutput(pattern, 1, remaining); err != nil { + return false + } + } + return true +} + // WaitForTCPReady polls a TCP address until a connection succeeds or timeout. func WaitForTCPReady(addr string, timeout time.Duration) error { if timeout <= 0 { diff --git a/test/e2e/pkg/process/process.go b/test/e2e/pkg/process/process.go index dba006c1..6f4e3e39 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -4,15 +4,37 @@ import ( "bytes" "context" "errors" + "fmt" "os/exec" + "strings" "sync" + "time" ) +// SafeBuffer is a thread-safe wrapper around bytes.Buffer. +// It is safe to call Write and String concurrently. +type SafeBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *SafeBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *SafeBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + type Process struct { cmd *exec.Cmd cancel context.CancelFunc - errorOutput *bytes.Buffer - stdOutput *bytes.Buffer + errorOutput *SafeBuffer + stdOutput *SafeBuffer done chan struct{} closeOne sync.Once @@ -36,8 +58,8 @@ func NewWithEnvs(path string, params []string, envs []string) *Process { cancel: cancel, done: make(chan struct{}), } - p.errorOutput = bytes.NewBufferString("") - p.stdOutput = bytes.NewBufferString("") + p.errorOutput = &SafeBuffer{} + p.stdOutput = &SafeBuffer{} cmd.Stderr = p.errorOutput cmd.Stdout = p.stdOutput return p @@ -101,3 +123,26 @@ func (p *Process) Output() string { func (p *Process) SetBeforeStopHandler(fn func()) { p.beforeStopHandler = fn } + +// WaitForOutput polls the combined process output until the pattern is found +// count time(s) or the timeout is reached. It also returns early if the process exits. +func (p *Process) WaitForOutput(pattern string, count int, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + output := p.Output() + if strings.Count(output, pattern) >= count { + return nil + } + select { + case <-p.Done(): + // Process exited, check one last time. + output = p.Output() + if strings.Count(output, pattern) >= count { + return nil + } + return fmt.Errorf("process exited before %d occurrence(s) of %q found", count, pattern) + case <-time.After(25 * time.Millisecond): + } + } + return fmt.Errorf("timeout waiting for %d occurrence(s) of %q", count, pattern) +} From 4f584f81d0af1a3cc5e5061c59d62a3ef136f7fe Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 12 Mar 2026 00:11:09 +0800 Subject: [PATCH 35/43] test/e2e: replace sleeps with event-driven waits in chaos/group/store tests (#5231) * test/e2e: replace sleeps with event-driven waits in chaos/group/store tests Replace 21 time.Sleep calls with deterministic waiting using WaitForOutput, WaitForTCPReady, and a new WaitForTCPUnreachable helper. Add CountOutput method for snapshot-based incremental log matching. * test/e2e: validate interval and cap dial/sleep to remaining deadline in WaitForTCPUnreachable --- test/e2e/framework/process.go | 24 ++++++++++++++++++++++++ test/e2e/pkg/process/process.go | 5 +++++ test/e2e/v1/features/chaos.go | 10 +++++----- test/e2e/v1/features/group.go | 16 ++++++++++------ test/e2e/v1/features/store.go | 27 +++++++++++++-------------- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index cca93b1d..f5faa637 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -144,6 +144,30 @@ func waitForClientProxyReady(configPath string, p *process.Process, timeout time return true } +// WaitForTCPUnreachable polls a TCP address until a connection fails or timeout. +func WaitForTCPUnreachable(addr string, interval, timeout time.Duration) error { + if interval <= 0 { + return fmt.Errorf("invalid interval for TCP unreachable on %s: interval must be positive", addr) + } + if timeout <= 0 { + return fmt.Errorf("invalid timeout for TCP unreachable on %s: timeout must be positive", addr) + } + deadline := time.Now().Add(timeout) + for { + remaining := time.Until(deadline) + if remaining <= 0 { + return fmt.Errorf("timeout waiting for TCP unreachable on %s", addr) + } + dialTimeout := min(interval, remaining) + conn, err := net.DialTimeout("tcp", addr, dialTimeout) + if err != nil { + return nil + } + conn.Close() + time.Sleep(min(interval, time.Until(deadline))) + } +} + // WaitForTCPReady polls a TCP address until a connection succeeds or timeout. func WaitForTCPReady(addr string, timeout time.Duration) error { if timeout <= 0 { diff --git a/test/e2e/pkg/process/process.go b/test/e2e/pkg/process/process.go index 6f4e3e39..8235461b 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -120,6 +120,11 @@ func (p *Process) Output() string { return p.stdOutput.String() + p.errorOutput.String() } +// CountOutput returns how many times pattern appears in the current accumulated output. +func (p *Process) CountOutput(pattern string) int { + return strings.Count(p.Output(), pattern) +} + func (p *Process) SetBeforeStopHandler(fn func()) { p.beforeStopHandler = fn } diff --git a/test/e2e/v1/features/chaos.go b/test/e2e/v1/features/chaos.go index f5f8b388..8c515251 100644 --- a/test/e2e/v1/features/chaos.go +++ b/test/e2e/v1/features/chaos.go @@ -41,24 +41,24 @@ var _ = ginkgo.Describe("[Feature: Chaos]", func() { // 2. stop frps, expect request failed _ = ps.Stop() - time.Sleep(200 * time.Millisecond) framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() // 3. restart frps, expect request success + successCount := pc.CountOutput("[tcp] start proxy success") _, _, err = f.RunFrps("-c", serverConfigPath) framework.ExpectNoError(err) - time.Sleep(2 * time.Second) + framework.ExpectNoError(pc.WaitForOutput("[tcp] start proxy success", successCount+1, 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).Ensure() // 4. stop frpc, expect request failed _ = pc.Stop() - time.Sleep(200 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort), 100*time.Millisecond, 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() // 5. restart frpc, expect request success - _, _, err = f.RunFrpc("-c", clientConfigPath) + newPc, _, err := f.RunFrpc("-c", clientConfigPath) framework.ExpectNoError(err) - time.Sleep(time.Second) + framework.ExpectNoError(newPc.WaitForOutput("[tcp] start proxy success", 1, 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) }) diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go index 610903d0..85b938a9 100644 --- a/test/e2e/v1/features/group.go +++ b/test/e2e/v1/features/group.go @@ -286,7 +286,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.intervalSeconds = 1 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses(serverConf, []string{clientConf}) + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) // check foo and bar is ok results := []string{} @@ -299,15 +299,17 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { framework.ExpectContainElements(results, []string{"foo", "bar"}) // close bar server, check foo is ok + failedCount := clientProcesses[0].CountOutput("[bar] health check failed") barServer.Close() - time.Sleep(2 * time.Second) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check failed", failedCount+1, 5*time.Second)) for range 10 { framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() } // resume bar server, check foo and bar is ok + successCount := clientProcesses[0].CountOutput("[bar] health check success") f.RunServer("", barServer) - time.Sleep(2 * time.Second) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check success", successCount+1, 5*time.Second)) results = []string{} for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { @@ -357,7 +359,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.path = "/healthz" `, fooPort, barPort) - f.RunProcesses(serverConf, []string{clientConf}) + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) // send first HTTP request var contents []string @@ -387,15 +389,17 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { framework.ExpectContainElements(results, []string{"foo", "bar"}) // close bar server, check foo is ok + failedCount := clientProcesses[0].CountOutput("[bar] health check failed") barServer.Close() - time.Sleep(2 * time.Second) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check failed", failedCount+1, 5*time.Second)) results = doFooBarHTTPRequest(vhostPort, "example.com") framework.ExpectContainElements(results, []string{"foo"}) framework.ExpectNotContainElements(results, []string{"bar"}) // resume bar server, check foo and bar is ok + successCount := clientProcesses[0].CountOutput("[bar] health check success") f.RunServer("", barServer) - time.Sleep(2 * time.Second) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check success", successCount+1, 5*time.Second)) results = doFooBarHTTPRequest(vhostPort, "example.com") framework.ExpectContainElements(results, []string{"foo", "bar"}) }) diff --git a/test/e2e/v1/features/store.go b/test/e2e/v1/features/store.go index 3fc6bb14..76a9b05a 100644 --- a/test/e2e/v1/features/store.go +++ b/test/e2e/v1/features/store.go @@ -31,7 +31,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) proxyConfig := map[string]any{ "name": "test-tcp", @@ -52,7 +52,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(time.Second) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort), 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) @@ -72,7 +72,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) proxyConfig := map[string]any{ "name": "test-tcp", @@ -93,7 +93,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(time.Second) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort1), 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort1).Ensure() proxyConfig["tcp"].(map[string]any)["remotePort"] = remotePort2 @@ -107,7 +107,8 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(time.Second) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort2), 5*time.Second)) + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort1), 100*time.Millisecond, 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort2).Ensure() framework.NewRequestExpect(f).Port(remotePort1).ExpectError(true).Ensure() }) @@ -126,7 +127,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) proxyConfig := map[string]any{ "name": "test-tcp", @@ -147,7 +148,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(time.Second) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort), 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { @@ -156,7 +157,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(time.Second) + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort), 100*time.Millisecond, 5*time.Second)) framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() }) @@ -174,7 +175,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) proxyConfig := map[string]any{ "name": "test-tcp", @@ -195,8 +196,6 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { return resp.Code == 200 }) - time.Sleep(500 * time.Millisecond) - framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") }).Ensure(func(resp *request.Response) bool { @@ -226,7 +225,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") @@ -248,7 +247,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) invalidBody, _ := json.Marshal(map[string]any{ "name": "bad-proxy", @@ -281,7 +280,7 @@ var _ = ginkgo.Describe("[Feature: Store]", func() { `, adminPort, f.TempDirectory) f.RunProcesses(serverConf, []string{clientConf}) - time.Sleep(500 * time.Millisecond) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) createBody, _ := json.Marshal(map[string]any{ "name": "proxy-a", From 6b1be922e155def020e78aa78f4b796b7a530ea4 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 12 Mar 2026 00:21:31 +0800 Subject: [PATCH 36/43] add AGENTS.md and CLAUDE.md, remove them from .gitignore (#5232) --- .gitignore | 3 +-- AGENTS.md | 34 ++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.gitignore b/.gitignore index fdb32dc3..47fb34ad 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,5 @@ client.key *.swp # AI -CLAUDE.md -AGENTS.md +.claude/ .sisyphus/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..4f3e66e8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS.md + +## Development Commands + +### Build +- `make build` - Build both frps and frpc binaries +- `make frps` - Build server binary only +- `make frpc` - Build client binary only +- `make all` - Build everything with formatting + +### Testing +- `make test` - Run unit tests +- `make e2e` - Run end-to-end tests +- `make e2e-trace` - Run e2e tests with trace logging +- `make alltest` - Run all tests including vet, unit tests, and e2e + +### Code Quality +- `make fmt` - Run go fmt +- `make fmt-more` - Run gofumpt for more strict formatting +- `make gci` - Run gci import organizer +- `make vet` - Run go vet +- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml) + +### Assets +- `make web` - Build web dashboards (frps and frpc) + +### Cleanup +- `make clean` - Remove built binaries and temporary files + +## Testing + +- E2E tests using Ginkgo/Gomega framework +- Mock servers in `/test/e2e/mock/` +- Run: `make e2e` or `make alltest` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 94a631fe9c22491672b016413bb4d68067adeafb Mon Sep 17 00:00:00 2001 From: Shani Pathak Date: Wed, 11 Mar 2026 21:54:46 +0530 Subject: [PATCH 37/43] auth/oidc: cache OIDC access token and refresh before expiry (#5175) * auth/oidc: cache OIDC access token and refresh before expiry - Use Config.TokenSource(ctx) once at init to create a persistent oauth2.TokenSource that caches the token and only refreshes on expiry - Wrap with oauth2.ReuseTokenSourceWithExpiry for configurable early refresh - Add tokenRefreshAdvanceDuration config option (default: 300s) - Add unit test verifying token caching with mock HTTP server * address review comments * auth/oidc: fallback to per-request token fetch when expires_in is missing When an OIDC provider omits the expires_in field, oauth2.ReuseTokenSource treats the cached token as valid forever and never refreshes it. This causes server-side OIDC verification to fail once the JWT's exp claim passes. Add a nonCachingTokenSource fallback: after fetching the initial token, if its Expiry is the zero value, swap the caching TokenSource for one that fetches a fresh token on every request, preserving the old behavior for providers that don't return expires_in. * auth/oidc: fix gosec lint and add test for zero-expiry fallback Suppress G101 false positive on test-only dummy token responses. Add test to verify per-request token fetch when expires_in is missing. Update caching test to account for eager initial token fetch. * fix lint --- pkg/auth/oidc.go | 54 ++++++++++++++++++------- pkg/auth/oidc_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 14 deletions(-) diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 9aeaf3c5..e63322f7 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -75,11 +75,23 @@ func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyUR return &http.Client{Transport: transport}, nil } +// nonCachingTokenSource wraps a clientcredentials.Config to fetch a fresh +// token on every call. This is used as a fallback when the OIDC provider +// does not return expires_in, which would cause a caching TokenSource to +// hold onto a stale token forever. +type nonCachingTokenSource struct { + cfg *clientcredentials.Config + ctx context.Context +} + +func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) { + return s.cfg.Token(s.ctx) +} + type OidcAuthProvider struct { additionalAuthScopes []v1.AuthScope - tokenGenerator *clientcredentials.Config - httpClient *http.Client + tokenSource oauth2.TokenSource } func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) { @@ -100,30 +112,44 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien EndpointParams: eps, } - // Create custom HTTP client if needed - var httpClient *http.Client + // Build the context that TokenSource will use for all future HTTP requests. + // context.Background() is appropriate here because the token source is + // long-lived and outlives any single request. + ctx := context.Background() if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" { - var err error - httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) + httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) if err != nil { return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err) } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + } + + // Create a persistent TokenSource that caches the token and refreshes + // it before expiry. This avoids making a new HTTP request to the OIDC + // provider on every heartbeat/ping. + tokenSource := tokenGenerator.TokenSource(ctx) + + // Fetch the initial token to check if the provider returns an expiry. + // If Expiry is the zero value (provider omitted expires_in), the cached + // TokenSource would treat the token as valid forever and never refresh it, + // even after the JWT's exp claim passes. In that case, fall back to + // fetching a fresh token on every request. + initialToken, err := tokenSource.Token() + if err != nil { + return nil, fmt.Errorf("failed to obtain initial OIDC token: %w", err) + } + if initialToken.Expiry.IsZero() { + tokenSource = &nonCachingTokenSource{cfg: tokenGenerator, ctx: ctx} } return &OidcAuthProvider{ additionalAuthScopes: additionalAuthScopes, - tokenGenerator: tokenGenerator, - httpClient: httpClient, + tokenSource: tokenSource, }, nil } func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { - ctx := context.Background() - if auth.httpClient != nil { - ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient) - } - - tokenObj, err := auth.tokenGenerator.Token(ctx) + tokenObj, err := auth.tokenSource.Token() if err != nil { return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) } diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go index 58054186..ad4f0246 100644 --- a/pkg/auth/oidc_test.go +++ b/pkg/auth/oidc_test.go @@ -2,6 +2,10 @@ package auth_test import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" "testing" "time" @@ -62,3 +66,90 @@ func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) { r.Error(err) r.Contains(err.Error(), "received different OIDC subject in login and ping") } + +func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + requestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "fresh-test-token", + "token_type": "Bearer", + }) + })) + defer tokenServer.Close() + + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + + // Constructor fetches the initial token (1 request). + // Each subsequent call should also fetch a fresh token since there is no expiry. + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("fresh-test-token", loginMsg.PrivilegeKey) + + for range 3 { + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("fresh-test-token", pingMsg.PrivilegeKey) + } + + // 1 initial (constructor) + 1 login + 3 pings = 5 requests + r.Equal(int32(5), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing") +} + +func TestOidcAuthProviderCachesToken(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + requestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "cached-test-token", + "token_type": "Bearer", + "expires_in": 3600, + }) + })) + defer tokenServer.Close() + + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + + // Constructor eagerly fetches the initial token (1 request). + r.Equal(int32(1), requestCount.Load()) + + // SetLogin should reuse the cached token + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("cached-test-token", loginMsg.PrivilegeKey) + r.Equal(int32(1), requestCount.Load()) + + // Subsequent calls should also reuse the cached token + for range 5 { + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("cached-test-token", pingMsg.PrivilegeKey) + } + r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused") +} From ff4ad2f90729e07b1dd9374eed9de4659493f3db Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 15 Mar 2026 22:29:45 +0800 Subject: [PATCH 38/43] auth/oidc: fix eager token fetch at startup, add validation and e2e tests (#5234) --- Release.md | 1 + client/service.go | 35 ++- client/service_test.go | 106 +++++++ pkg/auth/oidc.go | 67 ++++- pkg/auth/oidc_test.go | 112 +++++++- pkg/config/v1/validation/client.go | 5 + pkg/config/v1/validation/oidc.go | 57 ++++ pkg/config/v1/validation/oidc_test.go | 78 ++++++ pkg/util/http/server.go | 6 +- pkg/vnet/controller.go | 3 + test/e2e/mock/server/oidcserver/oidcserver.go | 258 ++++++++++++++++++ test/e2e/v1/basic/oidc.go | 192 +++++++++++++ 12 files changed, 885 insertions(+), 35 deletions(-) create mode 100644 pkg/config/v1/validation/oidc.go create mode 100644 pkg/config/v1/validation/oidc_test.go create mode 100644 test/e2e/mock/server/oidcserver/oidcserver.go create mode 100644 test/e2e/v1/basic/oidc.go diff --git a/Release.md b/Release.md index 87a2cc95..9bee09dc 100644 --- a/Release.md +++ b/Release.md @@ -7,3 +7,4 @@ * Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic. * Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded. * Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers. +* OIDC auth now caches the access token and refreshes it before expiry, avoiding a new token request on every heartbeat. Falls back to per-request fetch when the provider omits `expires_in`. diff --git a/client/service.go b/client/service.go index 26f1db18..f419b068 100644 --- a/client/service.go +++ b/client/service.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "net" + "net/http" "os" "runtime" "sync" @@ -162,15 +163,6 @@ func NewService(options ServiceOptions) (*Service, error) { return nil, err } - var webServer *httppkg.Server - if options.Common.WebServer.Port > 0 { - ws, err := httppkg.NewServer(options.Common.WebServer) - if err != nil { - return nil, err - } - webServer = ws - } - authRuntime, err := auth.BuildClientAuth(&options.Common.Auth) if err != nil { return nil, err @@ -191,6 +183,17 @@ func NewService(options ServiceOptions) (*Service, error) { proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs) visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs) + // Create the web server after all fallible steps so its listener is not + // leaked when an earlier error causes NewService to return. + var webServer *httppkg.Server + if options.Common.WebServer.Port > 0 { + ws, err := httppkg.NewServer(options.Common.WebServer) + if err != nil { + return nil, err + } + webServer = ws + } + s := &Service{ ctx: context.Background(), auth: authRuntime, @@ -229,22 +232,25 @@ func (svr *Service) Run(ctx context.Context) error { } if svr.vnetController != nil { + vnetController := svr.vnetController if err := svr.vnetController.Init(); err != nil { log.Errorf("init virtual network controller error: %v", err) + svr.stop() return err } go func() { log.Infof("virtual network controller start...") - if err := svr.vnetController.Run(); err != nil { + if err := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) { log.Warnf("virtual network controller exit with error: %v", err) } }() } if svr.webServer != nil { + webServer := svr.webServer go func() { - log.Infof("admin server listen on %s", svr.webServer.Address()) - if err := svr.webServer.Run(); err != nil { + log.Infof("admin server listen on %s", webServer.Address()) + if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Warnf("admin server exit with error: %v", err) } }() @@ -255,6 +261,7 @@ func (svr *Service) Run(ctx context.Context) error { if svr.ctl == nil { cancelCause := cancelErr{} _ = errors.As(context.Cause(svr.ctx), &cancelCause) + svr.stop() return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err) } @@ -497,6 +504,10 @@ func (svr *Service) stop() { svr.webServer.Close() svr.webServer = nil } + if svr.vnetController != nil { + _ = svr.vnetController.Stop() + svr.vnetController = nil + } } func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { diff --git a/client/service_test.go b/client/service_test.go index e1c6b587..29f141a1 100644 --- a/client/service_test.go +++ b/client/service_test.go @@ -1,14 +1,120 @@ package client import ( + "context" + "errors" + "net" "path/filepath" + "strconv" "strings" "testing" + "github.com/samber/lo" + "github.com/fatedier/frp/pkg/config/source" v1 "github.com/fatedier/frp/pkg/config/v1" ) +type failingConnector struct { + err error +} + +func (c *failingConnector) Open() error { + return c.err +} + +func (c *failingConnector) Connect() (net.Conn, error) { + return nil, c.err +} + +func (c *failingConnector) Close() error { + return nil +} + +func getFreeTCPPort(t *testing.T) int { + t.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on ephemeral port: %v", err) + } + defer ln.Close() + + return ln.Addr().(*net.TCPAddr).Port +} + +func TestRunStopsStartedComponentsOnInitialLoginFailure(t *testing.T) { + port := getFreeTCPPort(t) + agg := source.NewAggregator(source.NewConfigSource()) + + svr, err := NewService(ServiceOptions{ + Common: &v1.ClientCommonConfig{ + LoginFailExit: lo.ToPtr(true), + WebServer: v1.WebServerConfig{ + Addr: "127.0.0.1", + Port: port, + }, + }, + ConfigSourceAggregator: agg, + ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector { + return &failingConnector{err: errors.New("login boom")} + }, + }) + if err != nil { + t.Fatalf("new service: %v", err) + } + + err = svr.Run(context.Background()) + if err == nil { + t.Fatal("expected run error, got nil") + } + if !strings.Contains(err.Error(), "login boom") { + t.Fatalf("unexpected error: %v", err) + } + if svr.webServer != nil { + t.Fatal("expected web server to be cleaned up after initial login failure") + } + + ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + t.Fatalf("expected admin port to be released: %v", err) + } + _ = ln.Close() +} + +func TestNewServiceDoesNotLeakAdminListenerOnAuthBuildFailure(t *testing.T) { + port := getFreeTCPPort(t) + agg := source.NewAggregator(source.NewConfigSource()) + + _, err := NewService(ServiceOptions{ + Common: &v1.ClientCommonConfig{ + Auth: v1.AuthClientConfig{ + Method: v1.AuthMethodOIDC, + OIDC: v1.AuthOIDCClientConfig{ + TokenEndpointURL: "://bad", + }, + }, + WebServer: v1.WebServerConfig{ + Addr: "127.0.0.1", + Port: port, + }, + }, + ConfigSourceAggregator: agg, + }) + if err == nil { + t.Fatal("expected new service error, got nil") + } + if !strings.Contains(err.Error(), "auth.oidc.tokenEndpointURL") { + t.Fatalf("unexpected error: %v", err) + } + + ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + t.Fatalf("expected admin port to remain free: %v", err) + } + _ = ln.Close() +} + func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) { prevCommon := &v1.ClientCommonConfig{User: "old-user"} newCommon := &v1.ClientCommonConfig{User: "new-user"} diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index e63322f7..826a6715 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -30,6 +30,7 @@ import ( "golang.org/x/oauth2/clientcredentials" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/msg" ) @@ -88,6 +89,40 @@ func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) { return s.cfg.Token(s.ctx) } +// oidcTokenSource wraps a caching oauth2.TokenSource and, on the first +// successful Token() call, checks whether the provider returns an expiry. +// If not, it permanently switches to nonCachingTokenSource so that a fresh +// token is fetched every time. This avoids an eager network call at +// construction time, letting the login retry loop handle transient IdP +// outages. +type oidcTokenSource struct { + mu sync.Mutex + initialized bool + source oauth2.TokenSource + fallbackCfg *clientcredentials.Config + fallbackCtx context.Context +} + +func (s *oidcTokenSource) Token() (*oauth2.Token, error) { + s.mu.Lock() + if !s.initialized { + token, err := s.source.Token() + if err != nil { + s.mu.Unlock() + return nil, err + } + if token.Expiry.IsZero() { + s.source = &nonCachingTokenSource{cfg: s.fallbackCfg, ctx: s.fallbackCtx} + } + s.initialized = true + s.mu.Unlock() + return token, nil + } + source := s.source + s.mu.Unlock() + return source.Token() +} + type OidcAuthProvider struct { additionalAuthScopes []v1.AuthScope @@ -95,6 +130,10 @@ type OidcAuthProvider struct { } func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) { + if err := validation.ValidateOIDCClientCredentialsConfig(&cfg); err != nil { + return nil, err + } + eps := make(map[string][]string) for k, v := range cfg.AdditionalEndpointParams { eps[k] = []string{v} @@ -127,24 +166,22 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien // Create a persistent TokenSource that caches the token and refreshes // it before expiry. This avoids making a new HTTP request to the OIDC // provider on every heartbeat/ping. - tokenSource := tokenGenerator.TokenSource(ctx) - - // Fetch the initial token to check if the provider returns an expiry. - // If Expiry is the zero value (provider omitted expires_in), the cached - // TokenSource would treat the token as valid forever and never refresh it, - // even after the JWT's exp claim passes. In that case, fall back to - // fetching a fresh token on every request. - initialToken, err := tokenSource.Token() - if err != nil { - return nil, fmt.Errorf("failed to obtain initial OIDC token: %w", err) - } - if initialToken.Expiry.IsZero() { - tokenSource = &nonCachingTokenSource{cfg: tokenGenerator, ctx: ctx} - } + // + // We wrap it in an oidcTokenSource so that the first Token() call + // (deferred to SetLogin inside the login retry loop) probes whether the + // provider returns expires_in. If not, it switches to a non-caching + // source. This avoids an eager network call at construction time, which + // would prevent loopLoginUntilSuccess from retrying on transient IdP + // outages. + cachingSource := tokenGenerator.TokenSource(ctx) return &OidcAuthProvider{ additionalAuthScopes: additionalAuthScopes, - tokenSource: tokenSource, + tokenSource: &oidcTokenSource{ + source: cachingSource, + fallbackCfg: tokenGenerator, + fallbackCtx: ctx, + }, }, nil } diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go index ad4f0246..70e59883 100644 --- a/pkg/auth/oidc_test.go +++ b/pkg/auth/oidc_test.go @@ -91,8 +91,10 @@ func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) { ) r.NoError(err) - // Constructor fetches the initial token (1 request). - // Each subsequent call should also fetch a fresh token since there is no expiry. + // Constructor no longer fetches a token eagerly. + // The first SetLogin triggers the adaptive probe. + r.Equal(int32(0), requestCount.Load()) + loginMsg := &msg.Login{} err = provider.SetLogin(loginMsg) r.NoError(err) @@ -105,8 +107,8 @@ func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) { r.Equal("fresh-test-token", pingMsg.PrivilegeKey) } - // 1 initial (constructor) + 1 login + 3 pings = 5 requests - r.Equal(int32(5), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing") + // 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch) + r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing") } func TestOidcAuthProviderCachesToken(t *testing.T) { @@ -134,10 +136,10 @@ func TestOidcAuthProviderCachesToken(t *testing.T) { ) r.NoError(err) - // Constructor eagerly fetches the initial token (1 request). - r.Equal(int32(1), requestCount.Load()) + // Constructor no longer fetches eagerly; first SetLogin triggers the probe. + r.Equal(int32(0), requestCount.Load()) - // SetLogin should reuse the cached token + // SetLogin triggers the adaptive probe and caches the token. loginMsg := &msg.Login{} err = provider.SetLogin(loginMsg) r.NoError(err) @@ -153,3 +155,99 @@ func TestOidcAuthProviderCachesToken(t *testing.T) { } r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused") } + +func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + n := requestCount.Add(1) + // The oauth2 library retries once internally, so we need two + // consecutive failures to surface an error to the caller. + if n <= 2 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "temporarily_unavailable", + "error_description": "service is starting up", + }) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "retry-test-token", + "token_type": "Bearer", + "expires_in": 3600, + }) + })) + defer tokenServer.Close() + + // Constructor succeeds even though the IdP is "down". + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + r.Equal(int32(0), requestCount.Load()) + + // First SetLogin hits the IdP, which returns an error (after internal retry). + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.Error(err) + r.Equal(int32(2), requestCount.Load()) + + // Second SetLogin retries and succeeds. + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("retry-test-token", loginMsg.PrivilegeKey) + r.Equal(int32(3), requestCount.Load()) + + // Subsequent calls use cached token. + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("retry-test-token", pingMsg.PrivilegeKey) + r.Equal(int32(3), requestCount.Load()) +} + +func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) { + r := require.New(t) + tokenServer := httptest.NewServer(http.NotFoundHandler()) + defer tokenServer.Close() + + _, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: "://bad", + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.tokenEndpointURL") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + TokenEndpointURL: tokenServer.URL, + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.clientID is required") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "scope": "profile", + }, + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + Audience: "api", + AdditionalEndpointParams: map[string]string{"audience": "override"}, + }) + r.Error(err) + r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index eb4a0253..c90d525d 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -88,6 +88,11 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e if err := v.validateOIDCConfig(&c.OIDC); err != nil { errs = AppendError(errs, err) } + if c.Method == v1.AuthMethodOIDC && c.OIDC.TokenSource == nil { + if err := ValidateOIDCClientCredentialsConfig(&c.OIDC); err != nil { + errs = AppendError(errs, err) + } + } return nil, errs } diff --git a/pkg/config/v1/validation/oidc.go b/pkg/config/v1/validation/oidc.go new file mode 100644 index 00000000..c905e8e5 --- /dev/null +++ b/pkg/config/v1/validation/oidc.go @@ -0,0 +1,57 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "errors" + "net/url" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateOIDCClientCredentialsConfig(c *v1.AuthOIDCClientConfig) error { + var errs []string + + if c.ClientID == "" { + errs = append(errs, "auth.oidc.clientID is required") + } + + if c.TokenEndpointURL == "" { + errs = append(errs, "auth.oidc.tokenEndpointURL is required") + } else { + tokenURL, err := url.Parse(c.TokenEndpointURL) + if err != nil || !tokenURL.IsAbs() || tokenURL.Host == "" { + errs = append(errs, "auth.oidc.tokenEndpointURL must be an absolute http or https URL") + } else if tokenURL.Scheme != "http" && tokenURL.Scheme != "https" { + errs = append(errs, "auth.oidc.tokenEndpointURL must use http or https") + } + } + + if _, ok := c.AdditionalEndpointParams["scope"]; ok { + errs = append(errs, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + } + + if c.Audience != "" { + if _, ok := c.AdditionalEndpointParams["audience"]; ok { + errs = append(errs, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") + } + } + + if len(errs) == 0 { + return nil + } + return errors.New(strings.Join(errs, "; ")) +} diff --git a/pkg/config/v1/validation/oidc_test.go b/pkg/config/v1/validation/oidc_test.go new file mode 100644 index 00000000..bc21da6e --- /dev/null +++ b/pkg/config/v1/validation/oidc_test.go @@ -0,0 +1,78 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestValidateOIDCClientCredentialsConfig(t *testing.T) { + tokenServer := httptest.NewServer(http.NotFoundHandler()) + defer tokenServer.Close() + + t.Run("valid", func(t *testing.T) { + require.NoError(t, ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "resource": "api", + }, + })) + }) + + t.Run("invalid token endpoint url", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: "://bad", + }) + require.ErrorContains(t, err, "auth.oidc.tokenEndpointURL") + }) + + t.Run("missing client id", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + TokenEndpointURL: tokenServer.URL, + }) + require.ErrorContains(t, err, "auth.oidc.clientID is required") + }) + + t.Run("scope endpoint param is not allowed", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "scope": "email", + }, + }) + require.ErrorContains(t, err, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + }) + + t.Run("audience conflict", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + Audience: "api", + AdditionalEndpointParams: map[string]string{ + "audience": "override", + }, + }) + require.ErrorContains(t, err, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") + }) +} diff --git a/pkg/util/http/server.go b/pkg/util/http/server.go index 99bed364..0bca8993 100644 --- a/pkg/util/http/server.go +++ b/pkg/util/http/server.go @@ -100,7 +100,11 @@ func (s *Server) Run() error { } func (s *Server) Close() error { - return s.hs.Close() + err := s.hs.Close() + if s.ln != nil { + _ = s.ln.Close() + } + return err } type RouterRegisterHelper struct { diff --git a/pkg/vnet/controller.go b/pkg/vnet/controller.go index ca71a8c3..d5c97c66 100644 --- a/pkg/vnet/controller.go +++ b/pkg/vnet/controller.go @@ -131,6 +131,9 @@ func (c *Controller) handlePacket(buf []byte) { } func (c *Controller) Stop() error { + if c.tun == nil { + return nil + } return c.tun.Close() } diff --git a/test/e2e/mock/server/oidcserver/oidcserver.go b/test/e2e/mock/server/oidcserver/oidcserver.go new file mode 100644 index 00000000..d7aa1329 --- /dev/null +++ b/test/e2e/mock/server/oidcserver/oidcserver.go @@ -0,0 +1,258 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package oidcserver provides a minimal mock OIDC server for e2e testing. +// It implements three endpoints: +// - /.well-known/openid-configuration (discovery) +// - /jwks (JSON Web Key Set) +// - /token (client_credentials grant) +package oidcserver + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "net" + "net/http" + "strconv" + "sync/atomic" + "time" +) + +type Server struct { + bindAddr string + bindPort int + l net.Listener + hs *http.Server + + privateKey *rsa.PrivateKey + kid string + + clientID string + clientSecret string + audience string + subject string + expiresIn int // seconds; 0 means omit expires_in from token response + + tokenRequestCount atomic.Int64 +} + +type Option func(*Server) + +func WithBindPort(port int) Option { + return func(s *Server) { s.bindPort = port } +} + +func WithClientCredentials(id, secret string) Option { + return func(s *Server) { + s.clientID = id + s.clientSecret = secret + } +} + +func WithAudience(aud string) Option { + return func(s *Server) { s.audience = aud } +} + +func WithSubject(sub string) Option { + return func(s *Server) { s.subject = sub } +} + +func WithExpiresIn(seconds int) Option { + return func(s *Server) { s.expiresIn = seconds } +} + +func New(options ...Option) *Server { + s := &Server{ + bindAddr: "127.0.0.1", + kid: "test-key-1", + clientID: "test-client", + clientSecret: "test-secret", + audience: "frps", + subject: "test-service", + expiresIn: 3600, + } + for _, opt := range options { + opt(s) + } + return s +} + +func (s *Server) Run() error { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("generate RSA key: %w", err) + } + s.privateKey = key + + s.l, err = net.Listen("tcp", net.JoinHostPort(s.bindAddr, strconv.Itoa(s.bindPort))) + if err != nil { + return err + } + s.bindPort = s.l.Addr().(*net.TCPAddr).Port + + mux := http.NewServeMux() + mux.HandleFunc("/.well-known/openid-configuration", s.handleDiscovery) + mux.HandleFunc("/jwks", s.handleJWKS) + mux.HandleFunc("/token", s.handleToken) + + s.hs = &http.Server{ + Handler: mux, + ReadHeaderTimeout: time.Minute, + } + go func() { _ = s.hs.Serve(s.l) }() + return nil +} + +func (s *Server) Close() error { + if s.hs != nil { + return s.hs.Close() + } + return nil +} + +func (s *Server) BindAddr() string { return s.bindAddr } +func (s *Server) BindPort() int { return s.bindPort } + +func (s *Server) Issuer() string { + return fmt.Sprintf("http://%s:%d", s.bindAddr, s.bindPort) +} + +func (s *Server) TokenEndpoint() string { + return s.Issuer() + "/token" +} + +// TokenRequestCount returns the number of successful token requests served. +func (s *Server) TokenRequestCount() int64 { + return s.tokenRequestCount.Load() +} + +func (s *Server) handleDiscovery(w http.ResponseWriter, _ *http.Request) { + issuer := s.Issuer() + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "issuer": issuer, + "token_endpoint": issuer + "/token", + "jwks_uri": issuer + "/jwks", + "response_types_supported": []string{"code"}, + "subject_types_supported": []string{"public"}, + "id_token_signing_alg_values_supported": []string{"RS256"}, + }) +} + +func (s *Server) handleJWKS(w http.ResponseWriter, _ *http.Request) { + pub := &s.privateKey.PublicKey + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "keys": []map[string]any{ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": s.kid, + "n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()), + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()), + }, + }, + }) +} + +func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if err := r.ParseForm(); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_request", + }) + return + } + + if r.FormValue("grant_type") != "client_credentials" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "unsupported_grant_type", + }) + return + } + + // Accept credentials from Basic Auth or form body. + clientID, clientSecret, ok := r.BasicAuth() + if !ok { + clientID = r.FormValue("client_id") + clientSecret = r.FormValue("client_secret") + } + if clientID != s.clientID || clientSecret != s.clientSecret { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_client", + }) + return + } + + token, err := s.signJWT() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := map[string]any{ + "access_token": token, + "token_type": "Bearer", + } + if s.expiresIn > 0 { + resp["expires_in"] = s.expiresIn + } + + s.tokenRequestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +func (s *Server) signJWT() (string, error) { + now := time.Now() + header, _ := json.Marshal(map[string]string{ + "alg": "RS256", + "kid": s.kid, + "typ": "JWT", + }) + claims, _ := json.Marshal(map[string]any{ + "iss": s.Issuer(), + "sub": s.subject, + "aud": s.audience, + "iat": now.Unix(), + "exp": now.Add(1 * time.Hour).Unix(), + }) + + headerB64 := base64.RawURLEncoding.EncodeToString(header) + claimsB64 := base64.RawURLEncoding.EncodeToString(claims) + signingInput := headerB64 + "." + claimsB64 + + h := sha256.Sum256([]byte(signingInput)) + sig, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, h[:]) + if err != nil { + return "", err + } + return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil +} diff --git a/test/e2e/v1/basic/oidc.go b/test/e2e/v1/basic/oidc.go new file mode 100644 index 00000000..19539c76 --- /dev/null +++ b/test/e2e/v1/basic/oidc.go @@ -0,0 +1,192 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package basic + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/oidcserver" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: OIDC]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("should work with OIDC authentication", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should authenticate heartbeats with OIDC", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + serverPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d +log.level = "trace" +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, serverPort, oidcSrv.Issuer()) + + clientConf := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +loginFailExit = false +log.level = "trace" +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" +transport.heartbeatInterval = 1 + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, serverPort, oidcSrv.TokenEndpoint(), f.PortByName(framework.TCPEchoServerPort), remotePort) + + serverConfigPath := f.GenerateConfigFile(serverConf) + clientConfigPath := f.GenerateConfigFile(clientConf) + + _, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + clientProcess, _, err := f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + + // Wait for several authenticated heartbeat cycles instead of a fixed sleep. + err = clientProcess.WaitForOutput("send heartbeat to server", 3, 10*time.Second) + framework.ExpectNoError(err) + + // Proxy should still work: heartbeat auth has not failed. + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("should work when token has no expires_in", func() { + oidcSrv := oidcserver.New( + oidcserver.WithBindPort(f.AllocPort()), + oidcserver.WithExpiresIn(0), + ) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" +transport.heartbeatInterval = 1 + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).Ensure() + + countAfterLogin := oidcSrv.TokenRequestCount() + + // Wait for several heartbeat cycles instead of a fixed sleep. + // Each heartbeat fetches a fresh token in non-caching mode. + err := clientProcesses[0].WaitForOutput("send heartbeat to server", 3, 10*time.Second) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + + // Each heartbeat should have fetched a new token (non-caching mode). + countAfterHeartbeats := oidcSrv.TokenRequestCount() + framework.ExpectTrue( + countAfterHeartbeats > countAfterLogin, + "expected additional token requests for heartbeats, got %d before and %d after", + countAfterLogin, countAfterHeartbeats, + ) + }) + + ginkgo.It("should reject invalid OIDC credentials", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "wrong-secret" +auth.oidc.tokenEndpointURL = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure() + }) +}) From 85e8e2c830ff0b6bcaafbbb7174c8f15e330d4e2 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 16 Mar 2026 09:44:30 +0800 Subject: [PATCH 39/43] web/frpc: redesign frpc dashboard with sidebar nav, proxy/visitor list and detail views (#5237) --- .gitignore | 1 + client/api_router.go | 2 + client/config_manager.go | 42 + client/configmgmt/types.go | 3 + client/http/controller.go | 38 + client/http/controller_test.go | 131 ++ client/service.go | 11 + client/visitor/visitor_manager.go | 7 + web/frpc/components.d.ts | 39 +- web/frpc/index.html | 1 + web/frpc/package-lock.json | 136 ++ web/frpc/package.json | 1 + web/frpc/src/App.vue | 567 +++++--- web/frpc/src/api/frpc.ts | 15 +- web/frpc/src/assets/css/_form-layout.scss | 33 + web/frpc/src/assets/css/_index.scss | 2 + web/frpc/src/assets/css/_mixins.scss | 49 + web/frpc/src/assets/css/_variables.scss | 61 + web/frpc/src/assets/css/custom.css | 105 -- web/frpc/src/assets/css/dark.css | 223 ++-- web/frpc/src/assets/css/var.css | 117 ++ web/frpc/src/components/ActionButton.vue | 144 ++ web/frpc/src/components/BaseDialog.vue | 142 ++ web/frpc/src/components/ConfigField.vue | 249 ++++ web/frpc/src/components/ConfigSection.vue | 185 +++ web/frpc/src/components/ConfirmDialog.vue | 84 ++ web/frpc/src/components/FilterDropdown.vue | 102 ++ web/frpc/src/components/KeyValueEditor.vue | 113 +- web/frpc/src/components/PopoverMenu.vue | 303 +++++ web/frpc/src/components/PopoverMenuItem.vue | 125 ++ web/frpc/src/components/ProxyCard.vue | 493 ++----- web/frpc/src/components/StatCard.vue | 202 --- web/frpc/src/components/StatusPills.vue | 103 ++ web/frpc/src/components/StringListEditor.vue | 141 ++ .../proxy-form/ProxyAuthSection.vue | 40 + .../proxy-form/ProxyBackendSection.vue | 149 +++ .../proxy-form/ProxyBaseSection.vue | 51 + .../components/proxy-form/ProxyFormLayout.vue | 50 + .../proxy-form/ProxyHealthSection.vue | 52 + .../proxy-form/ProxyHttpSection.vue | 32 + .../proxy-form/ProxyLoadBalanceSection.vue | 31 + .../proxy-form/ProxyMetadataSection.vue | 29 + .../components/proxy-form/ProxyNatSection.vue | 29 + .../proxy-form/ProxyRemoteSection.vue | 41 + .../proxy-form/ProxyTransportSection.vue | 39 + .../visitor-form/VisitorBaseSection.vue | 40 + .../visitor-form/VisitorConnectionSection.vue | 43 + .../visitor-form/VisitorFormLayout.vue | 33 + .../visitor-form/VisitorTransportSection.vue | 32 + .../visitor-form/VisitorXtcpSection.vue | 47 + web/frpc/src/composables/useResponsive.ts | 8 + web/frpc/src/main.ts | 4 +- web/frpc/src/router/index.ts | 49 +- web/frpc/src/stores/client.ts | 28 + web/frpc/src/stores/proxy.ts | 132 ++ web/frpc/src/stores/visitor.ts | 68 + web/frpc/src/types/constants.ts | 32 + web/frpc/src/types/index.ts | 5 + .../types/{proxy.ts => proxy-converters.ts} | 298 +---- web/frpc/src/types/proxy-form.ts | 167 +++ web/frpc/src/types/proxy-status.ts | 13 + web/frpc/src/types/proxy-store.ts | 30 + web/frpc/src/views/ClientConfigure.vue | 442 ++----- web/frpc/src/views/Overview.vue | 1160 ---------------- web/frpc/src/views/ProxyDetail.vue | 303 +++++ web/frpc/src/views/ProxyEdit.vue | 1175 ++--------------- web/frpc/src/views/ProxyList.vue | 439 ++++++ web/frpc/src/views/VisitorDetail.vue | 206 +++ web/frpc/src/views/VisitorEdit.vue | 554 ++------ web/frpc/src/views/VisitorList.vue | 371 ++++++ web/frpc/vite.config.mts | 8 + 71 files changed, 5908 insertions(+), 4292 deletions(-) create mode 100644 web/frpc/src/assets/css/_form-layout.scss create mode 100644 web/frpc/src/assets/css/_index.scss create mode 100644 web/frpc/src/assets/css/_mixins.scss create mode 100644 web/frpc/src/assets/css/_variables.scss delete mode 100644 web/frpc/src/assets/css/custom.css create mode 100644 web/frpc/src/assets/css/var.css create mode 100644 web/frpc/src/components/ActionButton.vue create mode 100644 web/frpc/src/components/BaseDialog.vue create mode 100644 web/frpc/src/components/ConfigField.vue create mode 100644 web/frpc/src/components/ConfigSection.vue create mode 100644 web/frpc/src/components/ConfirmDialog.vue create mode 100644 web/frpc/src/components/FilterDropdown.vue create mode 100644 web/frpc/src/components/PopoverMenu.vue create mode 100644 web/frpc/src/components/PopoverMenuItem.vue delete mode 100644 web/frpc/src/components/StatCard.vue create mode 100644 web/frpc/src/components/StatusPills.vue create mode 100644 web/frpc/src/components/StringListEditor.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyAuthSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyBackendSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyBaseSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyFormLayout.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyHealthSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyHttpSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyLoadBalanceSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyMetadataSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyNatSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyRemoteSection.vue create mode 100644 web/frpc/src/components/proxy-form/ProxyTransportSection.vue create mode 100644 web/frpc/src/components/visitor-form/VisitorBaseSection.vue create mode 100644 web/frpc/src/components/visitor-form/VisitorConnectionSection.vue create mode 100644 web/frpc/src/components/visitor-form/VisitorFormLayout.vue create mode 100644 web/frpc/src/components/visitor-form/VisitorTransportSection.vue create mode 100644 web/frpc/src/components/visitor-form/VisitorXtcpSection.vue create mode 100644 web/frpc/src/composables/useResponsive.ts create mode 100644 web/frpc/src/stores/client.ts create mode 100644 web/frpc/src/stores/proxy.ts create mode 100644 web/frpc/src/stores/visitor.ts create mode 100644 web/frpc/src/types/constants.ts create mode 100644 web/frpc/src/types/index.ts rename web/frpc/src/types/{proxy.ts => proxy-converters.ts} (65%) create mode 100644 web/frpc/src/types/proxy-form.ts create mode 100644 web/frpc/src/types/proxy-status.ts create mode 100644 web/frpc/src/types/proxy-store.ts delete mode 100644 web/frpc/src/views/Overview.vue create mode 100644 web/frpc/src/views/ProxyDetail.vue create mode 100644 web/frpc/src/views/ProxyList.vue create mode 100644 web/frpc/src/views/VisitorDetail.vue create mode 100644 web/frpc/src/views/VisitorList.vue diff --git a/.gitignore b/.gitignore index 47fb34ad..161c114e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ client.key # AI .claude/ .sisyphus/ +.superpowers/ diff --git a/client/api_router.go b/client/api_router.go index ceab4fbc..82b73046 100644 --- a/client/api_router.go +++ b/client/api_router.go @@ -38,6 +38,8 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet) subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet) subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/proxy/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetProxyConfig)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/visitor/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetVisitorConfig)).Methods(http.MethodGet) if svr.storeSource != nil { subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet) diff --git a/client/config_manager.go b/client/config_manager.go index 1d3fb0ec..24512e9f 100644 --- a/client/config_manager.go +++ b/client/config_manager.go @@ -80,6 +80,48 @@ func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus { return m.svr.getAllProxyStatus() } +func (m *serviceConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) { + // Try running proxy manager first + ws, ok := m.svr.getProxyStatus(name) + if ok { + return ws.Cfg, true + } + + // Fallback to store + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource != nil { + cfg := storeSource.GetProxy(name) + if cfg != nil { + return cfg, true + } + } + return nil, false +} + +func (m *serviceConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) { + // Try running visitor manager first + cfg, ok := m.svr.getVisitorCfg(name) + if ok { + return cfg, true + } + + // Fallback to store + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource != nil { + vcfg := storeSource.GetVisitor(name) + if vcfg != nil { + return vcfg, true + } + } + return nil, false +} + func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool { if name == "" { return false diff --git a/client/configmgmt/types.go b/client/configmgmt/types.go index 090d0b7b..5a51a5e5 100644 --- a/client/configmgmt/types.go +++ b/client/configmgmt/types.go @@ -26,6 +26,9 @@ type ConfigManager interface { IsStoreProxyEnabled(name string) bool StoreEnabled() bool + GetProxyConfig(name string) (v1.ProxyConfigurer, bool) + GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) + ListStoreProxies() ([]v1.ProxyConfigurer, error) GetStoreProxy(name string) (v1.ProxyConfigurer, error) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) diff --git a/client/http/controller.go b/client/http/controller.go index d06396dc..57e8165d 100644 --- a/client/http/controller.go +++ b/client/http/controller.go @@ -162,6 +162,44 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.Pro return psr } +// GetProxyConfig handles GET /api/proxy/{name}/config +func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + cfg, ok := c.manager.GetProxyConfig(name) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name)) + } + + payload, err := model.ProxyDefinitionFromConfigurer(cfg) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return payload, nil +} + +// GetVisitorConfig handles GET /api/visitor/{name}/config +func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + cfg, ok := c.manager.GetVisitorConfig(name) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name)) + } + + payload, err := model.VisitorDefinitionFromConfigurer(cfg) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return payload, nil +} + func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) { proxies, err := c.manager.ListStoreProxies() if err != nil { diff --git a/client/http/controller_test.go b/client/http/controller_test.go index aa88c545..719fbcd4 100644 --- a/client/http/controller_test.go +++ b/client/http/controller_test.go @@ -26,6 +26,8 @@ type fakeConfigManager struct { getProxyStatusFn func() []*proxy.WorkingStatus isStoreProxyEnabledFn func(name string) bool storeEnabledFn func() bool + getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool) + getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool) listStoreProxiesFn func() ([]v1.ProxyConfigurer, error) getStoreProxyFn func(name string) (v1.ProxyConfigurer, error) @@ -82,6 +84,20 @@ func (m *fakeConfigManager) StoreEnabled() bool { return false } +func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) { + if m.getProxyConfigFn != nil { + return m.getProxyConfigFn(name) + } + return nil, false +} + +func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) { + if m.getVisitorConfigFn != nil { + return m.getVisitorConfigFn(name) + } + return nil, false +} + func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { if m.listStoreProxiesFn != nil { return m.listStoreProxiesFn() @@ -529,3 +545,118 @@ func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) { t.Fatalf("unexpected response payload: %#v", payload) } } + +func TestGetProxyConfigFromManager(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) { + if name == "ssh" { + cfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "ssh", + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 22, + }, + }, + } + return cfg, true + } + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "ssh"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.GetProxyConfig(ctx) + if err != nil { + t.Fatalf("get proxy config: %v", err) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestGetProxyConfigNotFound(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) { + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "missing"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.GetProxyConfig(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} + +func TestGetVisitorConfigFromManager(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) { + if name == "my-stcp" { + cfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "my-stcp", + Type: "stcp", + ServerName: "server1", + BindPort: 9000, + }, + } + return cfg, true + } + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.GetVisitorConfig(ctx) + if err != nil { + t.Fatalf("get visitor config: %v", err) + } + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestGetVisitorConfigNotFound(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) { + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "missing"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.GetVisitorConfig(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} diff --git a/client/service.go b/client/service.go index f419b068..5c51fd67 100644 --- a/client/service.go +++ b/client/service.go @@ -521,6 +521,17 @@ func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { return ctl.pm.GetProxyStatus(name) } +func (svr *Service) getVisitorCfg(name string) (v1.VisitorConfigurer, bool) { + svr.ctlMu.RLock() + ctl := svr.ctl + svr.ctlMu.RUnlock() + + if ctl == nil { + return nil, false + } + return ctl.vm.GetVisitorCfg(name) +} + func (svr *Service) StatusExporter() StatusExporter { return &statusExporterImpl{ getProxyStatusFunc: svr.getProxyStatus, diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go index b3539c69..b60f7047 100644 --- a/client/visitor/visitor_manager.go +++ b/client/visitor/visitor_manager.go @@ -191,6 +191,13 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error { return v.AcceptConn(conn) } +func (vm *Manager) GetVisitorCfg(name string) (v1.VisitorConfigurer, bool) { + vm.mu.RLock() + defer vm.mu.RUnlock() + cfg, ok := vm.cfgs[name] + return cfg, ok +} + type visitorHelperImpl struct { connectServerFn func() (net.Conn, error) msgTransporter transport.MessageTransporter diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index 700f3c42..4236f460 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -7,28 +7,45 @@ export {} declare module 'vue' { export interface GlobalComponents { - ElButton: typeof import('element-plus/es')['ElButton'] - ElCard: typeof import('element-plus/es')['ElCard'] - ElCol: typeof import('element-plus/es')['ElCol'] - ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition'] + ActionButton: typeof import('./src/components/ActionButton.vue')['default'] + BaseDialog: typeof import('./src/components/BaseDialog.vue')['default'] + ConfigField: typeof import('./src/components/ConfigField.vue')['default'] + ConfigSection: typeof import('./src/components/ConfigSection.vue')['default'] + ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default'] + ElDialog: typeof import('element-plus/es')['ElDialog'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] - ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElOption: typeof import('element-plus/es')['ElOption'] + ElPopover: typeof import('element-plus/es')['ElPopover'] ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] - ElRow: typeof import('element-plus/es')['ElRow'] - ElSelect: typeof import('element-plus/es')['ElSelect'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] - ElTag: typeof import('element-plus/es')['ElTag'] - ElTooltip: typeof import('element-plus/es')['ElTooltip'] + FilterDropdown: typeof import('./src/components/FilterDropdown.vue')['default'] KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default'] + PopoverMenu: typeof import('./src/components/PopoverMenu.vue')['default'] + PopoverMenuItem: typeof import('./src/components/PopoverMenuItem.vue')['default'] + ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default'] + ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default'] + ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default'] ProxyCard: typeof import('./src/components/ProxyCard.vue')['default'] + ProxyFormLayout: typeof import('./src/components/proxy-form/ProxyFormLayout.vue')['default'] + ProxyHealthSection: typeof import('./src/components/proxy-form/ProxyHealthSection.vue')['default'] + ProxyHttpSection: typeof import('./src/components/proxy-form/ProxyHttpSection.vue')['default'] + ProxyLoadBalanceSection: typeof import('./src/components/proxy-form/ProxyLoadBalanceSection.vue')['default'] + ProxyMetadataSection: typeof import('./src/components/proxy-form/ProxyMetadataSection.vue')['default'] + ProxyNatSection: typeof import('./src/components/proxy-form/ProxyNatSection.vue')['default'] + ProxyRemoteSection: typeof import('./src/components/proxy-form/ProxyRemoteSection.vue')['default'] + ProxyTransportSection: typeof import('./src/components/proxy-form/ProxyTransportSection.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - StatCard: typeof import('./src/components/StatCard.vue')['default'] + StatusPills: typeof import('./src/components/StatusPills.vue')['default'] + StringListEditor: typeof import('./src/components/StringListEditor.vue')['default'] + VisitorBaseSection: typeof import('./src/components/visitor-form/VisitorBaseSection.vue')['default'] + VisitorConnectionSection: typeof import('./src/components/visitor-form/VisitorConnectionSection.vue')['default'] + VisitorFormLayout: typeof import('./src/components/visitor-form/VisitorFormLayout.vue')['default'] + VisitorTransportSection: typeof import('./src/components/visitor-form/VisitorTransportSection.vue')['default'] + VisitorXtcpSection: typeof import('./src/components/visitor-form/VisitorXtcpSection.vue')['default'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/web/frpc/index.html b/web/frpc/index.html index 0c7caa7b..a893c9c1 100644 --- a/web/frpc/index.html +++ b/web/frpc/index.html @@ -3,6 +3,7 @@ + frp client diff --git a/web/frpc/package-lock.json b/web/frpc/package-lock.json index 204e6bf6..3ee94d8b 100644 --- a/web/frpc/package-lock.json +++ b/web/frpc/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "element-plus": "^2.13.0", + "pinia": "^3.0.4", "vue": "^3.5.26", "vue-router": "^4.6.4" }, @@ -2072,6 +2073,36 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-kit/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/eslint-config-prettier": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", @@ -2430,6 +2461,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2713,6 +2753,21 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4223,6 +4278,12 @@ "node": ">= 0.4" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4701,6 +4762,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4974,6 +5047,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -5582,6 +5661,36 @@ "node": ">=4" } }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -5859,6 +5968,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rolldown-string": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/rolldown-string/-/rolldown-string-0.2.1.tgz", @@ -6267,6 +6382,15 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6395,6 +6519,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/web/frpc/package.json b/web/frpc/package.json index 14eb419e..6e20d893 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "element-plus": "^2.13.0", + "pinia": "^3.0.4", "vue": "^3.5.26", "vue-router": "^4.6.4" }, diff --git a/web/frpc/src/App.vue b/web/frpc/src/App.vue index 44ebcbc9..b75c8a69 100644 --- a/web/frpc/src/App.vue +++ b/web/frpc/src/App.vue @@ -2,140 +2,160 @@
-
-
-
- -
- / - frp - Client - {{ - currentRouteName - }} -
- -
- - - - +
+ +
+
+ / + frp + Client
- + + + +
-
- -
+
+ +
- diff --git a/web/frpc/src/api/frpc.ts b/web/frpc/src/api/frpc.ts index 213c04fa..c7af90c0 100644 --- a/web/frpc/src/api/frpc.ts +++ b/web/frpc/src/api/frpc.ts @@ -5,7 +5,7 @@ import type { ProxyDefinition, VisitorListResp, VisitorDefinition, -} from '../types/proxy' +} from '../types' export const getStatus = () => { return http.get('/api/status') @@ -23,6 +23,19 @@ export const reloadConfig = () => { return http.get('/api/reload') } +// Config lookup API (any source) +export const getProxyConfig = (name: string) => { + return http.get( + `/api/proxy/${encodeURIComponent(name)}/config`, + ) +} + +export const getVisitorConfig = (name: string) => { + return http.get( + `/api/visitor/${encodeURIComponent(name)}/config`, + ) +} + // Store API - Proxies export const listStoreProxies = () => { return http.get('/api/store/proxies') diff --git a/web/frpc/src/assets/css/_form-layout.scss b/web/frpc/src/assets/css/_form-layout.scss new file mode 100644 index 00000000..a43fab6e --- /dev/null +++ b/web/frpc/src/assets/css/_form-layout.scss @@ -0,0 +1,33 @@ +@use './mixins' as *; + +/* Shared form layout styles for proxy/visitor form sections */ +.field-row { + display: grid; + gap: 16px; + align-items: start; +} + +.field-row.two-col { + grid-template-columns: 1fr 1fr; +} + +.field-row.three-col { + grid-template-columns: 1fr 1fr 1fr; +} + +.field-grow { + min-width: 0; +} + +.switch-field :deep(.el-form-item__content) { + min-height: 32px; + display: flex; + align-items: center; +} + +@include mobile { + .field-row.two-col, + .field-row.three-col { + grid-template-columns: 1fr; + } +} diff --git a/web/frpc/src/assets/css/_index.scss b/web/frpc/src/assets/css/_index.scss new file mode 100644 index 00000000..b70de87b --- /dev/null +++ b/web/frpc/src/assets/css/_index.scss @@ -0,0 +1,2 @@ +@forward './variables'; +@forward './mixins'; diff --git a/web/frpc/src/assets/css/_mixins.scss b/web/frpc/src/assets/css/_mixins.scss new file mode 100644 index 00000000..3861971b --- /dev/null +++ b/web/frpc/src/assets/css/_mixins.scss @@ -0,0 +1,49 @@ +@use './variables' as vars; + +@mixin mobile { + @media (max-width: #{vars.$breakpoint-mobile - 1px}) { + @content; + } +} + +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin flex-column { + display: flex; + flex-direction: column; +} + +@mixin page-scroll { + height: 100%; + overflow-y: auto; + padding: vars.$spacing-xl 40px; + + > * { + max-width: 960px; + margin: 0 auto; + } + + @include mobile { + padding: vars.$spacing-xl; + } +} + +@mixin custom-scrollbar { + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #d1d1d1; + border-radius: 3px; + } +} diff --git a/web/frpc/src/assets/css/_variables.scss b/web/frpc/src/assets/css/_variables.scss new file mode 100644 index 00000000..3df1ef19 --- /dev/null +++ b/web/frpc/src/assets/css/_variables.scss @@ -0,0 +1,61 @@ +// Typography +$font-size-xs: 11px; +$font-size-sm: 13px; +$font-size-md: 14px; +$font-size-lg: 15px; +$font-size-xl: 18px; + +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; + +// Colors - Text +$color-text-primary: var(--color-text-primary); +$color-text-secondary: var(--color-text-secondary); +$color-text-muted: var(--color-text-muted); +$color-text-light: var(--color-text-light); + +// Colors - Background +$color-bg-primary: var(--color-bg-primary); +$color-bg-secondary: var(--color-bg-secondary); +$color-bg-tertiary: var(--color-bg-tertiary); +$color-bg-muted: var(--color-bg-muted); +$color-bg-hover: var(--color-bg-hover); +$color-bg-active: var(--color-bg-active); + +// Colors - Border +$color-border: var(--color-border); +$color-border-light: var(--color-border-light); +$color-border-lighter: var(--color-border-lighter); + +// Colors - Status +$color-primary: var(--color-primary); +$color-danger: var(--color-danger); +$color-danger-dark: var(--color-danger-dark); +$color-danger-light: var(--color-danger-light); + +// Colors - Button +$color-btn-primary: var(--color-btn-primary); +$color-btn-primary-hover: var(--color-btn-primary-hover); + +// Spacing +$spacing-xs: 4px; +$spacing-sm: 8px; +$spacing-md: 12px; +$spacing-lg: 16px; +$spacing-xl: 20px; + +// Border Radius +$radius-sm: 6px; +$radius-md: 8px; + +// Transitions +$transition-fast: 0.15s ease; +$transition-medium: 0.2s ease; + +// Layout +$header-height: 50px; +$sidebar-width: 200px; + +// Breakpoints +$breakpoint-mobile: 768px; diff --git a/web/frpc/src/assets/css/custom.css b/web/frpc/src/assets/css/custom.css deleted file mode 100644 index ed128996..00000000 --- a/web/frpc/src/assets/css/custom.css +++ /dev/null @@ -1,105 +0,0 @@ -/* Modern Base Styles */ -* { - box-sizing: border-box; -} - -/* Smooth transitions for Element Plus components */ -.el-button, -.el-card, -.el-input, -.el-select, -.el-tag { - transition: all 0.3s ease; -} - -/* Card hover effects */ -.el-card:hover { - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08); -} - -/* Better scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; -} - -/* Better form layouts */ -.el-form-item { - margin-bottom: 18px; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .el-row { - margin-left: 0 !important; - margin-right: 0 !important; - } - - .el-col { - padding-left: 10px !important; - padding-right: 10px !important; - } -} - -/* Input enhancements */ -.el-input__wrapper { - transition: all 0.2s ease; -} - -.el-input__wrapper:hover { - box-shadow: 0 0 0 1px var(--el-border-color-hover) inset; -} - -/* Button enhancements */ -.el-button { - font-weight: 500; -} - -/* Tag enhancements */ -.el-tag { - font-weight: 500; -} - -/* Card enhancements */ -.el-card__header { - padding: 16px 20px; - border-bottom: 1px solid var(--el-border-color-lighter); -} - -.el-card__body { - padding: 20px; -} - -/* Table enhancements */ -.el-table { - font-size: 14px; -} - -.el-table th { - font-weight: 600; -} - -/* Empty state */ -.el-empty__description { - margin-top: 16px; - font-size: 14px; -} - -/* Loading state */ -.el-loading-mask { - border-radius: 12px; -} diff --git a/web/frpc/src/assets/css/dark.css b/web/frpc/src/assets/css/dark.css index 7c118fc3..1d090aec 100644 --- a/web/frpc/src/assets/css/dark.css +++ b/web/frpc/src/assets/css/dark.css @@ -1,48 +1,51 @@ -/* Dark Mode Theme */ +/* Dark mode styles */ html.dark { - --el-bg-color: #1e1e2e; - --el-bg-color-page: #1a1a2e; - --el-bg-color-overlay: #27293d; - --el-fill-color-blank: #1e1e2e; - background-color: #1a1a2e; + --el-bg-color: #212121; + --el-bg-color-page: #181818; + --el-bg-color-overlay: #303030; + --el-fill-color-blank: #212121; + --el-border-color: #404040; + --el-border-color-light: #353535; + --el-border-color-lighter: #2a2a2a; + --el-text-color-primary: #e5e7eb; + --el-text-color-secondary: #888888; + --el-text-color-placeholder: #afafaf; + background-color: #212121; + color-scheme: dark; } -html.dark body { - background-color: #1a1a2e; - color: #e5e7eb; +/* Scrollbar */ +html.dark ::-webkit-scrollbar { + width: 6px; + height: 6px; } -/* Dark mode scrollbar */ html.dark ::-webkit-scrollbar-track { - background: #27293d; + background: #303030; } html.dark ::-webkit-scrollbar-thumb { - background: #3a3d5c; + background: #404040; + border-radius: 3px; } html.dark ::-webkit-scrollbar-thumb:hover { - background: #4a4d6c; + background: #505050; } -/* Dark mode cards */ -html.dark .el-card { - background-color: #27293d; - border-color: #3a3d5c; +/* Form */ +html.dark .el-form-item__label { + color: #e5e7eb; } -html.dark .el-card__header { - border-bottom-color: #3a3d5c; -} - -/* Dark mode inputs */ +/* Input */ html.dark .el-input__wrapper { - background-color: #27293d; - box-shadow: 0 0 0 1px #3a3d5c inset; + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; } html.dark .el-input__wrapper:hover { - box-shadow: 0 0 0 1px #4a4d6c inset; + box-shadow: 0 0 0 1px #505050 inset; } html.dark .el-input__wrapper.is-focus { @@ -54,71 +57,44 @@ html.dark .el-input__inner { } html.dark .el-input__inner::placeholder { - color: #6b7280; + color: #afafaf; } -/* Dark mode textarea */ html.dark .el-textarea__inner { - background-color: #1e1e2d; - border-color: #3a3d5c; + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; color: #e5e7eb; } -html.dark .el-textarea__inner::placeholder { - color: #6b7280; +html.dark .el-textarea__inner:hover { + box-shadow: 0 0 0 1px #505050 inset; } -/* Dark mode table */ -html.dark .el-table { - background-color: #27293d; +html.dark .el-textarea__inner:focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +/* Select */ +html.dark .el-select__wrapper { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; +} + +html.dark .el-select__wrapper:hover { + box-shadow: 0 0 0 1px #505050 inset; +} + +html.dark .el-select__selected-item { color: #e5e7eb; } -html.dark .el-table th.el-table__cell { - background-color: #1e1e2e; - color: #e5e7eb; -} - -html.dark .el-table tr { - background-color: #27293d; -} - -html.dark .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell { - background-color: #1e1e2e; -} - -html.dark .el-table__row:hover > td.el-table__cell { - background-color: #2a2a3c !important; -} - -/* Dark mode tags */ -html.dark .el-tag--info { - background-color: #3a3d5c; - border-color: #3a3d5c; - color: #e5e7eb; -} - -/* Dark mode buttons */ -html.dark .el-button--default { - background-color: #27293d; - border-color: #3a3d5c; - color: #e5e7eb; -} - -html.dark .el-button--default:hover { - background-color: #2a2a3c; - border-color: #4a4d6c; - color: #fff; -} - -/* Dark mode select */ -html.dark .el-select .el-input__wrapper { - background-color: #27293d; +html.dark .el-select__placeholder { + color: #afafaf; } html.dark .el-select-dropdown { - background-color: #27293d; - border-color: #3a3d5c; + background: #303030; + border-color: #404040; } html.dark .el-select-dropdown__item { @@ -126,55 +102,92 @@ html.dark .el-select-dropdown__item { } html.dark .el-select-dropdown__item:hover { - background-color: #2a2a3c; + background: #3a3a3a; } -/* Dark mode dialog */ +html.dark .el-select-dropdown__item.is-selected { + color: var(--el-color-primary); +} + +html.dark .el-select-dropdown__item.is-disabled { + color: #666666; +} + +/* Tag */ +html.dark .el-tag--info { + background: #303030; + border-color: #404040; + color: #b0b0b0; +} + +/* Button */ +html.dark .el-button--default { + background: #303030; + border-color: #404040; + color: #e5e7eb; +} + +html.dark .el-button--default:hover { + background: #3a3a3a; + border-color: #505050; + color: #e5e7eb; +} + +/* Card */ +html.dark .el-card { + background: #212121; + border-color: #353535; + color: #b0b0b0; +} + +html.dark .el-card__header { + border-bottom-color: #353535; + color: #e5e7eb; +} + +/* Dialog */ html.dark .el-dialog { - background-color: #27293d; -} - -html.dark .el-dialog__header { - border-bottom-color: #3a3d5c; + background: #212121; } html.dark .el-dialog__title { color: #e5e7eb; } -html.dark .el-dialog__body { - color: #e5e7eb; +/* Message */ +html.dark .el-message { + background: #303030; + border-color: #404040; } -/* Dark mode message box */ -html.dark .el-message-box { - background-color: #27293d; - border-color: #3a3d5c; +html.dark .el-message--success { + background: #1e3d2e; + border-color: #3d6b4f; } -html.dark .el-message-box__title { - color: #e5e7eb; +html.dark .el-message--warning { + background: #3d3020; + border-color: #6b5020; } -html.dark .el-message-box__message { - color: #e5e7eb; +html.dark .el-message--error { + background: #3d2027; + border-color: #5c2d2d; } -/* Dark mode empty */ -html.dark .el-empty__description { - color: #9ca3af; -} - -/* Dark mode loading */ +/* Loading */ html.dark .el-loading-mask { - background-color: rgba(30, 30, 46, 0.9); + background-color: rgba(33, 33, 33, 0.9); } -html.dark .el-loading-text { - color: #e5e7eb; +/* Overlay */ +html.dark .el-overlay { + background-color: rgba(0, 0, 0, 0.6); } -/* Dark mode tooltip */ -html.dark .el-tooltip__trigger { - color: #e5e7eb; +/* Tooltip */ +html.dark .el-tooltip__popper { + background: #303030 !important; + border-color: #404040 !important; + color: #e5e7eb !important; } diff --git a/web/frpc/src/assets/css/var.css b/web/frpc/src/assets/css/var.css new file mode 100644 index 00000000..388a0521 --- /dev/null +++ b/web/frpc/src/assets/css/var.css @@ -0,0 +1,117 @@ +:root { + /* Text colors */ + --color-text-primary: #303133; + --color-text-secondary: #606266; + --color-text-muted: #909399; + --color-text-light: #c0c4cc; + --color-text-placeholder: #a8abb2; + + /* Background colors */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f9f9f9; + --color-bg-tertiary: #fafafa; + --color-bg-surface: #ffffff; + --color-bg-muted: #f4f4f5; + --color-bg-input: #ffffff; + --color-bg-hover: #efefef; + --color-bg-active: #eaeaea; + + /* Border colors */ + --color-border: #dcdfe6; + --color-border-light: #e4e7ed; + --color-border-lighter: #ebeef5; + --color-border-extra-light: #f2f6fc; + + /* Status colors */ + --color-primary: #409eff; + --color-primary-light: #ecf5ff; + --color-success: #67c23a; + --color-warning: #e6a23c; + --color-danger: #f56c6c; + --color-danger-dark: #c45656; + --color-danger-light: #fef0f0; + --color-info: #909399; + + /* Button colors */ + --color-btn-primary: #303133; + --color-btn-primary-hover: #4a4d5c; + + /* Element Plus mapping */ + --el-color-primary: var(--color-primary); + --el-color-success: var(--color-success); + --el-color-warning: var(--color-warning); + --el-color-danger: var(--color-danger); + --el-color-info: var(--color-info); + + --el-text-color-primary: var(--color-text-primary); + --el-text-color-regular: var(--color-text-secondary); + --el-text-color-secondary: var(--color-text-muted); + --el-text-color-placeholder: var(--color-text-placeholder); + + --el-bg-color: var(--color-bg-primary); + --el-bg-color-page: var(--color-bg-secondary); + --el-bg-color-overlay: var(--color-bg-primary); + + --el-border-color: var(--color-border); + --el-border-color-light: var(--color-border-light); + --el-border-color-lighter: var(--color-border-lighter); + --el-border-color-extra-light: var(--color-border-extra-light); + + --el-fill-color-blank: var(--color-bg-primary); + --el-fill-color-light: var(--color-bg-tertiary); + --el-fill-color: var(--color-bg-tertiary); + --el-fill-color-dark: var(--color-bg-hover); + --el-fill-color-darker: var(--color-bg-active); + + /* Input */ + --el-input-bg-color: var(--color-bg-input); + --el-input-border-color: var(--color-border); + --el-input-hover-border-color: var(--color-border-light); + + /* Dialog */ + --el-dialog-bg-color: var(--color-bg-primary); + --el-overlay-color: rgba(0, 0, 0, 0.5); +} + +html.dark { + /* Text colors */ + --color-text-primary: #e5e7eb; + --color-text-secondary: #b0b0b0; + --color-text-muted: #888888; + --color-text-light: #666666; + --color-text-placeholder: #afafaf; + + /* Background colors */ + --color-bg-primary: #212121; + --color-bg-secondary: #181818; + --color-bg-tertiary: #303030; + --color-bg-surface: #303030; + --color-bg-muted: #303030; + --color-bg-input: #2f2f2f; + --color-bg-hover: #3a3a3a; + --color-bg-active: #454545; + + /* Border colors */ + --color-border: #404040; + --color-border-light: #353535; + --color-border-lighter: #2a2a2a; + --color-border-extra-light: #222222; + + /* Status colors */ + --color-primary: #409eff; + --color-danger: #f87171; + --color-danger-dark: #f87171; + --color-danger-light: #3d2027; + --color-info: #888888; + + /* Button colors */ + --color-btn-primary: #404040; + --color-btn-primary-hover: #505050; + + /* Dark overrides */ + --el-text-color-regular: var(--color-text-primary); + --el-overlay-color: rgba(0, 0, 0, 0.7); + + background-color: #181818; + color-scheme: dark; +} diff --git a/web/frpc/src/components/ActionButton.vue b/web/frpc/src/components/ActionButton.vue new file mode 100644 index 00000000..6bbaae82 --- /dev/null +++ b/web/frpc/src/components/ActionButton.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/web/frpc/src/components/BaseDialog.vue b/web/frpc/src/components/BaseDialog.vue new file mode 100644 index 00000000..b1e85b5b --- /dev/null +++ b/web/frpc/src/components/BaseDialog.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/web/frpc/src/components/ConfigField.vue b/web/frpc/src/components/ConfigField.vue new file mode 100644 index 00000000..a6c2cdd4 --- /dev/null +++ b/web/frpc/src/components/ConfigField.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/web/frpc/src/components/ConfigSection.vue b/web/frpc/src/components/ConfigSection.vue new file mode 100644 index 00000000..1a795afb --- /dev/null +++ b/web/frpc/src/components/ConfigSection.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/web/frpc/src/components/ConfirmDialog.vue b/web/frpc/src/components/ConfirmDialog.vue new file mode 100644 index 00000000..cb23900e --- /dev/null +++ b/web/frpc/src/components/ConfirmDialog.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/web/frpc/src/components/FilterDropdown.vue b/web/frpc/src/components/FilterDropdown.vue new file mode 100644 index 00000000..a4db62db --- /dev/null +++ b/web/frpc/src/components/FilterDropdown.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/frpc/src/components/KeyValueEditor.vue b/web/frpc/src/components/KeyValueEditor.vue index 3472ebb4..9b467670 100644 --- a/web/frpc/src/components/KeyValueEditor.vue +++ b/web/frpc/src/components/KeyValueEditor.vue @@ -1,42 +1,51 @@ @@ -50,11 +59,13 @@ interface Props { modelValue: KVEntry[] keyPlaceholder?: string valuePlaceholder?: string + readonly?: boolean } const props = withDefaults(defineProps(), { keyPlaceholder: 'Key', valuePlaceholder: 'Value', + readonly: false, }) const emit = defineEmits<{ @@ -129,25 +140,45 @@ html.dark .kv-remove-btn:hover { display: inline-flex; align-items: center; gap: 6px; - padding: 6px 14px; - border: 1px dashed var(--el-border-color); - border-radius: 8px; + padding: 5px 12px; + border: 1px solid var(--color-border); + border-radius: 6px; background: transparent; - color: var(--el-text-color-secondary); + color: var(--color-text-secondary); font-size: 13px; cursor: pointer; - transition: all 0.2s; + transition: all 0.15s; align-self: flex-start; } .kv-add-btn svg { - width: 14px; - height: 14px; + width: 13px; + height: 13px; } .kv-add-btn:hover { - color: var(--el-color-primary); - border-color: var(--el-color-primary); - background: var(--el-color-primary-light-9); + background: var(--color-bg-hover); +} + +.kv-empty { + color: var(--el-text-color-secondary); + font-size: 13px; +} + +.kv-readonly-row { + display: flex; + gap: 8px; + padding: 4px 0; + font-size: 13px; +} + +.kv-readonly-key { + color: var(--el-text-color-secondary); + min-width: 100px; +} + +.kv-readonly-value { + color: var(--el-text-color-primary); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } diff --git a/web/frpc/src/components/PopoverMenu.vue b/web/frpc/src/components/PopoverMenu.vue new file mode 100644 index 00000000..8abd91ed --- /dev/null +++ b/web/frpc/src/components/PopoverMenu.vue @@ -0,0 +1,303 @@ + + + + + + + + + diff --git a/web/frpc/src/components/PopoverMenuItem.vue b/web/frpc/src/components/PopoverMenuItem.vue new file mode 100644 index 00000000..8df1e49b --- /dev/null +++ b/web/frpc/src/components/PopoverMenuItem.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue index f253ae15..9269c3a1 100644 --- a/web/frpc/src/components/ProxyCard.vue +++ b/web/frpc/src/components/ProxyCard.vue @@ -1,104 +1,49 @@ - diff --git a/web/frpc/src/views/Overview.vue b/web/frpc/src/views/Overview.vue deleted file mode 100644 index c8dfcdba..00000000 --- a/web/frpc/src/views/Overview.vue +++ /dev/null @@ -1,1160 +0,0 @@ - - - - - diff --git a/web/frpc/src/views/ProxyDetail.vue b/web/frpc/src/views/ProxyDetail.vue new file mode 100644 index 00000000..d87b72e8 --- /dev/null +++ b/web/frpc/src/views/ProxyDetail.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/web/frpc/src/views/ProxyEdit.vue b/web/frpc/src/views/ProxyEdit.vue index e94dce88..04183d2c 100644 --- a/web/frpc/src/views/ProxyEdit.vue +++ b/web/frpc/src/views/ProxyEdit.vue @@ -1,772 +1,71 @@ - - - diff --git a/web/frpc/src/views/ProxyList.vue b/web/frpc/src/views/ProxyList.vue new file mode 100644 index 00000000..5ac02463 --- /dev/null +++ b/web/frpc/src/views/ProxyList.vue @@ -0,0 +1,439 @@ + + + + + diff --git a/web/frpc/src/views/VisitorDetail.vue b/web/frpc/src/views/VisitorDetail.vue new file mode 100644 index 00000000..5816a373 --- /dev/null +++ b/web/frpc/src/views/VisitorDetail.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue index 429ed4ad..6a25785d 100644 --- a/web/frpc/src/views/VisitorEdit.vue +++ b/web/frpc/src/views/VisitorEdit.vue @@ -1,16 +1,18 @@ - - - diff --git a/web/frpc/src/views/VisitorList.vue b/web/frpc/src/views/VisitorList.vue new file mode 100644 index 00000000..b856c59e --- /dev/null +++ b/web/frpc/src/views/VisitorList.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/web/frpc/vite.config.mts b/web/frpc/vite.config.mts index cb8de991..39eb4d17 100644 --- a/web/frpc/vite.config.mts +++ b/web/frpc/vite.config.mts @@ -27,6 +27,14 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, + css: { + preprocessorOptions: { + scss: { + api: 'modern', + additionalData: `@use "@/assets/css/_index.scss" as *;`, + }, + }, + }, build: { assetsDir: '', chunkSizeWarningLimit: 1000, From 6cdef90113aafd2a85cf5aa885bcb9a2bdcbfa59 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 16 Mar 2026 23:22:42 +0800 Subject: [PATCH 40/43] web: bump dev dependencies in frpc and frps package-lock.json (#5239) --- .golangci.yml | 2 ++ web/frpc/package-lock.json | 42 +++++++++++++++++++------------------- web/frps/package-lock.json | 42 +++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4a8c92ec..89222745 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,6 +90,7 @@ linters: - third_party$ - builtin$ - examples$ + - node_modules formatters: enable: - gci @@ -112,6 +113,7 @@ formatters: - third_party$ - builtin$ - examples$ + - node_modules issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/web/frpc/package-lock.json b/web/frpc/package-lock.json index 3ee94d8b..9e857ee8 100644 --- a/web/frpc/package-lock.json +++ b/web/frpc/package-lock.json @@ -1660,16 +1660,6 @@ "win32" ] }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3958,9 +3948,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -4302,9 +4292,9 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -6135,6 +6125,16 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -6558,19 +6558,19 @@ } }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" diff --git a/web/frps/package-lock.json b/web/frps/package-lock.json index 287498dd..fb8f1e6b 100644 --- a/web/frps/package-lock.json +++ b/web/frps/package-lock.json @@ -1659,16 +1659,6 @@ "win32" ] }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3903,9 +3893,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -4241,9 +4231,9 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -6020,6 +6010,16 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -6422,19 +6422,19 @@ } }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" From 38a71a6803eec7040891a006f3d7a01bbfda9856 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 20 Mar 2026 03:33:44 +0800 Subject: [PATCH 41/43] web/frps: redesign frps dashboard with sidebar nav, responsive layout, and shared component workspace (#5246) --- .gitignore | 2 + web/frpc/Makefile | 2 +- web/frpc/components.d.ts | 6 - web/frpc/src/components/ConfigField.vue | 4 +- web/frpc/src/components/ProxyCard.vue | 6 +- web/frpc/src/views/ClientConfigure.vue | 7 +- web/frpc/src/views/ProxyDetail.vue | 2 +- web/frpc/src/views/ProxyEdit.vue | 7 +- web/frpc/src/views/ProxyList.vue | 13 +- web/frpc/src/views/VisitorDetail.vue | 2 +- web/frpc/src/views/VisitorEdit.vue | 7 +- web/frpc/src/views/VisitorList.vue | 16 +- web/frpc/tsconfig.json | 8 +- web/frpc/vite.config.mts | 8 +- web/frps/Makefile | 2 +- web/frps/components.d.ts | 5 +- web/frps/package-lock.json | 7646 ----------------- web/frps/src/App.vue | 487 +- web/frps/src/assets/css/dark.css | 184 +- web/frps/src/assets/css/var.css | 109 + web/frps/src/composables/useResponsive.ts | 8 + web/frps/src/main.ts | 2 +- web/frps/src/views/Proxies.vue | 139 +- web/frps/src/views/ProxyDetail.vue | 314 +- web/frps/tsconfig.json | 8 +- web/frps/vite.config.mts | 14 + web/{frpc => }/package-lock.json | 885 +- web/package.json | 5 + .../components/ActionButton.vue | 0 .../src => shared}/components/BaseDialog.vue | 9 +- .../components/ConfirmDialog.vue | 5 +- .../components/FilterDropdown.vue | 4 +- .../src => shared}/components/PopoverMenu.vue | 0 .../components/PopoverMenuItem.vue | 0 web/shared/css/_index.scss | 2 + web/shared/css/_mixins.scss | 49 + web/shared/css/_variables.scss | 61 + web/shared/package.json | 4 + 38 files changed, 1484 insertions(+), 8548 deletions(-) delete mode 100644 web/frps/package-lock.json create mode 100644 web/frps/src/assets/css/var.css create mode 100644 web/frps/src/composables/useResponsive.ts rename web/{frpc => }/package-lock.json (92%) create mode 100644 web/package.json rename web/{frpc/src => shared}/components/ActionButton.vue (100%) rename web/{frpc/src => shared}/components/BaseDialog.vue (93%) rename web/{frpc/src => shared}/components/ConfirmDialog.vue (92%) rename web/{frpc/src => shared}/components/FilterDropdown.vue (95%) rename web/{frpc/src => shared}/components/PopoverMenu.vue (100%) rename web/{frpc/src => shared}/components/PopoverMenuItem.vue (100%) create mode 100644 web/shared/css/_index.scss create mode 100644 web/shared/css/_mixins.scss create mode 100644 web/shared/css/_variables.scss create mode 100644 web/shared/package.json diff --git a/.gitignore b/.gitignore index 161c114e..1054d93f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ dist/ client.crt client.key +node_modules/ + # Cache *.swp diff --git a/web/frpc/Makefile b/web/frpc/Makefile index 6d2bdcf8..adf015e5 100644 --- a/web/frpc/Makefile +++ b/web/frpc/Makefile @@ -1,7 +1,7 @@ .PHONY: dist install build preview lint install: - @npm install + @cd .. && npm install build: install @npm run build diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index 4236f460..c58d0bca 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -7,11 +7,8 @@ export {} declare module 'vue' { export interface GlobalComponents { - ActionButton: typeof import('./src/components/ActionButton.vue')['default'] - BaseDialog: typeof import('./src/components/BaseDialog.vue')['default'] ConfigField: typeof import('./src/components/ConfigField.vue')['default'] ConfigSection: typeof import('./src/components/ConfigSection.vue')['default'] - ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] @@ -21,10 +18,7 @@ declare module 'vue' { ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] - FilterDropdown: typeof import('./src/components/FilterDropdown.vue')['default'] KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default'] - PopoverMenu: typeof import('./src/components/PopoverMenu.vue')['default'] - PopoverMenuItem: typeof import('./src/components/PopoverMenuItem.vue')['default'] ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default'] ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default'] ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default'] diff --git a/web/frpc/src/components/ConfigField.vue b/web/frpc/src/components/ConfigField.vue index a6c2cdd4..27719847 100644 --- a/web/frpc/src/components/ConfigField.vue +++ b/web/frpc/src/components/ConfigField.vue @@ -115,8 +115,8 @@ import { computed } from 'vue' import KeyValueEditor from './KeyValueEditor.vue' import StringListEditor from './StringListEditor.vue' -import PopoverMenu from './PopoverMenu.vue' -import PopoverMenuItem from './PopoverMenuItem.vue' +import PopoverMenu from '@shared/components/PopoverMenu.vue' +import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue' const props = withDefaults( defineProps<{ diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue index 9269c3a1..93f815a6 100644 --- a/web/frpc/src/components/ProxyCard.vue +++ b/web/frpc/src/components/ProxyCard.vue @@ -53,9 +53,9 @@ diff --git a/web/frps/tsconfig.json b/web/frps/tsconfig.json index 9e03e604..8795fb5c 100644 --- a/web/frps/tsconfig.json +++ b/web/frps/tsconfig.json @@ -18,8 +18,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"], + "@shared/*": ["../shared/*"] + } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/web/frps/vite.config.mts b/web/frps/vite.config.mts index 812cd10b..91f099f8 100644 --- a/web/frps/vite.config.mts +++ b/web/frps/vite.config.mts @@ -25,6 +25,20 @@ export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), + '@shared': fileURLToPath(new URL('../shared', import.meta.url)), + }, + dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'], + modules: [ + fileURLToPath(new URL('../node_modules', import.meta.url)), + 'node_modules', + ], + }, + css: { + preprocessorOptions: { + scss: { + api: 'modern', + additionalData: `@use "@shared/css/_index.scss" as *;`, + }, }, }, build: { diff --git a/web/frpc/package-lock.json b/web/package-lock.json similarity index 92% rename from web/frpc/package-lock.json rename to web/package-lock.json index 9e857ee8..15c6f2d9 100644 --- a/web/frpc/package-lock.json +++ b/web/package-lock.json @@ -1,10 +1,17 @@ { - "name": "frpc-dashboard", - "version": "0.0.1", + "name": "frp-web", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "frp-web", + "workspaces": [ + "shared", + "frpc", + "frps" + ] + }, + "frpc": { "name": "frpc-dashboard", "version": "0.0.1", "dependencies": { @@ -35,6 +42,36 @@ "vue-tsc": "^3.2.2" } }, + "frps": { + "name": "frps-dashboard", + "version": "0.0.1", + "dependencies": { + "element-plus": "^2.13.0", + "vue": "^3.5.26", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "24", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "@vueuse/core": "^14.1.0", + "eslint": "^9.39.0", + "eslint-plugin-vue": "^9.33.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.7.4", + "sass": "^1.97.2", + "terser": "^5.44.1", + "typescript": "^5.9.3", + "unplugin-auto-import": "^0.17.5", + "unplugin-element-plus": "^0.11.2", + "unplugin-vue-components": "^0.26.0", + "vite": "^7.3.0", + "vite-svg-loader": "^5.1.0", + "vue-tsc": "^3.2.2" + } + }, "node_modules/@antfu/utils": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", @@ -64,9 +101,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -92,12 +129,12 @@ } }, "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/@element-plus/icons-vue": { @@ -110,9 +147,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -127,9 +164,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -144,9 +181,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -161,9 +198,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -178,9 +215,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -195,9 +232,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -212,9 +249,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -229,9 +266,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -246,9 +283,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -263,9 +300,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -280,9 +317,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -297,9 +334,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -314,9 +351,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -331,9 +368,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -348,9 +385,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -365,9 +402,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -382,9 +419,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -399,9 +436,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -416,9 +453,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -433,9 +470,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -450,9 +487,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -467,9 +504,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -484,9 +521,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -501,9 +538,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -518,9 +555,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -535,9 +572,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -581,15 +618,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -653,9 +690,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -666,7 +703,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -708,9 +745,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -745,28 +782,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -919,6 +956,76 @@ "node": ">= 8" } }, + "node_modules/@nuxt/kit": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.2.tgz", + "integrity": "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "c12": "^3.3.3", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.8", + "ignore": "^7.0.5", + "jiti": "^2.6.1", + "klona": "^2.0.6", + "knitwork": "^1.3.0", + "mlly": "^1.8.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "rc9": "^3.0.0", + "scule": "^1.3.0", + "semver": "^7.7.4", + "tinyglobby": "^0.2.15", + "ufo": "^1.6.3", + "unctx": "^2.5.0", + "untyped": "^2.0.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@nuxt/kit/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@nuxt/kit/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@nuxt/kit/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -1690,9 +1797,9 @@ } }, "node_modules/@types/node": { - "version": "24.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", - "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1707,17 +1814,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1730,7 +1837,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1746,16 +1853,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3" }, "engines": { @@ -1771,14 +1878,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", "debug": "^4.4.3" }, "engines": { @@ -1793,14 +1900,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1811,9 +1918,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", "dev": true, "license": "MIT", "engines": { @@ -1828,15 +1935,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1853,9 +1960,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", "dev": true, "license": "MIT", "engines": { @@ -1867,16 +1974,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1895,16 +2002,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1919,13 +2026,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1950,9 +2057,9 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", - "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", "dev": true, "license": "MIT", "dependencies": { @@ -1962,7 +2069,7 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, @@ -1996,13 +2103,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", - "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.29", + "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -2021,47 +2128,50 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", - "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", - "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.29", - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", - "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } }, "node_modules/@vue/devtools-kit": { "version": "7.7.9", @@ -2078,12 +2188,6 @@ "superjson": "^2.2.2" } }, - "node_modules/@vue/devtools-kit/node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT" - }, "node_modules/@vue/devtools-shared": { "version": "7.7.9", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", @@ -2135,9 +2239,9 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", - "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", "dev": true, "license": "MIT", "dependencies": { @@ -2164,53 +2268,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", - "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.29" + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", - "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", - "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/runtime-core": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", - "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.29" + "vue": "3.5.30" } }, "node_modules/@vue/shared": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", - "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -2552,6 +2656,13 @@ "dev": true, "license": "MIT" }, + "node_modules/c12/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/c12/node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -2927,9 +3038,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -3106,18 +3217,18 @@ } }, "node_modules/element-plus": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.3.tgz", - "integrity": "sha512-RwLVtFpeHjZ4UCtHxVi1/sGR2cr2xOL7hqWa7qJc/+gdO6QavVG8Nw1C647obCb3tIg2ztMhNbIIjZUv+6z1og==", + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.5.tgz", + "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", "license": "MIT", "dependencies": { - "@ctrl/tinycolor": "^3.4.1", + "@ctrl/tinycolor": "^4.2.0", "@element-plus/icons-vue": "^2.3.2", "@floating-ui/dom": "^1.0.1", "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", "@types/lodash": "^4.17.20", "@types/lodash-es": "^4.17.12", - "@vueuse/core": "^10.11.0", + "@vueuse/core": "12.0.0", "async-validator": "^4.2.5", "dayjs": "^1.11.19", "lodash": "^4.17.23", @@ -3137,93 +3248,41 @@ "license": "MIT" }, "node_modules/element-plus/node_modules/@vueuse/core": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/element-plus/node_modules/@vueuse/metadata": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/element-plus/node_modules/@vueuse/shared": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", - "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", "license": "MIT", "dependencies": { - "vue-demi": ">=0.14.8" + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3398,9 +3457,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3411,32 +3470,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escape-string-regexp": { @@ -3453,25 +3512,25 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -3490,7 +3549,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3948,9 +4007,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3970,6 +4029,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/frp-shared": { + "resolved": "shared", + "link": true + }, + "node_modules/frpc-dashboard": { + "resolved": "frpc", + "link": true + }, + "node_modules/frps-dashboard": { + "resolved": "frps", + "link": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5044,16 +5115,16 @@ "license": "MIT" }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "ufo": "^1.6.3" } }, "node_modules/ms": { @@ -5603,10 +5674,9 @@ "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, "node_modules/picocolors": { @@ -5672,15 +5742,6 @@ } } }, - "node_modules/pinia/node_modules/@vue/devtools-api": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^7.7.9" - } - }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -5704,9 +5765,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -6105,14 +6166,14 @@ } }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -6126,9 +6187,9 @@ } }, "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -6610,9 +6671,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6629,9 +6690,9 @@ } }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -6700,9 +6761,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6831,16 +6892,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7135,9 +7196,9 @@ } }, "node_modules/unplugin-element-plus/node_modules/@nuxt/kit": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.1.tgz", - "integrity": "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz", + "integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==", "dev": true, "license": "MIT", "dependencies": { @@ -7150,7 +7211,7 @@ "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", - "mlly": "^1.8.0", + "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", @@ -7493,13 +7554,14 @@ } }, "node_modules/vite-svg-loader": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz", - "integrity": "sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.1.tgz", + "integrity": "sha512-RPzcXA/EpKJA0585x58DBgs7my2VfeJ+j2j1EoHY4Zh82Y7hV4cR1fElgy2aZi85+QSrcLLoTStQ5uZjD68u+Q==", "dev": true, "license": "MIT", "dependencies": { - "svgo": "^3.0.2" + "debug": "^4.3.4", + "svgo": "^3.3.3" }, "peerDependencies": { "vue": ">=3.2.13" @@ -7544,16 +7606,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", - "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-sfc": "3.5.29", - "@vue/runtime-dom": "3.5.29", - "@vue/server-renderer": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" @@ -7616,15 +7678,21 @@ "vue": "^3.5.0" } }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/vue-tsc": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", - "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.2.5" + "@vue/language-core": "3.2.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -7777,6 +7845,9 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "shared": { + "name": "frp-shared" } } } diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..5df16a59 --- /dev/null +++ b/web/package.json @@ -0,0 +1,5 @@ +{ + "name": "frp-web", + "private": true, + "workspaces": ["shared", "frpc", "frps"] +} diff --git a/web/frpc/src/components/ActionButton.vue b/web/shared/components/ActionButton.vue similarity index 100% rename from web/frpc/src/components/ActionButton.vue rename to web/shared/components/ActionButton.vue diff --git a/web/frpc/src/components/BaseDialog.vue b/web/shared/components/BaseDialog.vue similarity index 93% rename from web/frpc/src/components/BaseDialog.vue rename to web/shared/components/BaseDialog.vue index b1e85b5b..29dc0496 100644 --- a/web/frpc/src/components/BaseDialog.vue +++ b/web/shared/components/BaseDialog.vue @@ -21,7 +21,6 @@ diff --git a/web/frpc/src/components/ConfirmDialog.vue b/web/shared/components/ConfirmDialog.vue similarity index 92% rename from web/frpc/src/components/ConfirmDialog.vue rename to web/shared/components/ConfirmDialog.vue index cb23900e..4de6975a 100644 --- a/web/frpc/src/components/ConfirmDialog.vue +++ b/web/shared/components/ConfirmDialog.vue @@ -5,6 +5,7 @@ width="400px" :close-on-click-modal="false" :append-to-body="true" + :is-mobile="isMobile" >

{{ message }}