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 e8cc1fe9..4e42f0fe 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,19 +17,19 @@ 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: 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 + 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 502d0032..ebc0dca9 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,15 +15,15 @@ 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' - 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/.gitignore b/.gitignore index c6480f59..1054d93f 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 @@ -37,8 +25,12 @@ dist/ client.crt client.key +node_modules/ + # Cache *.swp # AI -CLAUDE.md +.claude/ +.sisyphus/ +.superpowers/ diff --git a/.golangci.yml b/.golangci.yml index f7c0e8bd..89222745 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,7 @@ linters: - lll - makezero - misspell + - modernize - prealloc - predeclared - revive @@ -33,13 +34,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: @@ -53,6 +48,9 @@ linters: ignore-rules: - cancelled - marshalled + modernize: + disable: + - omitzero unparam: check-exported: false exclusions: @@ -77,6 +75,9 @@ linters: - linters: - revive text: "avoid meaningless package names" + - linters: + - revive + text: "Go standard library package names" - linters: - unparam text: is always false @@ -89,6 +90,7 @@ linters: - third_party$ - builtin$ - examples$ + - node_modules formatters: enable: - gci @@ -111,6 +113,7 @@ formatters: - third_party$ - builtin$ - examples$ + - node_modules issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..23f0e734 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# 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` + +## Agent Runbooks + +Operational procedures for agents are in `doc/agents/`: +- `doc/agents/release.md` - Release process 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 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/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..9bee09dc 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,10 @@ ## 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. +* 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/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/client/api/controller.go b/client/api/controller.go deleted file mode 100644 index 6874b724..00000000 --- a/client/api/controller.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2025 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 api - -import ( - "cmp" - "fmt" - "net" - "net/http" - "os" - "slices" - "strconv" - "time" - - "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) -} - -// 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) -} - -// 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, - } -} - -// Reload handles GET /api/reload -func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { - strictConfigMode := false - strictStr := ctx.Query("strictConfig") - if strictStr != "" { - 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 := 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) - return nil, nil -} - -// Status handles GET /api/status -func (c *Controller) Status(ctx *httppkg.Context) (any, error) { - res := make(StatusResp) - ps := c.getProxyStatus() - if ps == nil { - return res, nil - } - - for _, status := range ps { - res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status)) - } - - for _, arrs := range res { - if len(arrs) <= 1 { - continue - } - slices.SortFunc(arrs, func(a, b ProxyStatusResp) int { - return cmp.Compare(a.Name, b.Name) - }) - } - return res, nil -} - -// 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) - if err != nil { - log.Warnf("load frpc config file error: %s", err.Error()) - return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) - } - return string(content), nil -} - -// PutConfig handles PUT /api/config -func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) { - body, err := ctx.Body() - if err != nil { - return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err)) - } - - if len(body) == 0 { - 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)) - } - return nil, nil -} - -// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus -func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp { - psr := ProxyStatusResp{ - Name: status.Name, - Type: status.Type, - Status: status.Phase, - Err: status.Err, - } - baseCfg := status.Cfg.GetBaseConfig() - if baseCfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)) - } - psr.Plugin = baseCfg.Plugin.Type - - if status.Err == "" { - psr.RemoteAddr = status.RemoteAddr - if slices.Contains([]string{"tcp", "udp"}, status.Type) { - psr.RemoteAddr = c.serverAddr + psr.RemoteAddr - } - } - return psr -} diff --git a/client/admin_api.go b/client/api_router.go similarity index 55% rename from client/admin_api.go rename to client/api_router.go index 09936352..82b73046 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" @@ -38,6 +38,22 @@ 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) + 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))), @@ -51,14 +67,11 @@ func healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) } -func newAPIController(svr *Service) *api.Controller { - return api.NewController(api.ControllerParams{ - GetProxyStatus: svr.getAllProxyStatus, - ServerAddr: svr.common.ServerAddr, - ConfigFilePath: svr.configFilePath, - UnsafeFeatures: svr.unsafeFeatures, - UpdateConfig: svr.UpdateAllConfigurer, - GracefulClose: svr.GracefulClose, +func newAPIController(svr *Service) *adminapi.Controller { + manager := newServiceConfigManager(svr) + return adminapi.NewController(adminapi.ControllerParams{ + ServerAddr: svr.common.ServerAddr, + Manager: manager, }) } diff --git a/client/config_manager.go b/client/config_manager.go new file mode 100644 index 00000000..24512e9f --- /dev/null +++ b/client/config_manager.go @@ -0,0 +1,464 @@ +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) 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 + } + + 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) (v1.ProxyConfigurer, error) { + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + 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) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + log.Infof("store: created proxy %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + 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) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: updated proxy %q", name) + return persisted, 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) (v1.VisitorConfigurer, error) { + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + 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) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: created visitor %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + 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) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: updated visitor %q", name) + return persisted, 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) 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") + } + 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..07ae3297 --- /dev/null +++ b/client/config_manager_test.go @@ -0,0 +1,137 @@ +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{}, + }, + } + + 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 { + 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..5a51a5e5 --- /dev/null +++ b/client/configmgmt/types.go @@ -0,0 +1,45 @@ +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 + + 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) + 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) (v1.VisitorConfigurer, error) + UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (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/http/controller.go b/client/http/controller.go new file mode 100644 index 00000000..57e8165d --- /dev/null +++ b/client/http/controller.go @@ -0,0 +1,433 @@ +// Copyright 2025 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 ( + "cmp" + "errors" + "fmt" + "net" + "net/http" + "slices" + "strconv" + "time" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/http/model" + "github.com/fatedier/frp/client/proxy" + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +// Controller handles HTTP API requests for frpc. +type Controller struct { + serverAddr string + manager configmgmt.ConfigManager +} + +// ControllerParams contains parameters for creating an APIController. +type ControllerParams struct { + ServerAddr string + Manager configmgmt.ConfigManager +} + +func NewController(params ControllerParams) *Controller { + return &Controller{ + 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()) +} + +// Reload handles GET /api/reload +func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { + strictConfigMode := false + strictStr := ctx.Query("strictConfig") + if strictStr != "" { + strictConfigMode, _ = strconv.ParseBool(strictStr) + } + + if err := c.manager.ReloadFromFile(strictConfigMode); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +// Stop handles POST /api/stop +func (c *Controller) Stop(ctx *httppkg.Context) (any, error) { + 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(model.StatusResp) + ps := c.manager.GetProxyStatus() + if ps == nil { + return res, nil + } + + for _, status := range ps { + res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status)) + } + + for _, arrs := range res { + if len(arrs) <= 1 { + continue + } + slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int { + return cmp.Compare(a.Name, b.Name) + }) + } + return res, nil +} + +// GetConfig handles GET /api/config +func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) { + content, err := c.manager.ReadConfigFile() + if err != nil { + return nil, c.toHTTPError(err) + } + return content, nil +} + +// PutConfig handles PUT /api/config +func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err)) + } + + if len(body) == 0 { + return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty") + } + + if err := c.manager.WriteConfigFile(body); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp { + psr := model.ProxyStatusResp{ + Name: status.Name, + Type: status.Type, + Status: status.Phase, + Err: status.Err, + } + baseCfg := status.Cfg.GetBaseConfig() + if baseCfg.LocalPort != 0 { + psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)) + } + psr.Plugin = baseCfg.Plugin.Type + + if status.Err == "" { + psr.RemoteAddr = status.RemoteAddr + if slices.Contains([]string{"tcp", "udp"}, status.Type) { + psr.RemoteAddr = c.serverAddr + psr.RemoteAddr + } + } + + if c.manager.IsStoreProxyEnabled(status.Name) { + psr.Source = model.SourceStore + } + 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 { + return nil, c.toHTTPError(err) + } + + resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))} + for _, p := range proxies { + payload, err := model.ProxyDefinitionFromConfigurer(p) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + 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 +} + +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) + } + + payload, err := model.ProxyDefinitionFromConfigurer(p) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return payload, 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 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 err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + 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) + } + + 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) { + 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 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 err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + 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) + } + + 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) { + 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 := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))} + for _, v := range visitors { + payload, err := model.VisitorDefinitionFromConfigurer(v) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + 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 +} + +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) + } + + payload, err := model.VisitorDefinitionFromConfigurer(v) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return payload, 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 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 err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + 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) + } + + 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) { + 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 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 err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + 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) + } + + 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) { + 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 +} diff --git a/client/http/controller_test.go b/client/http/controller_test.go new file mode 100644 index 00000000..719fbcd4 --- /dev/null +++ b/client/http/controller_test.go @@ -0,0 +1,662 @@ +package http + +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/http/model" + "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 + 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) + 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) (v1.VisitorConfigurer, error) + updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (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) 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() + } + 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) (v1.ProxyConfigurer, error) { + if m.createStoreProxyFn != nil { + return m.createStoreProxyFn(cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if m.updateStoreProxyFn != nil { + return m.updateStoreProxyFn(name, cfg) + } + return cfg, 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) (v1.VisitorConfigurer, error) { + if m.createStoreVisitFn != nil { + return m.createStoreVisitFn(cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if m.updateStoreVisitFn != nil { + return m.updateStoreVisitFn(name, cfg) + } + return cfg, 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 newRawTCPProxyConfig(name string) *v1.TCPProxyConfig { + return &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 10080, + }, + }, + } +} + +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 := []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) (v1.ProxyConfigurer, error) { + return nil, tc.err + }, + }, + } + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, tc.expectedCode) + }) + } +} + +func TestStoreVisitorErrorMapping(t *testing.T) { + 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) + + 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 TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) { + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + gotName = cfg.GetBaseConfig().Name + return cfg, nil + }, + }, + } + + 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) + + 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) + } + + 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 TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) { + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + gotName = cfg.GetBaseConfig().Name + return cfg, nil + }, + }, + } + + 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) + + 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) + } + + 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) + } +} + +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) + } +} + +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) + } +} + +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/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 72% rename from client/api/types.go rename to client/http/model/types.go index d7c930d0..63704850 100644 --- a/client/api/types.go +++ b/client/http/model/types.go @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package api +package model + +const SourceStore = "store" // StatusResp is the response for GET /api/status type StatusResp map[string][]ProxyStatusResp @@ -26,4 +28,15 @@ 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" +} + +// ProxyListResp is the response for GET /api/store/proxies +type ProxyListResp struct { + Proxies []ProxyDefinition `json:"proxies"` +} + +// VisitorListResp is the response for GET /api/store/visitors +type VisitorListResp struct { + 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/client/proxy/proxy.go b/client/proxy/proxy.go index 8faff38d..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), @@ -209,6 +226,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 } @@ -219,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/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/sudp.go b/client/proxy/sudp.go index 3a7af19c..21e34a41 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -17,7 +17,6 @@ package proxy import ( - "io" "net" "reflect" "strconv" @@ -25,17 +24,15 @@ 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" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) } type SUDPProxy struct { @@ -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 1fca9904..8547ef72 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -17,24 +17,21 @@ 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" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) } type UDPProxy struct { @@ -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 @@ -129,7 +112,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 +128,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/proxy/xtcp.go b/client/proxy/xtcp.go index 6e1deac3..41dc5229 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -27,13 +27,14 @@ 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" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy) + RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) } type XTCPProxy struct { @@ -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..5c51fd67 100644 --- a/client/service.go +++ b/client/service.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "net" + "net/http" "os" "runtime" "sync" @@ -29,6 +30,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 +64,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 +124,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 @@ -146,6 +163,28 @@ func NewService(options ServiceOptions) (*Service, error) { return nil, err } + authRuntime, err := auth.BuildClientAuth(&options.Common.Auth) + if err != nil { + 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) + + // 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) @@ -155,24 +194,24 @@ func NewService(options ServiceOptions) (*Service, error) { webServer = ws } - authRuntime, err := auth.BuildClientAuth(&options.Common.Auth) - if err != nil { - return nil, err - } - 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) } @@ -193,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) } }() @@ -219,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) } @@ -403,6 +446,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 +485,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 { @@ -423,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) { @@ -436,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, @@ -453,3 +549,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..29f141a1 --- /dev/null +++ b/client/service_test.go @@ -0,0 +1,246 @@ +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"} + + 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..03ec51fe 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -15,17 +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/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -41,10 +36,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() @@ -56,35 +51,10 @@ 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 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) @@ -95,61 +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() - newVisitorConnMsg := &msg.NewVisitorConn{ - RunID: sv.helper.RunID(), - ProxyName: sv.cfg.ServerName, - 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 284aee10..6014161c 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -16,20 +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/proto/udp" netpkg "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -75,6 +72,7 @@ func (sv *SUDPVisitor) dispatcher() { var ( visitorConn net.Conn + recycleFn func() err error firstPacket *msg.UDPPacket @@ -92,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: @@ -146,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 @@ -168,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 { @@ -183,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 } @@ -197,52 +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() - newVisitorConnMsg := &msg.NewVisitorConn{ - RunID: sv.helper.RunID(), - ProxyName: sv.cfg.ServerName, - 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 { - 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 { - return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err) - } - _ = visitorConn.SetReadDeadline(time.Time{}) - - if newVisitorConnRespMsg.Error != "" { - 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) - 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 87e4f29f..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" ) @@ -119,6 +127,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() @@ -130,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/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/client/visitor/xtcp.go b/client/visitor/xtcp.go index cdfeb1ab..e7a60895 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" @@ -64,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) @@ -92,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 { @@ -205,20 +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) - 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") @@ -280,8 +251,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 +282,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, @@ -371,6 +343,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/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/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/doc/agents/release.md b/doc/agents/release.md new file mode 100644 index 00000000..d8ec6267 --- /dev/null +++ b/doc/agents/release.md @@ -0,0 +1,80 @@ +# Release Process + +## 1. Update Release Notes + +Edit `Release.md` in the project root with the changes for this version: + +```markdown +## Features +* ... + +## Improvements +* ... + +## Fixes +* ... +``` + +This file is used by GoReleaser as the GitHub Release body. + +## 2. Bump Version + +Update the version string in `pkg/util/version/version.go`: + +```go +var version = "0.X.0" +``` + +Commit and push to `dev`: + +```bash +git add pkg/util/version/version.go Release.md +git commit -m "bump version to vX.Y.Z" +git push origin dev +``` + +## 3. Merge dev → master + +Create a PR from `dev` to `master`: + +```bash +gh pr create --base master --head dev --title "bump version" +``` + +Wait for CI to pass, then merge using **merge commit** (not squash). + +## 4. Tag the Release + +```bash +git checkout master +git pull origin master +git tag -a vX.Y.Z -m "bump version" +git push origin vX.Y.Z +``` + +## 5. Trigger GoReleaser + +Manually trigger the `goreleaser` workflow in GitHub Actions: + +```bash +gh workflow run goreleaser --ref master +``` + +GoReleaser will: +1. Run `package.sh` to cross-compile all platforms and create archives +2. Create a GitHub Release with all packages, using `Release.md` as release notes + +## Key Files + +| File | Purpose | +|------|---------| +| `pkg/util/version/version.go` | Version string | +| `Release.md` | Release notes (read by GoReleaser) | +| `.goreleaser.yml` | GoReleaser config | +| `package.sh` | Cross-compile and packaging script | +| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) | + +## Versioning + +- Minor release: `v0.X.0` +- Patch release: `v0.X.Y` (e.g., `v0.62.1`) 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/auth/oidc.go b/pkg/auth/oidc.go index d9377f32..826a6715 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -23,12 +23,14 @@ import ( "net/url" "os" "slices" + "sync" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" "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" ) @@ -74,14 +76,64 @@ 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) +} + +// 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 - tokenGenerator *clientcredentials.Config - httpClient *http.Client + tokenSource oauth2.TokenSource } 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} @@ -99,30 +151,42 @@ 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. + // + // 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, - tokenGenerator: tokenGenerator, - httpClient: httpClient, + tokenSource: &oidcTokenSource{ + source: cachingSource, + fallbackCfg: tokenGenerator, + fallbackCtx: ctx, + }, }, 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) } @@ -205,7 +269,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 +291,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri return &OidcAuthConsumer{ additionalAuthScopes: additionalAuthScopes, verifier: verifier, - subjectsFromLogin: []string{}, + subjectsFromLogin: make(map[string]struct{}), } } @@ -235,9 +300,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 +311,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/auth/oidc_test.go b/pkg/auth/oidc_test.go index 58054186..70e59883 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,188 @@ 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 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) + 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 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) { + 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 no longer fetches eagerly; first SetLogin triggers the probe. + r.Equal(int32(0), requestCount.Load()) + + // SetLogin triggers the adaptive probe and caches the 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") +} + +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/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/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/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/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/load.go b/pkg/config/load.go index 6e8c251d..38634fc5 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -17,6 +17,7 @@ package config import ( "bytes" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -33,6 +34,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" ) @@ -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 @@ -129,48 +145,136 @@ 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 { - v1.DisallowUnknownFieldsMu.Lock() - defer v1.DisallowUnknownFieldsMu.Unlock() - v1.DisallowUnknownFields = strict +// 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 + parsedFromTOML := false 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) + 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. + return formatTOMLError(tomlErr) } + // 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() + if err := decodeJSONContent(b, c, strict); err != nil { + return enhanceDecodeError(err, originalBytes, !parsedFromTOML) } - return decoder.Decode(c) + 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, !parsedFromTOML) + } + 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 strictErr + } + return err +} + +// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors. +func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error { + var typeErr *json.UnmarshalTypeError + if errors.As(err, &typeErr) && typeErr.Field != "" { + 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) + } + 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 { + 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, ".") + 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)) @@ -180,7 +284,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 +323,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 +467,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..b711a5c1 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" @@ -188,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) { @@ -273,6 +299,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) @@ -306,3 +495,111 @@ serverPort: 7000 require.Equal("127.0.0.1", clientCfg.ServerAddr) require.Equal(7000, clientCfg.ServerPort) } + +func TestTOMLSyntaxErrorWithPosition(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(), "toml") + require.Contains(err.Error(), "line") + require.Contains(err.Error(), "column") +} + +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") + require.NotContains(errMsg, "line") +} + +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) +} diff --git a/pkg/config/source/aggregator.go b/pkg/config/source/aggregator.go new file mode 100644 index 00000000..58496932 --- /dev/null +++ b/pkg/config/source/aggregator.go @@ -0,0 +1,109 @@ +// 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 ( + "cmp" + "errors" + "fmt" + "maps" + "slices" + "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 := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int { + return cmp.Compare(x.GetBaseConfig().Name, y.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 new file mode 100644 index 00000000..380c05cf --- /dev/null +++ b/pkg/config/source/aggregator_test.go @@ -0,0 +1,238 @@ +// 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_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) + + 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..d1bf4fb5 --- /dev/null +++ b/pkg/config/source/store.go @@ -0,0 +1,367 @@ +// 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" + "os" + "path/filepath" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +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 + } + + 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 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 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 +} + +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 := jsonx.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..bb5382b0 --- /dev/null +++ b/pkg/config/source/store_test.go @@ -0,0 +1,121 @@ +// 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 ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +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 := jsonx.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_UnknownFieldsAreIgnored(t *testing.T) { + require := require.New(t) + + 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")) +} 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 8fa3105a..972908ed 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)) @@ -143,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/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) 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..41bd9a4c 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -15,23 +15,11 @@ package v1 import ( - "sync" + "maps" "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. -// -// https://github.com/golang/go/issues/41144 -// https://github.com/golang/go/discussions/63397 -var ( - DisallowUnknownFields = false - DisallowUnknownFieldsMu sync.Mutex -) - type AuthScope string const ( @@ -104,6 +92,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 +134,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/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 1bbe5ac3..e7d1a6b7 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" - - "github.com/samber/lo" + "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" ) @@ -102,11 +99,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 +131,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) @@ -175,40 +199,24 @@ 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 { - 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) @@ -231,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 { @@ -271,6 +279,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 +305,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 +354,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 +385,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 +432,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 +462,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 +495,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 +525,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..0d6ca3d0 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" ) @@ -40,20 +37,21 @@ 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 { Complete() + Clone() ClientPluginOptions } type TypedClientPluginOptions struct { @@ -61,43 +59,25 @@ type TypedClientPluginOptions struct { ClientPluginOptions } -func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return nil +func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions { + out := c + if c.ClientPluginOptions != nil { + out.ClientPluginOptions = c.ClientPluginOptions.Clone() } + return out +} - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { +func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { + 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 { @@ -109,6 +89,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 +106,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 +128,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 +152,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 +171,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 +188,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 +206,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 +221,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 +238,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/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/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) } } diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 5017f57d..d1035d4f 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -15,14 +15,9 @@ 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" ) @@ -52,31 +47,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 @@ -88,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 { @@ -99,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 { @@ -146,12 +120,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 +154,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..6541e3de 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 ( @@ -27,11 +25,12 @@ const ( ) var visitorPluginOptionsTypeMap = map[string]reflect.Type{ - VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}), + VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](), } type VisitorPluginOptions interface { Complete() + Clone() VisitorPluginOptions } type TypedVisitorPluginOptions struct { @@ -39,43 +38,25 @@ type TypedVisitorPluginOptions struct { VisitorPluginOptions } -func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { - if len(b) == 4 && string(b) == "null" { - return nil +func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions { + out := c + if c.VisitorPluginOptions != nil { + out.VisitorPluginOptions = c.VisitorPluginOptions.Clone() } + return out +} - typeStruct := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &typeStruct); err != nil { +func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { + 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 { @@ -84,3 +65,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/metrics/mem/server.go b/pkg/metrics/mem/server.go index 677788d3..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 } } @@ -203,6 +199,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 +227,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 } @@ -237,31 +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 = &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") - } - break + proxyStats, ok := m.info.ProxyStatistics[proxyName] + if ok && proxyStats.ProxyType == proxyType { + res = toProxyStats(proxyName, proxyStats) } return } @@ -272,21 +249,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/msg/msg.go b/pkg/msg/msg.go index 7b36c2aa..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 @@ -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/naming/names.go b/pkg/naming/names.go new file mode 100644 index 00000000..3ca4dd9c --- /dev/null +++ b/pkg/naming/names.go @@ -0,0 +1,32 @@ +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 + } + if trimmed, ok := strings.CutPrefix(name, user+"."); ok { + return trimmed + } + 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/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/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/nathole/controller.go b/pkg/nathole/controller.go index c08c81c9..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 @@ -375,7 +377,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/nathole/nathole.go b/pkg/nathole/nathole.go index 72522fac..bea3d371 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 } @@ -408,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) @@ -418,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/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/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/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) { 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 c5fd684b..f27aecf3 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" @@ -92,10 +93,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 - } + known := maps.Clone(defaultFeatures) f := &featureGate{} f.known.Store(known) @@ -109,14 +107,8 @@ func (f *featureGate) SetFromMap(m map[string]bool) error { defer f.lock.Unlock() // Copy existing state - known := map[Feature]FeatureSpec{} - for k, v := range f.known.Load().(map[Feature]FeatureSpec) { - known[k] = v - } - enabled := map[Feature]bool{} - for k, v := range f.enabled.Load().(map[Feature]bool) { - enabled[k] = v - } + 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 { @@ -147,10 +139,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 - } + known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) // Add new features for name, spec := range features { @@ -171,8 +160,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/proto/udp/udp.go b/pkg/proto/udp/udp.go index 0ad8aae1..21c41680 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 { @@ -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) 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/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/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 } 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/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/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/pkg/util/net/udp.go b/pkg/util/net/udp.go index 79ebbb14..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 } @@ -168,11 +164,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/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), } diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 774af2cf..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 @@ -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/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/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/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 { 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/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/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/control.go b/server/control.go index 863104f3..314669ff 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,34 @@ 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 := min(sessionCtx.LoginMsg.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 +193,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 +205,7 @@ func (ctl *Control) Start() { } func (ctl *Control) Close() error { - ctl.conn.Close() + ctl.sessionCtx.Conn.Close() return nil } @@ -233,7 +213,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 +271,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 +284,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) @@ -323,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 @@ -330,38 +334,23 @@ 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() - 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.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, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - _ = ctl.pluginManager.CloseProxy(notifyContent) - }() + for _, pxy := range proxies { + ctl.closeProxy(pxy) } metrics.Server.CloseClient() - ctl.clientRegistry.MarkOfflineByRunID(ctl.runID) + ctl.sessionCtx.ClientRegistry.MarkOfflineByRunID(ctl.runID) xl.Infof("client exit success") close(ctl.doneCh) } @@ -380,15 +369,11 @@ func (ctl *Control) handleNewProxy(m msg.Message) { inMsg := m.(*msg.NewProxy) content := &plugin.NewProxyContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, + User: ctl.loginUserInfo(), 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 +386,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) } @@ -419,22 +404,18 @@ func (ctl *Control) handlePing(m msg.Message) { inMsg := m.(*msg.Ping) content := &plugin.PingContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, + User: ctl.loginUserInfo(), 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 +426,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 +449,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 +465,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 +497,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 +512,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,28 +531,12 @@ 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()) 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.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - _ = ctl.pluginManager.CloseProxy(notifyContent) - }() + ctl.closeProxy(pxy) return } 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 c0dcd5f7..d6bfbcff 100644 --- a/server/group/tcp.go +++ b/server/group/tcp.go @@ -17,107 +17,91 @@ 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) 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 } - 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 { @@ -132,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/server/api/controller.go b/server/http/controller.go similarity index 66% rename from server/api/controller.go rename to server/http/controller.go index 8c9827d8..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,20 +209,8 @@ 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" - c.fillProxyClientInfo(&proxyClientInfo{ - clientVersion: &proxyInfo.ClientVersion, - }, pxy) } else { proxyInfo.Status = "offline" } @@ -254,32 +226,20 @@ 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) { +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" - c.fillProxyClientInfo(&proxyClientInfo{ - clientVersion: &proxyInfo.ClientVersion, - }, pxy) } else { proxyInfo.Status = "offline" } @@ -294,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 { @@ -304,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" @@ -333,12 +280,13 @@ 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(), RunID: info.RunID, + Version: info.Version, Hostname: info.Hostname, ClientIP: info.IP, FirstConnectedAt: toUnix(info.FirstConnectedAt), @@ -351,37 +299,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 @@ -402,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 97% rename from server/api/types.go rename to server/http/model/types.go index b91422be..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" @@ -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/proxy/http.go b/server/proxy/http.go index 2c4f1fd4..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 { @@ -75,16 +75,13 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { } }() - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) + addrs := make([]string, 0) + for _, domain := range domains { routeConfig.Domain = domain for _, location := range locations { routeConfig.Location = location - tmpRouteConfig := routeConfig // handle group @@ -93,12 +90,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 +107,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 } @@ -168,6 +130,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 } } diff --git a/server/proxy/https.go b/server/proxy/https.go index f137ea7a..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 { @@ -53,23 +53,10 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { pxy.Close() } }() + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) + addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } - - 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 != "" { - domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost + for _, domain := range domains { l, err := pxy.listenForDomain(routeConfig, domain) if err != nil { return "", err diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 564eca28..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 } @@ -173,6 +174,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..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 { @@ -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..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 { @@ -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/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 6e95b66b..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 { @@ -72,26 +72,16 @@ func (pxy *TCPMuxProxy) httpConnectListen( } func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) + addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } - + for _, domain := range domains { addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) if err != nil { return "", err } } - if pxy.cfg.SubDomain != "" { - addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, - pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) - if err != nil { - return "", err - } - } - pxy.startCommonTCPListenersHandler() remoteAddr = strings.Join(addrs, ",") return remoteAddr, err diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 3751dc9b..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 { @@ -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, 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/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..28ccb451 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" @@ -195,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 @@ -606,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 @@ -622,13 +630,12 @@ 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() return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User) } - ctl.clientRegistry = svr.clientRegistry ctl.Start() @@ -654,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, } @@ -664,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) @@ -685,47 +692,8 @@ 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) } - -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/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 0b837e39..f5faa637 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -2,70 +2,85 @@ package framework import ( "fmt" + "maps" + "net" "os" "path/filepath" + "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" ) -// 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...) +// 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) - for name, port := range ports { - f.usedPorts[name] = port + maps.Copy(f.usedPorts, ports) + + // 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", serverPath, outs[0]) } - 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) + 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 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 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) } - time.Sleep(2 * time.Second) - currentClientProcesses := make([]*process.Process, 0, len(clientTemplates)) + // 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) + // 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 currentServerProcesses, currentClientProcesses + return serverProcess, clientProcesses } func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { @@ -73,11 +88,13 @@ 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 + select { + case <-p.Done(): + case <-time.After(2 * time.Second): + } + return p, p.Output(), nil } func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { @@ -85,10 +102,13 @@ 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 + select { + case <-p.Done(): + case <-time.After(1500 * time.Millisecond): + } + return p, p.Output(), nil } func (f *Framework) GenerateConfigFile(content string) string { @@ -98,3 +118,74 @@ func (f *Framework) GenerateConfigFile(content string) string { ExpectNoError(err) 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 +} + +// 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 { + 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 763d3353..0115c6cb 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(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(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(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(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(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 4399439d..a01f44aa 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 }} @@ -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 dc15f75c..57c143c5 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() @@ -90,11 +88,11 @@ 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 - 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": @@ -146,11 +144,11 @@ 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{} - 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 @@ -215,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 b69c6aed..ca294280 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(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() @@ -90,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) { @@ -122,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) { @@ -166,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( @@ -200,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) @@ -244,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), @@ -288,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/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/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/pkg/process/process.go b/test/e2e/pkg/process/process.go index e6721e1d..8235461b 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -3,15 +3,44 @@ package process 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 + waitErr error + + started bool beforeStopHandler func() stopped bool } @@ -27,20 +56,45 @@ 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("") + p.errorOutput = &SafeBuffer{} + p.stdOutput = &SafeBuffer{} cmd.Stderr = p.errorOutput cmd.Stdout = p.stdOutput return p } 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 +104,8 @@ func (p *Process) Stop() error { p.beforeStopHandler() } p.cancel() - return p.cmd.Wait() + <-p.done + return p.waitErr } func (p *Process) ErrorOutput() string { @@ -61,6 +116,38 @@ func (p *Process) StdOutput() string { return p.stdOutput.String() } +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 } + +// 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) +} 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/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 9d9d34cb..8994ba73 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(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(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(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(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(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/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() + }) +}) diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go index a3fe5992..f2714abb 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 }} @@ -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/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 f6bb1856..85b938a9 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() @@ -94,11 +92,11 @@ 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 - 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": @@ -159,11 +157,11 @@ 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 - for i := 0; i < 10; i++ { + for i := range 10 { framework.NewRequestExpect(f). Explain("times " + strconv.Itoa(i)). Port(vhostHTTPSPort). @@ -188,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(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() { @@ -226,11 +286,11 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.intervalSeconds = 1 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) // 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 @@ -239,17 +299,19 @@ 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) - for i := 0; i < 10; i++ { + 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 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 @@ -297,7 +359,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { healthCheck.path = "/healthz" `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) // send first HTTP request var contents []string @@ -327,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/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 new file mode 100644 index 00000000..76a9b05a --- /dev/null +++ b/test/e2e/v1/features/store.go @@ -0,0 +1,322 @@ +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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "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) + + 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 + }) + + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort), 5*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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "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) + + 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 + }) + + 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 + 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 + }) + + 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() + }) + + 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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "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) + + 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 + }) + + 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) { + 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 + }) + + 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() + }) + + 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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "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) + + 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 + }) + + 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(serverConf, []string{clientConf}) + 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") + }).Ensure(func(resp *request.Response) bool { + 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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + 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(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + 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/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go index 73e2d863..1ea25a6e 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(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() @@ -96,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) { @@ -130,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) { @@ -180,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( @@ -216,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) @@ -262,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), @@ -308,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) @@ -348,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), @@ -383,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), @@ -429,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() 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/Makefile b/web/frpc/Makefile index 57a88918..adf015e5 100644 --- a/web/frpc/Makefile +++ b/web/frpc/Makefile @@ -1,9 +1,9 @@ .PHONY: dist install build preview lint install: - @npm install + @cd .. && npm install -build: +build: install @npm run build dev: diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index f9d4522a..c58d0bca 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -7,20 +7,39 @@ 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'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ConfigField: typeof import('./src/components/ConfigField.vue')['default'] + ConfigSection: typeof import('./src/components/ConfigSection.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'] - ElRow: typeof import('element-plus/es')['ElRow'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 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'] + 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/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/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/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 deleted file mode 100644 index 6b0912eb..00000000 --- a/web/frpc/package-lock.json +++ /dev/null @@ -1,5857 +0,0 @@ -{ - "name": "frpc-dashboard", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frpc-dashboard", - "version": "0.0.1", - "dependencies": { - "element-plus": "^2.13.0", - "vue": "^3.5.26", - "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/tsconfig": "^0.8.1", - "@vueuse/core": "^14.1.0", - "eslint": "^8.56.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/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@antfu/utils": { - "version": "0.7.7", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@element-plus/icons-vue": { - "version": "2.3.2", - "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==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.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", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "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" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@popperjs/core": { - "name": "@sxzz/popperjs-es", - "version": "2.11.7", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/node": { - "version": "24.10.4", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.20.0", - "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", - "natural-compare": "^1.4.0", - "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" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", - "dev": true, - "license": "BSD-2-Clause", - "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" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.20.0", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "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.1", - "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" - }, - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/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/@ungap/structured-clone": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", - "vue": "^3.2.25" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.27", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.27" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.27", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.27", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/entities": { - "version": "7.0.0", - "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", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", - "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", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.6", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "license": "MIT" - }, - "node_modules/@vue/eslint-config-prettier": { - "version": "9.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0" - }, - "peerDependencies": { - "eslint": ">= 8.0.0", - "prettier": ">= 3.0.0" - } - }, - "node_modules/@vue/eslint-config-typescript": { - "version": "12.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "vue-eslint-parser": "^9.3.1" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", - "eslint-plugin-vue": "^9.0.0", - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/language-core": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.27", - "@vue/compiler-dom": "^3.5.0", - "@vue/shared": "^3.5.0", - "alien-signals": "^3.0.0", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1", - "picomatch": "^4.0.2" - } - }, - "node_modules/@vue/language-core/node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.26", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" - }, - "peerDependencies": { - "vue": "3.5.26" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.26", - "license": "MIT" - }, - "node_modules/@vue/tsconfig": { - "version": "0.8.1", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": "5.x", - "vue": "^3.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@vueuse/core": { - "version": "14.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.1.0", - "@vueuse/shared": "14.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@vueuse/metadata": { - "version": "14.1.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "14.1.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/alien-signals": { - "version": "3.1.2", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async-validator": { - "version": "4.2.5", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/c12": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^17.2.3", - "exsolve": "^1.0.8", - "giget": "^2.0.0", - "jiti": "^2.6.1", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^2.0.0", - "pkg-types": "^2.3.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "*" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/c12/node_modules/chokidar": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/c12/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/c12/node_modules/readdirp": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "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/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "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" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "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/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/csstype": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.19", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/define-properties": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/destr": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/element-plus": { - "version": "2.13.0", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^3.4.1", - "@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", - "async-validator": "^4.2.5", - "dayjs": "^1.11.19", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "lodash-unified": "^1.0.3", - "memoize-one": "^6.0.0", - "normalize-wheel-es": "^1.2.0" - }, - "peerDependencies": { - "vue": "^3.3.0" - } - }, - "node_modules/element-plus/node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "license": "MIT" - }, - "node_modules/element-plus/node_modules/@vueuse/core": { - "version": "10.11.1", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "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", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/element-plus/node_modules/@vueuse/shared": { - "version": "10.11.1", - "license": "MIT", - "dependencies": { - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "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", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/errx": { - "version": "0.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/es-abstract": { - "version": "1.21.1", - "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", - "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", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "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" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.56.0", - "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", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "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", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "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" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-vue": { - "version": "9.33.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "globals": "^13.24.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.3", - "vue-eslint-parser": "^9.4.3", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "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-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.3", - "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": { - "version": "9.6.1", - "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/espree/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/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exsolve": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "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/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/giget": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "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", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "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", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "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", - "dev": true, - "license": "ISC" - }, - "node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "5.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.6.1", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/klona": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/knitwork": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/local-pkg": { - "version": "0.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash-unified": { - "version": "1.0.3", - "license": "MIT", - "peerDependencies": { - "@types/lodash-es": "*", - "lodash": "*", - "lodash-es": "*" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/memoize-one": { - "version": "6.0.0", - "license": "MIT" - }, - "node_modules/memorystream": { - "version": "0.3.1", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/nice-try": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "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.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-wheel-es": { - "version": "1.2.0", - "license": "BSD-3-Clause" - }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nypm": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/nypm/node_modules/pathe": { - "version": "2.0.3", - "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", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "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", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.3.1", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-types": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss": { - "version": "8.5.6", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.7.4", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc9": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/read-pkg": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.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/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.21" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/rollup": { - "version": "4.55.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "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", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sass": { - "version": "1.97.2", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@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", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.12", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/string.prototype.padend": { - "version": "3.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "3.3.2", - "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" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/synckit": { - "version": "0.8.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/terser": { - "version": "5.44.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unctx": { - "version": "2.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21", - "unplugin": "^2.3.11" - } - }, - "node_modules/unctx/node_modules/estree-walker": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/unctx/node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unctx/node_modules/unplugin": { - "version": "2.3.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "dev": true, - "license": "MIT" - }, - "node_modules/unimport": { - "version": "3.7.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "acorn": "^8.11.2", - "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" - } - }, - "node_modules/unimport/node_modules/escape-string-regexp": { - "version": "5.0.0", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/unplugin": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.2", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.1" - } - }, - "node_modules/unplugin-auto-import": { - "version": "0.17.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@antfu/utils": "^0.7.7", - "@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" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@nuxt/kit": "^3.2.2", - "@vueuse/core": "*" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - }, - "@vueuse/core": { - "optional": true - } - } - }, - "node_modules/unplugin-auto-import/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/unplugin-auto-import/node_modules/minimatch": { - "version": "9.0.3", - "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/unplugin-element-plus": { - "version": "0.11.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@nuxt/kit": "^4.2.2", - "es-module-lexer": "^2.0.0", - "escape-string-regexp": "^5.0.0", - "rolldown-string": "^0.2.1", - "unplugin": "^2.3.11" - }, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/unplugin-element-plus/node_modules/@nuxt/kit": { - "version": "4.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "c12": "^3.3.2", - "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", - "mlly": "^1.8.0", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "rc9": "^2.1.2", - "scule": "^1.3.0", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ufo": "^1.6.1", - "unctx": "^2.4.1", - "untyped": "^2.0.0" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/unplugin-element-plus/node_modules/escape-string-regexp": { - "version": "5.0.0", - "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", - "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", - "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", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/unplugin-vue-components": { - "version": "0.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@antfu/utils": "^0.7.6", - "@rollup/pluginutils": "^5.0.4", - "chokidar": "^3.5.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.1", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.3", - "minimatch": "^9.0.3", - "resolve": "^1.22.4", - "unplugin": "^1.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2", - "vue": "2 || 3" - }, - "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/unplugin-vue-components/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/unplugin-vue-components/node_modules/local-pkg": { - "version": "0.4.3", - "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.3", - "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/untyped": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "defu": "^6.1.4", - "jiti": "^2.4.2", - "knitwork": "^1.2.0", - "scule": "^1.3.0" - }, - "bin": { - "untyped": "dist/cli.mjs" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-svg-loader": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "svgo": "^3.0.2" - }, - "peerDependencies": { - "vue": ">=3.2.13" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.26", - "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" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "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/vue-router": { - "version": "4.6.4", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/vue-tsc": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "2.4.27", - "@vue/language-core": "3.2.2" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/web/frpc/package.json b/web/frpc/package.json index b41d048a..6e20d893 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -2,28 +2,29 @@ "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", + "pinia": "^3.0.4", "vue": "^3.5.26", "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 +38,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..b75c8a69 100644 --- a/web/frpc/src/App.vue +++ b/web/frpc/src/App.vue @@ -2,134 +2,160 @@
-
-
-
- -
- / - frp - Client - {{ - currentRouteName - }} -
- -
- - - - +
+ +
+
+ / + frp + Client
- + + + +
-
- -
+
+ +
- diff --git a/web/frpc/src/api/frpc.ts b/web/frpc/src/api/frpc.ts index 63aeeb31..c7af90c0 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, + ProxyListResp, + ProxyDefinition, + VisitorListResp, + VisitorDefinition, +} from '../types' export const getStatus = () => { return http.get('/api/status') @@ -16,3 +22,71 @@ export const putConfig = (content: string) => { 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') +} + +export const getStoreProxy = (name: string) => { + return http.get( + `/api/store/proxies/${encodeURIComponent(name)}`, + ) +} + +export const createStoreProxy = (config: ProxyDefinition) => { + return http.post('/api/store/proxies', config) +} + +export const updateStoreProxy = (name: string, config: ProxyDefinition) => { + 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: VisitorDefinition) => { + return http.post('/api/store/visitors', config) +} + +export const updateStoreVisitor = ( + name: string, + config: VisitorDefinition, +) => { + 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/assets/css/_form-layout.scss b/web/frpc/src/assets/css/_form-layout.scss new file mode 100644 index 00000000..ca84433f --- /dev/null +++ b/web/frpc/src/assets/css/_form-layout.scss @@ -0,0 +1,33 @@ +@use '@shared/css/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/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/ConfigField.vue b/web/frpc/src/components/ConfigField.vue new file mode 100644 index 00000000..27719847 --- /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/KeyValueEditor.vue b/web/frpc/src/components/KeyValueEditor.vue new file mode 100644 index 00000000..9b467670 --- /dev/null +++ b/web/frpc/src/components/KeyValueEditor.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue index 70246f8c..93f815a6 100644 --- a/web/frpc/src/components/ProxyCard.vue +++ b/web/frpc/src/components/ProxyCard.vue @@ -1,35 +1,49 @@ - diff --git a/web/frpc/src/views/Overview.vue b/web/frpc/src/views/Overview.vue deleted file mode 100644 index 2cdbfd93..00000000 --- a/web/frpc/src/views/Overview.vue +++ /dev/null @@ -1,442 +0,0 @@ - - - - - diff --git a/web/frpc/src/views/ProxyDetail.vue b/web/frpc/src/views/ProxyDetail.vue new file mode 100644 index 00000000..9b238b4a --- /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 new file mode 100644 index 00000000..0a45d695 --- /dev/null +++ b/web/frpc/src/views/ProxyEdit.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/web/frpc/src/views/ProxyList.vue b/web/frpc/src/views/ProxyList.vue new file mode 100644 index 00000000..61661983 --- /dev/null +++ b/web/frpc/src/views/ProxyList.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/web/frpc/src/views/VisitorDetail.vue b/web/frpc/src/views/VisitorDetail.vue new file mode 100644 index 00000000..b038b1e5 --- /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 new file mode 100644 index 00000000..615e97e9 --- /dev/null +++ b/web/frpc/src/views/VisitorEdit.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/web/frpc/src/views/VisitorList.vue b/web/frpc/src/views/VisitorList.vue new file mode 100644 index 00000000..506b57f2 --- /dev/null +++ b/web/frpc/src/views/VisitorList.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/web/frpc/tsconfig.json b/web/frpc/tsconfig.json index 9e03e604..8795fb5c 100644 --- a/web/frpc/tsconfig.json +++ b/web/frpc/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/frpc/vite.config.mts b/web/frpc/vite.config.mts index cb8de991..6a9205d0 100644 --- a/web/frpc/vite.config.mts +++ b/web/frpc/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/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/Makefile b/web/frps/Makefile index 57a88918..adf015e5 100644 --- a/web/frps/Makefile +++ b/web/frps/Makefile @@ -1,9 +1,9 @@ .PHONY: dist install build preview lint install: - @npm install + @cd .. && npm install -build: +build: install @npm run build dev: diff --git a/web/frps/components.d.ts b/web/frps/components.d.ts index d361b7bc..b5a18d79 100644 --- a/web/frps/components.d.ts +++ b/web/frps/components.d.ts @@ -11,13 +11,12 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] + ElDialog: typeof import('element-plus/es')['ElDialog'] ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] + ElPopover: typeof import('element-plus/es')['ElPopover'] 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'] 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 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.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/App.vue b/web/frps/src/App.vue index bec48c9e..11b2790a 100644 --- a/web/frps/src/App.vue +++ b/web/frps/src/App.vue @@ -2,135 +2,195 @@
-
-
-
- -
- / - frp - Server - {{ - currentRouteName - }} -
- -
- - - - +
+ +
+
+ / + frp + Server
- + + + +
-
- -
+
+ +
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!', 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/frps/package-lock.json b/web/package-lock.json similarity index 61% rename from web/frps/package-lock.json rename to web/package-lock.json index 07c3ad48..15c6f2d9 100644 --- a/web/frps/package-lock.json +++ b/web/package-lock.json @@ -1,26 +1,33 @@ { - "name": "frps-dashboard", - "version": "0.0.1", + "name": "frp-web", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frps-dashboard", + "name": "frp-web", + "workspaces": [ + "shared", + "frpc", + "frps" + ] + }, + "frpc": { + "name": "frpc-dashboard", "version": "0.0.1", "dependencies": { "element-plus": "^2.13.0", + "pinia": "^3.0.4", "vue": "^3.5.26", "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,14 +42,34 @@ "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" + "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": { @@ -74,12 +101,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.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -89,9 +116,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,12 +129,12 @@ } }, "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": "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": { @@ -120,9 +147,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -137,9 +164,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -154,9 +181,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -171,9 +198,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -188,9 +215,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -205,9 +232,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -222,9 +249,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.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" ], @@ -239,9 +266,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -256,9 +283,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -273,9 +300,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -290,9 +317,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -307,9 +334,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -324,9 +351,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -341,9 +368,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -358,9 +385,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -375,9 +402,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -392,9 +419,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -409,9 +436,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -426,9 +453,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -443,9 +470,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -460,9 +487,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -477,9 +504,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -494,9 +521,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -511,9 +538,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -528,9 +555,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -545,9 +572,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.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -562,122 +589,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.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": { - "@humanwhocodes/object-schema": "^2.0.3", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.5" }, "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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "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.5", + "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.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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.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.11" + } + }, + "node_modules/@floating-ui/dom": { + "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.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "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": { + "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 +844,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", @@ -800,19 +956,89 @@ "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.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 +1048,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 +1085,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 +1106,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 +1127,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 +1148,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 +1169,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 +1190,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 +1211,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 +1232,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 +1253,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 +1274,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 +1295,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 +1316,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 +1336,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 +1375,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 +1404,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 +1432,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 +1446,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 +1460,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 +1474,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 +1488,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 +1502,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 +1516,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 +1530,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 +1544,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 +1558,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 +1572,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 +1586,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 +1600,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 +1614,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 +1628,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 +1642,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 +1656,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 +1670,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 +1684,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 +1698,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 +1712,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 +1726,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 +1740,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 +1754,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,23 +1767,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", - "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", @@ -1546,9 +1782,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 +1797,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.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "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 +1814,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.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.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.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", - "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.57.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.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": "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.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": { - "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.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.57.1", + "@typescript-eslint/types": "^8.57.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.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": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.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.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", "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.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.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" }, - "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.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": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1710,236 +1974,263 @@ } }, "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.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": { - "@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.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", + "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.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": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.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.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.57.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.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": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "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" } }, "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.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.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "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.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.26", - "@vue/shared": "3.5.26" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "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.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.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.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.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", - "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "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.26", - "@vue/shared": "3.5.26" + "@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", + "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-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": "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 +2239,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.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": { - "@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 +2254,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.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.26" + "@vue/shared": "3.5.30" } }, "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.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.26", - "@vue/shared": "3.5.26" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "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.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.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", + "@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.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", - "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "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.26", - "@vue/shared": "3.5.26" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.26" + "vue": "3.5.30" } }, "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.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -2033,15 +2337,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 +2355,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 +2365,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 +2378,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 +2390,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 +2424,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 +2454,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 +2461,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 +2517,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 +2533,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", @@ -2216,6 +2555,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", @@ -2224,13 +2572,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 +2649,43 @@ "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/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", + "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 +2701,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 +2761,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 +2804,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 +2838,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" }, @@ -2470,31 +2854,34 @@ "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": "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,10 +2983,64 @@ "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", - "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": { @@ -2627,13 +3068,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 +3119,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 +3144,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 +3189,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,23 +3201,38 @@ "url": "https://dotenvx.com" } }, - "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==", + "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": { - "@ctrl/tinycolor": "^3.4.1", + "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.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": "^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.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" @@ -2825,45 +3248,46 @@ "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/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/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 +3297,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 +3314,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 +3382,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 +3409,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 +3457,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.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2991,126 +3470,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.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": { - "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.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.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.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@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", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", "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", + "minimatch": "^3.1.5", "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 +3606,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 +3641,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 +3718,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 +3728,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 +3747,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 +3889,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 +3912,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 +3949,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 +3993,60 @@ } }, "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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "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/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", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -3704,23 +4057,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 +4097,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 +4182,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 +4225,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 +4298,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 +4311,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,15 +4339,11 @@ "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/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", @@ -4022,9 +4353,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": { @@ -4032,16 +4363,16 @@ } }, "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" }, "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 +4396,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 +4436,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 +4486,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 +4531,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 +4576,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 +4625,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 +4662,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 +4678,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 +4697,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 +4744,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 +4762,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 +4777,71 @@ "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/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", + "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 +4879,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 +4907,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 +4965,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 +5036,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,73 +5092,39 @@ "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" + "url": "https://github.com/sponsors/isaacs" } }, - "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" - } - }, - "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": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "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, + "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/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/mlly": { + "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": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, "node_modules/ms": { @@ -4789,6 +5253,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 +5449,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 +5497,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 +5524,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 +5636,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 +5674,25 @@ "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==", - "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": { - "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" @@ -5069,22 +5721,53 @@ "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/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": { - "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", @@ -5123,12 +5806,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 +5817,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 +5833,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 +5846,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 +5894,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 +5933,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,21 +6019,11 @@ "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/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", @@ -5349,9 +6042,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 +6058,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,30 +6110,70 @@ "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.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": { @@ -5453,20 +6186,14 @@ "@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==", + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=11.0.0" } }, "node_modules/scule": { @@ -5477,9 +6204,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 +6216,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 +6408,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 +6419,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 +6437,68 @@ } }, "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/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", + "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 +6508,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", @@ -5710,17 +6580,29 @@ "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": "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": { @@ -5737,19 +6619,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" @@ -5773,26 +6655,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.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5808,17 +6689,10 @@ "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", - "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": { @@ -5842,6 +6716,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 +6761,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.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": { - "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 +6799,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 +6891,51 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "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.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" + }, + "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 +6964,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 +7024,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 +7071,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 +7083,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 +7145,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 +7178,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 +7196,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.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": { - "c12": "^3.3.2", + "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", @@ -6222,22 +7211,42 @@ "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", - "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 +7257,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 +7336,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 +7420,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 +7479,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": { @@ -6439,18 +7554,50 @@ } }, "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" } }, + "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 +7606,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.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.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.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": "*" @@ -6479,55 +7626,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": { @@ -6545,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.2", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.2.tgz", - "integrity": "sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ==", + "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.27", - "@vue/language-core": "3.2.2" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -6570,48 +7709,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 +7744,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", @@ -6649,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/shared/components/ActionButton.vue b/web/shared/components/ActionButton.vue new file mode 100644 index 00000000..6bbaae82 --- /dev/null +++ b/web/shared/components/ActionButton.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/web/shared/components/BaseDialog.vue b/web/shared/components/BaseDialog.vue new file mode 100644 index 00000000..29dc0496 --- /dev/null +++ b/web/shared/components/BaseDialog.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/web/shared/components/ConfirmDialog.vue b/web/shared/components/ConfirmDialog.vue new file mode 100644 index 00000000..4de6975a --- /dev/null +++ b/web/shared/components/ConfirmDialog.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/web/shared/components/FilterDropdown.vue b/web/shared/components/FilterDropdown.vue new file mode 100644 index 00000000..8aedd227 --- /dev/null +++ b/web/shared/components/FilterDropdown.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/web/shared/components/PopoverMenu.vue b/web/shared/components/PopoverMenu.vue new file mode 100644 index 00000000..8abd91ed --- /dev/null +++ b/web/shared/components/PopoverMenu.vue @@ -0,0 +1,303 @@ + + + + + + + + + diff --git a/web/shared/components/PopoverMenuItem.vue b/web/shared/components/PopoverMenuItem.vue new file mode 100644 index 00000000..8df1e49b --- /dev/null +++ b/web/shared/components/PopoverMenuItem.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/web/shared/css/_index.scss b/web/shared/css/_index.scss new file mode 100644 index 00000000..b70de87b --- /dev/null +++ b/web/shared/css/_index.scss @@ -0,0 +1,2 @@ +@forward './variables'; +@forward './mixins'; diff --git a/web/shared/css/_mixins.scss b/web/shared/css/_mixins.scss new file mode 100644 index 00000000..3861971b --- /dev/null +++ b/web/shared/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/shared/css/_variables.scss b/web/shared/css/_variables.scss new file mode 100644 index 00000000..3df1ef19 --- /dev/null +++ b/web/shared/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/shared/package.json b/web/shared/package.json new file mode 100644 index 00000000..a2d05f93 --- /dev/null +++ b/web/shared/package.json @@ -0,0 +1,4 @@ +{ + "name": "frp-shared", + "private": true +}