From d2169990d3a93f5898fdd7dee72533149afd38fe Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 16:49:17 +0800 Subject: [PATCH 01/17] feat: initialize independent Sense product skeleton (#21) --- Sense/LICENSES/SOURCES.md | 15 + Sense/LICENSES/go-admin-LICENSE | 22 + Sense/LICENSES/go-admin-ui-LICENSE | 22 + Sense/README.md | 55 + Sense/config/sense.env.example | 7 + Sense/scripts/bootstrap/check-toolchain.ps1 | 20 + Sense/scripts/bootstrap/smoke.ps1 | 38 + Sense/server/cmd/sense/root.go | 79 + Sense/server/config/config.go | 5 + Sense/server/go.mod | 13 + Sense/server/go.sum | 26 + Sense/server/internal/platform/app.go | 103 + Sense/server/internal/platform/app_test.go | 31 + Sense/server/internal/platform/config.go | 63 + Sense/server/internal/platform/config_test.go | 23 + Sense/server/internal/platform/context.go | 11 + Sense/server/internal/platform/database.go | 31 + Sense/server/internal/platform/http.go | 39 + Sense/server/internal/platform/migrations.go | 63 + Sense/server/main.go | 13 + Sense/ui/.gitignore | 4 + Sense/ui/package.json | 53 + Sense/ui/pnpm-lock.yaml | 8091 +++++++++++++++++ Sense/ui/src/bootstrap/App.vue | 7 + Sense/ui/src/bootstrap/Dashboard.vue | 55 + Sense/ui/src/bootstrap/index.html | 14 + Sense/ui/src/bootstrap/main.js | 19 + Sense/ui/src/bootstrap/request.js | 16 + Sense/ui/src/bootstrap/router.js | 45 + Sense/ui/src/bootstrap/store.js | 11 + Sense/ui/src/bootstrap/theme.css | 25 + Sense/ui/src/layout/AppLayout.vue | 97 + Sense/ui/vue.config.js | 24 + 33 files changed, 9140 insertions(+) create mode 100644 Sense/LICENSES/SOURCES.md create mode 100644 Sense/LICENSES/go-admin-LICENSE create mode 100644 Sense/LICENSES/go-admin-ui-LICENSE create mode 100644 Sense/README.md create mode 100644 Sense/config/sense.env.example create mode 100644 Sense/scripts/bootstrap/check-toolchain.ps1 create mode 100644 Sense/scripts/bootstrap/smoke.ps1 create mode 100644 Sense/server/cmd/sense/root.go create mode 100644 Sense/server/config/config.go create mode 100644 Sense/server/go.mod create mode 100644 Sense/server/go.sum create mode 100644 Sense/server/internal/platform/app.go create mode 100644 Sense/server/internal/platform/app_test.go create mode 100644 Sense/server/internal/platform/config.go create mode 100644 Sense/server/internal/platform/config_test.go create mode 100644 Sense/server/internal/platform/context.go create mode 100644 Sense/server/internal/platform/database.go create mode 100644 Sense/server/internal/platform/http.go create mode 100644 Sense/server/internal/platform/migrations.go create mode 100644 Sense/server/main.go create mode 100644 Sense/ui/.gitignore create mode 100644 Sense/ui/package.json create mode 100644 Sense/ui/pnpm-lock.yaml create mode 100644 Sense/ui/src/bootstrap/App.vue create mode 100644 Sense/ui/src/bootstrap/Dashboard.vue create mode 100644 Sense/ui/src/bootstrap/index.html create mode 100644 Sense/ui/src/bootstrap/main.js create mode 100644 Sense/ui/src/bootstrap/request.js create mode 100644 Sense/ui/src/bootstrap/router.js create mode 100644 Sense/ui/src/bootstrap/store.js create mode 100644 Sense/ui/src/bootstrap/theme.css create mode 100644 Sense/ui/src/layout/AppLayout.vue create mode 100644 Sense/ui/vue.config.js diff --git a/Sense/LICENSES/SOURCES.md b/Sense/LICENSES/SOURCES.md new file mode 100644 index 0000000..814907c --- /dev/null +++ b/Sense/LICENSES/SOURCES.md @@ -0,0 +1,15 @@ +# Upstream sources + +Sense is an independent product derived from the frozen GoAdmin baseline in +the repository root `goadmin-baseline.json`. + +- go-admin: `https://github.com/go-admin-team/go-admin`, commit + `f06540883b41d03782bb6b2c4150f298f328c6b6`, MIT. +- go-admin-ui: `https://github.com/go-admin-team/go-admin-ui`, commit + `67d393d713877572fab0b897296a4c1d525fc81d`, MIT. +- go-admin-doc: documentation reference only, commit + `424855aacf6905f3fde860c3331385cb25529a0d`, MIT. + +The local copies under `D:\github\goadmin` are read-only references and are +not a distributable or reproducible source identifier. + diff --git a/Sense/LICENSES/go-admin-LICENSE b/Sense/LICENSES/go-admin-LICENSE new file mode 100644 index 0000000..bd6e50a --- /dev/null +++ b/Sense/LICENSES/go-admin-LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 go-admin-team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Sense/LICENSES/go-admin-ui-LICENSE b/Sense/LICENSES/go-admin-ui-LICENSE new file mode 100644 index 0000000..0c1c40a --- /dev/null +++ b/Sense/LICENSES/go-admin-ui-LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 wenjianzhang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Sense/README.md b/Sense/README.md new file mode 100644 index 0000000..f732326 --- /dev/null +++ b/Sense/README.md @@ -0,0 +1,55 @@ +# Sense + +Sense is the independently deployable intelligent NVR and video-sensing +management product in YoVision. It uses Go 1.26.5, PostgreSQL, Vue 3 and +Element Plus. Brain and Bell are not required for startup or smoke tests. + +## Toolchain + +Use the exact versions from the repository root `goadmin-baseline.json`: + +- Go 1.26.5 +- Node.js 22.22.1 +- pnpm 9.15.1 + +The repositories under `D:\github\goadmin` are read-only references. Do not +develop Sense inside them. + +```powershell +$env:PATH = 'C:\Users\ila20\AppData\Local\Temp\yovision-tools\go1.26.5\go\bin;' + $env:PATH +powershell -ExecutionPolicy Bypass -File .\Sense\scripts\bootstrap\check-toolchain.ps1 +``` + +## Backend + +Production/default startup requires an independent PostgreSQL URL. No default +password or signing secret is provided. + +```powershell +cd Sense\server +$env:SENSE_DATABASE_MODE = 'postgres' +$env:SENSE_DATABASE_URL = '' +go run . +``` + +For an isolated smoke test only: + +```powershell +powershell -ExecutionPolicy Bypass -File .\Sense\scripts\bootstrap\smoke.ps1 +``` + +Health endpoints are `GET /healthz` and `GET /readyz`. Upstream demo and code +generation routes are deliberately not registered. + +## Frontend + +```powershell +cd Sense\ui +corepack pnpm@9.15.1 install --frozen-lockfile +corepack pnpm@9.15.1 lint +corepack pnpm@9.15.1 build +``` + +The frontend automatically discovers project-local route and store modules. +Feature tickets add one route module without exposing unrelated GoAdmin demo +pages. diff --git a/Sense/config/sense.env.example b/Sense/config/sense.env.example new file mode 100644 index 0000000..8a216bd --- /dev/null +++ b/Sense/config/sense.env.example @@ -0,0 +1,7 @@ +# Copy values into a repository-external environment store. Do not commit secrets. +SENSE_HTTP_ADDRESS=127.0.0.1:18080 +SENSE_DATABASE_MODE=postgres +SENSE_DATABASE_URL= +SENSE_LOG_LEVEL=info +SENSE_UI_STATIC_DIR= + diff --git a/Sense/scripts/bootstrap/check-toolchain.ps1 b/Sense/scripts/bootstrap/check-toolchain.ps1 new file mode 100644 index 0000000..c80a0d2 --- /dev/null +++ b/Sense/scripts/bootstrap/check-toolchain.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = 'Stop' + +$expectedGo = 'go version go1.26.5 windows/amd64' +$actualGo = (& go version).Trim() +if ($actualGo -ne $expectedGo) { + throw "Go toolchain mismatch. Expected '$expectedGo', got '$actualGo'." +} + +$actualNode = (& node --version).Trim() +if ($actualNode -ne 'v22.22.1') { + throw "Node.js mismatch. Expected 'v22.22.1', got '$actualNode'." +} + +$actualPnpm = (& corepack pnpm@9.15.1 --version).Trim() +if ($actualPnpm -ne '9.15.1') { + throw "pnpm mismatch. Expected '9.15.1', got '$actualPnpm'." +} + +Write-Output 'Sense toolchain matches goadmin-baseline.json.' + diff --git a/Sense/scripts/bootstrap/smoke.ps1 b/Sense/scripts/bootstrap/smoke.ps1 new file mode 100644 index 0000000..eb36aac --- /dev/null +++ b/Sense/scripts/bootstrap/smoke.ps1 @@ -0,0 +1,38 @@ +$ErrorActionPreference = 'Stop' + +$senseRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$serverRoot = Join-Path $senseRoot 'server' +$env:SENSE_DATABASE_MODE = 'memory' +$env:SENSE_HTTP_ADDRESS = '127.0.0.1:18080' + +$process = Start-Process -FilePath 'go' -ArgumentList @('run', '.') -WorkingDirectory $serverRoot -PassThru -WindowStyle Hidden +try { + $ready = $false + for ($attempt = 0; $attempt -lt 30; $attempt++) { + try { + $health = Invoke-RestMethod -Uri 'http://127.0.0.1:18080/healthz' -TimeoutSec 1 + $readiness = Invoke-RestMethod -Uri 'http://127.0.0.1:18080/readyz' -TimeoutSec 1 + if ($health.status -eq 'ok' -and $readiness.status -eq 'ready') { + $ready = $true + break + } + } catch { + Start-Sleep -Milliseconds 250 + } + } + if (-not $ready) { + throw 'Sense smoke server did not become ready.' + } + + try { + Invoke-WebRequest -Uri 'http://127.0.0.1:18080/api/v1/demo' -TimeoutSec 2 | Out-Null + throw 'Disabled demo route unexpectedly returned success.' + } catch { + if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw } + } + Write-Output 'Sense independent smoke test passed.' +} finally { + if (-not $process.HasExited) { Stop-Process -Id $process.Id } + $process.WaitForExit() +} + diff --git a/Sense/server/cmd/sense/root.go b/Sense/server/cmd/sense/root.go new file mode 100644 index 0000000..235d093 --- /dev/null +++ b/Sense/server/cmd/sense/root.go @@ -0,0 +1,79 @@ +package sense + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "yovision.local/sense/internal/platform" +) + +type moduleRegistrar func(*platform.App) error + +var moduleRegistrars []moduleRegistrar + +// registerModule is intentionally package-local. Feature integration files in +// this directory append one registrar without changing the shared bootstrap. +func registerModule(registrar moduleRegistrar) { + moduleRegistrars = append(moduleRegistrars, registrar) +} + +func Run() error { + cfg, err := platform.LoadConfig() + if err != nil { + return err + } + + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel()})) + database, err := platform.OpenDatabase(context.Background(), cfg) + if err != nil { + return err + } + if database != nil { + defer database.Close() + } + + app := platform.NewApp(cfg, database, logger) + for _, registrar := range moduleRegistrars { + if err := registrar(app); err != nil { + return fmt.Errorf("register Sense module: %w", err) + } + } + if err := app.ApplyMigrations(context.Background()); err != nil { + return err + } + + server := &http.Server{ + Addr: cfg.HTTPAddress, + Handler: app.Handler(), + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + errCh := make(chan error, 1) + go func() { + logger.Info("Sense server started", "address", cfg.HTTPAddress, "database_mode", cfg.DatabaseMode) + errCh <- server.ListenAndServe() + }() + + stopCh := make(chan os.Signal, 1) + signal.Notify(stopCh, syscall.SIGINT, syscall.SIGTERM) + select { + case <-stopCh: + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + return server.Shutdown(ctx) + case serveErr := <-errCh: + if serveErr == http.ErrServerClosed { + return nil + } + return serveErr + } +} diff --git a/Sense/server/config/config.go b/Sense/server/config/config.go new file mode 100644 index 0000000..f7a0a1f --- /dev/null +++ b/Sense/server/config/config.go @@ -0,0 +1,5 @@ +package config + +// Package config exists as the stable public configuration boundary for Sense. +// Runtime parsing is implemented in internal/platform so feature packages do +// not read process environment variables directly. diff --git a/Sense/server/go.mod b/Sense/server/go.mod new file mode 100644 index 0000000..2029838 --- /dev/null +++ b/Sense/server/go.mod @@ -0,0 +1,13 @@ +module yovision.local/sense + +go 1.26.5 + +require github.com/jackc/pgx/v5 v5.8.0 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect +) diff --git a/Sense/server/go.sum b/Sense/server/go.sum new file mode 100644 index 0000000..87a6c8a --- /dev/null +++ b/Sense/server/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Sense/server/internal/platform/app.go b/Sense/server/internal/platform/app.go new file mode 100644 index 0000000..397c1ed --- /dev/null +++ b/Sense/server/internal/platform/app.go @@ -0,0 +1,103 @@ +package platform + +import ( + "database/sql" + "io/fs" + "log/slog" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +type App struct { + config Config + database *sql.DB + logger *slog.Logger + mux *http.ServeMux + migrations []Migration +} + +func NewApp(cfg Config, database *sql.DB, logger *slog.Logger) *App { + app := &App{config: cfg, database: database, logger: logger, mux: http.NewServeMux()} + app.registerPlatformRoutes() + return app +} + +func (a *App) Config() Config { return a.config } +func (a *App) Database() *sql.DB { return a.database } +func (a *App) Logger() *slog.Logger { return a.logger } + +func (a *App) Handle(pattern string, handler http.Handler) { + a.mux.Handle(pattern, handler) +} + +func (a *App) HandleFunc(pattern string, handler http.HandlerFunc) { + a.mux.HandleFunc(pattern, handler) +} + +func (a *App) Handler() http.Handler { + return requestSecurityHeaders(a.mux) +} + +func (a *App) registerPlatformRoutes() { + a.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { + WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "product": "sense"}) + }) + a.mux.HandleFunc("GET /readyz", a.ready) + a.mux.HandleFunc("GET /api/v1/system/info", func(w http.ResponseWriter, _ *http.Request) { + WriteJSON(w, http.StatusOK, map[string]any{ + "product": "Sense", + "database": a.config.DatabaseMode, + "modules": []string{}, + }) + }) + // Deliberately do not register upstream demo/code-generation routes. + if a.config.StaticDir != "" { + a.registerStaticFiles(a.config.StaticDir) + } +} + +func (a *App) ready(w http.ResponseWriter, r *http.Request) { + if a.database == nil { + WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "memory"}) + return + } + ctx, cancel := timeLimitedContext(r, 2*time.Second) + defer cancel() + if err := a.database.PingContext(ctx); err != nil { + WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready", "database": "unavailable"}) + return + } + WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "postgres"}) +} + +func (a *App) registerStaticFiles(directory string) { + absolute, err := filepath.Abs(directory) + if err != nil { + a.logger.Warn("invalid UI static directory", "error", err) + return + } + fileSystem := os.DirFS(absolute) + a.mux.Handle("GET /assets/", http.StripPrefix("/", http.FileServer(http.FS(fileSystem)))) + a.mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(r.URL.Path)), "/") + if path == "." || path == "" { + path = "index.html" + } + if _, err := fs.Stat(fileSystem, path); err != nil { + path = "index.html" + } + http.ServeFileFS(w, r, fileSystem, path) + }) +} + +func requestSecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} diff --git a/Sense/server/internal/platform/app_test.go b/Sense/server/internal/platform/app_test.go new file mode 100644 index 0000000..390aad2 --- /dev/null +++ b/Sense/server/internal/platform/app_test.go @@ -0,0 +1,31 @@ +package platform + +import ( + "io" + "log/slog" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPlatformRoutesAndDisabledDemo(t *testing.T) { + app := NewApp(Config{DatabaseMode: DatabaseModeMemory}, nil, slog.New(slog.NewTextHandler(io.Discard, nil))) + + for _, test := range []struct { + path string + want int + }{ + {path: "/healthz", want: http.StatusOK}, + {path: "/readyz", want: http.StatusOK}, + {path: "/api/v1/system/info", want: http.StatusOK}, + {path: "/api/v1/demo", want: http.StatusNotFound}, + {path: "/api/v1/system/tools", want: http.StatusNotFound}, + } { + req := httptest.NewRequest(http.MethodGet, test.path, nil) + res := httptest.NewRecorder() + app.Handler().ServeHTTP(res, req) + if res.Code != test.want { + t.Errorf("GET %s status = %d, want %d", test.path, res.Code, test.want) + } + } +} diff --git a/Sense/server/internal/platform/config.go b/Sense/server/internal/platform/config.go new file mode 100644 index 0000000..66cb1e6 --- /dev/null +++ b/Sense/server/internal/platform/config.go @@ -0,0 +1,63 @@ +package platform + +import ( + "fmt" + "log/slog" + "os" + "strings" +) + +const ( + DatabaseModePostgres = "postgres" + DatabaseModeMemory = "memory" +) + +type Config struct { + HTTPAddress string + DatabaseMode string + DatabaseURL string + StaticDir string + LogLevelName string +} + +func LoadConfig() (Config, error) { + cfg := Config{ + HTTPAddress: valueOrDefault("SENSE_HTTP_ADDRESS", "127.0.0.1:18080"), + DatabaseMode: strings.ToLower(valueOrDefault("SENSE_DATABASE_MODE", DatabaseModePostgres)), + DatabaseURL: strings.TrimSpace(os.Getenv("SENSE_DATABASE_URL")), + StaticDir: strings.TrimSpace(os.Getenv("SENSE_UI_STATIC_DIR")), + LogLevelName: strings.ToLower(valueOrDefault("SENSE_LOG_LEVEL", "info")), + } + + switch cfg.DatabaseMode { + case DatabaseModePostgres: + if cfg.DatabaseURL == "" { + return Config{}, fmt.Errorf("SENSE_DATABASE_URL is required when SENSE_DATABASE_MODE=postgres") + } + case DatabaseModeMemory: + // Explicitly limited to local smoke tests and automated tests. + default: + return Config{}, fmt.Errorf("unsupported SENSE_DATABASE_MODE %q", cfg.DatabaseMode) + } + return cfg, nil +} + +func (c Config) LogLevel() slog.Level { + switch c.LogLevelName { + case "debug": + return slog.LevelDebug + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func valueOrDefault(key, fallback string) string { + if value := strings.TrimSpace(os.Getenv(key)); value != "" { + return value + } + return fallback +} diff --git a/Sense/server/internal/platform/config_test.go b/Sense/server/internal/platform/config_test.go new file mode 100644 index 0000000..8b1cbf0 --- /dev/null +++ b/Sense/server/internal/platform/config_test.go @@ -0,0 +1,23 @@ +package platform + +import "testing" + +func TestLoadConfigRequiresPostgresURL(t *testing.T) { + t.Setenv("SENSE_DATABASE_MODE", "postgres") + t.Setenv("SENSE_DATABASE_URL", "") + if _, err := LoadConfig(); err == nil { + t.Fatal("expected missing PostgreSQL URL to fail") + } +} + +func TestLoadConfigAllowsExplicitMemoryMode(t *testing.T) { + t.Setenv("SENSE_DATABASE_MODE", "memory") + t.Setenv("SENSE_DATABASE_URL", "") + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() error = %v", err) + } + if cfg.DatabaseMode != DatabaseModeMemory { + t.Fatalf("DatabaseMode = %q", cfg.DatabaseMode) + } +} diff --git a/Sense/server/internal/platform/context.go b/Sense/server/internal/platform/context.go new file mode 100644 index 0000000..ee1a685 --- /dev/null +++ b/Sense/server/internal/platform/context.go @@ -0,0 +1,11 @@ +package platform + +import ( + "context" + "net/http" + "time" +) + +func timeLimitedContext(r *http.Request, timeout time.Duration) (context.Context, context.CancelFunc) { + return context.WithTimeout(r.Context(), timeout) +} diff --git a/Sense/server/internal/platform/database.go b/Sense/server/internal/platform/database.go new file mode 100644 index 0000000..c59d22b --- /dev/null +++ b/Sense/server/internal/platform/database.go @@ -0,0 +1,31 @@ +package platform + +import ( + "context" + "database/sql" + "fmt" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +func OpenDatabase(ctx context.Context, cfg Config) (*sql.DB, error) { + if cfg.DatabaseMode == DatabaseModeMemory { + return nil, nil + } + database, err := sql.Open("pgx", cfg.DatabaseURL) + if err != nil { + return nil, fmt.Errorf("open Sense PostgreSQL: %w", err) + } + database.SetMaxOpenConns(20) + database.SetMaxIdleConns(5) + database.SetConnMaxLifetime(30 * time.Minute) + + pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := database.PingContext(pingCtx); err != nil { + database.Close() + return nil, fmt.Errorf("connect Sense PostgreSQL: %w", err) + } + return database, nil +} diff --git a/Sense/server/internal/platform/http.go b/Sense/server/internal/platform/http.go new file mode 100644 index 0000000..f49bf25 --- /dev/null +++ b/Sense/server/internal/platform/http.go @@ -0,0 +1,39 @@ +package platform + +import ( + "encoding/json" + "errors" + "net/http" +) + +type APIError struct { + Status int + Code string + Message string +} + +func (e *APIError) Error() string { return e.Message } + +func WriteJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(value) +} + +func WriteError(w http.ResponseWriter, err error) { + var apiErr *APIError + if errors.As(err, &apiErr) { + WriteJSON(w, apiErr.Status, map[string]any{"error": map[string]string{"code": apiErr.Code, "message": apiErr.Message}}) + return + } + WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": map[string]string{"code": "internal_error", "message": "服务暂时不可用"}}) +} + +func DecodeJSON(r *http.Request, target any) error { + decoder := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return &APIError{Status: http.StatusBadRequest, Code: "invalid_request", Message: "请求内容格式不正确"} + } + return nil +} diff --git a/Sense/server/internal/platform/migrations.go b/Sense/server/internal/platform/migrations.go new file mode 100644 index 0000000..ac34545 --- /dev/null +++ b/Sense/server/internal/platform/migrations.go @@ -0,0 +1,63 @@ +package platform + +import ( + "context" + "database/sql" + "fmt" + "sort" +) + +type Migration struct { + Version int64 + Name string + SQL string +} + +func (a *App) RegisterMigration(migration Migration) { + a.migrations = append(a.migrations, migration) +} + +func (a *App) ApplyMigrations(ctx context.Context) error { + if a.database == nil { + return nil + } + if _, err := a.database.ExecContext(ctx, ` +CREATE TABLE IF NOT EXISTS sense_schema_migrations ( + version BIGINT PRIMARY KEY, + name TEXT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() +)`); err != nil { + return fmt.Errorf("create Sense migration table: %w", err) + } + + sort.Slice(a.migrations, func(i, j int) bool { return a.migrations[i].Version < a.migrations[j].Version }) + for _, migration := range a.migrations { + if err := applyMigration(ctx, a.database, migration); err != nil { + return err + } + } + return nil +} + +func applyMigration(ctx context.Context, database *sql.DB, migration Migration) error { + tx, err := database.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("begin migration %d: %w", migration.Version, err) + } + defer tx.Rollback() + + var exists bool + if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM sense_schema_migrations WHERE version = $1)`, migration.Version).Scan(&exists); err != nil { + return fmt.Errorf("check migration %d: %w", migration.Version, err) + } + if exists { + return tx.Commit() + } + if _, err := tx.ExecContext(ctx, migration.SQL); err != nil { + return fmt.Errorf("apply migration %d (%s): %w", migration.Version, migration.Name, err) + } + if _, err := tx.ExecContext(ctx, `INSERT INTO sense_schema_migrations(version, name) VALUES($1, $2)`, migration.Version, migration.Name); err != nil { + return fmt.Errorf("record migration %d: %w", migration.Version, err) + } + return tx.Commit() +} diff --git a/Sense/server/main.go b/Sense/server/main.go new file mode 100644 index 0000000..5b0205b --- /dev/null +++ b/Sense/server/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + sensecmd "yovision.local/sense/cmd/sense" +) + +func main() { + if err := sensecmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/Sense/ui/.gitignore b/Sense/ui/.gitignore new file mode 100644 index 0000000..e20daba --- /dev/null +++ b/Sense/ui/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.eslintcache + diff --git a/Sense/ui/package.json b/Sense/ui/package.json new file mode 100644 index 0000000..8ff77bd --- /dev/null +++ b/Sense/ui/package.json @@ -0,0 +1,53 @@ +{ + "name": "yovision-sense-ui", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@9.15.1", + "engines": { + "node": "22.22.1", + "pnpm": "9.15.1" + }, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "eslint \"src/**/*.{js,vue}\"" + }, + "dependencies": { + "@element-plus/icons-vue": "2.3.2", + "axios": "1.13.6", + "element-plus": "2.14.4", + "vue": "3.5.41", + "vue-router": "4.6.4", + "vuex": "4.1.0" + }, + "devDependencies": { + "@babel/eslint-parser": "7.28.6", + "@vue/cli-plugin-babel": "5.0.9", + "@vue/cli-service": "5.0.9", + "eslint": "8.57.1", + "eslint-plugin-vue": "9.33.0" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:vue/vue3-recommended" + ], + "parserOptions": { + "parser": "@babel/eslint-parser", + "requireConfigFile": false + }, + "rules": { + "vue/multi-word-component-names": "off" + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} + diff --git a/Sense/ui/pnpm-lock.yaml b/Sense/ui/pnpm-lock.yaml new file mode 100644 index 0000000..fda3cfb --- /dev/null +++ b/Sense/ui/pnpm-lock.yaml @@ -0,0 +1,8091 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: 2.3.2 + version: 2.3.2(vue@3.5.41) + axios: + specifier: 1.13.6 + version: 1.13.6 + element-plus: + specifier: 2.14.4 + version: 2.14.4(vue@3.5.41) + vue: + specifier: 3.5.41 + version: 3.5.41 + vue-router: + specifier: 4.6.4 + version: 4.6.4(vue@3.5.41) + vuex: + specifier: 4.1.0 + version: 4.1.0(vue@3.5.41) + devDependencies: + '@babel/eslint-parser': + specifier: 7.28.6 + version: 7.28.6(@babel/core@7.29.7)(eslint@8.57.1) + '@vue/cli-plugin-babel': + specifier: 5.0.9 + version: 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1))(postcss@8.5.26)(vue@3.5.41) + '@vue/cli-service': + specifier: 5.0.9 + version: 5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1) + eslint: + specifier: 8.57.1 + version: 8.57.1 + eslint-plugin-vue: + specifier: 9.33.0 + version: 9.33.0(eslint@8.57.1) + +packages: + + '@achrinza/node-ipc@9.2.10': + resolution: {integrity: sha512-rCkw57K82y1XA9KwBmuMrupFQr9VOS4Rn77vW2UD2j0+HjlP/npSON9COkUIfocd95B4wv5EpfWMr6lGD4lN3A==} + engines: {node: 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22 || 23 || 24 || 25} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/eslint-parser@7.28.6': + resolution: {integrity: sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.29.7': + resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.29.7': + resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.29.7': + resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7': + resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7': + resolution: {integrity: sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7': + resolution: {integrity: sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7': + resolution: {integrity: sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7': + resolution: {integrity: sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7': + resolution: {integrity: sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-decorators@7.29.7': + resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.29.7': + resolution: {integrity: sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.29.7': + resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.7': + resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.29.7': + resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.29.7': + resolution: {integrity: sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.29.7': + resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.29.7': + resolution: {integrity: sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.29.7': + resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.29.7': + resolution: {integrity: sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.29.7': + resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.29.7': + resolution: {integrity: sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.29.7': + resolution: {integrity: sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.29.7': + resolution: {integrity: sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.29.7': + resolution: {integrity: sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.29.7': + resolution: {integrity: sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.29.7': + resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.29.7': + resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.29.7': + resolution: {integrity: sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.29.7': + resolution: {integrity: sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.29.7': + resolution: {integrity: sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7': + resolution: {integrity: sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.29.7': + resolution: {integrity: sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.29.7': + resolution: {integrity: sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.8': + resolution: {integrity: sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.29.7': + resolution: {integrity: sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.29.7': + resolution: {integrity: sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.29.7': + resolution: {integrity: sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.29.7': + resolution: {integrity: sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.29.7': + resolution: {integrity: sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.29.7': + resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.29.7': + resolution: {integrity: sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.29.7': + resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.29.7': + resolution: {integrity: sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.8': + resolution: {integrity: sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.29.7': + resolution: {integrity: sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.29.7': + resolution: {integrity: sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.7': + resolution: {integrity: sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.29.7': + resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.29.8': + resolution: {integrity: sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.29.7': + resolution: {integrity: sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.29.7': + resolution: {integrity: sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.29.7': + resolution: {integrity: sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.29.7': + resolution: {integrity: sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.29.7': + resolution: {integrity: sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.29.7': + resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7': + resolution: {integrity: sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.7': + resolution: {integrity: sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + + '@node-ipc/js-queue@2.0.3': + resolution: {integrity: sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==} + engines: {node: '>=1.0.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@soda/friendly-errors-webpack-plugin@1.8.1': + resolution: {integrity: sha512-h2ooWqP8XuFqTXT+NyAFbrArzfQA7R6HTezADrvD9Re8fxMLTPPniLdqVTdDaO0eIoLaAwKT+d6w+5GeTk7Vbg==} + engines: {node: '>=8.0.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + '@soda/get-current-script@1.0.2': + resolution: {integrity: sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==} + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@4.19.9': + resolution: {integrity: sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==} + + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vue/babel-helper-vue-jsx-merge-props@1.4.0': + resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-plugin-transform-vue-jsx@1.4.0': + resolution: {integrity: sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-preset-app@5.0.9': + resolution: {integrity: sha512-0rKOF4s/AhaRMJLybxOCgXfwtYhO3pwDSL/q/W8wRs1LzmHAc77FyTXWlun6VyKiSKwSdtH7CvOiWqq+DfofdA==} + peerDependencies: + '@babel/core': '*' + core-js: ^3 + vue: ^2 || ^3.2.13 + peerDependenciesMeta: + core-js: + optional: true + vue: + optional: true + + '@vue/babel-preset-jsx@1.4.0': + resolution: {integrity: sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + vue: '*' + peerDependenciesMeta: + vue: + optional: true + + '@vue/babel-sugar-composition-api-inject-h@1.4.0': + resolution: {integrity: sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-sugar-composition-api-render-instance@1.4.0': + resolution: {integrity: sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-sugar-functional-vue@1.4.0': + resolution: {integrity: sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-sugar-inject-h@1.4.0': + resolution: {integrity: sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-sugar-v-model@1.4.0': + resolution: {integrity: sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/babel-sugar-v-on@1.4.0': + resolution: {integrity: sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/cli-overlay@5.0.9': + resolution: {integrity: sha512-aBdZWrYKxLuFz1FDsk/muFD7GycrsW73Gi11yRc7R2W7Bm8mDRc9HKAI790gdg4NV+chkDFmfkegjg5iMDEpAA==} + + '@vue/cli-plugin-babel@5.0.9': + resolution: {integrity: sha512-oDZt1Kfe4KGNtig3/3zFo2pIeDJij2uS0M6S+tAqQno4Zpla2D8Hk/AR5PrstUd/HmhHZYJoGyF78MOfj3SbWg==} + peerDependencies: + '@vue/cli-service': ^3.0.0 || ^4.0.0 || ^5.0.0-0 + + '@vue/cli-plugin-router@5.0.9': + resolution: {integrity: sha512-kopbO/8kIl5CAffwgptXEwV509i+M0FfwW4sSkgQ2RzpxOYBjQZvp+096mjZfFcWKSmryNP/ri/Mnu78vmhlhw==} + peerDependencies: + '@vue/cli-service': ^3.0.0 || ^4.0.0 || ^5.0.0-0 + + '@vue/cli-plugin-vuex@5.0.9': + resolution: {integrity: sha512-AQhgGNFVd4Pu2crvS0a+hRckgrJv07gzOASdbLd3I72wkT43dd01MLRp8IBRRsu92t3MXenW86AZUCbQBz3//A==} + peerDependencies: + '@vue/cli-service': ^3.0.0 || ^4.0.0 || ^5.0.0-0 + + '@vue/cli-service@5.0.9': + resolution: {integrity: sha512-yTX7GVyM19tEbd+y5/gA6MkVKA6K61nVYHYAivD61Hx6odVFmQsaC3/R3cWAHM1P5oVKCevBbumPljbT+tFG2w==} + engines: {node: ^12.0.0 || >= 14.0.0} + hasBin: true + peerDependencies: + cache-loader: '*' + less-loader: '*' + pug-plain-loader: '*' + raw-loader: '*' + sass-loader: '*' + stylus-loader: '*' + vue-template-compiler: ^2.0.0 + webpack-sources: '*' + peerDependenciesMeta: + cache-loader: + optional: true + less-loader: + optional: true + pug-plain-loader: + optional: true + raw-loader: + optional: true + sass-loader: + optional: true + stylus-loader: + optional: true + vue-template-compiler: + optional: true + webpack-sources: + optional: true + + '@vue/cli-shared-utils@5.0.9': + resolution: {integrity: sha512-lf4KykiG8j9KwvNVi7fKtASmHuLsxCcCsflVU2b2CHMRuR4weOIV3zuuCrjWKjk0APn/MHJhgCjJGzHMbTtd5w==} + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + + '@vue/component-compiler-utils@3.3.0': + resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + + '@vue/web-component-wrapper@1.3.0': + resolution: {integrity: sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==} + + '@vueuse/core@14.3.0': + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.3.0': + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + autoprefixer@10.5.4: + resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + babel-loader@8.4.1: + resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + + babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.13: + resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bonjour-service@1.4.4: + resolution: {integrity: sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + + case-sensitive-paths-webpack-plugin@2.4.0: + resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} + engines: {node: '>=4'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + clipboardy@2.3.0: + resolution: {integrity: sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==} + engines: {node: '>=8'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + consolidate@0.15.1: + resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} + engines: {node: '>= 0.10.0'} + deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog + peerDependencies: + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + babel-core: ^6.26.3 + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jade: ^1.11.0 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + marko: ^3.14.4 + mote: ^0.2.0 + mustache: ^3.0.0 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + razor-tmpl: ^1.3.1 + react: ^16.13.1 + react-dom: ^16.13.1 + slm: ^2.0.0 + squirrelly: ^5.1.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-jade: '*' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + arc-templates: + optional: true + atpl: + optional: true + babel-core: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jade: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + marko: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + razor-tmpl: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + squirrelly: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-jade: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-webpack-plugin@9.1.0: + resolution: {integrity: sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.1.0 + + core-js-compat@3.50.0: + resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} + engines: {node: '>=6.4.0'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-declaration-sorter@6.4.1: + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-minimizer-webpack-plugin@3.4.1: + resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@parcel/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@5.2.14: + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + cssnano-utils@3.1.0: + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + cssnano@5.1.15: + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@1.5.2: + resolution: {integrity: sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==} + engines: {node: '>=0.10.0'} + + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + easy-stack@1.0.1: + resolution: {integrity: sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==} + engines: {node: '>=6.0.0'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.405: + resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==} + + element-plus@2.14.4: + resolution: {integrity: sha512-vMKR9tFcLeNrJgFXA3zhUn6YuRKUQW9d0btakBR8U1Iq8MfzkjMOGcgs5de2VgiLldHt69brmuBHpxc3bK4ZgQ==} + peerDependencies: + vue: ^3.3.7 + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-pubsub@4.3.0: + resolution: {integrity: sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==} + engines: {node: '>=4.0.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-sum@1.0.2: + resolution: {integrity: sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==} + + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-tags@2.0.0: + resolution: {integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==} + engines: {node: '>=4'} + + html-webpack-plugin@5.6.8: + resolution: {integrity: sha512-MZmKQcTnhEh1SPSyMiEytIeDZDUoBZVorNHivQGXMASHf/BSGGOrKa2xQ5bGx3TCe1n109ecCt+cpww7wwWhKA==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x || 2.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-middleware@2.0.10: + resolution: {integrity: sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} + engines: {node: '>= 10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-file-esm@1.0.0: + resolution: {integrity: sha512-rZlaNKb4Mr8WlRu2A9XdeoKgnO5aA53XdPHgCKVyCrQ/rWi89RET1+bq37Ru46obaQXeiX4vmFIm1vks41hoSA==} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + joi@17.13.4: + resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} + + js-message@1.0.7: + resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==} + engines: {node: '>=0.6.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + launch-editor-middleware@2.14.1: + resolution: {integrity: sha512-Y8rM8ujxag67bXKCML5RLK5neOwj9rcgYlhgPAG0roa3sM6gjBjq7Bl/mE63XcdAmjMMsPtJhd1ry8+wstwQew==} + + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaultsdeep@4.6.1: + resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.mapvalues@4.6.0: + resolution: {integrity: sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@2.3.0: + resolution: {integrity: sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==} + engines: {node: '>=4'} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-source-map@1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mini-css-extract-plugin@2.10.2: + resolution: {integrity: sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minimizer-webpack-plugin@5.6.1: + resolution: {integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + module-alias@2.3.4: + resolution: {integrity: sha512-bOclZt8hkpuGgSSoG07PKmvzTizROilUTvLNyrMqvlC9snhs7y7GzjNWAVbISIOlhCP1T14rH1PDAV9iNyBq/w==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + portfinder@1.0.38: + resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} + engines: {node: '>= 10.12'} + + postcss-calc@8.2.4: + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + + postcss-colormin@5.3.1: + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-convert-values@5.1.3: + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-comments@5.1.2: + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-duplicates@5.1.0: + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-empty@5.1.1: + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-overridden@5.1.0: + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-loader@6.2.1: + resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-merge-longhand@5.1.7: + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-merge-rules@5.1.4: + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-font-values@5.1.0: + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-gradients@5.1.1: + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-params@5.1.4: + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-selectors@5.2.1: + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-normalize-charset@5.1.0: + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-display-values@5.1.0: + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-positions@5.1.1: + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-repeat-style@5.1.1: + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-string@5.1.0: + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-timing-functions@5.1.0: + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-unicode@5.1.1: + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-url@5.1.0: + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-whitespace@5.1.1: + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-ordered-values@5.1.3: + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-reduce-initial@5.1.2: + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-reduce-transforms@5.1.0: + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.5: + resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} + engines: {node: '>=4'} + + postcss-svgo@5.1.0: + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-unique-selectors@5.1.1: + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress-webpack-plugin@1.0.16: + resolution: {integrity: sha512-sdiHuuKOzELcBANHfrupYo+r99iPRyOnw15qX+rNlVUqXGfjXdH4IgxriKwG1kNJwVswKQHMdj1hYZMcb9jFaA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-index@1.9.2: + resolution: {integrity: sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stylehacks@5.1.1: + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@2.8.3: + resolution: {integrity: sha512-5EZD0pafXX6PphdwOGCiVLDSaV1xyuQao2blHajHLsPxr07q4mmEjdtXEWgG07ae2mIz8Ex2CDXNCTiXhy3Khw==} + engines: {node: '>=10.13.0'} + hasBin: true + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.50.0: + resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-loader@3.0.4: + resolution: {integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.27.0 || ^5.0.0 + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vue-component-type-helpers@3.3.9: + resolution: {integrity: sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==} + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-hot-reload-api@2.3.4: + resolution: {integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==} + + vue-loader@15.11.1: + resolution: {integrity: sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==} + peerDependencies: + '@vue/compiler-sfc': ^3.0.8 + cache-loader: '*' + css-loader: '*' + prettier: '*' + vue-template-compiler: '*' + webpack: ^3.0.0 || ^4.1.0 || ^5.0.0-0 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + cache-loader: + optional: true + prettier: + optional: true + vue-template-compiler: + optional: true + + vue-loader@17.4.2: + resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==} + peerDependencies: + '@vue/compiler-sfc': '*' + vue: '*' + webpack: ^4.1.0 || ^5.0.0-0 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + vue: + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-style-loader@4.1.3: + resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==} + + vue-template-es2015-compiler@1.9.1: + resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vuex@4.1.0: + resolution: {integrity: sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==} + peerDependencies: + vue: ^3.2.0 + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-bundle-analyzer@4.10.2: + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} + hasBin: true + + webpack-chain@6.5.1: + resolution: {integrity: sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==} + engines: {node: '>=8'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + webpack-dev-server@4.15.2: + resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.4.6: + resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==} + + webpack@5.109.2: + resolution: {integrity: sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + websocket-driver@0.7.5: + resolution: {integrity: sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@3.0.1: + resolution: {integrity: sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==} + engines: {node: '>=4'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@achrinza/node-ipc@9.2.10': + dependencies: + '@node-ipc/js-queue': 2.0.3 + event-pubsub: 4.3.0 + js-message: 1.0.7 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/eslint-parser@7.28.6(@babel/core@7.29.7)(eslint@8.57.1)': + dependencies: + '@babel/core': 7.29.7 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.8 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-wrap-function': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helper-wrap-function@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/template': 7.29.7 + + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.8(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regenerator@7.29.8(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-spread@7.29.8(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/preset-env@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-systemjs': 7.29.8(@babel/core@7.29.7) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.29.8(@babel/core@7.29.7) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-spread': 7.29.8(@babel/core@7.29.7) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.50.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.8 + esutils: 2.0.3 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@ctrl/tinycolor@4.2.0': {} + + '@discoveryjs/json-ext@0.5.7': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.41)': + dependencies: + vue: 3.5.41 + + '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + dependencies: + eslint-scope: 5.1.1 + + '@node-ipc/js-queue@2.0.3': + dependencies: + easy-stack: 1.0.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@polka/url@1.0.0-next.29': {} + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@soda/friendly-errors-webpack-plugin@1.8.1(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26))': + dependencies: + chalk: 3.0.0 + error-stack-parser: 2.1.4 + string-width: 4.2.3 + strip-ansi: 6.0.1 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + '@soda/get-current-script@1.0.2': {} + + '@sxzz/popperjs-es@2.11.8': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 26.2.0 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 26.2.0 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.1.3 + '@types/node': 26.2.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 26.2.0 + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@4.19.9': + dependencies: + '@types/node': 26.2.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 26.2.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.9 + '@types/qs': 6.15.1 + '@types/serve-static': 1.15.10 + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 26.2.0 + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/mime@1.3.5': {} + + '@types/minimist@1.2.5': {} + + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 26.2.0 + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/parse-json@4.0.2': {} + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/retry@0.12.0': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 26.2.0 + + '@types/send@1.2.1': + dependencies: + '@types/node': 26.2.0 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.25 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 26.2.0 + '@types/send': 0.17.6 + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 26.2.0 + + '@types/web-bluetooth@0.0.21': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.2.0 + + '@ungap/structured-clone@1.3.3': {} + + '@vue/babel-helper-vue-jsx-merge-props@1.4.0': {} + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7) + '@vue/shared': 3.5.41 + optionalDependencies: + '@babel/core': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/parser': 7.29.8 + '@vue/compiler-sfc': 3.5.41 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-transform-vue-jsx@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + html-tags: 2.0.0 + lodash.kebabcase: 4.1.1 + svg-tags: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-preset-app@5.0.9(@babel/core@7.29.7)(vue@3.5.41)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7) + '@babel/runtime': 7.29.7 + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7) + '@vue/babel-preset-jsx': 1.4.0(@babel/core@7.29.7)(vue@3.5.41) + babel-plugin-dynamic-import-node: 2.3.3 + core-js-compat: 3.50.0 + semver: 7.8.5 + optionalDependencies: + vue: 3.5.41 + transitivePeerDependencies: + - supports-color + + '@vue/babel-preset-jsx@1.4.0(@babel/core@7.29.7)(vue@3.5.41)': + dependencies: + '@babel/core': 7.29.7 + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-composition-api-inject-h': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-composition-api-render-instance': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-functional-vue': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-inject-h': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-v-model': 1.4.0(@babel/core@7.29.7) + '@vue/babel-sugar-v-on': 1.4.0(@babel/core@7.29.7) + optionalDependencies: + vue: 3.5.41 + transitivePeerDependencies: + - supports-color + + '@vue/babel-sugar-composition-api-inject-h@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + + '@vue/babel-sugar-composition-api-render-instance@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + + '@vue/babel-sugar-functional-vue@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + + '@vue/babel-sugar-inject-h@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + + '@vue/babel-sugar-v-model@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.29.7) + camelcase: 5.3.1 + html-tags: 2.0.0 + svg-tags: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-sugar-v-on@1.4.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.29.7) + camelcase: 5.3.1 + transitivePeerDependencies: + - supports-color + + '@vue/cli-overlay@5.0.9': {} + + '@vue/cli-plugin-babel@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1))(postcss@8.5.26)(vue@3.5.41)': + dependencies: + '@babel/core': 7.29.7 + '@vue/babel-preset-app': 5.0.9(@babel/core@7.29.7)(vue@3.5.41) + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1) + '@vue/cli-shared-utils': 5.0.9 + babel-loader: 8.4.1(@babel/core@7.29.7)(webpack@5.109.2(postcss@8.5.26)) + thread-loader: 3.0.4(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - core-js + - cssnano + - csso + - encoding + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - supports-color + - uglify-js + - vue + - webpack-cli + + '@vue/cli-plugin-router@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1))': + dependencies: + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1) + '@vue/cli-shared-utils': 5.0.9 + transitivePeerDependencies: + - encoding + + '@vue/cli-plugin-vuex@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1))': + dependencies: + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1) + + '@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1)': + dependencies: + '@babel/helper-compilation-targets': 7.29.7 + '@soda/friendly-errors-webpack-plugin': 1.8.1(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + '@soda/get-current-script': 1.0.2 + '@types/minimist': 1.2.5 + '@vue/cli-overlay': 5.0.9 + '@vue/cli-plugin-router': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1)) + '@vue/cli-plugin-vuex': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.41)(lodash@4.18.1)(vue@3.5.41)(webpack-sources@3.5.1)) + '@vue/cli-shared-utils': 5.0.9 + '@vue/component-compiler-utils': 3.3.0(lodash@4.18.1) + '@vue/vue-loader-v15': vue-loader@15.11.1(@vue/compiler-sfc@3.5.41)(css-loader@6.11.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)))(lodash@4.18.1)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + '@vue/web-component-wrapper': 1.3.0 + acorn: 8.18.0 + acorn-walk: 8.3.5 + address: 1.2.2 + autoprefixer: 10.5.4(postcss@8.5.26) + browserslist: 4.28.8 + case-sensitive-paths-webpack-plugin: 2.4.0 + cli-highlight: 2.1.11 + clipboardy: 2.3.0 + cliui: 7.0.4 + copy-webpack-plugin: 9.1.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + css-loader: 6.11.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + css-minimizer-webpack-plugin: 3.4.1(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + cssnano: 5.1.15(postcss@8.5.26) + debug: 4.4.3 + default-gateway: 6.0.3 + dotenv: 10.0.0 + dotenv-expand: 5.1.0 + fs-extra: 9.1.0 + globby: 11.1.0 + hash-sum: 2.0.0 + html-webpack-plugin: 5.6.8(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + is-file-esm: 1.0.0 + launch-editor-middleware: 2.14.1 + lodash.defaultsdeep: 4.6.1 + lodash.mapvalues: 4.6.0 + mini-css-extract-plugin: 2.10.2(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + minimist: 1.2.8 + module-alias: 2.3.4 + portfinder: 1.0.38 + postcss: 8.5.26 + postcss-loader: 6.2.1(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + progress-webpack-plugin: 1.0.16(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + ssri: 8.0.1 + terser-webpack-plugin: 5.6.1(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + thread-loader: 3.0.4(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + vue-loader: 17.4.2(@vue/compiler-sfc@3.5.41)(vue@3.5.41)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + vue-style-loader: 4.1.3 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + webpack-bundle-analyzer: 4.10.2 + webpack-chain: 6.5.1 + webpack-dev-server: 4.15.2(debug@4.4.3)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + webpack-merge: 5.10.0 + webpack-virtual-modules: 0.4.6 + whatwg-fetch: 3.6.20 + optionalDependencies: + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - '@vue/compiler-sfc' + - arc-templates + - atpl + - babel-core + - bracket-template + - bufferutil + - clean-css + - coffee-script + - csso + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - encoding + - esbuild + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - html-minifier-terser + - htmling + - jade + - jazz + - jqtpl + - just + - lightningcss + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - prettier + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - uglify-js + - underscore + - utf-8-validate + - vash + - velocityjs + - vue + - walrus + - webpack-cli + - whiskers + + '@vue/cli-shared-utils@5.0.9': + dependencies: + '@achrinza/node-ipc': 9.2.10 + chalk: 4.1.2 + execa: 1.0.0 + joi: 17.13.4 + launch-editor: 2.14.1 + lru-cache: 6.0.0 + node-fetch: 2.7.0 + open: 8.4.2 + ora: 5.4.1 + read-pkg: 5.2.0 + semver: 7.8.5 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - encoding + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.26 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/component-compiler-utils@3.3.0(lodash@4.18.1)': + dependencies: + consolidate: 0.15.1(lodash@4.18.1) + hash-sum: 1.0.2 + lru-cache: 4.1.5 + merge-source-map: 1.1.0 + postcss: 7.0.39 + postcss-selector-parser: 6.1.4 + source-map: 0.6.1 + vue-template-es2015-compiler: 1.9.1 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vue/web-component-wrapper@1.3.0': {} + + '@vueuse/core@14.3.0(vue@3.5.41)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.3.0 + '@vueuse/shared': 14.3.0(vue@3.5.41) + vue: 3.5.41 + + '@vueuse/metadata@14.3.0': {} + + '@vueuse/shared@14.3.0(vue@3.5.41)': + dependencies: + vue: 3.5.41 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + address@1.2.2: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@3.2.0: {} + + ansi-html-community@0.0.8: {} + + ansi-regex@3.0.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arch@2.2.0: {} + + argparse@2.0.1: {} + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + autoprefixer@10.5.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + caniuse-lite: 1.0.30001809 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + axios@1.13.6: + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.6 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-loader@8.4.1(@babel/core@7.29.7)(webpack@5.109.2(postcss@8.5.26)): + dependencies: + '@babel/core': 7.29.7 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + babel-plugin-dynamic-import-node@2.3.3: + dependencies: + object.assign: 4.1.7 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.50.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.50.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.13: {} + + batch@0.6.1: {} + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.7.2: {} + + body-parser@1.20.6: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.4.4: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.13 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.405 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.8 + caniuse-lite: 1.0.30001809 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001809: {} + + case-sensitive-paths-webpack-plugin@2.4.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrome-trace-event@1.0.4: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cli-spinners@2.9.2: {} + + clipboardy@2.3.0: + dependencies: + arch: 2.2.0 + execa: 1.0.0 + is-wsl: 2.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clone@1.0.4: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + commondir@1.0.1: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + connect-history-api-fallback@2.0.0: {} + + consolidate@0.15.1(lodash@4.18.1): + dependencies: + bluebird: 3.7.2 + optionalDependencies: + lodash: 4.18.1 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + copy-webpack-plugin@9.1.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + fast-glob: 3.3.3 + glob-parent: 6.0.2 + globby: 11.1.0 + normalize-path: 3.0.0 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + core-js-compat@3.50.0: + dependencies: + browserslist: 4.28.8 + + core-util-is@1.0.3: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-declaration-sorter@6.4.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + css-loader@6.11.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.26) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.26) + postcss-modules-scope: 3.2.1(postcss@8.5.26) + postcss-modules-values: 4.0.0(postcss@8.5.26) + postcss-value-parser: 4.2.0 + semver: 7.8.5 + optionalDependencies: + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + css-minimizer-webpack-plugin@3.4.1(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + cssnano: 5.1.15(postcss@8.5.26) + jest-worker: 27.5.1 + postcss: 8.5.26 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + source-map: 0.6.1 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@5.2.14(postcss@8.5.26): + dependencies: + css-declaration-sorter: 6.4.1(postcss@8.5.26) + cssnano-utils: 3.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-calc: 8.2.4(postcss@8.5.26) + postcss-colormin: 5.3.1(postcss@8.5.26) + postcss-convert-values: 5.1.3(postcss@8.5.26) + postcss-discard-comments: 5.1.2(postcss@8.5.26) + postcss-discard-duplicates: 5.1.0(postcss@8.5.26) + postcss-discard-empty: 5.1.1(postcss@8.5.26) + postcss-discard-overridden: 5.1.0(postcss@8.5.26) + postcss-merge-longhand: 5.1.7(postcss@8.5.26) + postcss-merge-rules: 5.1.4(postcss@8.5.26) + postcss-minify-font-values: 5.1.0(postcss@8.5.26) + postcss-minify-gradients: 5.1.1(postcss@8.5.26) + postcss-minify-params: 5.1.4(postcss@8.5.26) + postcss-minify-selectors: 5.2.1(postcss@8.5.26) + postcss-normalize-charset: 5.1.0(postcss@8.5.26) + postcss-normalize-display-values: 5.1.0(postcss@8.5.26) + postcss-normalize-positions: 5.1.1(postcss@8.5.26) + postcss-normalize-repeat-style: 5.1.1(postcss@8.5.26) + postcss-normalize-string: 5.1.0(postcss@8.5.26) + postcss-normalize-timing-functions: 5.1.0(postcss@8.5.26) + postcss-normalize-unicode: 5.1.1(postcss@8.5.26) + postcss-normalize-url: 5.1.0(postcss@8.5.26) + postcss-normalize-whitespace: 5.1.1(postcss@8.5.26) + postcss-ordered-values: 5.1.3(postcss@8.5.26) + postcss-reduce-initial: 5.1.2(postcss@8.5.26) + postcss-reduce-transforms: 5.1.0(postcss@8.5.26) + postcss-svgo: 5.1.0(postcss@8.5.26) + postcss-unique-selectors: 5.1.1(postcss@8.5.26) + + cssnano-utils@3.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + cssnano@5.1.15(postcss@8.5.26): + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.5.26) + lilconfig: 2.1.0 + postcss: 8.5.26 + yaml: 1.10.3 + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + csstype@3.2.3: {} + + dayjs@1.11.21: {} + + debounce@1.2.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge@1.5.2: {} + + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-node@2.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dotenv-expand@5.1.0: {} + + dotenv@10.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + easy-stack@1.0.1: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.405: {} + + element-plus@2.14.4(vue@3.5.41): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.41) + '@floating-ui/dom': 1.8.0 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 14.3.0(vue@3.5.41) + async-validator: 4.2.5 + dayjs: 1.11.21 + lodash: 4.18.1 + lodash-es: 4.18.1 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.41 + vue-component-type-helpers: 3.3.9 + + emoji-regex@8.0.0: {} + + emojis-list@3.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@2.2.0: {} + + entities@7.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@9.33.0(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) + eslint: 8.57.1 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.4 + semver: 7.8.5 + vue-eslint-parser: 9.4.3(eslint@8.57.1) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.3 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + 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.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-pubsub@4.3.0: {} + + eventemitter3@4.0.7: {} + + events@3.3.0: {} + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.6 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.5: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.5 + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@5.0.2: {} + + flatted@3.4.4: {} + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@0.5.2: {} + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + + get-stream@6.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handle-thing@2.0.1: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-sum@1.0.2: {} + + hash-sum@2.0.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + highlight.js@10.7.3: {} + + hosted-git-info@2.8.9: {} + + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.50.0 + + html-tags@2.0.0: {} + + html-webpack-plugin@5.6.8(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.18.1 + pretty-error: 4.0.0 + tapable: 2.3.3 + optionalDependencies: + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + http-deceiver@1.2.7: {} + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-middleware@2.0.10(@types/express@4.17.25)(debug@4.4.3): + dependencies: + '@types/http-proxy': 1.17.17 + http-proxy: 1.18.1(debug@4.4.3) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.25 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1(debug@4.4.3): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0(debug@4.4.3) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.5.0: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-file-esm@1.0.0: + dependencies: + read-pkg-up: 7.0.1 + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@3.0.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + javascript-stringify@2.1.0: {} + + jest-worker@27.5.1: + dependencies: + '@types/node': 26.2.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + joi@17.13.4: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + js-message@1.0.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + klona@2.0.6: {} + + launch-editor-middleware@2.14.1: + dependencies: + launch-editor: 2.14.1 + + launch-editor@2.14.1: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.10.0 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.2: {} + + loader-utils@1.4.2: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.18.1 + lodash-es: 4.18.1 + + lodash.debounce@4.0.8: {} + + lodash.defaultsdeep@4.6.1: {} + + lodash.kebabcase@4.1.1: {} + + lodash.mapvalues@4.6.0: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.uniq@4.5.0: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@2.3.0: + dependencies: + ansi-escapes: 3.2.0 + cli-cursor: 2.1.0 + wrap-ansi: 3.0.1 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.14: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + memoize-one@6.0.0: {} + + merge-descriptors@1.0.3: {} + + merge-source-map@1.1.0: + dependencies: + source-map: 0.6.1 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + + mini-css-extract-plugin@2.10.2(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + schema-utils: 4.3.3 + tapable: 2.3.3 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimist@1.2.8: {} + + minimizer-webpack-plugin@5.6.1(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.50.0 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + optionalDependencies: + cssnano: 5.1.15(postcss@8.5.26) + postcss: 8.5.26 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + module-alias@2.3.4: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.18: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + neo-async@2.6.2: {} + + nice-try@1.0.5: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.4.0: {} + + node-releases@2.0.53: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.12 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + obuf@1.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + opener@1.5.2: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-try@2.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.13: {} + + path-type@4.0.0: {} + + picocolors@0.2.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + portfinder@1.0.38: + dependencies: + async: 3.2.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + postcss-calc@8.2.4(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser: 6.1.4 + postcss-value-parser: 4.2.0 + + postcss-colormin@5.3.1(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-convert-values@5.1.3(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-discard-comments@5.1.2(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-discard-duplicates@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-discard-empty@5.1.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-discard-overridden@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-loader@6.2.1(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + cosmiconfig: 7.1.0 + klona: 2.0.6 + postcss: 8.5.26 + semver: 7.8.5 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + postcss-merge-longhand@5.1.7(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.5.26) + + postcss-merge-rules@5.1.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-selector-parser: 6.1.4 + + postcss-minify-font-values@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@5.1.1(postcss@8.5.26): + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-minify-params@5.1.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + cssnano-utils: 3.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@5.2.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser: 6.1.4 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.26): + dependencies: + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-selector-parser: 7.1.5 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser: 7.1.5 + + postcss-modules-values@4.0.0(postcss@8.5.26): + dependencies: + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + + postcss-normalize-charset@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + + postcss-normalize-display-values@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@5.1.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@5.1.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@5.1.1(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@5.1.0(postcss@8.5.26): + dependencies: + normalize-url: 6.1.0 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@5.1.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@5.1.3(postcss@8.5.26): + dependencies: + cssnano-utils: 3.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@5.1.2(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + caniuse-api: 3.0.0 + postcss: 8.5.26 + + postcss-reduce-transforms@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + postcss-selector-parser@6.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.5: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@5.1.0(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + svgo: 2.8.3 + + postcss-unique-selectors@5.1.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser: 6.1.4 + + postcss-value-parser@4.2.0: {} + + postcss@7.0.39: + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: + optional: true + + pretty-error@4.0.0: + dependencies: + lodash: 4.18.1 + renderkid: 3.0.0 + + process-nextick-args@2.0.1: {} + + progress-webpack-plugin@1.0.16(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + chalk: 2.4.2 + figures: 2.0.0 + log-update: 2.3.0 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + pseudomap@1.0.2: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + queue-microtask@1.2.3: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + range-parser@1.3.0: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.2: + dependencies: + jsesc: 3.1.0 + + relateurl@0.2.7: {} + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.18.1 + strip-ansi: 6.0.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.6.1: {} + + schema-utils@2.7.1: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.14 + node-forge: 1.4.0 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-index@1.9.2: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.8.1 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.10.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.5 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + spdy-transport@3.0.0: + dependencies: + debug: 4.4.3 + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.3 + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + + stable@0.1.8: {} + + stackframe@1.3.4: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + stylehacks@5.1.1(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + postcss: 8.5.26 + postcss-selector-parser: 6.1.4 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-tags@1.0.0: {} + + svgo@2.8.3: + dependencies: + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.1.1 + sax: 1.6.1 + stable: 0.1.8 + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.50.0 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + optionalDependencies: + cssnano: 5.1.15(postcss@8.5.26) + postcss: 8.5.26 + + terser@5.50.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-loader@3.0.4(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + json-parse-better-errors: 1.0.2 + loader-runner: 4.3.2 + loader-utils: 2.0.4 + neo-async: 2.6.2 + schema-utils: 3.3.0 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + thunky@1.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + undici-types@8.3.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utila@0.4.0: {} + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vary@1.1.2: {} + + vue-component-type-helpers@3.3.9: {} + + vue-eslint-parser@9.4.3(eslint@8.57.1): + dependencies: + debug: 4.4.3 + eslint: 8.57.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + lodash: 4.18.1 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + vue-hot-reload-api@2.3.4: {} + + vue-loader@15.11.1(@vue/compiler-sfc@3.5.41)(css-loader@6.11.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)))(lodash@4.18.1)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + '@vue/component-compiler-utils': 3.3.0(lodash@4.18.1) + css-loader: 6.11.0(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + hash-sum: 1.0.2 + loader-utils: 1.4.2 + vue-hot-reload-api: 2.3.4 + vue-style-loader: 4.1.3 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + optionalDependencies: + '@vue/compiler-sfc': 3.5.41 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + + vue-loader@17.4.2(@vue/compiler-sfc@3.5.41)(vue@3.5.41)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + chalk: 4.1.2 + hash-sum: 2.0.0 + watchpack: 2.5.2 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + optionalDependencies: + '@vue/compiler-sfc': 3.5.41 + vue: 3.5.41 + + vue-router@4.6.4(vue@3.5.41): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.41 + + vue-style-loader@4.1.3: + dependencies: + hash-sum: 1.0.2 + loader-utils: 1.4.2 + + vue-template-es2015-compiler@1.9.1: {} + + vue@3.5.41: + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + + vuex@4.1.0(vue@3.5.41): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.41 + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-bundle-analyzer@4.10.2: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.18.0 + acorn-walk: 8.3.5 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + webpack-chain@6.5.1: + dependencies: + deepmerge: 1.5.2 + javascript-stringify: 2.1.0 + + webpack-dev-middleware@5.3.4(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.3.0 + schema-utils: 4.3.3 + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + + webpack-dev-server@4.15.2(debug@4.4.3)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.4.4 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.22.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.10(@types/express@4.17.25)(debug@4.4.3) + ipaddr.js: 2.5.0 + launch-editor: 2.14.1 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.3 + selfsigned: 2.4.1 + serve-index: 1.9.2 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + ws: 8.21.3 + optionalDependencies: + webpack: 5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.5.1: {} + + webpack-virtual-modules@0.4.6: {} + + webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.18.0 + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)(webpack@5.109.2(cssnano@5.1.15(postcss@8.5.26))(postcss@8.5.26)) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + websocket-driver@0.7.5: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-fetch@3.6.20: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wildcard@2.0.1: {} + + word-wrap@1.2.5: {} + + wrap-ansi@3.0.1: + dependencies: + string-width: 2.1.1 + strip-ansi: 4.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.13: {} + + ws@8.21.3: {} + + xml-name-validator@4.0.0: {} + + y18n@5.0.8: {} + + yallist@2.1.2: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@1.10.3: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yocto-queue@0.1.0: {} diff --git a/Sense/ui/src/bootstrap/App.vue b/Sense/ui/src/bootstrap/App.vue new file mode 100644 index 0000000..f571bf8 --- /dev/null +++ b/Sense/ui/src/bootstrap/App.vue @@ -0,0 +1,7 @@ + + + + diff --git a/Sense/ui/src/bootstrap/Dashboard.vue b/Sense/ui/src/bootstrap/Dashboard.vue new file mode 100644 index 0000000..fc9b54e --- /dev/null +++ b/Sense/ui/src/bootstrap/Dashboard.vue @@ -0,0 +1,55 @@ + + + + + + diff --git a/Sense/ui/src/bootstrap/index.html b/Sense/ui/src/bootstrap/index.html new file mode 100644 index 0000000..86dea06 --- /dev/null +++ b/Sense/ui/src/bootstrap/index.html @@ -0,0 +1,14 @@ + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + diff --git a/Sense/ui/src/bootstrap/main.js b/Sense/ui/src/bootstrap/main.js new file mode 100644 index 0000000..59fbd7f --- /dev/null +++ b/Sense/ui/src/bootstrap/main.js @@ -0,0 +1,19 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + +import App from './App.vue' +import router from './router' +import store from './store' +import './theme.css' + +const app = createApp(App) +Object.entries(ElementPlusIconsVue).forEach(([name, component]) => { + app.component(name, component) +}) +app.use(ElementPlus) +app.use(store) +app.use(router) +app.mount('#app') + diff --git a/Sense/ui/src/bootstrap/request.js b/Sense/ui/src/bootstrap/request.js new file mode 100644 index 0000000..f51b824 --- /dev/null +++ b/Sense/ui/src/bootstrap/request.js @@ -0,0 +1,16 @@ +import axios from 'axios' + +const request = axios.create({ + baseURL: '/api/v1', + timeout: 15000, + withCredentials: true, + headers: { 'X-Product': 'sense' } +}) + +request.interceptors.response.use( + (response) => response.data, + (error) => Promise.reject(error.response?.data?.error || error) +) + +export default request + diff --git a/Sense/ui/src/bootstrap/router.js b/Sense/ui/src/bootstrap/router.js new file mode 100644 index 0000000..572561f --- /dev/null +++ b/Sense/ui/src/bootstrap/router.js @@ -0,0 +1,45 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import AppLayout from '../layout/AppLayout.vue' +import Dashboard from './Dashboard.vue' + +const featureContext = require.context('../', true, /router\/modules\/sense-.*\.js$/) +const featureRoutes = featureContext.keys().flatMap((key) => { + const module = featureContext(key).default + return Array.isArray(module) ? module : [module] +}) + +const publicRoutes = featureRoutes.filter((route) => route.meta?.public) +const protectedRoutes = featureRoutes.filter((route) => !route.meta?.public) + +const router = createRouter({ + history: createWebHistory(), + routes: [ + ...publicRoutes, + { + path: '/', + component: AppLayout, + children: [ + { + path: '', + name: 'sense-dashboard', + component: Dashboard, + meta: { title: '工作台', icon: 'Odometer', order: 0 } + }, + ...protectedRoutes + ] + }, + { path: '/:pathMatch(.*)*', redirect: '/' } + ] +}) + +router.beforeEach((to) => { + if (to.meta.public) return true + const session = window.sessionStorage.getItem('sense_session') + const hasIdentityModule = router.hasRoute('sense-login') + if (hasIdentityModule && !session) return { name: 'sense-login', query: { redirect: to.fullPath } } + return true +}) + +export default router + diff --git a/Sense/ui/src/bootstrap/store.js b/Sense/ui/src/bootstrap/store.js new file mode 100644 index 0000000..3c56a36 --- /dev/null +++ b/Sense/ui/src/bootstrap/store.js @@ -0,0 +1,11 @@ +import { createStore } from 'vuex' + +const moduleContext = require.context('../', true, /store\/modules\/sense-.*\.js$/) +const modules = {} +moduleContext.keys().forEach((key) => { + const name = key.split('/').pop().replace('.js', '') + modules[name] = moduleContext(key).default +}) + +export default createStore({ modules }) + diff --git a/Sense/ui/src/bootstrap/theme.css b/Sense/ui/src/bootstrap/theme.css new file mode 100644 index 0000000..9331a8d --- /dev/null +++ b/Sense/ui/src/bootstrap/theme.css @@ -0,0 +1,25 @@ +:root { + --sense-bg: #f2f3f5; + --sense-sidebar: #001529; + --sense-primary: #1890ff; + --sense-text: #1d2129; + --sense-muted: #86909c; + --sense-border: #e5e6eb; +} + +* { box-sizing: border-box; } +html, body, #app { min-width: 1080px; min-height: 100%; margin: 0; } +body { + background: var(--sense-bg); + color: var(--sense-text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; +} +.page-container { padding: 20px; } +.page-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } +.page-heading h1 { margin: 0 0 6px; font-size: 22px; font-weight: 600; } +.page-heading p { margin: 0; color: var(--sense-muted); font-size: 14px; } +.search-toolbar { margin-bottom: 16px; padding: 16px 16px 0; background: #fff; border: 1px solid var(--sense-border); border-radius: 4px; } +.table-container { padding: 16px; background: #fff; border: 1px solid var(--sense-border); border-radius: 4px; } +.table-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } +.pagination { display: flex; justify-content: flex-end; margin-top: 16px; } + diff --git a/Sense/ui/src/layout/AppLayout.vue b/Sense/ui/src/layout/AppLayout.vue new file mode 100644 index 0000000..ff05b0a --- /dev/null +++ b/Sense/ui/src/layout/AppLayout.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/Sense/ui/vue.config.js b/Sense/ui/vue.config.js new file mode 100644 index 0000000..777ca68 --- /dev/null +++ b/Sense/ui/vue.config.js @@ -0,0 +1,24 @@ +const { defineConfig } = require('@vue/cli-service') + +module.exports = defineConfig({ + transpileDependencies: true, + publicPath: '/', + outputDir: 'dist', + pages: { + index: { + entry: 'src/bootstrap/main.js', + template: 'src/bootstrap/index.html', + title: 'Sense 视频感知管理' + } + }, + devServer: { + port: 18081, + proxy: { + '/api': { + target: process.env.SENSE_API_PROXY || 'http://127.0.0.1:18080', + changeOrigin: true + } + } + } +}) + -- 2.34.1 From 2e22252feb6f92913b0a5919bca7edb7017f1ceb Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 16:51:44 +0800 Subject: [PATCH 02/17] docs: document Sense product skeleton (#21) --- docs/00-project-profile.md | 10 +++++-- docs/02-architecture-and-code-map.md | 18 +++++++++++-- docs/04-local-development-and-verification.md | 26 +++++++++++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md index 26e3d5c..7915e6e 100644 --- a/docs/00-project-profile.md +++ b/docs/00-project-profile.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Project-Profile wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Project-Profile.- -wiki_revision: 5561227400539f0a3ba7713be16c171feb811fcb -synchronized_at: 2026-08-12T01:26:47Z +wiki_revision: 3f5324fdcb32700a628b7434f1d7800e4bb0f64f +synchronized_at: 2026-08-12T08:50:45Z # 项目档案 @@ -92,3 +92,9 @@ Sense、Bell 共用的可复现技术基线记录在仓库根 `goadmin-baseline. 固定工具链为 Go 1.26.5、Node.js 22.22.1、pnpm 9.15.1。go-admin-ui 的 `package.json` version 仍为 2.0.9,但冻结提交位于 `v2.0.9` 后 170 个提交,因此不得只用“2.0.9”代表当前基线。 三个上游仓库均使用 MIT 许可证。派生代码必须保留许可证和版权声明。Sense、Bell 使用同一上游基线,但分别维护代码、配置、构建、测试、版本和发布。任何上游或工具链升级必须建立影响 Sense/Bell 的协同工单。 + + +## Sense 当前实现基线 + +Sense 已从冻结的 GoAdmin 基线初始化为独立产品:后端使用 Go 1.26.5 与 PostgreSQL,管理端使用 Node.js 22.22.1、pnpm 9.15.1、Vue 3.5.41、Element Plus 2.14.4 和 Vue CLI 5.0.9。产品代码只位于 `Sense/`,不依赖 Brain 或 Bell 启动;`D:\github\goadmin` 仍为只读参考。 + diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index c17bb8c..43f6cc7 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.- -wiki_revision: 04b5eaea579729e427f1b2b8962399e861d5eb53 -synchronized_at: 2026-08-11T10:30:34Z +wiki_revision: f16f2956d3d0f260501a356b5fe1087abdc5eaf5 +synchronized_at: 2026-08-12T08:50:52Z # 架构与代码地图 @@ -79,3 +79,17 @@ Sense/Brain 生成事件 - 业务预警与运维告警分开。 - Area/设备/规则在各独立产品内有自己的业务投影,不以共享数据库同步。 - 单项目工单不得顺带修改其他项目;共享契约变更必须由协调工单统筹。 + + +## Sense 代码入口与模块边界 + +- `Sense/server/main.go`:后端进程入口,调用 `cmd/sense` 启动。 +- `Sense/server/cmd/sense/root.go`:配置、数据库、迁移、模块注册和优雅退出编排;后续功能通过独立 `modules_.go` 注册,不反复修改共享入口。 +- `Sense/server/internal/platform/`:PostgreSQL、迁移、HTTP/JSON、健康检查与静态资源宿主。 +- `Sense/ui/src/bootstrap/`:Vue、路由、Vuex、请求封装和功能模块自动发现。 +- `Sense/ui/src/layout/AppLayout.vue`:基于 go-admin-ui 交互约定的侧栏、顶部导航、标签栏和页面容器。 +- `Sense/config/`:无秘密配置模板;真实数据库 URL 和后续签名密钥只能由仓库外环境提供。 +- `Sense/scripts/bootstrap/`:精确工具链检查和无需 Brain/Bell 的内存模式 smoke。 + +默认只注册工作台、健康检查和系统信息;go-admin 演示与代码生成菜单、路由和后端接口均未注册。正式启动默认要求独立 PostgreSQL;`memory` 只用于自动化测试和本地 smoke。 + diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index 74d471e..807f673 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Local-Development-and-Verification wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.- -wiki_revision: 1eb27d082ebf892343c9a5eed6f9363363234903 -synchronized_at: 2026-08-12T01:26:57Z +wiki_revision: 0a0354b0fed4c7edc68a1c6c887a0f2886e47541 +synchronized_at: 2026-08-12T08:50:58Z # 本地开发与验证 @@ -110,3 +110,25 @@ pnpm --version 截至 2026-08-12 的本机核对结果:Go 1.23.0,不满足 go-admin `go.mod` 声明的 Go 1.26.5;Node.js 22.22.1 符合基线;pnpm 尚未安装,Corepack 0.34.6 可用。因此后端构建和 pnpm 前端构建均未完成验证。安装隔离的 Go 1.26.5 并通过 Corepack 启用 pnpm 9.15.1 后,才可执行后续骨架构建验收。 `go-admin-doc` 只用于理解上游框架;项目自己的架构、开发规则和版本事实仍以 yovision Wiki、Agent 规则与 `goadmin-baseline.json` 为准。 + + +## Sense 本地构建与验证 + +使用根 `goadmin-baseline.json` 的精确版本。当前机器可用的隔离 Go 路径为开发机临时工具目录,不应写入部署配置。 + +```powershell +$env:PATH = 'C:\Users\ila20\AppData\Local\Temp\yovision-tools\go1.26.5\go\bin;' + $env:PATH +powershell -ExecutionPolicy Bypass -File .\Sense\scripts\bootstrap\check-toolchain.ps1 +cd Sense\server +go test ./... +go build ./... +cd ..\ui +corepack pnpm@9.15.1 install --frozen-lockfile +corepack pnpm@9.15.1 lint +corepack pnpm@9.15.1 build +cd ..\.. +powershell -ExecutionPolicy Bypass -File .\Sense\scripts\bootstrap\smoke.ps1 +``` + +正式后端启动必须设置 `SENSE_DATABASE_MODE=postgres` 和仓库外提供的 `SENSE_DATABASE_URL`。健康检查为 `GET /healthz`,就绪检查为 `GET /readyz`。本地 smoke 明确设置 `SENSE_DATABASE_MODE=memory`,不能代替 PostgreSQL 部署验证。 + -- 2.34.1 From 2c0e37eff7e3d7deca6003aa4d878cafc814e373 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 16:55:45 +0800 Subject: [PATCH 03/17] docs: archive task #21 for acceptance --- .../task/21-Sense独立GoAdmin产品骨架.md | 72 +++++++++++++++++++ wiki-docs.json | 4 ++ 2 files changed, 76 insertions(+) create mode 100644 docs/task/21-Sense独立GoAdmin产品骨架.md diff --git a/docs/task/21-Sense独立GoAdmin产品骨架.md b/docs/task/21-Sense独立GoAdmin产品骨架.md new file mode 100644 index 0000000..2eeb646 --- /dev/null +++ b/docs/task/21-Sense独立GoAdmin产品骨架.md @@ -0,0 +1,72 @@ + +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Task-21-Sense独立GoAdmin产品骨架 +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-21-Sense%E7%8B%AC%E7%AB%8BGoAdmin%E4%BA%A7%E5%93%81%E9%AA%A8%E6%9E%B6.- +wiki_revision: 3213d5f0fde9fdffd39602f5a7a7236d68ffec89 +synchronized_at: 2026-08-12T08:55:33Z + + +# 21 Sense 独立 GoAdmin 产品骨架 + +- 类型:需求 +- 所属 Epic:#7 +- 所属 MVP / 版本:#8 +- 状态:待验收 +- 日期:2026-08-12 +- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/21 +- Wiki 页面:Task-21-Sense独立GoAdmin产品骨架 +- Wiki revision:见本地镜像头 + +## 背景与目标 + +从已验收的冻结 GoAdmin 基线初始化 Sense 自有 Go 后端、Vue 管理端、PostgreSQL 配置和模块注册边界,使产品在 Brain、Bell 未启动时独立构建、运行和测试。 + +## 最终方案 + +1. 后端使用 Go 1.26.5、`net/http` 与 pgx PostgreSQL 驱动,提供配置校验、迁移注册、健康/就绪检查、模块扩展点和优雅退出。 +2. 正式/default 模式必须提供仓库外 `SENSE_DATABASE_URL`;`memory` 仅供自动测试和 smoke。 +3. 前端使用冻结的 Vue 3、Element Plus、Vue Router、Vuex 和 Vue CLI,复用 go-admin-ui 的侧栏、顶部导航、标签栏、页面容器与反馈模式。 +4. UI 只显示 Sense 工作台;go-admin 演示、代码生成和无关管理菜单/路由/API 均未注册。 +5. 后续业务功能通过独立后端注册文件及前端自动发现的 route/store module 接入,避免反复修改共享入口。 +6. 保留 go-admin、go-admin-ui MIT 许可证与完整来源 commit;本机参考仓库保持未修改。 + +实施期间增加 `Sense/ui/.gitignore` 精确 write path,用于排除 pnpm/Vue CLI 构建产物;不改变功能和验收结果。 + +## 修改文件 + +- `Sense/server/`:进程入口、配置、PostgreSQL、迁移、HTTP 平台和测试。 +- `Sense/ui/`:Vue/Element Plus 应用外壳、模块发现、请求封装和锁文件。 +- `Sense/config/`:无秘密配置模板。 +- `Sense/scripts/bootstrap/`:工具链检查与独立 smoke。 +- `Sense/LICENSES/`:上游许可证及来源记录。 +- `Sense/README.md`:构建、启动和验证入口。 +- Wiki `Project-Profile`、`Architecture-and-Code-Map`、`Local-Development-and-Verification` 及镜像。 + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| 冻结版本、许可证和来源可追溯且未修改参考仓库 | 通过 | +| Go 后端与 Vue 管理端可构建 | 通过 | +| 默认 UI/后端无演示和无关模块入口 | 通过 | +| 配置与仓库无默认密码、签名密钥或摄像头凭据 | 通过 | +| Brain、Bell 未启动时独立 smoke | 通过 | +| 独立 PostgreSQL 实例连接 | 待部署环境验证 | + +## 测试 + +- Go 1.26.5 `go test ./...`:通过。 +- Go 1.26.5 `go build ./...`:通过。 +- `corepack pnpm@9.15.1 install --frozen-lockfile`:通过。 +- `corepack pnpm@9.15.1 lint --quiet`:通过。 +- `corepack pnpm@9.15.1 build`:通过;仅有 Element Plus vendor bundle 体积建议警告。 +- `Sense/scripts/bootstrap/check-toolchain.ps1`:通过。 +- `Sense/scripts/bootstrap/smoke.ps1`:通过,含健康、就绪和禁用 demo 路由检查。 +- `python dev_scripts/check_harness.py --strict`:通过。 +- `git diff --check`:通过。 +- **未验证部分**:当前机器没有 PostgreSQL 客户端、Docker 或获准测试实例,未执行真实 PostgreSQL 连接;真实部署时必须验证。浏览器人工视觉验收留给用户,自动化构建已通过。 + +## 相关提交 + +- `d216999` feat: initialize independent Sense product skeleton (#21) +- `2e22252` docs: document Sense product skeleton (#21) diff --git a/wiki-docs.json b/wiki-docs.json index b62855a..78aea2c 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -83,6 +83,10 @@ { "page": "Task-5-冻结Sense与Bell-GoAdmin技术基线", "path": "docs/task/5-冻结Sense与Bell-GoAdmin技术基线.md" + }, + { + "page": "Task-21-Sense独立GoAdmin产品骨架", + "path": "docs/task/21-Sense独立GoAdmin产品骨架.md" } ] } -- 2.34.1 From b9416ebc0880eb048d80c7089abd8b0a759c5fad Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 17:06:26 +0800 Subject: [PATCH 04/17] feat: add Sense identity RBAC and audit (#22) --- Sense/server/app/sense/identity/http.go | 222 ++++++++++++++++++ Sense/server/app/sense/identity/http_test.go | 59 +++++ Sense/server/app/sense/identity/migration.go | 30 +++ Sense/server/app/sense/identity/password.go | 37 +++ .../server/app/sense/identity/permissions.go | 42 ++++ Sense/server/app/sense/identity/postgres.go | 132 +++++++++++ Sense/server/app/sense/identity/service.go | 159 +++++++++++++ .../server/app/sense/identity/service_test.go | 77 ++++++ Sense/server/app/sense/identity/store.go | 171 ++++++++++++++ Sense/server/app/sense/identity/token.go | 50 ++++ Sense/server/app/sense/identity/types.go | 59 +++++ Sense/server/cmd/sense/modules_identity.go | 42 ++++ .../sense_identity/2026081201_identity.sql | 27 +++ Sense/server/go.mod | 10 +- Sense/server/go.sum | 12 +- Sense/server/internal/platform/http.go | 6 +- Sense/tests/identity/README.md | 10 + Sense/ui/src/api/sense/identity/index.js | 10 + Sense/ui/src/layout/AppLayout.vue | 11 +- Sense/ui/src/router/modules/sense-identity.js | 25 ++ Sense/ui/src/store/modules/sense-identity.js | 45 ++++ Sense/ui/src/views/sense/identity/Audit.vue | 30 +++ Sense/ui/src/views/sense/identity/Login.vue | 81 +++++++ Sense/ui/src/views/sense/identity/Users.vue | 102 ++++++++ 24 files changed, 1438 insertions(+), 11 deletions(-) create mode 100644 Sense/server/app/sense/identity/http.go create mode 100644 Sense/server/app/sense/identity/http_test.go create mode 100644 Sense/server/app/sense/identity/migration.go create mode 100644 Sense/server/app/sense/identity/password.go create mode 100644 Sense/server/app/sense/identity/permissions.go create mode 100644 Sense/server/app/sense/identity/postgres.go create mode 100644 Sense/server/app/sense/identity/service.go create mode 100644 Sense/server/app/sense/identity/service_test.go create mode 100644 Sense/server/app/sense/identity/store.go create mode 100644 Sense/server/app/sense/identity/token.go create mode 100644 Sense/server/app/sense/identity/types.go create mode 100644 Sense/server/cmd/sense/modules_identity.go create mode 100644 Sense/server/database/migrations/sense_identity/2026081201_identity.sql create mode 100644 Sense/tests/identity/README.md create mode 100644 Sense/ui/src/api/sense/identity/index.js create mode 100644 Sense/ui/src/router/modules/sense-identity.js create mode 100644 Sense/ui/src/store/modules/sense-identity.js create mode 100644 Sense/ui/src/views/sense/identity/Audit.vue create mode 100644 Sense/ui/src/views/sense/identity/Login.vue create mode 100644 Sense/ui/src/views/sense/identity/Users.vue diff --git a/Sense/server/app/sense/identity/http.go b/Sense/server/app/sense/identity/http.go new file mode 100644 index 0000000..977eee0 --- /dev/null +++ b/Sense/server/app/sense/identity/http.go @@ -0,0 +1,222 @@ +package identity + +import ( + "context" + "crypto/rand" + "errors" + "net/http" + "strings" + "sync/atomic" + "time" + + "yovision.local/sense/internal/platform" +) + +const sessionCookieName = "sense_session" + +type principalContextKey struct{} + +type Module struct { + service *Service + cfg Config +} + +var activeModule atomic.Pointer[Module] + +func NewModule(service *Service, cfg Config) *Module { return &Module{service: service, cfg: cfg} } + +func (m *Module) Register(app *platform.App) { + activeModule.Store(m) + app.HandleFunc("POST /api/v1/identity/bootstrap", m.bootstrap) + app.HandleFunc("POST /api/v1/identity/login", m.login) + app.Handle("POST /api/v1/identity/logout", m.Authenticated(http.HandlerFunc(m.logout))) + app.Handle("GET /api/v1/identity/me", m.Authenticated(http.HandlerFunc(m.me))) + app.Handle("GET /api/v1/identity/users", m.Require(PermissionUsersRead, http.HandlerFunc(m.users))) + app.Handle("POST /api/v1/identity/users", m.Require(PermissionUsersWrite, http.HandlerFunc(m.users))) + app.Handle("PATCH /api/v1/identity/users/{id}", m.Require(PermissionUsersWrite, http.HandlerFunc(m.updateUser))) + app.Handle("GET /api/v1/identity/audit", m.Require(PermissionAuditRead, http.HandlerFunc(m.audit))) +} + +func Require(permission string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + module := activeModule.Load() + if module == nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusServiceUnavailable, Code: "identity_not_ready", Message: "身份服务尚未就绪"}) + return + } + module.Require(permission, next).ServeHTTP(w, r) + }) +} + +func PrincipalFromContext(ctx context.Context) (Principal, bool) { + principal, ok := ctx.Value(principalContextKey{}).(Principal) + return principal, ok +} + +func (m *Module) Authenticated(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-Product") != "sense" { + platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"}) + return + } + cookie, err := r.Cookie(sessionCookieName) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"}) + return + } + principal, err := m.service.Authenticate(r.Context(), cookie.Value) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"}) + return + } + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), principalContextKey{}, principal))) + }) +} + +func (m *Module) Require(permission string, next http.Handler) http.Handler { + return m.Authenticated(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + principal, _ := PrincipalFromContext(r.Context()) + if !HasPermission(principal.Role, permission) { + platform.WriteError(w, &platform.APIError{Status: http.StatusForbidden, Code: "forbidden", Message: "没有执行此操作的权限"}) + return + } + next.ServeHTTP(w, r) + })) +} + +func (m *Module) bootstrap(w http.ResponseWriter, r *http.Request) { + var request struct { + Username string `json:"username"` + DisplayName string `json:"display_name"` + Password string `json:"password"` + } + if err := platform.DecodeJSON(r, &request); err != nil { + platform.WriteError(w, err) + return + } + user, err := m.service.Bootstrap(r.Context(), r.Header.Get("X-Sense-Bootstrap-Token"), request.Username, request.DisplayName, request.Password) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusForbidden, Code: "bootstrap_denied", Message: "安全初始化不可用或输入不符合要求"}) + return + } + platform.WriteJSON(w, http.StatusCreated, user) +} + +func (m *Module) login(w http.ResponseWriter, r *http.Request) { + var request struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := platform.DecodeJSON(r, &request); err != nil { + platform.WriteError(w, err) + return + } + principal, token, err := m.service.Login(r.Context(), request.Username, request.Password) + if err != nil { + // A small constant delay and one generic message reduce account discovery. + time.Sleep(80*time.Millisecond + randomJitter()) + platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "invalid_credentials", Message: "用户名或密码不正确"}) + return + } + http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: token, Path: "/api/v1", HttpOnly: true, Secure: m.cfg.SecureCookie, SameSite: http.SameSiteStrictMode, MaxAge: int(m.cfg.SessionTTL.Seconds())}) + platform.WriteJSON(w, http.StatusOK, principal) +} + +func randomJitter() time.Duration { + var value [1]byte + _, _ = rand.Read(value[:]) + return time.Duration(value[0]%40) * time.Millisecond +} + +func (m *Module) logout(w http.ResponseWriter, r *http.Request) { + principal, _ := PrincipalFromContext(r.Context()) + if err := m.service.Logout(r.Context(), principal); err != nil { + platform.WriteError(w, err) + return + } + http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/api/v1", HttpOnly: true, Secure: m.cfg.SecureCookie, SameSite: http.SameSiteStrictMode, MaxAge: -1}) + w.WriteHeader(http.StatusNoContent) +} + +func (m *Module) me(w http.ResponseWriter, r *http.Request) { + principal, _ := PrincipalFromContext(r.Context()) + platform.WriteJSON(w, http.StatusOK, principal) +} + +func (m *Module) users(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + users, err := m.service.ListUsers(r.Context()) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, map[string]any{"items": users, "total": len(users)}) + return + } + var request struct { + Username string `json:"username"` + DisplayName string `json:"display_name"` + Role string `json:"role"` + Password string `json:"password"` + } + if err := platform.DecodeJSON(r, &request); err != nil { + platform.WriteError(w, err) + return + } + principal, _ := PrincipalFromContext(r.Context()) + user, err := m.service.CreateUser(r.Context(), principal, request.Username, request.DisplayName, request.Role, request.Password) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "user_not_created", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusCreated, user) +} + +func (m *Module) updateUser(w http.ResponseWriter, r *http.Request) { + var request struct { + DisplayName string `json:"display_name"` + Role string `json:"role"` + Enabled bool `json:"enabled"` + } + if err := platform.DecodeJSON(r, &request); err != nil { + platform.WriteError(w, err) + return + } + principal, _ := PrincipalFromContext(r.Context()) + user, err := m.service.UpdateUser(r.Context(), principal, r.PathValue("id"), request.DisplayName, request.Role, request.Enabled) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "user_not_updated", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusOK, user) +} + +func (m *Module) audit(w http.ResponseWriter, r *http.Request) { + entries, err := m.service.ListAudit(r.Context()) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, map[string]any{"items": entries, "total": len(entries)}) +} + +func SigningKeyOrRandom(raw string, allowRandom bool) ([]byte, error) { + if len(raw) >= 32 { + return []byte(raw), nil + } + if !allowRandom { + return nil, errors.New("SENSE_IDENTITY_SIGNING_KEY must contain at least 32 bytes") + } + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, err + } + return key, nil +} + +func SecureCookieFromEnvironment(value string, memoryMode bool) bool { + if memoryMode { + return strings.EqualFold(value, "true") + } + return !strings.EqualFold(value, "false") +} diff --git a/Sense/server/app/sense/identity/http_test.go b/Sense/server/app/sense/identity/http_test.go new file mode 100644 index 0000000..d96302c --- /dev/null +++ b/Sense/server/app/sense/identity/http_test.go @@ -0,0 +1,59 @@ +package identity + +import ( + "bytes" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + "time" + + "yovision.local/sense/internal/platform" +) + +func TestHTTPLoginCookieAndProductBoundary(t *testing.T) { + store := NewMemoryStore() + cfg := Config{SigningKey: []byte("0123456789abcdef0123456789abcdef"), BootstrapToken: "bootstrap-test", SessionTTL: time.Hour} + service, err := NewService(store, cfg) + if err != nil { + t.Fatal(err) + } + app := platform.NewApp(platform.Config{DatabaseMode: platform.DatabaseModeMemory}, nil, slog.New(slog.NewTextHandler(io.Discard, nil))) + NewModule(service, cfg).Register(app) + + bootstrap := httptest.NewRequest(http.MethodPost, "/api/v1/identity/bootstrap", bytes.NewBufferString(`{"username":"admin","display_name":"管理员","password":"StrongPass2026"}`)) + bootstrap.Header.Set("X-Sense-Bootstrap-Token", "bootstrap-test") + bootstrapResult := httptest.NewRecorder() + app.Handler().ServeHTTP(bootstrapResult, bootstrap) + if bootstrapResult.Code != http.StatusCreated { + t.Fatalf("bootstrap status = %d, body = %s", bootstrapResult.Code, bootstrapResult.Body.String()) + } + + login := httptest.NewRequest(http.MethodPost, "/api/v1/identity/login", bytes.NewBufferString(`{"username":"admin","password":"StrongPass2026"}`)) + loginResult := httptest.NewRecorder() + app.Handler().ServeHTTP(loginResult, login) + if loginResult.Code != http.StatusOK { + t.Fatalf("login status = %d, body = %s", loginResult.Code, loginResult.Body.String()) + } + cookies := loginResult.Result().Cookies() + if len(cookies) != 1 || cookies[0].Name != sessionCookieName || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode { + t.Fatalf("unexpected session cookies: %#v", cookies) + } + + me := httptest.NewRequest(http.MethodGet, "/api/v1/identity/me", nil) + me.AddCookie(cookies[0]) + me.Header.Set("X-Product", "bell") + foreignResult := httptest.NewRecorder() + app.Handler().ServeHTTP(foreignResult, me) + if foreignResult.Code != http.StatusUnauthorized { + t.Fatalf("Bell product header status = %d", foreignResult.Code) + } + + me.Header.Set("X-Product", "sense") + senseResult := httptest.NewRecorder() + app.Handler().ServeHTTP(senseResult, me) + if senseResult.Code != http.StatusOK { + t.Fatalf("Sense product header status = %d", senseResult.Code) + } +} diff --git a/Sense/server/app/sense/identity/migration.go b/Sense/server/app/sense/identity/migration.go new file mode 100644 index 0000000..90442a9 --- /dev/null +++ b/Sense/server/app/sense/identity/migration.go @@ -0,0 +1,30 @@ +package identity + +const MigrationSQL = ` +CREATE TABLE IF NOT EXISTS sense_identity_users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + role TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); +CREATE TABLE IF NOT EXISTS sense_identity_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES sense_identity_users(id), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ NULL +); +CREATE TABLE IF NOT EXISTS sense_identity_audit ( + id TEXT PRIMARY KEY, + actor_id TEXT NULL REFERENCES sense_identity_users(id), + action TEXT NOT NULL, + target TEXT NOT NULL DEFAULT '', + outcome TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL +); +CREATE INDEX IF NOT EXISTS sense_identity_audit_created_idx ON sense_identity_audit(created_at DESC); +` diff --git a/Sense/server/app/sense/identity/password.go b/Sense/server/app/sense/identity/password.go new file mode 100644 index 0000000..578e336 --- /dev/null +++ b/Sense/server/app/sense/identity/password.go @@ -0,0 +1,37 @@ +package identity + +import ( + "fmt" + "strings" + "unicode" + + "golang.org/x/crypto/bcrypt" +) + +func ValidatePassword(username, password string) error { + if len([]rune(password)) < 12 { + return fmt.Errorf("密码至少需要 12 个字符") + } + var upper, lower, digit bool + for _, value := range password { + upper = upper || unicode.IsUpper(value) + lower = lower || unicode.IsLower(value) + digit = digit || unicode.IsDigit(value) + } + if !upper || !lower || !digit { + return fmt.Errorf("密码必须同时包含大写字母、小写字母和数字") + } + if username != "" && strings.Contains(strings.ToLower(password), strings.ToLower(username)) { + return fmt.Errorf("密码不能包含用户名") + } + return nil +} + +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +func VerifyPassword(hash, password string) bool { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil +} diff --git a/Sense/server/app/sense/identity/permissions.go b/Sense/server/app/sense/identity/permissions.go new file mode 100644 index 0000000..3ac8bca --- /dev/null +++ b/Sense/server/app/sense/identity/permissions.go @@ -0,0 +1,42 @@ +package identity + +var rolePermissions = map[string][]string{ + RoleAdministrator: { + PermissionUsersRead, PermissionUsersWrite, PermissionAuditRead, + PermissionDeviceRead, PermissionDeviceWrite, + PermissionMediaRead, PermissionMediaWrite, + PermissionAreaRead, PermissionAreaWrite, + }, + RoleOperator: { + PermissionDeviceRead, PermissionDeviceWrite, + PermissionMediaRead, PermissionMediaWrite, + PermissionAreaRead, PermissionAreaWrite, + }, + RoleSiteAdmin: { + PermissionUsersRead, PermissionAuditRead, + PermissionDeviceRead, PermissionDeviceWrite, + PermissionMediaRead, PermissionAreaRead, PermissionAreaWrite, + }, + RoleViewer: { + PermissionDeviceRead, PermissionMediaRead, PermissionAreaRead, + }, +} + +func ValidRole(role string) bool { + _, ok := rolePermissions[role] + return ok +} + +func PermissionsForRole(role string) []string { + permissions := rolePermissions[role] + return append([]string(nil), permissions...) +} + +func HasPermission(role, permission string) bool { + for _, candidate := range rolePermissions[role] { + if candidate == permission { + return true + } + } + return false +} diff --git a/Sense/server/app/sense/identity/postgres.go b/Sense/server/app/sense/identity/postgres.go new file mode 100644 index 0000000..6eb9268 --- /dev/null +++ b/Sense/server/app/sense/identity/postgres.go @@ -0,0 +1,132 @@ +package identity + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "time" +) + +type PostgresStore struct{ database *sql.DB } + +func NewPostgresStore(database *sql.DB) *PostgresStore { return &PostgresStore{database: database} } + +func (s *PostgresStore) BootstrapUser(ctx context.Context, user User) error { + tx, err := s.database.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) + if err != nil { + return err + } + defer tx.Rollback() + var count int + if err := tx.QueryRowContext(ctx, `SELECT count(*) FROM sense_identity_users`).Scan(&count); err != nil { + return err + } + if count != 0 { + return ErrBootstrapDone + } + if _, err := tx.ExecContext(ctx, `INSERT INTO sense_identity_users(id, username, display_name, role, enabled, password_hash, created_at, updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, user.ID, user.Username, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.CreatedAt, user.UpdatedAt); err != nil { + return err + } + return tx.Commit() +} + +func (s *PostgresStore) CreateUser(ctx context.Context, user User) error { + _, err := s.database.ExecContext(ctx, `INSERT INTO sense_identity_users(id, username, display_name, role, enabled, password_hash, created_at, updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, user.ID, user.Username, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.CreatedAt, user.UpdatedAt) + return err +} + +func (s *PostgresStore) UpdateUser(ctx context.Context, user User) error { + result, err := s.database.ExecContext(ctx, `UPDATE sense_identity_users SET display_name=$2, role=$3, enabled=$4, password_hash=$5, updated_at=$6 WHERE id=$1`, user.ID, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.UpdatedAt) + if err != nil { + return err + } + rows, _ := result.RowsAffected() + if rows == 0 { + return ErrNotFound + } + return nil +} + +func (s *PostgresStore) FindUserByUsername(ctx context.Context, username string) (User, error) { + return scanUser(s.database.QueryRowContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users WHERE lower(username)=lower($1)`, username)) +} + +func (s *PostgresStore) FindUserByID(ctx context.Context, id string) (User, error) { + return scanUser(s.database.QueryRowContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users WHERE id=$1`, id)) +} + +type rowScanner interface{ Scan(...any) error } + +func scanUser(row rowScanner) (User, error) { + var user User + err := row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Role, &user.Enabled, &user.PasswordHash, &user.CreatedAt, &user.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return User{}, ErrNotFound + } + return user, err +} + +func (s *PostgresStore) ListUsers(ctx context.Context) ([]User, error) { + rows, err := s.database.QueryContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users ORDER BY username`) + if err != nil { + return nil, err + } + defer rows.Close() + var users []User + for rows.Next() { + user, err := scanUser(rows) + if err != nil { + return nil, err + } + users = append(users, user) + } + return users, rows.Err() +} + +func (s *PostgresStore) CreateSession(ctx context.Context, session Session) error { + _, err := s.database.ExecContext(ctx, `INSERT INTO sense_identity_sessions(id,user_id,expires_at) VALUES($1,$2,$3)`, session.ID, session.UserID, session.ExpiresAt) + return err +} + +func (s *PostgresStore) SessionActive(ctx context.Context, id string, now time.Time) (bool, error) { + var active bool + err := s.database.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM sense_identity_sessions WHERE id=$1 AND revoked_at IS NULL AND expires_at>$2)`, id, now).Scan(&active) + return active, err +} + +func (s *PostgresStore) RevokeSession(ctx context.Context, id string, now time.Time) error { + _, err := s.database.ExecContext(ctx, `UPDATE sense_identity_sessions SET revoked_at=$2 WHERE id=$1`, id, now) + return err +} + +func (s *PostgresStore) AppendAudit(ctx context.Context, entry AuditEntry) error { + metadata, err := json.Marshal(entry.Metadata) + if err != nil { + return err + } + _, err = s.database.ExecContext(ctx, `INSERT INTO sense_identity_audit(id,actor_id,action,target,outcome,metadata,created_at) VALUES($1,NULLIF($2,''),$3,$4,$5,$6,$7)`, entry.ID, entry.ActorID, entry.Action, entry.Target, entry.Outcome, metadata, entry.CreatedAt) + return err +} + +func (s *PostgresStore) ListAudit(ctx context.Context, limit int) ([]AuditEntry, error) { + if limit <= 0 || limit > 200 { + limit = 100 + } + rows, err := s.database.QueryContext(ctx, `SELECT id,COALESCE(actor_id,''),action,target,outcome,metadata,created_at FROM sense_identity_audit ORDER BY created_at DESC LIMIT $1`, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var entries []AuditEntry + for rows.Next() { + var entry AuditEntry + var metadata []byte + if err := rows.Scan(&entry.ID, &entry.ActorID, &entry.Action, &entry.Target, &entry.Outcome, &metadata, &entry.CreatedAt); err != nil { + return nil, err + } + _ = json.Unmarshal(metadata, &entry.Metadata) + entries = append(entries, entry) + } + return entries, rows.Err() +} diff --git a/Sense/server/app/sense/identity/service.go b/Sense/server/app/sense/identity/service.go new file mode 100644 index 0000000..bca385b --- /dev/null +++ b/Sense/server/app/sense/identity/service.go @@ -0,0 +1,159 @@ +package identity + +import ( + "context" + "crypto/subtle" + "errors" + "fmt" + "strings" + "time" +) + +type Config struct { + SigningKey []byte + BootstrapToken string + SecureCookie bool + SessionTTL time.Duration +} + +type Service struct { + store Store + cfg Config + now func() time.Time +} + +func NewService(store Store, cfg Config) (*Service, error) { + if len(cfg.SigningKey) < 32 { + return nil, fmt.Errorf("Sense identity signing key must contain at least 32 bytes") + } + if cfg.SessionTTL <= 0 { + cfg.SessionTTL = 8 * time.Hour + } + return &Service{store: store, cfg: cfg, now: time.Now}, nil +} + +func (s *Service) Bootstrap(ctx context.Context, token, username, displayName, password string) (User, error) { + if s.cfg.BootstrapToken == "" || subtle.ConstantTimeCompare([]byte(token), []byte(s.cfg.BootstrapToken)) != 1 { + return User{}, errors.New("bootstrap denied") + } + username = strings.TrimSpace(username) + if username == "" || len(username) > 64 { + return User{}, fmt.Errorf("用户名格式不正确") + } + if err := ValidatePassword(username, password); err != nil { + return User{}, err + } + hash, err := HashPassword(password) + if err != nil { + return User{}, err + } + now := s.now().UTC() + user := User{ID: newID("usr"), Username: username, DisplayName: strings.TrimSpace(displayName), Role: RoleAdministrator, Enabled: true, PasswordHash: hash, CreatedAt: now, UpdatedAt: now} + if err := s.store.BootstrapUser(ctx, user); err != nil { + return User{}, err + } + s.audit(ctx, user.ID, "identity.bootstrap", user.ID, "success", nil) + return user, nil +} + +func (s *Service) Login(ctx context.Context, username, password string) (Principal, string, error) { + user, err := s.store.FindUserByUsername(ctx, strings.TrimSpace(username)) + if err != nil || !user.Enabled || !VerifyPassword(user.PasswordHash, password) { + s.audit(ctx, "", "identity.login", "", "failure", map[string]any{"reason": "invalid_credentials"}) + return Principal{}, "", errors.New("invalid credentials") + } + now := s.now().UTC() + session := Session{ID: newID("ses"), UserID: user.ID, ExpiresAt: now.Add(s.cfg.SessionTTL)} + if err := s.store.CreateSession(ctx, session); err != nil { + return Principal{}, "", err + } + raw, err := issueToken(s.cfg.SigningKey, user, session, now) + if err != nil { + return Principal{}, "", err + } + s.audit(ctx, user.ID, "identity.login", user.ID, "success", nil) + return principalFor(user, session.ID), raw, nil +} + +func (s *Service) Authenticate(ctx context.Context, raw string) (Principal, error) { + claims, err := parseToken(s.cfg.SigningKey, raw, s.now()) + if err != nil { + return Principal{}, err + } + active, err := s.store.SessionActive(ctx, claims.ID, s.now()) + if err != nil || !active { + return Principal{}, errors.New("inactive Sense session") + } + user, err := s.store.FindUserByID(ctx, claims.Subject) + if err != nil || !user.Enabled || user.Role != claims.Role { + return Principal{}, errors.New("invalid Sense principal") + } + return principalFor(user, claims.ID), nil +} + +func (s *Service) Logout(ctx context.Context, principal Principal) error { + if err := s.store.RevokeSession(ctx, principal.SessionID, s.now().UTC()); err != nil && !errors.Is(err, ErrNotFound) { + return err + } + s.audit(ctx, principal.UserID, "identity.logout", principal.UserID, "success", nil) + return nil +} + +func (s *Service) ListUsers(ctx context.Context) ([]User, error) { return s.store.ListUsers(ctx) } +func (s *Service) ListAudit(ctx context.Context) ([]AuditEntry, error) { + return s.store.ListAudit(ctx, 100) +} + +func (s *Service) CreateUser(ctx context.Context, actor Principal, username, displayName, role, password string) (User, error) { + if !ValidRole(role) { + return User{}, fmt.Errorf("角色不正确") + } + username = strings.TrimSpace(username) + if username == "" || len(username) > 64 { + return User{}, fmt.Errorf("用户名格式不正确") + } + if err := ValidatePassword(username, password); err != nil { + return User{}, err + } + hash, err := HashPassword(password) + if err != nil { + return User{}, err + } + now := s.now().UTC() + user := User{ID: newID("usr"), Username: username, DisplayName: strings.TrimSpace(displayName), Role: role, Enabled: true, PasswordHash: hash, CreatedAt: now, UpdatedAt: now} + if err := s.store.CreateUser(ctx, user); err != nil { + return User{}, err + } + s.audit(ctx, actor.UserID, "identity.user.create", user.ID, "success", map[string]any{"role": role}) + return user, nil +} + +func (s *Service) UpdateUser(ctx context.Context, actor Principal, id, displayName, role string, enabled bool) (User, error) { + if !ValidRole(role) { + return User{}, fmt.Errorf("角色不正确") + } + user, err := s.store.FindUserByID(ctx, id) + if err != nil { + return User{}, err + } + if actor.UserID == id && !enabled { + return User{}, fmt.Errorf("不能停用当前登录账户") + } + user.DisplayName = strings.TrimSpace(displayName) + user.Role = role + user.Enabled = enabled + user.UpdatedAt = s.now().UTC() + if err := s.store.UpdateUser(ctx, user); err != nil { + return User{}, err + } + s.audit(ctx, actor.UserID, "identity.user.update", user.ID, "success", map[string]any{"role": role, "enabled": enabled}) + return user, nil +} + +func (s *Service) audit(ctx context.Context, actor, action, target, outcome string, metadata map[string]any) { + _ = s.store.AppendAudit(ctx, AuditEntry{ID: newID("aud"), ActorID: actor, Action: action, Target: target, Outcome: outcome, Metadata: metadata, CreatedAt: s.now().UTC()}) +} + +func principalFor(user User, sessionID string) Principal { + return Principal{UserID: user.ID, Username: user.Username, DisplayName: user.DisplayName, Role: user.Role, Permissions: PermissionsForRole(user.Role), SessionID: sessionID} +} diff --git a/Sense/server/app/sense/identity/service_test.go b/Sense/server/app/sense/identity/service_test.go new file mode 100644 index 0000000..a6adccb --- /dev/null +++ b/Sense/server/app/sense/identity/service_test.go @@ -0,0 +1,77 @@ +package identity + +import ( + "context" + "testing" + "time" +) + +func newTestService(t *testing.T) (*Service, *MemoryStore) { + t.Helper() + store := NewMemoryStore() + service, err := NewService(store, Config{SigningKey: []byte("0123456789abcdef0123456789abcdef"), BootstrapToken: "one-time-token", SessionTTL: time.Hour}) + if err != nil { + t.Fatal(err) + } + return service, store +} + +func TestBootstrapLoginRolesAndRevocation(t *testing.T) { + service, _ := newTestService(t) + ctx := context.Background() + admin, err := service.Bootstrap(ctx, "one-time-token", "admin", "管理员", "StrongPass2026") + if err != nil { + t.Fatalf("Bootstrap() error = %v", err) + } + if _, err := service.Bootstrap(ctx, "one-time-token", "another", "", "SecondStrong2026"); err != ErrBootstrapDone { + t.Fatalf("second Bootstrap() error = %v", err) + } + principal, raw, err := service.Login(ctx, "admin", "StrongPass2026") + if err != nil { + t.Fatalf("Login() error = %v", err) + } + if principal.Role != RoleAdministrator || !HasPermission(principal.Role, PermissionUsersWrite) { + t.Fatalf("unexpected principal: %#v", principal) + } + authenticated, err := service.Authenticate(ctx, raw) + if err != nil || authenticated.UserID != admin.ID { + t.Fatalf("Authenticate() = %#v, %v", authenticated, err) + } + if err := service.Logout(ctx, authenticated); err != nil { + t.Fatal(err) + } + if _, err := service.Authenticate(ctx, raw); err == nil { + t.Fatal("revoked token remained valid") + } +} + +func TestInvalidLoginIsUniformAndAudited(t *testing.T) { + service, _ := newTestService(t) + ctx := context.Background() + if _, _, err := service.Login(ctx, "missing", "WrongPass2026"); err == nil { + t.Fatal("missing user login succeeded") + } + entries, err := service.ListAudit(ctx) + if err != nil || len(entries) != 1 || entries[0].Outcome != "failure" { + t.Fatalf("audit entries = %#v, %v", entries, err) + } +} + +func TestBellTokenIsRejected(t *testing.T) { + service, _ := newTestService(t) + if _, err := service.Authenticate(context.Background(), "not-a-sense-token"); err == nil { + t.Fatal("foreign token accepted") + } +} + +func TestRoleMatrix(t *testing.T) { + if HasPermission(RoleViewer, PermissionDeviceWrite) { + t.Fatal("viewer received write permission") + } + if !HasPermission(RoleOperator, PermissionMediaWrite) { + t.Fatal("operator missing media write permission") + } + if HasPermission(RoleSiteAdmin, PermissionUsersWrite) { + t.Fatal("site admin received user write permission") + } +} diff --git a/Sense/server/app/sense/identity/store.go b/Sense/server/app/sense/identity/store.go new file mode 100644 index 0000000..d2d7c98 --- /dev/null +++ b/Sense/server/app/sense/identity/store.go @@ -0,0 +1,171 @@ +package identity + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "sort" + "strings" + "sync" + "time" +) + +var ( + ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") + ErrBootstrapDone = errors.New("bootstrap already completed") +) + +type Store interface { + BootstrapUser(context.Context, User) error + CreateUser(context.Context, User) error + UpdateUser(context.Context, User) error + FindUserByUsername(context.Context, string) (User, error) + FindUserByID(context.Context, string) (User, error) + ListUsers(context.Context) ([]User, error) + CreateSession(context.Context, Session) error + SessionActive(context.Context, string, time.Time) (bool, error) + RevokeSession(context.Context, string, time.Time) error + AppendAudit(context.Context, AuditEntry) error + ListAudit(context.Context, int) ([]AuditEntry, error) +} + +type MemoryStore struct { + mu sync.RWMutex + users map[string]User + byName map[string]string + sessions map[string]Session + audit []AuditEntry +} + +func NewMemoryStore() *MemoryStore { + return &MemoryStore{users: map[string]User{}, byName: map[string]string{}, sessions: map[string]Session{}} +} + +func (s *MemoryStore) BootstrapUser(_ context.Context, user User) error { + s.mu.Lock() + defer s.mu.Unlock() + if len(s.users) != 0 { + return ErrBootstrapDone + } + return s.createUserLocked(user) +} + +func (s *MemoryStore) CreateUser(_ context.Context, user User) error { + s.mu.Lock() + defer s.mu.Unlock() + return s.createUserLocked(user) +} + +func (s *MemoryStore) createUserLocked(user User) error { + key := strings.ToLower(user.Username) + if _, exists := s.byName[key]; exists { + return ErrAlreadyExists + } + s.users[user.ID] = user + s.byName[key] = user.ID + return nil +} + +func (s *MemoryStore) UpdateUser(_ context.Context, user User) error { + s.mu.Lock() + defer s.mu.Unlock() + previous, exists := s.users[user.ID] + if !exists { + return ErrNotFound + } + delete(s.byName, strings.ToLower(previous.Username)) + s.users[user.ID] = user + s.byName[strings.ToLower(user.Username)] = user.ID + return nil +} + +func (s *MemoryStore) FindUserByUsername(_ context.Context, username string) (User, error) { + s.mu.RLock() + defer s.mu.RUnlock() + id, exists := s.byName[strings.ToLower(username)] + if !exists { + return User{}, ErrNotFound + } + return s.users[id], nil +} + +func (s *MemoryStore) FindUserByID(_ context.Context, id string) (User, error) { + s.mu.RLock() + defer s.mu.RUnlock() + user, exists := s.users[id] + if !exists { + return User{}, ErrNotFound + } + return user, nil +} + +func (s *MemoryStore) ListUsers(_ context.Context) ([]User, error) { + s.mu.RLock() + defer s.mu.RUnlock() + users := make([]User, 0, len(s.users)) + for _, user := range s.users { + users = append(users, user) + } + sort.Slice(users, func(i, j int) bool { return users[i].Username < users[j].Username }) + return users, nil +} + +func (s *MemoryStore) CreateSession(_ context.Context, session Session) error { + s.mu.Lock() + defer s.mu.Unlock() + s.sessions[session.ID] = session + return nil +} + +func (s *MemoryStore) SessionActive(_ context.Context, id string, now time.Time) (bool, error) { + s.mu.RLock() + defer s.mu.RUnlock() + session, exists := s.sessions[id] + return exists && session.RevokedAt == nil && now.Before(session.ExpiresAt), nil +} + +func (s *MemoryStore) RevokeSession(_ context.Context, id string, now time.Time) error { + s.mu.Lock() + defer s.mu.Unlock() + session, exists := s.sessions[id] + if !exists { + return ErrNotFound + } + session.RevokedAt = &now + s.sessions[id] = session + return nil +} + +func (s *MemoryStore) AppendAudit(_ context.Context, entry AuditEntry) error { + s.mu.Lock() + defer s.mu.Unlock() + s.audit = append(s.audit, entry) + return nil +} + +func (s *MemoryStore) ListAudit(_ context.Context, limit int) ([]AuditEntry, error) { + s.mu.RLock() + defer s.mu.RUnlock() + if limit <= 0 || limit > 200 { + limit = 100 + } + start := len(s.audit) - limit + if start < 0 { + start = 0 + } + entries := append([]AuditEntry(nil), s.audit[start:]...) + for left, right := 0, len(entries)-1; left < right; left, right = left+1, right-1 { + entries[left], entries[right] = entries[right], entries[left] + } + return entries, nil +} + +func newID(prefix string) string { + buffer := make([]byte, 16) + if _, err := rand.Read(buffer); err != nil { + panic("cryptographic random source unavailable") + } + return prefix + "_" + hex.EncodeToString(buffer) +} diff --git a/Sense/server/app/sense/identity/token.go b/Sense/server/app/sense/identity/token.go new file mode 100644 index 0000000..2ab5dc5 --- /dev/null +++ b/Sense/server/app/sense/identity/token.go @@ -0,0 +1,50 @@ +package identity + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +const ( + tokenIssuer = "yovision-sense" + tokenAudience = "sense-ui" +) + +type sessionClaims struct { + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func issueToken(key []byte, user User, session Session, now time.Time) (string, error) { + claims := sessionClaims{ + Username: user.Username, + Role: user.Role, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: tokenIssuer, + Subject: user.ID, + Audience: jwt.ClaimStrings{tokenAudience}, + ExpiresAt: jwt.NewNumericDate(session.ExpiresAt), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now.Add(-time.Minute)), + ID: session.ID, + }, + } + return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key) +} + +func parseToken(key []byte, raw string, now time.Time) (*sessionClaims, error) { + claims := &sessionClaims{} + token, err := jwt.ParseWithClaims(raw, claims, func(token *jwt.Token) (any, error) { + if token.Method != jwt.SigningMethodHS256 { + return nil, fmt.Errorf("unexpected signing method") + } + return key, nil + }, jwt.WithIssuer(tokenIssuer), jwt.WithAudience(tokenAudience), jwt.WithTimeFunc(func() time.Time { return now })) + if err != nil || !token.Valid { + return nil, fmt.Errorf("invalid Sense session") + } + return claims, nil +} diff --git a/Sense/server/app/sense/identity/types.go b/Sense/server/app/sense/identity/types.go new file mode 100644 index 0000000..07e1721 --- /dev/null +++ b/Sense/server/app/sense/identity/types.go @@ -0,0 +1,59 @@ +package identity + +import "time" + +const ( + RoleAdministrator = "administrator" + RoleOperator = "operator" + RoleSiteAdmin = "site_admin" + RoleViewer = "viewer" +) + +const ( + PermissionUsersRead = "identity.users.read" + PermissionUsersWrite = "identity.users.write" + PermissionAuditRead = "identity.audit.read" + PermissionDeviceRead = "device.read" + PermissionDeviceWrite = "device.write" + PermissionMediaRead = "media.read" + PermissionMediaWrite = "media.write" + PermissionAreaRead = "area.read" + PermissionAreaWrite = "area.write" +) + +type User struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Role string `json:"role"` + Enabled bool `json:"enabled"` + PasswordHash string `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Session struct { + ID string + UserID string + ExpiresAt time.Time + RevokedAt *time.Time +} + +type AuditEntry struct { + ID string `json:"id"` + ActorID string `json:"actor_id,omitempty"` + Action string `json:"action"` + Target string `json:"target,omitempty"` + Outcome string `json:"outcome"` + Metadata map[string]any `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type Principal struct { + UserID string `json:"user_id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Role string `json:"role"` + Permissions []string `json:"permissions"` + SessionID string `json:"-"` +} diff --git a/Sense/server/cmd/sense/modules_identity.go b/Sense/server/cmd/sense/modules_identity.go new file mode 100644 index 0000000..ce553d6 --- /dev/null +++ b/Sense/server/cmd/sense/modules_identity.go @@ -0,0 +1,42 @@ +package sense + +import ( + "fmt" + "os" + "time" + + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +func init() { + registerModule(func(app *platform.App) error { + memoryMode := app.Config().DatabaseMode == platform.DatabaseModeMemory + key, err := identity.SigningKeyOrRandom(os.Getenv("SENSE_IDENTITY_SIGNING_KEY"), memoryMode) + if err != nil { + return err + } + cfg := identity.Config{ + SigningKey: key, + BootstrapToken: os.Getenv("SENSE_BOOTSTRAP_TOKEN"), + SecureCookie: identity.SecureCookieFromEnvironment(os.Getenv("SENSE_COOKIE_SECURE"), memoryMode), + SessionTTL: 8 * time.Hour, + } + var store identity.Store + if memoryMode { + store = identity.NewMemoryStore() + } else { + if cfg.BootstrapToken == "" { + return fmt.Errorf("SENSE_BOOTSTRAP_TOKEN is required until secure initialization is completed") + } + store = identity.NewPostgresStore(app.Database()) + } + service, err := identity.NewService(store, cfg) + if err != nil { + return err + } + app.RegisterMigration(platform.Migration{Version: 2026081201, Name: "sense_identity", SQL: identity.MigrationSQL}) + identity.NewModule(service, cfg).Register(app) + return nil + }) +} diff --git a/Sense/server/database/migrations/sense_identity/2026081201_identity.sql b/Sense/server/database/migrations/sense_identity/2026081201_identity.sql new file mode 100644 index 0000000..d5a9a86 --- /dev/null +++ b/Sense/server/database/migrations/sense_identity/2026081201_identity.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS sense_identity_users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + role TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); +CREATE TABLE IF NOT EXISTS sense_identity_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES sense_identity_users(id), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ NULL +); +CREATE TABLE IF NOT EXISTS sense_identity_audit ( + id TEXT PRIMARY KEY, + actor_id TEXT NULL REFERENCES sense_identity_users(id), + action TEXT NOT NULL, + target TEXT NOT NULL DEFAULT '', + outcome TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL +); +CREATE INDEX IF NOT EXISTS sense_identity_audit_created_idx ON sense_identity_audit(created_at DESC); + diff --git a/Sense/server/go.mod b/Sense/server/go.mod index 2029838..b2054d9 100644 --- a/Sense/server/go.mod +++ b/Sense/server/go.mod @@ -2,12 +2,16 @@ module yovision.local/sense go 1.26.5 -require github.com/jackc/pgx/v5 v5.8.0 +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/jackc/pgx/v5 v5.8.0 + golang.org/x/crypto v0.55.0 +) require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/text v0.41.0 // indirect ) diff --git a/Sense/server/go.sum b/Sense/server/go.sum index 87a6c8a..823716d 100644 --- a/Sense/server/go.sum +++ b/Sense/server/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -16,10 +18,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/Sense/server/internal/platform/http.go b/Sense/server/internal/platform/http.go index f49bf25..8bef4cc 100644 --- a/Sense/server/internal/platform/http.go +++ b/Sense/server/internal/platform/http.go @@ -3,6 +3,7 @@ package platform import ( "encoding/json" "errors" + "io" "net/http" ) @@ -30,10 +31,13 @@ func WriteError(w http.ResponseWriter, err error) { } func DecodeJSON(r *http.Request, target any) error { - decoder := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20)) + decoder := json.NewDecoder(io.LimitReader(r.Body, (1<<20)+1)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return &APIError{Status: http.StatusBadRequest, Code: "invalid_request", Message: "请求内容格式不正确"} } + if err := decoder.Decode(&struct{}{}); err != io.EOF { + return &APIError{Status: http.StatusBadRequest, Code: "invalid_request", Message: "请求内容格式不正确"} + } return nil } diff --git a/Sense/tests/identity/README.md b/Sense/tests/identity/README.md new file mode 100644 index 0000000..f3a058c --- /dev/null +++ b/Sense/tests/identity/README.md @@ -0,0 +1,10 @@ +# Identity acceptance boundary + +Automated identity tests live with the Go package at +`Sense/server/app/sense/identity`. They use generated, non-production values +and never contain a real password, token, cookie or signing key. + +The acceptance checks cover bootstrap uniqueness, password hashing, login, +role permissions, product-specific token validation, session revocation and +audit records. PostgreSQL migration execution remains a deployment-environment +integration check. diff --git a/Sense/ui/src/api/sense/identity/index.js b/Sense/ui/src/api/sense/identity/index.js new file mode 100644 index 0000000..62faa6d --- /dev/null +++ b/Sense/ui/src/api/sense/identity/index.js @@ -0,0 +1,10 @@ +import request from '../../../bootstrap/request' + +export const login = (data) => request.post('/identity/login', data) +export const logout = () => request.post('/identity/logout') +export const currentUser = () => request.get('/identity/me') +export const listUsers = () => request.get('/identity/users') +export const createUser = (data) => request.post('/identity/users', data) +export const updateUser = (id, data) => request.patch(`/identity/users/${id}`, data) +export const listAudit = () => request.get('/identity/audit') + diff --git a/Sense/ui/src/layout/AppLayout.vue b/Sense/ui/src/layout/AppLayout.vue index ff05b0a..fbe0b96 100644 --- a/Sense/ui/src/layout/AppLayout.vue +++ b/Sense/ui/src/layout/AppLayout.vue @@ -56,13 +56,17 @@ diff --git a/Sense/ui/src/router/modules/sense-identity.js b/Sense/ui/src/router/modules/sense-identity.js new file mode 100644 index 0000000..427fa52 --- /dev/null +++ b/Sense/ui/src/router/modules/sense-identity.js @@ -0,0 +1,25 @@ +import Login from '../../views/sense/identity/Login.vue' +import Users from '../../views/sense/identity/Users.vue' +import Audit from '../../views/sense/identity/Audit.vue' + +export default [ + { + path: '/login', + name: 'sense-login', + component: Login, + meta: { title: '登录', public: true, hidden: true } + }, + { + path: 'identity/users', + name: 'sense-users', + component: Users, + meta: { title: '用户与权限', icon: 'UserFilled', order: 90, permission: 'identity.users.read' } + }, + { + path: 'identity/audit', + name: 'sense-audit', + component: Audit, + meta: { title: '操作记录', icon: 'Tickets', order: 91, permission: 'identity.audit.read' } + } +] + diff --git a/Sense/ui/src/store/modules/sense-identity.js b/Sense/ui/src/store/modules/sense-identity.js new file mode 100644 index 0000000..c645509 --- /dev/null +++ b/Sense/ui/src/store/modules/sense-identity.js @@ -0,0 +1,45 @@ +import * as identityApi from '../../api/sense/identity' + +function loadPrincipal() { + try { + return JSON.parse(window.sessionStorage.getItem('sense_session')) + } catch { + return null + } +} + +export default { + namespaced: true, + state: () => ({ principal: loadPrincipal() }), + getters: { + permissions: (state) => state.principal?.permissions || [], + hasPermission: (state) => (permission) => (state.principal?.permissions || []).includes(permission) + }, + mutations: { + setPrincipal(state, principal) { + state.principal = principal + if (principal) window.sessionStorage.setItem('sense_session', JSON.stringify(principal)) + else window.sessionStorage.removeItem('sense_session') + } + }, + actions: { + async login({ commit }, credentials) { + const principal = await identityApi.login(credentials) + commit('setPrincipal', principal) + return principal + }, + async refresh({ commit }) { + const principal = await identityApi.currentUser() + commit('setPrincipal', principal) + return principal + }, + async logout({ commit }) { + try { + await identityApi.logout() + } finally { + commit('setPrincipal', null) + } + } + } +} + diff --git a/Sense/ui/src/views/sense/identity/Audit.vue b/Sense/ui/src/views/sense/identity/Audit.vue new file mode 100644 index 0000000..7ac0c46 --- /dev/null +++ b/Sense/ui/src/views/sense/identity/Audit.vue @@ -0,0 +1,30 @@ + + + + diff --git a/Sense/ui/src/views/sense/identity/Login.vue b/Sense/ui/src/views/sense/identity/Login.vue new file mode 100644 index 0000000..341d831 --- /dev/null +++ b/Sense/ui/src/views/sense/identity/Login.vue @@ -0,0 +1,81 @@ + + + + + + diff --git a/Sense/ui/src/views/sense/identity/Users.vue b/Sense/ui/src/views/sense/identity/Users.vue new file mode 100644 index 0000000..88c4145 --- /dev/null +++ b/Sense/ui/src/views/sense/identity/Users.vue @@ -0,0 +1,102 @@ + + + + + + -- 2.34.1 From 5b8f4a4c8c671097d362bf2799949883cf144c1e Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 17:10:13 +0800 Subject: [PATCH 05/17] feat: add Sense device inventory and credential boundary (#23) --- Sense/server/app/sense/device/crypto.go | 53 ++++++ Sense/server/app/sense/device/http.go | 123 ++++++++++++++ Sense/server/app/sense/device/migration.go | 3 + Sense/server/app/sense/device/service.go | 126 ++++++++++++++ Sense/server/app/sense/device/service_test.go | 58 +++++++ Sense/server/app/sense/device/store.go | 160 ++++++++++++++++++ Sense/server/app/sense/device/types.go | 41 +++++ Sense/server/app/sense/identity/audit_port.go | 13 ++ Sense/server/cmd/sense/modules_device.go | 26 +++ .../sense_device/2026081202_device.sql | 3 + Sense/tests/device/README.md | 6 + Sense/ui/src/api/sense/device/index.js | 8 + Sense/ui/src/router/modules/sense-device.js | 9 + Sense/ui/src/views/sense/device/Devices.vue | 75 ++++++++ 14 files changed, 704 insertions(+) create mode 100644 Sense/server/app/sense/device/crypto.go create mode 100644 Sense/server/app/sense/device/http.go create mode 100644 Sense/server/app/sense/device/migration.go create mode 100644 Sense/server/app/sense/device/service.go create mode 100644 Sense/server/app/sense/device/service_test.go create mode 100644 Sense/server/app/sense/device/store.go create mode 100644 Sense/server/app/sense/device/types.go create mode 100644 Sense/server/app/sense/identity/audit_port.go create mode 100644 Sense/server/cmd/sense/modules_device.go create mode 100644 Sense/server/database/migrations/sense_device/2026081202_device.sql create mode 100644 Sense/tests/device/README.md create mode 100644 Sense/ui/src/api/sense/device/index.js create mode 100644 Sense/ui/src/router/modules/sense-device.js create mode 100644 Sense/ui/src/views/sense/device/Devices.vue diff --git a/Sense/server/app/sense/device/crypto.go b/Sense/server/app/sense/device/crypto.go new file mode 100644 index 0000000..d26779a --- /dev/null +++ b/Sense/server/app/sense/device/crypto.go @@ -0,0 +1,53 @@ +package device + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" +) + +type CredentialVault struct{ key []byte } + +func NewCredentialVault(encodedKey string, allowRandom bool) (*CredentialVault, error) { + var key []byte + if encodedKey != "" { + decoded, err := base64.StdEncoding.DecodeString(encodedKey) + if err != nil { + return nil, fmt.Errorf("SENSE_CREDENTIAL_KEY must be base64 encoded") + } + key = decoded + } else if allowRandom { + key = make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, err + } + } + if len(key) != 32 { + return nil, fmt.Errorf("SENSE_CREDENTIAL_KEY must decode to 32 bytes") + } + return &CredentialVault{key: key}, nil +} + +func (v *CredentialVault) Encrypt(username, password string) ([]byte, error) { + plaintext, err := json.Marshal(map[string]string{"username": username, "password": password}) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(v.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, plaintext, []byte("sense-device-credential-v1")), nil +} diff --git a/Sense/server/app/sense/device/http.go b/Sense/server/app/sense/device/http.go new file mode 100644 index 0000000..ec08ab6 --- /dev/null +++ b/Sense/server/app/sense/device/http.go @@ -0,0 +1,123 @@ +package device + +import ( + "net/http" + "strconv" + + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +type Module struct{ service *Service } + +func NewModule(service *Service) *Module { return &Module{service: service} } +func (m *Module) Register(app *platform.App) { + app.Handle("GET /api/v1/devices", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.list))) + app.Handle("POST /api/v1/devices", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.create))) + app.Handle("GET /api/v1/devices/{id}", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.get))) + app.Handle("PATCH /api/v1/devices/{id}", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.update))) + app.Handle("POST /api/v1/devices/{id}/credentials", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.credential))) + app.Handle("POST /api/v1/devices/{id}/disable", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.disable))) +} +func (m *Module) list(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + size, _ := strconv.Atoi(r.URL.Query().Get("page_size")) + result, err := m.service.List(r.Context(), ListFilter{Keyword: r.URL.Query().Get("keyword"), Page: page, PageSize: size}) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, result) +} +func (m *Module) get(w http.ResponseWriter, r *http.Request) { + item, err := m.service.Get(r.Context(), r.PathValue("id")) + if err != nil { + writeDeviceError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func (m *Module) create(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + Location string `json:"location"` + Modality string `json:"modality"` + Capabilities []string `json:"capabilities"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Create(r.Context(), actor, req.Name, req.Location, req.Modality, req.Capabilities) + if err != nil { + writeDeviceError(w, err) + return + } + platform.WriteJSON(w, http.StatusCreated, item) +} +func (m *Module) update(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + Location string `json:"location"` + Capabilities []string `json:"capabilities"` + Version int64 `json:"version"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Update(r.Context(), actor, r.PathValue("id"), req.Name, req.Location, req.Capabilities, req.Version) + if err != nil { + writeDeviceError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func (m *Module) credential(w http.ResponseWriter, r *http.Request) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.SetCredential(r.Context(), actor, r.PathValue("id"), req.Username, req.Password) + if err != nil { + writeDeviceError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func (m *Module) disable(w http.ResponseWriter, r *http.Request) { + var req struct { + Version int64 `json:"version"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Disable(r.Context(), actor, r.PathValue("id"), req.Version) + if err != nil { + writeDeviceError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func writeDeviceError(w http.ResponseWriter, err error) { + status := http.StatusBadRequest + code := "device_invalid" + if err == ErrNotFound { + status = http.StatusNotFound + code = "device_not_found" + } + if err == ErrConflict { + status = http.StatusConflict + code = "version_conflict" + } + platform.WriteError(w, &platform.APIError{Status: status, Code: code, Message: err.Error()}) +} diff --git a/Sense/server/app/sense/device/migration.go b/Sense/server/app/sense/device/migration.go new file mode 100644 index 0000000..aab11fe --- /dev/null +++ b/Sense/server/app/sense/device/migration.go @@ -0,0 +1,3 @@ +package device + +const MigrationSQL = `CREATE TABLE IF NOT EXISTS sense_devices(id TEXT PRIMARY KEY,name TEXT NOT NULL,location TEXT NOT NULL DEFAULT '',modality TEXT NOT NULL,capabilities TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,adapter_status TEXT NOT NULL,credential_ciphertext BYTEA NULL,version BIGINT NOT NULL,created_at TIMESTAMPTZ NOT NULL,updated_at TIMESTAMPTZ NOT NULL);CREATE INDEX IF NOT EXISTS sense_devices_created_idx ON sense_devices(created_at DESC);` diff --git a/Sense/server/app/sense/device/service.go b/Sense/server/app/sense/device/service.go new file mode 100644 index 0000000..0f7da54 --- /dev/null +++ b/Sense/server/app/sense/device/service.go @@ -0,0 +1,126 @@ +package device + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "strings" + "time" + + "yovision.local/sense/app/sense/identity" +) + +type Service struct { + store Store + vault *CredentialVault + now func() time.Time +} + +func NewService(store Store, vault *CredentialVault) *Service { + return &Service{store: store, vault: vault, now: time.Now} +} + +func (s *Service) Create(ctx context.Context, actor identity.Principal, name, location, modality string, capabilities []string) (Device, error) { + name = strings.TrimSpace(name) + if name == "" { + return Device{}, fmt.Errorf("设备名称不能为空") + } + if modality == "" { + modality = ModalityVideo + } + adapter := AdapterNotReady + if modality == ModalityVideo { + adapter = AdapterReady + } + now := s.now().UTC() + item := Device{ID: newID(), Name: name, Location: strings.TrimSpace(location), Modality: modality, Capabilities: capabilities, Status: StatusPending, AdapterStatus: adapter, Version: 1, CreatedAt: now, UpdatedAt: now} + if err := s.store.Create(ctx, item); err != nil { + return Device{}, err + } + identity.RecordAudit(ctx, actor.UserID, "device.create", item.ID, "success", map[string]any{"modality": modality}) + return item, nil +} +func (s *Service) Get(ctx context.Context, id string) (Device, error) { + item, err := s.store.Get(ctx, id) + if err == nil { + item.CredentialConfigured = len(item.CredentialCiphertext) > 0 + item.CredentialCiphertext = nil + } + return item, err +} +func (s *Service) List(ctx context.Context, filter ListFilter) (Page, error) { + page, err := s.store.List(ctx, filter) + for i := range page.Items { + page.Items[i].CredentialCiphertext = nil + } + return page, err +} +func (s *Service) Update(ctx context.Context, actor identity.Principal, id, name, location string, capabilities []string, expected int64) (Device, error) { + item, err := s.store.Get(ctx, id) + if err != nil { + return Device{}, err + } + item.Name = strings.TrimSpace(name) + item.Location = strings.TrimSpace(location) + item.Capabilities = capabilities + item.Version++ + item.UpdatedAt = s.now().UTC() + if item.Name == "" { + return Device{}, fmt.Errorf("设备名称不能为空") + } + if err := s.store.Update(ctx, item, expected); err != nil { + return Device{}, err + } + identity.RecordAudit(ctx, actor.UserID, "device.update", id, "success", map[string]any{"version": item.Version}) + item.CredentialConfigured = len(item.CredentialCiphertext) > 0 + item.CredentialCiphertext = nil + return item, nil +} +func (s *Service) SetCredential(ctx context.Context, actor identity.Principal, id, username, password string) (Device, error) { + if strings.TrimSpace(username) == "" || password == "" { + return Device{}, fmt.Errorf("用户名和密码不能为空") + } + item, err := s.store.Get(ctx, id) + if err != nil { + return Device{}, err + } + ciphertext, err := s.vault.Encrypt(username, password) + if err != nil { + return Device{}, err + } + expected := item.Version + item.CredentialCiphertext = ciphertext + item.CredentialConfigured = true + item.Version++ + item.UpdatedAt = s.now().UTC() + if err := s.store.Update(ctx, item, expected); err != nil { + return Device{}, err + } + identity.RecordAudit(ctx, actor.UserID, "device.credential.update", id, "success", map[string]any{"configured": true}) + item.CredentialCiphertext = nil + return item, nil +} +func (s *Service) Disable(ctx context.Context, actor identity.Principal, id string, expected int64) (Device, error) { + item, err := s.store.Get(ctx, id) + if err != nil { + return Device{}, err + } + item.Status = StatusDisabled + item.Version++ + item.UpdatedAt = s.now().UTC() + if err := s.store.Update(ctx, item, expected); err != nil { + return Device{}, err + } + identity.RecordAudit(ctx, actor.UserID, "device.disable", id, "success", nil) + item.CredentialConfigured = len(item.CredentialCiphertext) > 0 + item.CredentialCiphertext = nil + return item, nil +} +func newID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return "dev_" + hex.EncodeToString(b) +} diff --git a/Sense/server/app/sense/device/service_test.go b/Sense/server/app/sense/device/service_test.go new file mode 100644 index 0000000..64521dc --- /dev/null +++ b/Sense/server/app/sense/device/service_test.go @@ -0,0 +1,58 @@ +package device + +import ( + "context" + "encoding/base64" + "strings" + "testing" + "yovision.local/sense/app/sense/identity" +) + +func testService(t *testing.T) *Service { + t.Helper() + vault, err := NewCredentialVault(base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef")), false) + if err != nil { + t.Fatal(err) + } + return NewService(NewMemoryStore(), vault) +} +func TestDeviceLifecycleAndCredentialNeverReturned(t *testing.T) { + svc := testService(t) + ctx := context.Background() + actor := identity.Principal{UserID: "test"} + item, err := svc.Create(ctx, actor, "东门摄像机", "东门", ModalityVideo, []string{"video"}) + if err != nil { + t.Fatal(err) + } + if item.AdapterStatus != AdapterReady { + t.Fatalf("adapter=%s", item.AdapterStatus) + } + item, err = svc.SetCredential(ctx, actor, item.ID, "camera-user", "camera-password") + if err != nil { + t.Fatal(err) + } + if !item.CredentialConfigured || len(item.CredentialCiphertext) != 0 { + t.Fatal("credential leaked or not configured") + } + page, err := svc.List(ctx, ListFilter{Page: 1, PageSize: 20}) + if err != nil || page.Total != 1 || len(page.Items[0].CredentialCiphertext) != 0 { + t.Fatalf("page=%#v err=%v", page, err) + } +} +func TestNonVideoAdapterNotReadyAndPaginationNotCappedAt16(t *testing.T) { + svc := testService(t) + ctx := context.Background() + for i := 0; i < 20; i++ { + item, err := svc.Create(ctx, identity.Principal{}, strings.Repeat("x", i+1), "", "radar", nil) + if err != nil { + t.Fatal(err) + } + if item.AdapterStatus != AdapterNotReady { + t.Fatal("unsupported adapter shown ready") + } + } + page, err := svc.List(ctx, ListFilter{Page: 1, PageSize: 20}) + if err != nil || len(page.Items) != 20 { + t.Fatalf("items=%d err=%v", len(page.Items), err) + } +} diff --git a/Sense/server/app/sense/device/store.go b/Sense/server/app/sense/device/store.go new file mode 100644 index 0000000..a0a32f1 --- /dev/null +++ b/Sense/server/app/sense/device/store.go @@ -0,0 +1,160 @@ +package device + +import ( + "context" + "database/sql" + "errors" + "sort" + "strings" + "sync" + "time" +) + +var ( + ErrNotFound = errors.New("device not found") + ErrConflict = errors.New("device version conflict") +) + +type Store interface { + Create(context.Context, Device) error + Get(context.Context, string) (Device, error) + Update(context.Context, Device, int64) error + List(context.Context, ListFilter) (Page, error) +} + +type MemoryStore struct { + mu sync.RWMutex + devices map[string]Device +} + +func NewMemoryStore() *MemoryStore { return &MemoryStore{devices: map[string]Device{}} } + +func (s *MemoryStore) Create(_ context.Context, item Device) error { + s.mu.Lock() + defer s.mu.Unlock() + s.devices[item.ID] = item + return nil +} + +func (s *MemoryStore) Get(_ context.Context, id string) (Device, error) { + s.mu.RLock() + defer s.mu.RUnlock() + item, ok := s.devices[id] + if !ok { + return Device{}, ErrNotFound + } + return item, nil +} + +func (s *MemoryStore) Update(_ context.Context, item Device, expected int64) error { + s.mu.Lock() + defer s.mu.Unlock() + current, ok := s.devices[item.ID] + if !ok { + return ErrNotFound + } + if current.Version != expected { + return ErrConflict + } + s.devices[item.ID] = item + return nil +} + +func (s *MemoryStore) List(_ context.Context, filter ListFilter) (Page, error) { + s.mu.RLock() + defer s.mu.RUnlock() + items := make([]Device, 0, len(s.devices)) + keyword := strings.ToLower(strings.TrimSpace(filter.Keyword)) + for _, item := range s.devices { + if keyword == "" || strings.Contains(strings.ToLower(item.Name+" "+item.Location), keyword) { + items = append(items, item) + } + } + sort.Slice(items, func(i, j int) bool { return items[i].CreatedAt.After(items[j].CreatedAt) }) + page, size := normalizePage(filter.Page, filter.PageSize) + total := len(items) + start := (page - 1) * size + if start > total { + start = total + } + end := start + size + if end > total { + end = total + } + return Page{Items: append([]Device(nil), items[start:end]...), Total: total, Page: page, PageSize: size}, nil +} + +type PostgresStore struct{ database *sql.DB } + +func NewPostgresStore(database *sql.DB) *PostgresStore { return &PostgresStore{database: database} } + +func (s *PostgresStore) Create(ctx context.Context, item Device) error { + _, err := s.database.ExecContext(ctx, `INSERT INTO sense_devices(id,name,location,modality,capabilities,status,adapter_status,credential_ciphertext,version,created_at,updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, item.ID, item.Name, item.Location, item.Modality, strings.Join(item.Capabilities, ","), item.Status, item.AdapterStatus, item.CredentialCiphertext, item.Version, item.CreatedAt, item.UpdatedAt) + return err +} +func (s *PostgresStore) Get(ctx context.Context, id string) (Device, error) { + var item Device + var capabilities string + err := s.database.QueryRowContext(ctx, `SELECT id,name,location,modality,capabilities,status,adapter_status,credential_ciphertext,version,created_at,updated_at FROM sense_devices WHERE id=$1`, id).Scan(&item.ID, &item.Name, &item.Location, &item.Modality, &capabilities, &item.Status, &item.AdapterStatus, &item.CredentialCiphertext, &item.Version, &item.CreatedAt, &item.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return Device{}, ErrNotFound + } + item.Capabilities = splitCapabilities(capabilities) + item.CredentialConfigured = len(item.CredentialCiphertext) > 0 + return item, err +} +func (s *PostgresStore) Update(ctx context.Context, item Device, expected int64) error { + result, err := s.database.ExecContext(ctx, `UPDATE sense_devices SET name=$2,location=$3,modality=$4,capabilities=$5,status=$6,adapter_status=$7,credential_ciphertext=$8,version=$9,updated_at=$10 WHERE id=$1 AND version=$11`, item.ID, item.Name, item.Location, item.Modality, strings.Join(item.Capabilities, ","), item.Status, item.AdapterStatus, item.CredentialCiphertext, item.Version, item.UpdatedAt, expected) + if err != nil { + return err + } + rows, _ := result.RowsAffected() + if rows == 0 { + return ErrConflict + } + return nil +} +func (s *PostgresStore) List(ctx context.Context, filter ListFilter) (Page, error) { + page, size := normalizePage(filter.Page, filter.PageSize) + keyword := "%" + strings.TrimSpace(filter.Keyword) + "%" + var total int + if err := s.database.QueryRowContext(ctx, `SELECT count(*) FROM sense_devices WHERE name ILIKE $1 OR location ILIKE $1`, keyword).Scan(&total); err != nil { + return Page{}, err + } + rows, err := s.database.QueryContext(ctx, `SELECT id,name,location,modality,capabilities,status,adapter_status,(credential_ciphertext IS NOT NULL),version,created_at,updated_at FROM sense_devices WHERE name ILIKE $1 OR location ILIKE $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, keyword, size, (page-1)*size) + if err != nil { + return Page{}, err + } + defer rows.Close() + items := []Device{} + for rows.Next() { + var item Device + var caps string + if err := rows.Scan(&item.ID, &item.Name, &item.Location, &item.Modality, &caps, &item.Status, &item.AdapterStatus, &item.CredentialConfigured, &item.Version, &item.CreatedAt, &item.UpdatedAt); err != nil { + return Page{}, err + } + item.Capabilities = splitCapabilities(caps) + items = append(items, item) + } + return Page{Items: items, Total: total, Page: page, PageSize: size}, rows.Err() +} +func normalizePage(page, size int) (int, int) { + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + if size > 100 { + size = 100 + } + return page, size +} +func splitCapabilities(value string) []string { + if value == "" { + return []string{} + } + return strings.Split(value, ",") +} + +var _ = time.Time{} diff --git a/Sense/server/app/sense/device/types.go b/Sense/server/app/sense/device/types.go new file mode 100644 index 0000000..6dc773d --- /dev/null +++ b/Sense/server/app/sense/device/types.go @@ -0,0 +1,41 @@ +package device + +import "time" + +const ( + ModalityVideo = "video" + StatusPending = "pending" + StatusActive = "active" + StatusOffline = "offline" + StatusDisabled = "disabled" + AdapterReady = "ready" + AdapterNotReady = "adapter_not_ready" +) + +type Device struct { + ID string `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + Modality string `json:"modality"` + Capabilities []string `json:"capabilities"` + Status string `json:"status"` + AdapterStatus string `json:"adapter_status"` + CredentialConfigured bool `json:"credential_configured"` + CredentialCiphertext []byte `json:"-"` + Version int64 `json:"version"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListFilter struct { + Keyword string + Page int + PageSize int +} + +type Page struct { + Items []Device `json:"items"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} diff --git a/Sense/server/app/sense/identity/audit_port.go b/Sense/server/app/sense/identity/audit_port.go new file mode 100644 index 0000000..732ec1e --- /dev/null +++ b/Sense/server/app/sense/identity/audit_port.go @@ -0,0 +1,13 @@ +package identity + +import "context" + +// RecordAudit is the narrow project-internal audit port used by Sense business +// modules. It never accepts secrets and does not expose identity persistence. +func RecordAudit(ctx context.Context, actor, action, target, outcome string, metadata map[string]any) { + module := activeModule.Load() + if module == nil { + return + } + module.service.audit(ctx, actor, action, target, outcome, metadata) +} diff --git a/Sense/server/cmd/sense/modules_device.go b/Sense/server/cmd/sense/modules_device.go new file mode 100644 index 0000000..b29169a --- /dev/null +++ b/Sense/server/cmd/sense/modules_device.go @@ -0,0 +1,26 @@ +package sense + +import ( + "os" + "yovision.local/sense/app/sense/device" + "yovision.local/sense/internal/platform" +) + +func init() { + registerModule(func(app *platform.App) error { + memory := app.Config().DatabaseMode == platform.DatabaseModeMemory + vault, err := device.NewCredentialVault(os.Getenv("SENSE_CREDENTIAL_KEY"), memory) + if err != nil { + return err + } + var store device.Store + if memory { + store = device.NewMemoryStore() + } else { + store = device.NewPostgresStore(app.Database()) + } + app.RegisterMigration(platform.Migration{Version: 2026081202, Name: "sense_device", SQL: device.MigrationSQL}) + device.NewModule(device.NewService(store, vault)).Register(app) + return nil + }) +} diff --git a/Sense/server/database/migrations/sense_device/2026081202_device.sql b/Sense/server/database/migrations/sense_device/2026081202_device.sql new file mode 100644 index 0000000..b968803 --- /dev/null +++ b/Sense/server/database/migrations/sense_device/2026081202_device.sql @@ -0,0 +1,3 @@ +CREATE TABLE IF NOT EXISTS sense_devices(id TEXT PRIMARY KEY,name TEXT NOT NULL,location TEXT NOT NULL DEFAULT '',modality TEXT NOT NULL,capabilities TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,adapter_status TEXT NOT NULL,credential_ciphertext BYTEA NULL,version BIGINT NOT NULL,created_at TIMESTAMPTZ NOT NULL,updated_at TIMESTAMPTZ NOT NULL); +CREATE INDEX IF NOT EXISTS sense_devices_created_idx ON sense_devices(created_at DESC); + diff --git a/Sense/tests/device/README.md b/Sense/tests/device/README.md new file mode 100644 index 0000000..27d7201 --- /dev/null +++ b/Sense/tests/device/README.md @@ -0,0 +1,6 @@ +# Device acceptance + +Package tests verify CRUD state, pagination beyond 16 items, unsupported +adapter status, optimistic versions and that encrypted camera credentials are +write-only. Fixtures use generated values only; no address or real credential +is stored in the repository. diff --git a/Sense/ui/src/api/sense/device/index.js b/Sense/ui/src/api/sense/device/index.js new file mode 100644 index 0000000..9a284c1 --- /dev/null +++ b/Sense/ui/src/api/sense/device/index.js @@ -0,0 +1,8 @@ +import request from '../../../bootstrap/request' + +export const listDevices = (params) => request.get('/devices', { params }) +export const createDevice = (data) => request.post('/devices', data) +export const updateDevice = (id, data) => request.patch(`/devices/${id}`, data) +export const updateCredential = (id, data) => request.post(`/devices/${id}/credentials`, data) +export const disableDevice = (id, version) => request.post(`/devices/${id}/disable`, { version }) + diff --git a/Sense/ui/src/router/modules/sense-device.js b/Sense/ui/src/router/modules/sense-device.js new file mode 100644 index 0000000..4637470 --- /dev/null +++ b/Sense/ui/src/router/modules/sense-device.js @@ -0,0 +1,9 @@ +import Devices from '../../views/sense/device/Devices.vue' + +export default { + path: 'devices', + name: 'sense-devices', + component: Devices, + meta: { title: '设备管理', icon: 'VideoCamera', order: 10, permission: 'device.read' } +} + diff --git a/Sense/ui/src/views/sense/device/Devices.vue b/Sense/ui/src/views/sense/device/Devices.vue new file mode 100644 index 0000000..9c27d89 --- /dev/null +++ b/Sense/ui/src/views/sense/device/Devices.vue @@ -0,0 +1,75 @@ + + + + + -- 2.34.1 From 4f17cca91fe70e875e3815e8aaa96f354dd936d8 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 17:14:34 +0800 Subject: [PATCH 06/17] feat: add controlled ONVIF and RTSP admission (#24) --- .../server/app/sense/adapters/onvif/client.go | 109 ++++++++++++++++ .../app/sense/adapters/onvif/discovery.go | 80 ++++++++++++ .../server/app/sense/adapters/onvif/parser.go | 56 +++++++++ .../app/sense/adapters/onvif/parser_test.go | 31 +++++ .../app/sense/adapters/rtsp/verifier.go | 74 +++++++++++ Sense/server/app/sense/admission/http.go | 49 ++++++++ Sense/server/app/sense/admission/service.go | 119 ++++++++++++++++++ .../app/sense/admission/service_test.go | 46 +++++++ .../app/sense/device/credential_port.go | 32 +++++ Sense/server/app/sense/device/crypto.go | 27 ++++ Sense/server/app/sense/device/http.go | 1 + Sense/server/cmd/sense/modules_admission.go | 18 +++ Sense/testdata/onvif/profiles.xml | 2 + Sense/testdata/onvif/stream-uri.xml | 2 + Sense/tests/admission/README.md | 6 + Sense/ui/src/api/sense/admission/index.js | 6 + .../ui/src/router/modules/sense-admission.js | 3 + .../src/views/sense/admission/Admission.vue | 52 ++++++++ 18 files changed, 713 insertions(+) create mode 100644 Sense/server/app/sense/adapters/onvif/client.go create mode 100644 Sense/server/app/sense/adapters/onvif/discovery.go create mode 100644 Sense/server/app/sense/adapters/onvif/parser.go create mode 100644 Sense/server/app/sense/adapters/onvif/parser_test.go create mode 100644 Sense/server/app/sense/adapters/rtsp/verifier.go create mode 100644 Sense/server/app/sense/admission/http.go create mode 100644 Sense/server/app/sense/admission/service.go create mode 100644 Sense/server/app/sense/admission/service_test.go create mode 100644 Sense/server/app/sense/device/credential_port.go create mode 100644 Sense/server/cmd/sense/modules_admission.go create mode 100644 Sense/testdata/onvif/profiles.xml create mode 100644 Sense/testdata/onvif/stream-uri.xml create mode 100644 Sense/tests/admission/README.md create mode 100644 Sense/ui/src/api/sense/admission/index.js create mode 100644 Sense/ui/src/router/modules/sense-admission.js create mode 100644 Sense/ui/src/views/sense/admission/Admission.vue diff --git a/Sense/server/app/sense/adapters/onvif/client.go b/Sense/server/app/sense/adapters/onvif/client.go new file mode 100644 index 0000000..c03cb87 --- /dev/null +++ b/Sense/server/app/sense/adapters/onvif/client.go @@ -0,0 +1,109 @@ +package onvif + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +type Credential struct { + Username string + Password string +} +type Profile struct { + Token string `json:"token"` + Name string `json:"name"` + Width int `json:"width"` + Height int `json:"height"` + Encoding string `json:"encoding"` + StreamURI string `json:"stream_uri"` +} +type Client interface { + Profiles(context.Context, string, Credential) ([]Profile, error) +} + +type HTTPClient struct{ client *http.Client } + +func NewHTTPClient(timeout time.Duration) *HTTPClient { + if timeout <= 0 { + timeout = 8 * time.Second + } + return &HTTPClient{client: &http.Client{Timeout: timeout}} +} +func (c *HTTPClient) Profiles(ctx context.Context, address string, credential Credential) ([]Profile, error) { + endpoint, err := validateEndpoint(address) + if err != nil { + return nil, err + } + body := `` + data, err := c.soap(ctx, endpoint, credential, body) + if err != nil { + return nil, err + } + profiles, err := ParseProfiles(data) + if err != nil { + return nil, err + } + for i := range profiles { + uriBody := fmt.Sprintf(`RTP-UnicastRTSP%s`, xmlEscape(profiles[i].Token)) + streamData, err := c.soap(ctx, endpoint, credential, uriBody) + if err != nil { + return nil, err + } + profiles[i].StreamURI, err = ParseStreamURI(streamData) + if err != nil { + return nil, err + } + } + return profiles, nil +} +func (c *HTTPClient) soap(ctx context.Context, endpoint string, credential Credential, body string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8") + if credential.Username != "" { + req.SetBasicAuth(credential.Username, credential.Password) + } + res, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("onvif request failed: %w", err) + } + defer res.Body.Close() + data, err := io.ReadAll(io.LimitReader(res.Body, 2<<20)) + if err != nil { + return nil, err + } + if res.StatusCode == http.StatusUnauthorized { + return nil, fmt.Errorf("authentication_failed") + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + return nil, fmt.Errorf("onvif_http_%d", res.StatusCode) + } + return data, nil +} +func validateEndpoint(value string) (string, error) { + parsed, err := url.Parse(value) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return "", fmt.Errorf("invalid ONVIF address") + } + if parsed.User != nil { + return "", fmt.Errorf("credentials must not appear in ONVIF address") + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return "", fmt.Errorf("unsupported ONVIF scheme") + } + return parsed.String(), nil +} +func xmlEscape(value string) string { + var b strings.Builder + _ = xml.EscapeText(&b, []byte(value)) + return b.String() +} diff --git a/Sense/server/app/sense/adapters/onvif/discovery.go b/Sense/server/app/sense/adapters/onvif/discovery.go new file mode 100644 index 0000000..7a41b81 --- /dev/null +++ b/Sense/server/app/sense/adapters/onvif/discovery.go @@ -0,0 +1,80 @@ +package onvif + +import ( + "context" + "fmt" + "net" + "strings" + "time" +) + +const discoveryProbe = `uuid:sense-controlled-discoveryurn:schemas-xmlsoap-org:ws:2005:04:discoveryhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probedn:NetworkVideoTransmitter` + +func Discover(ctx context.Context, localIP string, timeout time.Duration) ([]string, error) { + ip := net.ParseIP(localIP) + if ip == nil { + return nil, fmt.Errorf("approved discovery interface is not configured") + } + approved := false + interfaces, _ := net.Interfaces() + for _, iface := range interfaces { + addresses, _ := iface.Addrs() + for _, address := range addresses { + if strings.HasPrefix(address.String(), ip.String()+"/") { + approved = true + } + } + } + if !approved { + return nil, fmt.Errorf("approved discovery IP is not a local interface") + } + connection, err := net.ListenUDP("udp4", &net.UDPAddr{IP: ip, Port: 0}) + if err != nil { + return nil, err + } + defer connection.Close() + deadline := time.Now().Add(timeout) + _ = connection.SetDeadline(deadline) + target := &net.UDPAddr{IP: net.ParseIP("239.255.255.250"), Port: 3702} + if _, err := connection.WriteToUDP([]byte(discoveryProbe), target); err != nil { + return nil, err + } + found := map[string]bool{} + buffer := make([]byte, 65535) + for { + select { + case <-ctx.Done(): + return keys(found), nil + default: + } + n, _, err := connection.ReadFromUDP(buffer) + if err != nil { + if networkErr, ok := err.(net.Error); ok && networkErr.Timeout() { + return keys(found), nil + } + return nil, err + } + for _, value := range extractXAddrs(string(buffer[:n])) { + found[value] = true + } + } +} +func extractXAddrs(value string) []string { + startTag, endTag := "", "" + start := strings.Index(value, startTag) + if start < 0 { + return nil + } + end := strings.Index(value[start:], endTag) + if end < 0 { + return nil + } + return strings.Fields(value[start+len(startTag) : start+end]) +} +func keys(values map[string]bool) []string { + result := make([]string, 0, len(values)) + for value := range values { + result = append(result, value) + } + return result +} diff --git a/Sense/server/app/sense/adapters/onvif/parser.go b/Sense/server/app/sense/adapters/onvif/parser.go new file mode 100644 index 0000000..56b2665 --- /dev/null +++ b/Sense/server/app/sense/adapters/onvif/parser.go @@ -0,0 +1,56 @@ +package onvif + +import ( + "encoding/xml" + "fmt" + "strings" +) + +type profileEnvelope struct { + Profiles []struct { + Token string `xml:"token,attr"` + Name string `xml:"Name"` + Encoder struct { + Encoding string `xml:"Encoding"` + Resolution struct { + Width int `xml:"Width"` + Height int `xml:"Height"` + } `xml:"Resolution"` + } `xml:"VideoEncoderConfiguration"` + } `xml:"Body>GetProfilesResponse>Profiles"` +} + +func ParseProfiles(data []byte) ([]Profile, error) { + var envelope profileEnvelope + if err := xml.Unmarshal(data, &envelope); err != nil { + return nil, fmt.Errorf("parse ONVIF profiles: %w", err) + } + if len(envelope.Profiles) == 0 { + return nil, fmt.Errorf("no_profiles") + } + result := make([]Profile, 0, len(envelope.Profiles)) + for _, value := range envelope.Profiles { + result = append(result, Profile{Token: value.Token, Name: value.Name, Width: value.Encoder.Resolution.Width, Height: value.Encoder.Resolution.Height, Encoding: value.Encoder.Encoding}) + } + return result, nil +} +func ParseStreamURI(data []byte) (string, error) { + decoder := xml.NewDecoder(strings.NewReader(string(data))) + for { + token, err := decoder.Token() + if err != nil { + return "", fmt.Errorf("stream_uri_not_found") + } + start, ok := token.(xml.StartElement) + if ok && start.Name.Local == "Uri" { + var value string + if err := decoder.DecodeElement(&value, &start); err != nil { + return "", err + } + if strings.Contains(value, "@") { + return "", fmt.Errorf("stream_uri_contains_credentials") + } + return value, nil + } + } +} diff --git a/Sense/server/app/sense/adapters/onvif/parser_test.go b/Sense/server/app/sense/adapters/onvif/parser_test.go new file mode 100644 index 0000000..1da8a27 --- /dev/null +++ b/Sense/server/app/sense/adapters/onvif/parser_test.go @@ -0,0 +1,31 @@ +package onvif + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseFixtureProfilesAndStreamURI(t *testing.T) { + profilesData, err := os.ReadFile(filepath.Join("..", "..", "..", "..", "..", "testdata", "onvif", "profiles.xml")) + if err != nil { + t.Fatal(err) + } + profiles, err := ParseProfiles(profilesData) + if err != nil || len(profiles) != 2 || profiles[0].Width != 1920 { + t.Fatalf("profiles=%#v err=%v", profiles, err) + } + uriData, err := os.ReadFile(filepath.Join("..", "..", "..", "..", "..", "testdata", "onvif", "stream-uri.xml")) + if err != nil { + t.Fatal(err) + } + uri, err := ParseStreamURI(uriData) + if err != nil || uri != "rtsp://camera.invalid/live/main" { + t.Fatalf("uri=%q err=%v", uri, err) + } +} +func TestRejectCredentialInStreamURI(t *testing.T) { + if _, err := ParseStreamURI([]byte(`rtsp://user:pass@camera.invalid/live`)); err == nil { + t.Fatal("credential URI accepted") + } +} diff --git a/Sense/server/app/sense/adapters/rtsp/verifier.go b/Sense/server/app/sense/adapters/rtsp/verifier.go new file mode 100644 index 0000000..7d1a365 --- /dev/null +++ b/Sense/server/app/sense/adapters/rtsp/verifier.go @@ -0,0 +1,74 @@ +package rtsp + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "net" + "net/url" + "strings" + "time" +) + +type Credential struct { + Username string + Password string +} +type Result struct { + Status string `json:"status"` + LatencyMS int64 `json:"latency_ms"` + Detail string `json:"detail,omitempty"` +} +type Verifier interface { + Verify(context.Context, string, Credential) (Result, error) +} +type NetVerifier struct{ Timeout time.Duration } + +func (v NetVerifier) Verify(ctx context.Context, raw string, credential Credential) (Result, error) { + parsed, err := url.Parse(raw) + if err != nil || parsed.Scheme != "rtsp" || parsed.Host == "" { + return Result{}, fmt.Errorf("invalid_rtsp_uri") + } + if parsed.User != nil { + return Result{}, fmt.Errorf("rtsp_uri_contains_credentials") + } + address := parsed.Host + if !strings.Contains(address, ":") { + address += ":554" + } + timeout := v.Timeout + if timeout <= 0 { + timeout = 5 * time.Second + } + dialer := net.Dialer{Timeout: timeout} + started := time.Now() + connection, err := dialer.DialContext(ctx, "tcp", address) + if err != nil { + return Result{Status: "unreachable", Detail: "无法连接视频端口"}, nil + } + defer connection.Close() + _ = connection.SetDeadline(time.Now().Add(timeout)) + authorization := "" + if credential.Username != "" { + authorization = "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(credential.Username+":"+credential.Password)) + "\r\n" + } + request := fmt.Sprintf("OPTIONS %s RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: YoVision-Sense\r\n%s\r\n", parsed.String(), authorization) + if _, err := connection.Write([]byte(request)); err != nil { + return Result{}, err + } + line, err := bufio.NewReader(connection).ReadString('\n') + if err != nil { + return Result{Status: "timeout", Detail: "等待视频响应超时"}, nil + } + status := "ready" + detail := "码流可访问" + if strings.Contains(line, " 401 ") { + status = "authentication_failed" + detail = "设备拒绝了当前凭据" + } else if !strings.Contains(line, " 200 ") { + status = "failed" + detail = "设备返回非成功状态" + } + return Result{Status: status, LatencyMS: time.Since(started).Milliseconds(), Detail: detail}, nil +} diff --git a/Sense/server/app/sense/admission/http.go b/Sense/server/app/sense/admission/http.go new file mode 100644 index 0000000..2dca49b --- /dev/null +++ b/Sense/server/app/sense/admission/http.go @@ -0,0 +1,49 @@ +package admission + +import ( + "net/http" + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +type Module struct{ service *Service } + +func NewModule(service *Service) *Module { return &Module{service: service} } +func (m *Module) Register(app *platform.App) { + app.Handle("GET /api/v1/admission/discover", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.discover))) + app.Handle("POST /api/v1/admission/probe", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.probe))) + app.Handle("GET /api/v1/admission/devices/{id}", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.get))) +} +func (m *Module) discover(w http.ResponseWriter, r *http.Request) { + items, err := m.service.Discover(r.Context()) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusServiceUnavailable, Code: "discovery_unavailable", Message: "未配置获准的发现网卡"}) + return + } + platform.WriteJSON(w, http.StatusOK, map[string]any{"items": items}) +} +func (m *Module) probe(w http.ResponseWriter, r *http.Request) { + var request struct { + DeviceID string `json:"device_id"` + Address string `json:"address"` + } + if err := platform.DecodeJSON(r, &request); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + result, err := m.service.Probe(r.Context(), actor, request.DeviceID, request.Address) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "admission_failed", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusOK, result) +} +func (m *Module) get(w http.ResponseWriter, r *http.Request) { + result, ok := m.service.Get(r.PathValue("id")) + if !ok { + platform.WriteError(w, &platform.APIError{Status: http.StatusNotFound, Code: "admission_not_found", Message: "尚未执行接入验证"}) + return + } + platform.WriteJSON(w, http.StatusOK, result) +} diff --git a/Sense/server/app/sense/admission/service.go b/Sense/server/app/sense/admission/service.go new file mode 100644 index 0000000..d980b2b --- /dev/null +++ b/Sense/server/app/sense/admission/service.go @@ -0,0 +1,119 @@ +package admission + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "time" + + "yovision.local/sense/app/sense/adapters/onvif" + "yovision.local/sense/app/sense/adapters/rtsp" + "yovision.local/sense/app/sense/device" + "yovision.local/sense/app/sense/identity" +) + +type Profile struct { + Token string `json:"token"` + Name string `json:"name"` + Width int `json:"width"` + Height int `json:"height"` + Encoding string `json:"encoding"` + StreamURI string `json:"stream_uri"` + Kind string `json:"kind"` + Verification rtsp.Result `json:"verification"` +} +type Result struct { + DeviceID string `json:"device_id"` + Address string `json:"address"` + Status string `json:"status"` + Detail string `json:"detail"` + Profiles []Profile `json:"profiles"` + CheckedAt time.Time `json:"checked_at"` +} +type Service struct { + onvif onvif.Client + rtsp rtsp.Verifier + discoveryIP string + discoveryTimeout time.Duration + mu sync.RWMutex + results map[string]Result + now func() time.Time +} + +func NewService(client onvif.Client, verifier rtsp.Verifier, discoveryIP string) *Service { + return &Service{onvif: client, rtsp: verifier, discoveryIP: discoveryIP, discoveryTimeout: 3 * time.Second, results: map[string]Result{}, now: time.Now} +} +func (s *Service) Discover(ctx context.Context) ([]string, error) { + if strings.TrimSpace(s.discoveryIP) == "" { + return nil, fmt.Errorf("discovery_not_configured") + } + return onvif.Discover(ctx, s.discoveryIP, s.discoveryTimeout) +} +func (s *Service) Probe(ctx context.Context, actor identity.Principal, deviceID, address string) (Result, error) { + credential, err := device.ReadCredential(ctx, deviceID) + if err != nil { + return Result{}, fmt.Errorf("credential_required") + } + profiles, err := s.onvif.Profiles(ctx, address, onvif.Credential{Username: credential.Username, Password: credential.Password}) + if err != nil { + status, detail := classify(err) + result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, CheckedAt: s.now().UTC()} + s.save(result) + identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "failure", map[string]any{"status": status}) + return result, nil + } + items := make([]Profile, 0, len(profiles)) + for _, profile := range profiles { + verification, verifyErr := s.rtsp.Verify(ctx, profile.StreamURI, rtsp.Credential{Username: credential.Username, Password: credential.Password}) + if verifyErr != nil { + verification = rtsp.Result{Status: "failed", Detail: "视频地址格式不正确"} + } + kind := "other" + items = append(items, Profile{Token: profile.Token, Name: profile.Name, Width: profile.Width, Height: profile.Height, Encoding: profile.Encoding, StreamURI: profile.StreamURI, Kind: kind, Verification: verification}) + } + sort.Slice(items, func(i, j int) bool { return items[i].Width*items[i].Height > items[j].Width*items[j].Height }) + if len(items) > 0 { + items[0].Kind = "main" + } + if len(items) > 1 { + items[len(items)-1].Kind = "sub" + } + status := "ready" + detail := "设备与视频 Profile 已验证" + for _, item := range items { + if item.Verification.Status != "ready" { + status = "profile_failed" + detail = "部分视频 Profile 验证失败" + } + } + result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, Profiles: items, CheckedAt: s.now().UTC()} + s.save(result) + identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "success", map[string]any{"profile_count": len(items), "status": status}) + return result, nil +} +func (s *Service) Get(deviceID string) (Result, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + value, ok := s.results[deviceID] + return value, ok +} +func (s *Service) save(result Result) { + s.mu.Lock() + defer s.mu.Unlock() + s.results[result.DeviceID] = result +} +func classify(err error) (string, string) { + value := strings.ToLower(err.Error()) + switch { + case strings.Contains(value, "authentication"): + return "authentication_failed", "设备拒绝了当前凭据,请更新后重试" + case strings.Contains(value, "deadline") || strings.Contains(value, "timeout"): + return "timeout", "设备响应超时" + case strings.Contains(value, "clock") || strings.Contains(value, "time"): + return "clock_skew", "设备时间可能不准确,请校时后重试" + default: + return "unreachable", "无法读取设备信息,请检查地址和网络" + } +} diff --git a/Sense/server/app/sense/admission/service_test.go b/Sense/server/app/sense/admission/service_test.go new file mode 100644 index 0000000..5a407c0 --- /dev/null +++ b/Sense/server/app/sense/admission/service_test.go @@ -0,0 +1,46 @@ +package admission + +import ( + "context" + "encoding/base64" + "testing" + "yovision.local/sense/app/sense/adapters/onvif" + "yovision.local/sense/app/sense/adapters/rtsp" + "yovision.local/sense/app/sense/device" + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +type fakeONVIF struct{} + +func (fakeONVIF) Profiles(context.Context, string, onvif.Credential) ([]onvif.Profile, error) { + return []onvif.Profile{{Token: "main", Name: "主码流", Width: 1920, Height: 1080, Encoding: "H264", StreamURI: "rtsp://camera.invalid/main"}, {Token: "sub", Name: "子码流", Width: 640, Height: 360, Encoding: "H264", StreamURI: "rtsp://camera.invalid/sub"}}, nil +} + +type fakeRTSP struct{} + +func (fakeRTSP) Verify(context.Context, string, rtsp.Credential) (rtsp.Result, error) { + return rtsp.Result{Status: "ready"}, nil +} +func TestProbeProfilesWithoutCredentialURI(t *testing.T) { + vault, err := device.NewCredentialVault(base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef")), false) + if err != nil { + t.Fatal(err) + } + store := device.NewMemoryStore() + deviceService := device.NewService(store, vault) + module := device.NewModule(deviceService) + module.Register(platform.NewApp(platform.Config{DatabaseMode: platform.DatabaseModeMemory}, nil, nil)) + item, err := deviceService.Create(context.Background(), identity.Principal{}, "camera", "", device.ModalityVideo, nil) + if err != nil { + t.Fatal(err) + } + if _, err = deviceService.SetCredential(context.Background(), identity.Principal{}, item.ID, "fixture-user", "fixture-password"); err != nil { + t.Fatal(err) + } + service := NewService(fakeONVIF{}, fakeRTSP{}, "") + result, err := service.Probe(context.Background(), identity.Principal{}, item.ID, "http://camera.invalid/onvif/device_service") + if err != nil || result.Status != "ready" || result.Profiles[0].Kind != "main" || result.Profiles[1].Kind != "sub" { + t.Fatalf("result=%#v err=%v", result, err) + } +} diff --git a/Sense/server/app/sense/device/credential_port.go b/Sense/server/app/sense/device/credential_port.go new file mode 100644 index 0000000..aee2741 --- /dev/null +++ b/Sense/server/app/sense/device/credential_port.go @@ -0,0 +1,32 @@ +package device + +import ( + "context" + "fmt" + "sync/atomic" +) + +type Credential struct { + Username string + Password string +} + +var activeService atomic.Pointer[Service] + +// ReadCredential is an internal adapter port. Credentials must never be +// returned from HTTP handlers, logged, or placed in a URL. +func ReadCredential(ctx context.Context, id string) (Credential, error) { + service := activeService.Load() + if service == nil { + return Credential{}, fmt.Errorf("device service is not ready") + } + item, err := service.store.Get(ctx, id) + if err != nil { + return Credential{}, err + } + if len(item.CredentialCiphertext) == 0 { + return Credential{}, fmt.Errorf("device credential is not configured") + } + username, password, err := service.vault.Decrypt(item.CredentialCiphertext) + return Credential{Username: username, Password: password}, err +} diff --git a/Sense/server/app/sense/device/crypto.go b/Sense/server/app/sense/device/crypto.go index d26779a..294536d 100644 --- a/Sense/server/app/sense/device/crypto.go +++ b/Sense/server/app/sense/device/crypto.go @@ -51,3 +51,30 @@ func (v *CredentialVault) Encrypt(username, password string) ([]byte, error) { } return gcm.Seal(nonce, nonce, plaintext, []byte("sense-device-credential-v1")), nil } + +func (v *CredentialVault) Decrypt(ciphertext []byte) (string, string, error) { + block, err := aes.NewCipher(v.key) + if err != nil { + return "", "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", "", err + } + if len(ciphertext) < gcm.NonceSize() { + return "", "", fmt.Errorf("invalid credential ciphertext") + } + nonce, encrypted := ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():] + plaintext, err := gcm.Open(nil, nonce, encrypted, []byte("sense-device-credential-v1")) + if err != nil { + return "", "", fmt.Errorf("decrypt device credential: %w", err) + } + var value struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := json.Unmarshal(plaintext, &value); err != nil { + return "", "", err + } + return value.Username, value.Password, nil +} diff --git a/Sense/server/app/sense/device/http.go b/Sense/server/app/sense/device/http.go index ec08ab6..858e600 100644 --- a/Sense/server/app/sense/device/http.go +++ b/Sense/server/app/sense/device/http.go @@ -12,6 +12,7 @@ type Module struct{ service *Service } func NewModule(service *Service) *Module { return &Module{service: service} } func (m *Module) Register(app *platform.App) { + activeService.Store(m.service) app.Handle("GET /api/v1/devices", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.list))) app.Handle("POST /api/v1/devices", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.create))) app.Handle("GET /api/v1/devices/{id}", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.get))) diff --git a/Sense/server/cmd/sense/modules_admission.go b/Sense/server/cmd/sense/modules_admission.go new file mode 100644 index 0000000..30c2712 --- /dev/null +++ b/Sense/server/cmd/sense/modules_admission.go @@ -0,0 +1,18 @@ +package sense + +import ( + "os" + "time" + "yovision.local/sense/app/sense/adapters/onvif" + "yovision.local/sense/app/sense/adapters/rtsp" + "yovision.local/sense/app/sense/admission" + "yovision.local/sense/internal/platform" +) + +func init() { + registerModule(func(app *platform.App) error { + service := admission.NewService(onvif.NewHTTPClient(8*time.Second), rtsp.NetVerifier{Timeout: 5 * time.Second}, os.Getenv("SENSE_ONVIF_DISCOVERY_IP")) + admission.NewModule(service).Register(app) + return nil + }) +} diff --git a/Sense/testdata/onvif/profiles.xml b/Sense/testdata/onvif/profiles.xml new file mode 100644 index 0000000..736d7ec --- /dev/null +++ b/Sense/testdata/onvif/profiles.xml @@ -0,0 +1,2 @@ + +主码流H26419201080子码流H264640360 diff --git a/Sense/testdata/onvif/stream-uri.xml b/Sense/testdata/onvif/stream-uri.xml new file mode 100644 index 0000000..9178c7b --- /dev/null +++ b/Sense/testdata/onvif/stream-uri.xml @@ -0,0 +1,2 @@ + +rtsp://camera.invalid/live/main diff --git a/Sense/tests/admission/README.md b/Sense/tests/admission/README.md new file mode 100644 index 0000000..f1fa438 --- /dev/null +++ b/Sense/tests/admission/README.md @@ -0,0 +1,6 @@ +# Admission acceptance + +Fixture tests cover ONVIF profile parsing, credential-free StreamUri handling, +main/sub classification and RTSP result mapping. Controlled WS-Discovery is +disabled until `SENSE_ONVIF_DISCOVERY_IP` names an approved local interface. +No discovery scan or real camera access runs during automated tests. diff --git a/Sense/ui/src/api/sense/admission/index.js b/Sense/ui/src/api/sense/admission/index.js new file mode 100644 index 0000000..1d56960 --- /dev/null +++ b/Sense/ui/src/api/sense/admission/index.js @@ -0,0 +1,6 @@ +import request from '../../../bootstrap/request' + +export const discoverDevices = () => request.get('/admission/discover') +export const probeDevice = (data) => request.post('/admission/probe', data) +export const admissionResult = (deviceID) => request.get(`/admission/devices/${deviceID}`) + diff --git a/Sense/ui/src/router/modules/sense-admission.js b/Sense/ui/src/router/modules/sense-admission.js new file mode 100644 index 0000000..6d4791b --- /dev/null +++ b/Sense/ui/src/router/modules/sense-admission.js @@ -0,0 +1,3 @@ +import Admission from '../../views/sense/admission/Admission.vue' +export default { path: 'admission', name: 'sense-admission', component: Admission, meta: { title: '视频接入', icon: 'Connection', order: 20, permission: 'device.write' } } + diff --git a/Sense/ui/src/views/sense/admission/Admission.vue b/Sense/ui/src/views/sense/admission/Admission.vue new file mode 100644 index 0000000..4d42e85 --- /dev/null +++ b/Sense/ui/src/views/sense/admission/Admission.vue @@ -0,0 +1,52 @@ + + + + -- 2.34.1 From 398534aabe18236a59c6023c1dd65c3756fc6d7d Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 17:21:14 +0800 Subject: [PATCH 07/17] feat: add MediaMTX process and reconciliation control (#25) --- .../app/sense/adapters/mediamtx/client.go | 92 ++++++++++++ .../sense/adapters/mediamtx/client_test.go | 37 +++++ .../app/sense/adapters/mediamtx/supervisor.go | 113 ++++++++++++++ Sense/server/app/sense/admission/http.go | 1 + .../app/sense/admission/profile_port.go | 24 +++ Sense/server/app/sense/media/http.go | 64 ++++++++ Sense/server/app/sense/media/migration.go | 3 + Sense/server/app/sense/media/service.go | 140 ++++++++++++++++++ Sense/server/app/sense/media/service_test.go | 110 ++++++++++++++ Sense/server/app/sense/media/store.go | 78 ++++++++++ Sense/server/app/sense/media/types.go | 16 ++ Sense/server/cmd/sense/modules_media.go | 30 ++++ .../sense_media/2026081203_media.sql | 2 + Sense/testdata/mediamtx/path-status.json | 1 + Sense/tests/media/README.md | 6 + Sense/ui/src/api/sense/media/index.js | 7 + Sense/ui/src/router/modules/sense-media.js | 3 + Sense/ui/src/views/sense/media/Media.vue | 38 +++++ 18 files changed, 765 insertions(+) create mode 100644 Sense/server/app/sense/adapters/mediamtx/client.go create mode 100644 Sense/server/app/sense/adapters/mediamtx/client_test.go create mode 100644 Sense/server/app/sense/adapters/mediamtx/supervisor.go create mode 100644 Sense/server/app/sense/admission/profile_port.go create mode 100644 Sense/server/app/sense/media/http.go create mode 100644 Sense/server/app/sense/media/migration.go create mode 100644 Sense/server/app/sense/media/service.go create mode 100644 Sense/server/app/sense/media/service_test.go create mode 100644 Sense/server/app/sense/media/store.go create mode 100644 Sense/server/app/sense/media/types.go create mode 100644 Sense/server/cmd/sense/modules_media.go create mode 100644 Sense/server/database/migrations/sense_media/2026081203_media.sql create mode 100644 Sense/testdata/mediamtx/path-status.json create mode 100644 Sense/tests/media/README.md create mode 100644 Sense/ui/src/api/sense/media/index.js create mode 100644 Sense/ui/src/router/modules/sense-media.js create mode 100644 Sense/ui/src/views/sense/media/Media.vue diff --git a/Sense/server/app/sense/adapters/mediamtx/client.go b/Sense/server/app/sense/adapters/mediamtx/client.go new file mode 100644 index 0000000..c139c97 --- /dev/null +++ b/Sense/server/app/sense/adapters/mediamtx/client.go @@ -0,0 +1,92 @@ +package mediamtx + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +type Source struct { + Path string + URI string + Username string + Password string +} +type PathStatus struct { + Name string `json:"name"` + Ready bool `json:"ready"` + Readers int `json:"readers"` + BytesReceived int64 `json:"bytes_received"` +} +type Controller interface { + Apply(context.Context, Source) error + Status(context.Context, string) (PathStatus, error) +} +type HTTPController struct { + base string + client *http.Client +} + +func NewHTTPController(base string) *HTTPController { + return &HTTPController{base: strings.TrimRight(base, "/"), client: &http.Client{Timeout: 5 * time.Second}} +} +func (c *HTTPController) Apply(ctx context.Context, source Source) error { + parsed, err := url.Parse(source.URI) + if err != nil || parsed.User != nil { + return fmt.Errorf("invalid credential-free media source") + } + if source.Username != "" { + parsed.User = url.UserPassword(source.Username, source.Password) + } + payload := map[string]any{"source": parsed.String(), "sourceOnDemand": true, "rtspTransport": "tcp"} + data, _ := json.Marshal(payload) + endpoint := c.base + "/v3/config/paths/replace/" + url.PathEscape(source.Path) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + res, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("mediamtx control unavailable: %w", err) + } + defer res.Body.Close() + io.Copy(io.Discard, io.LimitReader(res.Body, 1<<20)) + if res.StatusCode < 200 || res.StatusCode >= 300 { + return fmt.Errorf("mediamtx apply returned %d", res.StatusCode) + } + return nil +} +func (c *HTTPController) Status(ctx context.Context, path string) (PathStatus, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+"/v3/paths/get/"+url.PathEscape(path), nil) + if err != nil { + return PathStatus{}, err + } + res, err := c.client.Do(req) + if err != nil { + return PathStatus{}, err + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return PathStatus{Name: path}, nil + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + return PathStatus{}, fmt.Errorf("mediamtx status returned %d", res.StatusCode) + } + var raw struct { + Name string `json:"name"` + Ready bool `json:"ready"` + BytesReceived int64 `json:"bytesReceived"` + Readers []any `json:"readers"` + } + if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&raw); err != nil { + return PathStatus{}, err + } + return PathStatus{Name: raw.Name, Ready: raw.Ready, Readers: len(raw.Readers), BytesReceived: raw.BytesReceived}, nil +} diff --git a/Sense/server/app/sense/adapters/mediamtx/client_test.go b/Sense/server/app/sense/adapters/mediamtx/client_test.go new file mode 100644 index 0000000..ce9ab6e --- /dev/null +++ b/Sense/server/app/sense/adapters/mediamtx/client_test.go @@ -0,0 +1,37 @@ +package mediamtx + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestApplyBuildsCredentialSourceOnlyInTransientBody(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.String(), "password") { + t.Fatal("credential leaked in control URL") + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + client := NewHTTPController(server.URL) + if err := client.Apply(context.Background(), Source{Path: "sense_test", URI: "rtsp://camera.invalid/main", Username: "fixture-user", Password: "fixture-password"}); err != nil { + t.Fatal(err) + } + if body["source"] != "rtsp://fixture-user:fixture-password@camera.invalid/main" { + t.Fatalf("body=%#v", body) + } +} +func TestApplyRejectsCredentialURI(t *testing.T) { + client := NewHTTPController("http://127.0.0.1") + if err := client.Apply(context.Background(), Source{Path: "x", URI: "rtsp://user:pass@camera.invalid/main"}); err == nil { + t.Fatal("credential URI accepted") + } +} diff --git a/Sense/server/app/sense/adapters/mediamtx/supervisor.go b/Sense/server/app/sense/adapters/mediamtx/supervisor.go new file mode 100644 index 0000000..8c8b92c --- /dev/null +++ b/Sense/server/app/sense/adapters/mediamtx/supervisor.go @@ -0,0 +1,113 @@ +package mediamtx + +import ( + "context" + "fmt" + "os" + "os/exec" + "sync" + "time" +) + +type ProcessState struct { + Running bool `json:"running"` + PID int `json:"pid,omitempty"` + Restarts int `json:"restarts"` + LastExit string `json:"last_exit,omitempty"` +} +type Process interface { + Start(context.Context) error + Stop(context.Context) error + State() ProcessState +} +type Supervisor struct { + binary string + config string + maxRestarts int + mu sync.Mutex + command *exec.Cmd + state ProcessState +} + +func NewSupervisor(binary, config string, maxRestarts int) *Supervisor { + if maxRestarts < 0 { + maxRestarts = 0 + } + return &Supervisor{binary: binary, config: config, maxRestarts: maxRestarts} +} +func (s *Supervisor) Start(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.state.Running { + return nil + } + if s.binary == "" { + return fmt.Errorf("SENSE_MEDIAMTX_BINARY is not configured") + } + if s.state.LastExit != "" { + if s.state.Restarts >= s.maxRestarts { + return fmt.Errorf("MediaMTX restart limit reached") + } + s.state.Restarts++ + } + arguments := []string{} + if s.config != "" { + arguments = append(arguments, s.config) + } + command := exec.CommandContext(context.Background(), s.binary, arguments...) + if err := command.Start(); err != nil { + return fmt.Errorf("start MediaMTX: %w", err) + } + s.command = command + s.state.Running = true + s.state.PID = command.Process.Pid + s.state.LastExit = "" + go s.wait(command) + return nil +} +func (s *Supervisor) wait(command *exec.Cmd) { + err := command.Wait() + s.mu.Lock() + defer s.mu.Unlock() + if s.command != command { + return + } + s.state.Running = false + s.state.PID = 0 + if err != nil { + s.state.LastExit = err.Error() + } else { + s.state.LastExit = "exited" + } +} +func (s *Supervisor) Stop(ctx context.Context) error { + s.mu.Lock() + command := s.command + if command == nil || !s.state.Running { + s.mu.Unlock() + return nil + } + process := command.Process + s.mu.Unlock() + if err := process.Signal(os.Interrupt); err != nil { + if killErr := process.Kill(); killErr != nil { + return fmt.Errorf("stop owned MediaMTX process: %w", killErr) + } + } + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + s.mu.Lock() + running := s.state.Running + s.mu.Unlock() + if !running { + return nil + } + } + } +} +func (s *Supervisor) State() ProcessState { s.mu.Lock(); defer s.mu.Unlock(); return s.state } diff --git a/Sense/server/app/sense/admission/http.go b/Sense/server/app/sense/admission/http.go index 2dca49b..8410e23 100644 --- a/Sense/server/app/sense/admission/http.go +++ b/Sense/server/app/sense/admission/http.go @@ -10,6 +10,7 @@ type Module struct{ service *Service } func NewModule(service *Service) *Module { return &Module{service: service} } func (m *Module) Register(app *platform.App) { + setCurrentService(m.service) app.Handle("GET /api/v1/admission/discover", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.discover))) app.Handle("POST /api/v1/admission/probe", identity.Require(identity.PermissionDeviceWrite, http.HandlerFunc(m.probe))) app.Handle("GET /api/v1/admission/devices/{id}", identity.Require(identity.PermissionDeviceRead, http.HandlerFunc(m.get))) diff --git a/Sense/server/app/sense/admission/profile_port.go b/Sense/server/app/sense/admission/profile_port.go new file mode 100644 index 0000000..ecf7087 --- /dev/null +++ b/Sense/server/app/sense/admission/profile_port.go @@ -0,0 +1,24 @@ +package admission + +import "fmt" + +var currentService *Service + +func setCurrentService(service *Service) { currentService = service } + +// VerifiedProfile is a Sense-internal projection, not a cross-product contract. +func VerifiedProfile(deviceID, token string) (Profile, error) { + if currentService == nil { + return Profile{}, fmt.Errorf("admission service is not ready") + } + result, ok := currentService.Get(deviceID) + if !ok { + return Profile{}, fmt.Errorf("device admission has not been verified") + } + for _, profile := range result.Profiles { + if profile.Token == token && profile.Verification.Status == "ready" { + return profile, nil + } + } + return Profile{}, fmt.Errorf("verified profile not found") +} diff --git a/Sense/server/app/sense/media/http.go b/Sense/server/app/sense/media/http.go new file mode 100644 index 0000000..121a73f --- /dev/null +++ b/Sense/server/app/sense/media/http.go @@ -0,0 +1,64 @@ +package media + +import ( + "net/http" + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +type Module struct{ service *Service } + +func NewModule(service *Service) *Module { return &Module{service: service} } +func (m *Module) Register(app *platform.App) { + app.Handle("GET /api/v1/media/routes", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.list))) + app.Handle("POST /api/v1/media/routes", identity.Require(identity.PermissionMediaWrite, http.HandlerFunc(m.configure))) + app.Handle("POST /api/v1/media/routes/{id}/reconcile", identity.Require(identity.PermissionMediaWrite, http.HandlerFunc(m.reconcile))) + app.Handle("POST /api/v1/media/routes/{id}/stop", identity.Require(identity.PermissionMediaWrite, http.HandlerFunc(m.stop))) + app.Handle("GET /api/v1/media/process", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.process))) +} +func (m *Module) list(w http.ResponseWriter, r *http.Request) { + items, err := m.service.List(r.Context()) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, map[string]any{"items": items, "total": len(items)}) +} +func (m *Module) configure(w http.ResponseWriter, r *http.Request) { + var req struct { + DeviceID string `json:"device_id"` + ProfileToken string `json:"profile_token"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Configure(r.Context(), actor, req.DeviceID, req.ProfileToken) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "media_invalid", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusCreated, item) +} +func (m *Module) reconcile(w http.ResponseWriter, r *http.Request) { + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Reconcile(r.Context(), actor, r.PathValue("id")) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "media_reconcile_failed", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func (m *Module) stop(w http.ResponseWriter, r *http.Request) { + actor, _ := identity.PrincipalFromContext(r.Context()) + item, err := m.service.Stop(r.Context(), actor, r.PathValue("id")) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, item) +} +func (m *Module) process(w http.ResponseWriter, _ *http.Request) { + platform.WriteJSON(w, http.StatusOK, m.service.ProcessState()) +} diff --git a/Sense/server/app/sense/media/migration.go b/Sense/server/app/sense/media/migration.go new file mode 100644 index 0000000..9e4008b --- /dev/null +++ b/Sense/server/app/sense/media/migration.go @@ -0,0 +1,3 @@ +package media + +const MigrationSQL = `CREATE TABLE IF NOT EXISTS sense_media_routes(id TEXT PRIMARY KEY,device_id TEXT NOT NULL,profile_token TEXT NOT NULL,path TEXT NOT NULL UNIQUE,desired TEXT NOT NULL,actual TEXT NOT NULL,readers INTEGER NOT NULL DEFAULT 0,detail TEXT NOT NULL DEFAULT '',version BIGINT NOT NULL,updated_at TIMESTAMPTZ NOT NULL);` diff --git a/Sense/server/app/sense/media/service.go b/Sense/server/app/sense/media/service.go new file mode 100644 index 0000000..8197930 --- /dev/null +++ b/Sense/server/app/sense/media/service.go @@ -0,0 +1,140 @@ +package media + +import ( + "context" + "strings" + "time" + + "yovision.local/sense/app/sense/adapters/mediamtx" + "yovision.local/sense/app/sense/admission" + "yovision.local/sense/app/sense/device" + "yovision.local/sense/app/sense/identity" +) + +type Service struct { + store Store + process mediamtx.Process + controller mediamtx.Controller + profile func(string, string) (admission.Profile, error) + credential func(context.Context, string) (device.Credential, error) + now func() time.Time +} + +func NewService(store Store, process mediamtx.Process, controller mediamtx.Controller) *Service { + return &Service{store: store, process: process, controller: controller, profile: admission.VerifiedProfile, credential: device.ReadCredential, now: time.Now} +} +func (s *Service) Configure(ctx context.Context, actor identity.Principal, deviceID, profileToken string) (Route, error) { + if _, err := s.profile(deviceID, profileToken); err != nil { + return Route{}, err + } + id := deviceID + ":" + profileToken + path := "sense_" + safe(deviceID) + "_" + safe(profileToken) + now := s.now().UTC() + route := Route{ID: id, DeviceID: deviceID, ProfileToken: profileToken, Path: path, Desired: "running", Actual: "pending", Detail: "等待状态对账", Version: 1, UpdatedAt: now} + if previous, err := s.store.Get(ctx, id); err == nil { + route.Version = previous.Version + 1 + } + if err := s.store.Save(ctx, route); err != nil { + return Route{}, err + } + identity.RecordAudit(ctx, actor.UserID, "media.configure", id, "success", map[string]any{"path": path}) + return route, nil +} +func (s *Service) Reconcile(ctx context.Context, actor identity.Principal, id string) (Route, error) { + route, err := s.store.Get(ctx, id) + if err != nil { + return Route{}, err + } + if route.Desired == "stopped" { + route.Actual = "stopped" + route.Detail = "已停止" + route.Readers = 0 + route.Version++ + route.UpdatedAt = s.now().UTC() + return route, s.store.Save(ctx, route) + } + if err := s.process.Start(ctx); err != nil { + route.Actual = "process_failed" + route.Detail = err.Error() + route.Version++ + route.UpdatedAt = s.now().UTC() + _ = s.store.Save(ctx, route) + return route, nil + } + profile, err := s.profile(route.DeviceID, route.ProfileToken) + if err != nil { + return Route{}, err + } + credential, err := s.credential(ctx, route.DeviceID) + if err != nil { + return Route{}, err + } + source := mediamtx.Source{Path: route.Path, URI: profile.StreamURI, Username: credential.Username, Password: credential.Password} + if err := s.controller.Apply(ctx, source); err != nil { + route.Actual = "apply_failed" + route.Detail = err.Error() + route.Version++ + route.UpdatedAt = s.now().UTC() + _ = s.store.Save(ctx, route) + return route, nil + } + status, err := s.controller.Status(ctx, route.Path) + if err != nil { + route.Actual = "unconverged" + route.Detail = "尚未取得媒体状态" + } else if status.Ready { + route.Actual = "ready" + route.Detail = "上游拉流正常" + route.Readers = status.Readers + } else { + route.Actual = "waiting" + route.Detail = "等待上游拉流" + route.Readers = status.Readers + } + route.Version++ + route.UpdatedAt = s.now().UTC() + if err := s.store.Save(ctx, route); err != nil { + return Route{}, err + } + identity.RecordAudit(ctx, actor.UserID, "media.reconcile", id, "success", map[string]any{"actual": route.Actual}) + return route, nil +} +func (s *Service) Stop(ctx context.Context, actor identity.Principal, id string) (Route, error) { + route, err := s.store.Get(ctx, id) + if err != nil { + return Route{}, err + } + route.Desired = "stopped" + route.Actual = "stopped" + route.Readers = 0 + route.Detail = "已停止" + route.Version++ + route.UpdatedAt = s.now().UTC() + if err := s.store.Save(ctx, route); err != nil { + return Route{}, err + } + items, _ := s.store.List(ctx) + allStopped := true + for _, item := range items { + if item.ID != id && item.Desired == "running" { + allStopped = false + } + } + if allStopped { + stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + _ = s.process.Stop(stopCtx) + } + identity.RecordAudit(ctx, actor.UserID, "media.stop", id, "success", nil) + return route, nil +} +func (s *Service) List(ctx context.Context) ([]Route, error) { return s.store.List(ctx) } +func (s *Service) ProcessState() mediamtx.ProcessState { return s.process.State() } +func safe(value string) string { + return strings.Map(func(r rune) rune { + if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-' { + return r + } + return '_' + }, value) +} diff --git a/Sense/server/app/sense/media/service_test.go b/Sense/server/app/sense/media/service_test.go new file mode 100644 index 0000000..5f536d3 --- /dev/null +++ b/Sense/server/app/sense/media/service_test.go @@ -0,0 +1,110 @@ +package media + +import ( + "context" + "errors" + "testing" + + "yovision.local/sense/app/sense/adapters/mediamtx" + "yovision.local/sense/app/sense/admission" + "yovision.local/sense/app/sense/device" + "yovision.local/sense/app/sense/identity" +) + +type fakeProcess struct { + state mediamtx.ProcessState + startErr error + stopped bool +} + +func (p *fakeProcess) Start(context.Context) error { + if p.startErr != nil { + return p.startErr + } + p.state.Running = true + p.state.PID = 123 + return nil +} +func (p *fakeProcess) Stop(context.Context) error { + p.stopped = true + p.state.Running = false + p.state.PID = 0 + return nil +} +func (p *fakeProcess) State() mediamtx.ProcessState { return p.state } + +type fakeController struct { + applyErr error + status mediamtx.PathStatus +} + +func (f fakeController) Apply(context.Context, mediamtx.Source) error { return f.applyErr } +func (f fakeController) Status(context.Context, string) (mediamtx.PathStatus, error) { + if f.status.Name == "error" { + return mediamtx.PathStatus{}, errors.New("timeout") + } + if !f.status.Ready { + return mediamtx.PathStatus{Ready: true, Readers: 2}, nil + } + return f.status, nil +} + +func TestStoreAndProcessBoundary(t *testing.T) { + store := NewMemoryStore() + process := &fakeProcess{} + service := NewService(store, process, fakeController{}) + route := Route{ID: "dev:main", DeviceID: "dev", ProfileToken: "main", Path: "sense_dev_main", Desired: "running", Actual: "pending", Version: 1} + if err := store.Save(context.Background(), route); err != nil { + t.Fatal(err) + } + items, err := service.List(context.Background()) + if err != nil || len(items) != 1 { + t.Fatalf("items=%#v err=%v", items, err) + } + if process.State().Running { + t.Fatal("process started without explicit reconcile") + } +} + +func preparedService(t *testing.T, process *fakeProcess, controller fakeController) (*Service, Route) { + t.Helper() + service := NewService(NewMemoryStore(), process, controller) + service.profile = func(string, string) (admission.Profile, error) { + return admission.Profile{StreamURI: "rtsp://camera.invalid/main"}, nil + } + service.credential = func(context.Context, string) (device.Credential, error) { + return device.Credential{Username: "fixture", Password: "fixture"}, nil + } + route, err := service.Configure(context.Background(), identity.Principal{}, "device", "main") + if err != nil { + t.Fatal(err) + } + return service, route +} + +func TestReconcileReadyAndStopOwnedProcess(t *testing.T) { + process := &fakeProcess{} + service, route := preparedService(t, process, fakeController{status: mediamtx.PathStatus{Ready: true, Readers: 2}}) + result, err := service.Reconcile(context.Background(), identity.Principal{}, route.ID) + if err != nil || result.Actual != "ready" || result.Readers != 2 { + t.Fatalf("result=%#v err=%v", result, err) + } + if _, err := service.Stop(context.Background(), identity.Principal{}, route.ID); err != nil || !process.stopped { + t.Fatalf("stop err=%v stopped=%v", err, process.stopped) + } +} + +func TestReconcileRecordsFailureStates(t *testing.T) { + process := &fakeProcess{startErr: errors.New("restart limit reached")} + service, route := preparedService(t, process, fakeController{}) + result, err := service.Reconcile(context.Background(), identity.Principal{}, route.ID) + if err != nil || result.Actual != "process_failed" { + t.Fatalf("result=%#v err=%v", result, err) + } + process.startErr = nil + service.controller = fakeController{applyErr: errors.New("control timeout")} + result, err = service.Reconcile(context.Background(), identity.Principal{}, route.ID) + if err != nil || result.Actual != "apply_failed" { + t.Fatalf("result=%#v err=%v", result, err) + } +} diff --git a/Sense/server/app/sense/media/store.go b/Sense/server/app/sense/media/store.go new file mode 100644 index 0000000..9adc872 --- /dev/null +++ b/Sense/server/app/sense/media/store.go @@ -0,0 +1,78 @@ +package media + +import ( + "context" + "database/sql" + "errors" + "sync" +) + +var ErrNotFound = errors.New("media route not found") + +type Store interface { + Save(context.Context, Route) error + Get(context.Context, string) (Route, error) + List(context.Context) ([]Route, error) +} +type MemoryStore struct { + mu sync.RWMutex + items map[string]Route +} + +func NewMemoryStore() *MemoryStore { return &MemoryStore{items: map[string]Route{}} } +func (s *MemoryStore) Save(_ context.Context, item Route) error { + s.mu.Lock() + defer s.mu.Unlock() + s.items[item.ID] = item + return nil +} +func (s *MemoryStore) Get(_ context.Context, id string) (Route, error) { + s.mu.RLock() + defer s.mu.RUnlock() + item, ok := s.items[id] + if !ok { + return Route{}, ErrNotFound + } + return item, nil +} +func (s *MemoryStore) List(_ context.Context) ([]Route, error) { + s.mu.RLock() + defer s.mu.RUnlock() + items := make([]Route, 0, len(s.items)) + for _, item := range s.items { + items = append(items, item) + } + return items, nil +} + +type PostgresStore struct{ database *sql.DB } + +func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{database: db} } +func (s *PostgresStore) Save(ctx context.Context, item Route) error { + _, err := s.database.ExecContext(ctx, `INSERT INTO sense_media_routes(id,device_id,profile_token,path,desired,actual,readers,detail,version,updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(id) DO UPDATE SET desired=EXCLUDED.desired,actual=EXCLUDED.actual,readers=EXCLUDED.readers,detail=EXCLUDED.detail,version=EXCLUDED.version,updated_at=EXCLUDED.updated_at`, item.ID, item.DeviceID, item.ProfileToken, item.Path, item.Desired, item.Actual, item.Readers, item.Detail, item.Version, item.UpdatedAt) + return err +} +func (s *PostgresStore) Get(ctx context.Context, id string) (Route, error) { + var item Route + err := s.database.QueryRowContext(ctx, `SELECT id,device_id,profile_token,path,desired,actual,readers,detail,version,updated_at FROM sense_media_routes WHERE id=$1`, id).Scan(&item.ID, &item.DeviceID, &item.ProfileToken, &item.Path, &item.Desired, &item.Actual, &item.Readers, &item.Detail, &item.Version, &item.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return Route{}, ErrNotFound + } + return item, err +} +func (s *PostgresStore) List(ctx context.Context) ([]Route, error) { + rows, err := s.database.QueryContext(ctx, `SELECT id,device_id,profile_token,path,desired,actual,readers,detail,version,updated_at FROM sense_media_routes ORDER BY updated_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Route + for rows.Next() { + var item Route + if err := rows.Scan(&item.ID, &item.DeviceID, &item.ProfileToken, &item.Path, &item.Desired, &item.Actual, &item.Readers, &item.Detail, &item.Version, &item.UpdatedAt); err != nil { + return nil, err + } + items = append(items, item) + } + return items, rows.Err() +} diff --git a/Sense/server/app/sense/media/types.go b/Sense/server/app/sense/media/types.go new file mode 100644 index 0000000..1bd6a11 --- /dev/null +++ b/Sense/server/app/sense/media/types.go @@ -0,0 +1,16 @@ +package media + +import "time" + +type Route struct { + ID string `json:"id"` + DeviceID string `json:"device_id"` + ProfileToken string `json:"profile_token"` + Path string `json:"path"` + Desired string `json:"desired"` + Actual string `json:"actual"` + Readers int `json:"readers"` + Detail string `json:"detail"` + Version int64 `json:"version"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/Sense/server/cmd/sense/modules_media.go b/Sense/server/cmd/sense/modules_media.go new file mode 100644 index 0000000..da4e7fa --- /dev/null +++ b/Sense/server/cmd/sense/modules_media.go @@ -0,0 +1,30 @@ +package sense + +import ( + "os" + "yovision.local/sense/app/sense/adapters/mediamtx" + "yovision.local/sense/app/sense/media" + "yovision.local/sense/internal/platform" +) + +func init() { + registerModule(func(app *platform.App) error { + var store media.Store + if app.Config().DatabaseMode == platform.DatabaseModeMemory { + store = media.NewMemoryStore() + } else { + store = media.NewPostgresStore(app.Database()) + } + process := mediamtx.NewSupervisor(os.Getenv("SENSE_MEDIAMTX_BINARY"), os.Getenv("SENSE_MEDIAMTX_CONFIG"), 3) + controller := mediamtx.NewHTTPController(valueOr("SENSE_MEDIAMTX_API", "http://127.0.0.1:9997")) + app.RegisterMigration(platform.Migration{Version: 2026081203, Name: "sense_media", SQL: media.MigrationSQL}) + media.NewModule(media.NewService(store, process, controller)).Register(app) + return nil + }) +} +func valueOr(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/Sense/server/database/migrations/sense_media/2026081203_media.sql b/Sense/server/database/migrations/sense_media/2026081203_media.sql new file mode 100644 index 0000000..845a65f --- /dev/null +++ b/Sense/server/database/migrations/sense_media/2026081203_media.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS sense_media_routes(id TEXT PRIMARY KEY,device_id TEXT NOT NULL,profile_token TEXT NOT NULL,path TEXT NOT NULL UNIQUE,desired TEXT NOT NULL,actual TEXT NOT NULL,readers INTEGER NOT NULL DEFAULT 0,detail TEXT NOT NULL DEFAULT '',version BIGINT NOT NULL,updated_at TIMESTAMPTZ NOT NULL); + diff --git a/Sense/testdata/mediamtx/path-status.json b/Sense/testdata/mediamtx/path-status.json new file mode 100644 index 0000000..42a8bd8 --- /dev/null +++ b/Sense/testdata/mediamtx/path-status.json @@ -0,0 +1 @@ +{"name":"sense_fixture_main","ready":true,"bytesReceived":4096,"readers":[{"type":"webrtc"}]} diff --git a/Sense/tests/media/README.md b/Sense/tests/media/README.md new file mode 100644 index 0000000..26e19b7 --- /dev/null +++ b/Sense/tests/media/README.md @@ -0,0 +1,6 @@ +# Media acceptance + +Tests keep MediaMTX behind process and control interfaces. They verify that +the credential-bearing MediaMTX control request is localhost-only and transient, +never returned or persisted by Sense, and no process starts before an explicit reconcile. Real MediaMTX binary, +camera pull, reader counts and crash recovery require the deployment lab. diff --git a/Sense/ui/src/api/sense/media/index.js b/Sense/ui/src/api/sense/media/index.js new file mode 100644 index 0000000..6d0045e --- /dev/null +++ b/Sense/ui/src/api/sense/media/index.js @@ -0,0 +1,7 @@ +import request from '../../../bootstrap/request' +export const listRoutes = () => request.get('/media/routes') +export const configureRoute = (data) => request.post('/media/routes', data) +export const reconcileRoute = (id) => request.post(`/media/routes/${encodeURIComponent(id)}/reconcile`) +export const stopRoute = (id) => request.post(`/media/routes/${encodeURIComponent(id)}/stop`) +export const processState = () => request.get('/media/process') + diff --git a/Sense/ui/src/router/modules/sense-media.js b/Sense/ui/src/router/modules/sense-media.js new file mode 100644 index 0000000..cace760 --- /dev/null +++ b/Sense/ui/src/router/modules/sense-media.js @@ -0,0 +1,3 @@ +import Media from '../../views/sense/media/Media.vue' +export default { path: 'media', name: 'sense-media', component: Media, meta: { title: '视频服务', icon: 'Monitor', order: 30, permission: 'media.read' } } + diff --git a/Sense/ui/src/views/sense/media/Media.vue b/Sense/ui/src/views/sense/media/Media.vue new file mode 100644 index 0000000..1496fb1 --- /dev/null +++ b/Sense/ui/src/views/sense/media/Media.vue @@ -0,0 +1,38 @@ + + + -- 2.34.1 From 8509699fff5749fbbd60d3cec298116270c4e293 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 17:24:47 +0800 Subject: [PATCH 08/17] feat: add protected single-stream Sense live view (#26) --- Sense/server/app/sense/liveview/http.go | 66 +++++++++++ Sense/server/app/sense/liveview/service.go | 112 ++++++++++++++++++ .../server/app/sense/liveview/service_test.go | 46 +++++++ Sense/server/app/sense/media/http.go | 1 + Sense/server/app/sense/media/route_port.go | 25 ++++ Sense/server/cmd/sense/modules_liveview.go | 18 +++ Sense/testdata/liveview/session.json | 1 + Sense/tests/liveview/README.md | 7 ++ Sense/ui/src/api/sense/liveview/index.js | 5 + .../sense/liveview/StreamPlayer.vue | 14 +++ Sense/ui/src/router/modules/sense-liveview.js | 3 + .../ui/src/views/sense/liveview/LiveView.vue | 19 +++ 12 files changed, 317 insertions(+) create mode 100644 Sense/server/app/sense/liveview/http.go create mode 100644 Sense/server/app/sense/liveview/service.go create mode 100644 Sense/server/app/sense/liveview/service_test.go create mode 100644 Sense/server/app/sense/media/route_port.go create mode 100644 Sense/server/cmd/sense/modules_liveview.go create mode 100644 Sense/testdata/liveview/session.json create mode 100644 Sense/tests/liveview/README.md create mode 100644 Sense/ui/src/api/sense/liveview/index.js create mode 100644 Sense/ui/src/components/sense/liveview/StreamPlayer.vue create mode 100644 Sense/ui/src/router/modules/sense-liveview.js create mode 100644 Sense/ui/src/views/sense/liveview/LiveView.vue diff --git a/Sense/server/app/sense/liveview/http.go b/Sense/server/app/sense/liveview/http.go new file mode 100644 index 0000000..1e0c444 --- /dev/null +++ b/Sense/server/app/sense/liveview/http.go @@ -0,0 +1,66 @@ +package liveview + +import ( + "html/template" + "net/http" + "yovision.local/sense/app/sense/identity" + "yovision.local/sense/internal/platform" +) + +type Module struct{ service *Service } + +func NewModule(service *Service) *Module { return &Module{service: service} } +func (m *Module) Register(app *platform.App) { + app.Handle("GET /api/v1/liveview/routes", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.routes))) + app.Handle("POST /api/v1/liveview/sessions", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.create))) + app.Handle("GET /api/v1/liveview/sessions/{id}", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.get))) + app.Handle("GET /api/v1/liveview/sessions/{id}/player", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.player))) +} +func (m *Module) routes(w http.ResponseWriter, r *http.Request) { + items, err := m.service.Routes(r.Context()) + if err != nil { + platform.WriteError(w, err) + return + } + platform.WriteJSON(w, http.StatusOK, map[string]any{"items": items}) +} +func (m *Module) create(w http.ResponseWriter, r *http.Request) { + var req struct { + RouteID string `json:"route_id"` + } + if err := platform.DecodeJSON(r, &req); err != nil { + platform.WriteError(w, err) + return + } + principal, _ := identity.PrincipalFromContext(r.Context()) + session, err := m.service.Create(r.Context(), principal.UserID, req.RouteID) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "liveview_unavailable", Message: err.Error()}) + return + } + platform.WriteJSON(w, http.StatusCreated, session) +} +func (m *Module) get(w http.ResponseWriter, r *http.Request) { + principal, _ := identity.PrincipalFromContext(r.Context()) + session, err := m.service.Get(r.Context(), principal.UserID, r.PathValue("id")) + if err != nil { + platform.WriteError(w, &platform.APIError{Status: http.StatusGone, Code: "session_expired", Message: "播放会话已过期,请重新打开"}) + return + } + platform.WriteJSON(w, http.StatusOK, session) +} + +var playerTemplate = template.Must(template.New("player").Parse(``)) + +func (m *Module) player(w http.ResponseWriter, r *http.Request) { + principal, _ := identity.PrincipalFromContext(r.Context()) + target, err := m.service.EmbedURL(r.Context(), principal.UserID, r.PathValue("id")) + if err != nil { + http.Error(w, "播放会话已过期", http.StatusGone) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-src http: https:; style-src 'unsafe-inline'") + _ = playerTemplate.Execute(w, target) +} diff --git a/Sense/server/app/sense/liveview/service.go b/Sense/server/app/sense/liveview/service.go new file mode 100644 index 0000000..101ee80 --- /dev/null +++ b/Sense/server/app/sense/liveview/service.go @@ -0,0 +1,112 @@ +package liveview + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net/url" + "strings" + "sync" + "time" + + "yovision.local/sense/app/sense/media" +) + +type Session struct { + ID string `json:"id"` + OwnerID string `json:"-"` + RouteID string `json:"route_id"` + DeviceID string `json:"device_id"` + ProfileToken string `json:"profile_token"` + PlayerURL string `json:"player_url"` + Status string `json:"status"` + Detail string `json:"detail"` + ExpiresAt time.Time `json:"expires_at"` +} +type Service struct { + base *url.URL + ttl time.Duration + route func(context.Context, string) (media.Route, error) + routes func(context.Context) ([]media.Route, error) + mu sync.RWMutex + sessions map[string]Session + now func() time.Time +} + +func NewService(rawBase string, ttl time.Duration) (*Service, error) { + parsed, err := url.Parse(strings.TrimRight(rawBase, "/")) + if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil { + return nil, fmt.Errorf("invalid MediaMTX WebRTC base URL") + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return nil, fmt.Errorf("unsupported MediaMTX WebRTC scheme") + } + if ttl <= 0 || ttl > 10*time.Minute { + ttl = 2 * time.Minute + } + return &Service{base: parsed, ttl: ttl, route: media.PlaybackRoute, routes: media.PlaybackRoutes, sessions: map[string]Session{}, now: time.Now}, nil +} +func (s *Service) Routes(ctx context.Context) ([]media.Route, error) { + items, err := s.routes(ctx) + if err != nil { + return nil, err + } + result := items[:0] + for _, item := range items { + if item.Desired == "running" { + result = append(result, item) + } + } + return result, nil +} +func (s *Service) Create(ctx context.Context, owner, routeID string) (Session, error) { + route, err := s.route(ctx, routeID) + if err != nil { + return Session{}, err + } + if route.Desired != "running" { + return Session{}, fmt.Errorf("media route is stopped") + } + id := newID() + session := Session{ID: id, OwnerID: owner, RouteID: route.ID, DeviceID: route.DeviceID, ProfileToken: route.ProfileToken, PlayerURL: "/api/v1/liveview/sessions/" + id + "/player", Status: route.Actual, Detail: route.Detail, ExpiresAt: s.now().UTC().Add(s.ttl)} + s.mu.Lock() + s.sessions[id] = session + s.mu.Unlock() + return session, nil +} +func (s *Service) Get(ctx context.Context, owner, id string) (Session, error) { + s.mu.RLock() + session, ok := s.sessions[id] + s.mu.RUnlock() + if !ok || session.OwnerID != owner || !s.now().Before(session.ExpiresAt) { + return Session{}, fmt.Errorf("playback session expired") + } + route, err := s.route(ctx, session.RouteID) + if err != nil { + return Session{}, err + } + session.Status = route.Actual + session.Detail = route.Detail + return session, nil +} +func (s *Service) EmbedURL(ctx context.Context, owner, id string) (string, error) { + session, err := s.Get(ctx, owner, id) + if err != nil { + return "", err + } + route, err := s.route(ctx, session.RouteID) + if err != nil { + return "", err + } + target := *s.base + target.Path = strings.TrimRight(target.Path, "/") + "/" + route.Path + return target.String(), nil +} +func newID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return "view_" + hex.EncodeToString(b) +} diff --git a/Sense/server/app/sense/liveview/service_test.go b/Sense/server/app/sense/liveview/service_test.go new file mode 100644 index 0000000..7bd13f9 --- /dev/null +++ b/Sense/server/app/sense/liveview/service_test.go @@ -0,0 +1,46 @@ +package liveview + +import ( + "context" + "testing" + "time" + + "yovision.local/sense/app/sense/media" +) + +func TestRejectCredentialBaseURL(t *testing.T) { + if _, err := NewService("http://user:pass@127.0.0.1:8889", time.Minute); err == nil { + t.Fatal("credential base URL accepted") + } +} + +func TestSessionIsOwnerBoundAndExpires(t *testing.T) { + service, err := NewService("http://127.0.0.1:8889", time.Minute) + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 8, 12, 0, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + route := media.Route{ID: "device:main", DeviceID: "device", ProfileToken: "main", Path: "sense_device_main", Desired: "running", Actual: "ready"} + service.route = func(context.Context, string) (media.Route, error) { return route, nil } + session, err := service.Create(context.Background(), "owner-a", route.ID) + if err != nil { + t.Fatal(err) + } + if _, err := service.Get(context.Background(), "owner-b", session.ID); err == nil { + t.Fatal("other owner accessed session") + } + now = now.Add(2 * time.Minute) + if _, err := service.Get(context.Background(), "owner-a", session.ID); err == nil { + t.Fatal("expired session remained active") + } +} +func TestTTLIsBounded(t *testing.T) { + service, err := NewService("http://127.0.0.1:8889", time.Hour) + if err != nil { + t.Fatal(err) + } + if service.ttl != 2*time.Minute { + t.Fatalf("ttl=%v", service.ttl) + } +} diff --git a/Sense/server/app/sense/media/http.go b/Sense/server/app/sense/media/http.go index 121a73f..495668b 100644 --- a/Sense/server/app/sense/media/http.go +++ b/Sense/server/app/sense/media/http.go @@ -10,6 +10,7 @@ type Module struct{ service *Service } func NewModule(service *Service) *Module { return &Module{service: service} } func (m *Module) Register(app *platform.App) { + setActiveService(m.service) app.Handle("GET /api/v1/media/routes", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.list))) app.Handle("POST /api/v1/media/routes", identity.Require(identity.PermissionMediaWrite, http.HandlerFunc(m.configure))) app.Handle("POST /api/v1/media/routes/{id}/reconcile", identity.Require(identity.PermissionMediaWrite, http.HandlerFunc(m.reconcile))) diff --git a/Sense/server/app/sense/media/route_port.go b/Sense/server/app/sense/media/route_port.go new file mode 100644 index 0000000..59596f8 --- /dev/null +++ b/Sense/server/app/sense/media/route_port.go @@ -0,0 +1,25 @@ +package media + +import ( + "context" + "fmt" + "sync/atomic" +) + +var activeService atomic.Pointer[Service] + +func setActiveService(service *Service) { activeService.Store(service) } +func PlaybackRoute(ctx context.Context, id string) (Route, error) { + service := activeService.Load() + if service == nil { + return Route{}, fmt.Errorf("media service is not ready") + } + return service.store.Get(ctx, id) +} +func PlaybackRoutes(ctx context.Context) ([]Route, error) { + service := activeService.Load() + if service == nil { + return nil, fmt.Errorf("media service is not ready") + } + return service.store.List(ctx) +} diff --git a/Sense/server/cmd/sense/modules_liveview.go b/Sense/server/cmd/sense/modules_liveview.go new file mode 100644 index 0000000..34e131d --- /dev/null +++ b/Sense/server/cmd/sense/modules_liveview.go @@ -0,0 +1,18 @@ +package sense + +import ( + "time" + "yovision.local/sense/app/sense/liveview" + "yovision.local/sense/internal/platform" +) + +func init() { + registerModule(func(app *platform.App) error { + service, err := liveview.NewService(valueOr("SENSE_MEDIAMTX_WEBRTC_BASE", "http://127.0.0.1:8889"), 2*time.Minute) + if err != nil { + return err + } + liveview.NewModule(service).Register(app) + return nil + }) +} diff --git a/Sense/testdata/liveview/session.json b/Sense/testdata/liveview/session.json new file mode 100644 index 0000000..6050878 --- /dev/null +++ b/Sense/testdata/liveview/session.json @@ -0,0 +1 @@ +{"route_id":"fixture-device:main","status":"ready","expires_in_seconds":120} diff --git a/Sense/tests/liveview/README.md b/Sense/tests/liveview/README.md new file mode 100644 index 0000000..971c9f1 --- /dev/null +++ b/Sense/tests/liveview/README.md @@ -0,0 +1,7 @@ +# Live-view acceptance + +Sense creates owner-bound, two-minute playback sessions only for configured +media routes. Camera credentials and long-lived tokens are never placed in the +page URL. Automated tests cover URL validation and bounded TTL; a real browser, +MediaMTX WebRTC page and laboratory stream remain target-environment checks. + diff --git a/Sense/ui/src/api/sense/liveview/index.js b/Sense/ui/src/api/sense/liveview/index.js new file mode 100644 index 0000000..650aa43 --- /dev/null +++ b/Sense/ui/src/api/sense/liveview/index.js @@ -0,0 +1,5 @@ +import request from '../../../bootstrap/request' +export const playbackRoutes = () => request.get('/liveview/routes') +export const createSession = (routeID) => request.post('/liveview/sessions', { route_id: routeID }) +export const getSession = (id) => request.get(`/liveview/sessions/${id}`) + diff --git a/Sense/ui/src/components/sense/liveview/StreamPlayer.vue b/Sense/ui/src/components/sense/liveview/StreamPlayer.vue new file mode 100644 index 0000000..3dd5e38 --- /dev/null +++ b/Sense/ui/src/components/sense/liveview/StreamPlayer.vue @@ -0,0 +1,14 @@ +