From a99d005ba382a0cc34635c20ba45d8c925544732 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Wed, 15 Jul 2026 15:31:48 +0100 Subject: [PATCH 01/14] Add outbound relay (smarthost) admin UI Self-hosted mail sent directly from home/residential IPs is widely rejected by Gmail/Outlook (IP reputation, Spamhaus PBL) and many ISPs block port 25, so direct delivery fails regardless of SPF/DKIM/DMARC. The industry-standard fix, as shipped by mailcow/mailu/docker-mailserver, is to route outbound mail through an authenticated relay (smarthost). Productizes the manual workaround from platform issue #553 so it also survives upgrades: - Persist relay settings (enabled/host/port/user/password) in the [relay] section of user_mail.cfg under $SNAP_COMMON. - Postfix main.cf template renders a relayhost + SASL block when relay is enabled; RegenerateConfigs writes and postmaps sasl_passwd, so the relay is reapplied on every install/refresh. - New webui daemon (cmd/webui, rest.Server) serves GET/POST /api/relay over a unix socket, reusing the installer to apply changes and restart postfix via the platform. - nginx serves a Vue 3 + Element Plus admin page at /admin/ and proxies /api/ to the webui socket, alongside Roundcube webmail (mailcow-style split of webmail vs server admin). - Playwright spec covers the relay form end to end. Also bump Roundcube 1.6.1 -> 1.6.15 (LTS security releases). --- .drone.jsonnet | 10 +- bin/service.webui.sh | 3 + cli/build.sh | 1 + cli/cmd/webui/main.go | 34 + cli/installer/installer.go | 15 + cli/installer/relay.go | 73 + cli/rest/webui.go | 96 ++ config/nginx/nginx.conf | 10 + config/postfix/main.cf | 8 + meta/snap.yaml | 10 + test/e2e/specs/03-relay.spec.ts | 21 + www/.gitignore | 2 + www/build.sh | 10 + www/index.html | 12 + www/package-lock.json | 2585 ++++++++++++++++++++++++++++++ www/package.json | 22 + www/src/App.vue | 21 + www/src/components/RelayForm.vue | 106 ++ www/src/main.js | 6 + www/vite.config.js | 22 + 20 files changed, 3066 insertions(+), 1 deletion(-) create mode 100755 bin/service.webui.sh create mode 100644 cli/cmd/webui/main.go create mode 100644 cli/installer/relay.go create mode 100644 cli/rest/webui.go create mode 100644 test/e2e/specs/03-relay.spec.ts create mode 100644 www/.gitignore create mode 100644 www/build.sh create mode 100644 www/index.html create mode 100644 www/package-lock.json create mode 100644 www/package.json create mode 100644 www/src/App.vue create mode 100644 www/src/components/RelayForm.vue create mode 100644 www/src/main.js create mode 100644 www/vite.config.js diff --git a/.drone.jsonnet b/.drone.jsonnet index 420e1e6..00ad695 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,10 +1,11 @@ local name = 'mail'; -local roundcube = '1.6.1'; +local roundcube = '1.6.15'; local dovecot = '2.3.16'; local nginx = '1.24.0'; local postfix = '3.4.28'; local python = '3.12-slim-bookworm'; local golang = '1.24.0'; +local node = '20'; local debian = 'bookworm-slim'; local bullseye = 'bullseye-slim'; local php = 'php:8.0.30-fpm-bullseye'; @@ -142,6 +143,13 @@ local build(arch, test_ui) = [{ './cli/build.sh', ], }, + { + name: 'www', + image: 'node:' + node, + commands: [ + './www/build.sh', + ], + }, { name: 'package', image: 'debian:' + debian, diff --git a/bin/service.webui.sh b/bin/service.webui.sh new file mode 100755 index 0000000..45c4361 --- /dev/null +++ b/bin/service.webui.sh @@ -0,0 +1,3 @@ +#!/bin/bash +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +exec $DIR/webui unix ${SNAP_COMMON}/webui.socket diff --git a/cli/build.sh b/cli/build.sh index c072ac1..0e0de4f 100755 --- a/cli/build.sh +++ b/cli/build.sh @@ -11,3 +11,4 @@ CGO_ENABLED=0 go build -buildvcs=false -o "$OUT_HOOKS/configure" ./cmd/config CGO_ENABLED=0 go build -buildvcs=false -o "$OUT_HOOKS/pre-refresh" ./cmd/pre-refresh CGO_ENABLED=0 go build -buildvcs=false -o "$OUT_HOOKS/post-refresh" ./cmd/post-refresh CGO_ENABLED=0 go build -buildvcs=false -o "$OUT_BIN/cli" ./cmd/cli +CGO_ENABLED=0 go build -buildvcs=false -o "$OUT_BIN/webui" ./cmd/webui diff --git a/cli/cmd/webui/main.go b/cli/cmd/webui/main.go new file mode 100644 index 0000000..9372b90 --- /dev/null +++ b/cli/cmd/webui/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "hooks/installer" + "hooks/rest" + "os" + + "github.com/spf13/cobra" + "github.com/syncloud/golib/log" +) + +func main() { + logger := log.Logger() + + var rootCmd = &cobra.Command{ + Use: "webui", + SilenceUsage: true, + } + + rootCmd.AddCommand(&cobra.Command{ + Use: "unix [socket]", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return rest.NewServer(installer.New(logger), args[0], logger).Start() + }, + }) + + err := rootCmd.Execute() + if err != nil { + fmt.Print(err) + os.Exit(1) + } +} diff --git a/cli/installer/installer.go b/cli/installer/installer.go index 4a06d0c..143cbcb 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -23,6 +23,7 @@ const ( DbUser = "mail" DbPass = "mail" SystemdDovecot = "mail.dovecot" + SystemdPostfix = "mail.postfix" ) var dkimKeyPattern = regexp.MustCompile(`(?s).*p=(.*?)".*`) @@ -39,6 +40,9 @@ type Variables struct { DeviceDomainName string AppDomainName string Timezone string + Relay bool + RelayHost string + RelayPort int } type Installer struct { @@ -100,6 +104,10 @@ func (i *Installer) RegenerateConfigs() error { if err != nil { return err } + relay, err := i.GetRelay() + if err != nil { + return err + } variables := Variables{ AppDir: i.appDir, @@ -113,6 +121,9 @@ func (i *Installer) RegenerateConfigs() error { DeviceDomainName: deviceDomainName, AppDomainName: appDomainName, Timezone: tz, + Relay: relay.Enabled, + RelayHost: relay.Host, + RelayPort: relay.Port, } templatesPath := path.Join(i.appDir, "config") @@ -120,6 +131,10 @@ func (i *Installer) RegenerateConfigs() error { return err } + if err := i.applySasl(relay); err != nil { + return err + } + return linux.Chown(i.configPath, UserName) } diff --git a/cli/installer/relay.go b/cli/installer/relay.go new file mode 100644 index 0000000..b46eb5f --- /dev/null +++ b/cli/installer/relay.go @@ -0,0 +1,73 @@ +package installer + +import ( + "fmt" + "os" + "path" + "strconv" + + "gopkg.in/ini.v1" +) + +const DefaultRelayPort = 587 + +type RelayConfig struct { + Enabled bool `json:"enabled"` + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Password string `json:"password"` +} + +func (i *Installer) GetRelay() (RelayConfig, error) { + cfg, err := ini.LooseLoad(i.userConfigFile) + if err != nil { + return RelayConfig{}, err + } + section := cfg.Section("relay") + return RelayConfig{ + Enabled: section.Key("enabled").MustBool(false), + Host: section.Key("host").String(), + Port: section.Key("port").MustInt(DefaultRelayPort), + User: section.Key("user").String(), + Password: section.Key("password").String(), + }, nil +} + +func (i *Installer) SetRelay(relay RelayConfig) error { + cfg, err := ini.LooseLoad(i.userConfigFile) + if err != nil { + return err + } + section := cfg.Section("relay") + section.Key("enabled").SetValue(strconv.FormatBool(relay.Enabled)) + section.Key("host").SetValue(relay.Host) + section.Key("port").SetValue(strconv.Itoa(relay.Port)) + section.Key("user").SetValue(relay.User) + section.Key("password").SetValue(relay.Password) + return cfg.SaveTo(i.userConfigFile) +} + +func (i *Installer) ApplyRelay() error { + if err := i.RegenerateConfigs(); err != nil { + return err + } + return i.platformClient.RestartService(SystemdPostfix) +} + +func (i *Installer) applySasl(relay RelayConfig) error { + saslFile := path.Join(i.configPath, "postfix", "sasl_passwd") + if !relay.Enabled { + _ = os.Remove(saslFile) + _ = os.Remove(saslFile + ".db") + return nil + } + line := fmt.Sprintf("[%s]:%d %s:%s\n", relay.Host, relay.Port, relay.User, relay.Password) + if err := os.WriteFile(saslFile, []byte(line), 0600); err != nil { + return err + } + postmap := path.Join(i.appDir, "postfix", "usr", "sbin", "postmap") + postfixConfigDir := path.Join(i.configPath, "postfix") + _, err := i.executor.RunDir(postfixConfigDir, postmap, "-c", postfixConfigDir, "hash:"+saslFile) + return err +} diff --git a/cli/rest/webui.go b/cli/rest/webui.go new file mode 100644 index 0000000..e16ff10 --- /dev/null +++ b/cli/rest/webui.go @@ -0,0 +1,96 @@ +package rest + +import ( + "encoding/json" + "net" + "net/http" + "os" + + "hooks/installer" + + "go.uber.org/zap" +) + +type Server struct { + installer *installer.Installer + socket string + logger *zap.Logger +} + +type response struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +func NewServer(inst *installer.Installer, socket string, logger *zap.Logger) *Server { + return &Server{installer: inst, socket: socket, logger: logger} +} + +func (s *Server) Start() error { + _ = os.Remove(s.socket) + listener, err := net.Listen("unix", s.socket) + if err != nil { + return err + } + if err := os.Chmod(s.socket, 0666); err != nil { + return err + } + mux := http.NewServeMux() + mux.HandleFunc("/api/relay", s.relay) + s.logger.Info("webui started", zap.String("socket", s.socket)) + return http.Serve(listener, mux) +} + +func (s *Server) relay(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + relay, err := s.installer.GetRelay() + if err != nil { + s.fail(w, err) + return + } + relay.Password = "" + s.ok(w, relay) + case http.MethodPost: + var relay installer.RelayConfig + if err := json.NewDecoder(r.Body).Decode(&relay); err != nil { + s.fail(w, err) + return + } + if relay.Password == "" { + current, err := s.installer.GetRelay() + if err != nil { + s.fail(w, err) + return + } + relay.Password = current.Password + } + if err := s.installer.SetRelay(relay); err != nil { + s.fail(w, err) + return + } + if err := s.installer.ApplyRelay(); err != nil { + s.fail(w, err) + return + } + s.ok(w, "ok") + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (s *Server) ok(w http.ResponseWriter, data interface{}) { + s.write(w, http.StatusOK, response{Success: true, Data: data}) +} + +func (s *Server) fail(w http.ResponseWriter, err error) { + s.logger.Error("request failed", zap.Error(err)) + s.write(w, http.StatusInternalServerError, response{Success: false, Message: err.Error()}) +} + +func (s *Server) write(w http.ResponseWriter, status int, body response) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(body) +} diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index d590a48..b386ddf 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -27,6 +27,16 @@ http { index index.php index.html index.htm; client_max_body_size 100M; + location /api/ { + proxy_pass http://unix:{{ .AppDataDir }}/webui.socket:; + } + + location /admin/ { + alias {{ .AppDir }}/www/; + index index.html; + try_files $uri $uri/ /admin/index.html; + } + location / { try_files $uri $uri/ /index.php?q=$uri&$args; } diff --git a/config/postfix/main.cf b/config/postfix/main.cf index 5a800fa..8b2ddbe 100644 --- a/config/postfix/main.cf +++ b/config/postfix/main.cf @@ -330,7 +330,15 @@ mynetworks_style = host # # If you're connected via UUCP, see also the default_transport parameter. # +{{ if .Relay }} +relayhost = [{{ .RelayHost }}]:{{ .RelayPort }} +smtp_sasl_auth_enable = yes +smtp_sasl_security_options = noanonymous +smtp_sasl_password_maps = hash:{{ .AppDataDir }}/config/postfix/sasl_passwd +smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt +{{ else }} relayhost = +{{ end }} #relayhost = [gateway.my.domain] #relayhost = [mailserver.isp.tld] #relayhost = uucphost diff --git a/meta/snap.yaml b/meta/snap.yaml index 1585626..47637df 100644 --- a/meta/snap.yaml +++ b/meta/snap.yaml @@ -46,6 +46,16 @@ apps: restart-condition: always after: [postgresql] before: [nginx] + webui: + user: mail + command: bin/service.webui.sh + daemon: simple + plugs: + - network + - network-bind + restart-condition: always + after: [postgresql] + before: [nginx] nginx: command: bin/service.nginx.sh user: mail diff --git a/test/e2e/specs/03-relay.spec.ts b/test/e2e/specs/03-relay.spec.ts new file mode 100644 index 0000000..35cbbe5 --- /dev/null +++ b/test/e2e/specs/03-relay.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test' +import { shoot } from '../helpers/screenshot' + +test.describe('mail admin relay', () => { + test('configure outbound relay', async ({ page }, testInfo) => { + await page.goto('/admin/') + await expect(page.getByTestId('admin-title')).toBeVisible() + await shoot(page, testInfo, 'admin') + + await page.getByTestId('relay-enabled').click() + await page.getByTestId('relay-host').locator('input').fill('smtp.gmail.com') + await page.getByTestId('relay-port').locator('input').fill('587') + await page.getByTestId('relay-user').locator('input').fill('user@gmail.com') + await page.getByTestId('relay-password').locator('input').fill('app-password') + await shoot(page, testInfo, 'relay-filled') + + await page.getByTestId('relay-save').click() + await expect(page.getByText('Saved')).toBeVisible() + await shoot(page, testInfo, 'relay-saved') + }) +}) diff --git a/www/.gitignore b/www/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/www/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/www/build.sh b/www/build.sh new file mode 100644 index 0000000..01bf117 --- /dev/null +++ b/www/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash -xe +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd "$DIR" + +npm install +npm run build + +OUT=../build/snap/www +mkdir -p "$OUT" +cp -r dist/* "$OUT" diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..4729010 --- /dev/null +++ b/www/index.html @@ -0,0 +1,12 @@ + + + + + + Mail admin + + +
+ + + diff --git a/www/package-lock.json b/www/package-lock.json new file mode 100644 index 0000000..68080d4 --- /dev/null +++ b/www/package-lock.json @@ -0,0 +1,2585 @@ +{ + "name": "mail-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mail-admin", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.0", + "element-plus": "^2.4.1", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "unplugin-auto-import": "^0.17.0", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.3.tgz", + "integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.5" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", + "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.4", + "unimport": "^3.7.2", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", + "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.4", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "minimatch": "^9.0.3", + "resolve": "^1.22.4", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.7.tgz", + "integrity": "sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==", + "license": "MIT" + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/www/package.json b/www/package.json new file mode 100644 index 0000000..8d39fc4 --- /dev/null +++ b/www/package.json @@ -0,0 +1,22 @@ +{ + "name": "mail-admin", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.0", + "element-plus": "^2.4.1", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "unplugin-auto-import": "^0.17.0", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.0.0" + } +} diff --git a/www/src/App.vue b/www/src/App.vue new file mode 100644 index 0000000..4ad1146 --- /dev/null +++ b/www/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/www/src/components/RelayForm.vue b/www/src/components/RelayForm.vue new file mode 100644 index 0000000..1595d7d --- /dev/null +++ b/www/src/components/RelayForm.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/www/src/main.js b/www/src/main.js new file mode 100644 index 0000000..351fa45 --- /dev/null +++ b/www/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' + +createApp(App).use(ElementPlus).mount('#app') diff --git a/www/vite.config.js b/www/vite.config.js new file mode 100644 index 0000000..821b5fd --- /dev/null +++ b/www/vite.config.js @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +export default defineConfig({ + base: '/admin/', + plugins: [ + vue(), + AutoImport({ resolvers: [ElementPlusResolver()] }), + Components({ resolvers: [ElementPlusResolver()] }) + ], + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + } + } + } +}) From 451e31d0cae1704d51ae9ea3da27e0775ca43174 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Wed, 15 Jul 2026 16:37:45 +0100 Subject: [PATCH 02/14] Migrate app state from $SNAP_COMMON to $SNAP_DATA $SNAP_DATA is snapshotted and rolled back transactionally on a failed refresh; $SNAP_COMMON is shared across revisions and never rolls back. The app kept all mutable state (Postgres DB, DKIM keys, maildirs, spool, relay credentials, generated configs) in $SNAP_COMMON, so a rolled-back binary could face newer-format data. Move it all to $SNAP_DATA, keeping only web.socket in $SNAP_COMMON (the Syncloud platform routes app traffic to that hardcoded path). - installer data root common -> current; add AppCommonDir so nginx still binds web.socket in $SNAP_COMMON. - MigrateCommonToData() renames every $SNAP_COMMON entry (except sockets) into $SNAP_DATA once, guarded by a marker; runs at the top of InitConfig so both install and post-refresh migrate existing devices. - Service scripts use $SNAP_DATA (except the web.socket line); php-fpm.conf pid/listen paths templated via AppDataDir. --- bin/service.dovecot.sh | 4 ++-- bin/service.nginx.sh | 2 +- bin/service.opendkim.sh | 2 +- bin/service.php-fpm.sh | 4 ++-- bin/service.postfix.sh | 4 ++-- bin/service.postgresql.sh | 4 ++-- bin/service.webui.sh | 2 +- cli/installer/installer.go | 42 +++++++++++++++++++++++++++++++++++++- config/nginx/nginx.conf | 2 +- config/php/php-fpm.conf | 4 ++-- 10 files changed, 55 insertions(+), 15 deletions(-) diff --git a/bin/service.dovecot.sh b/bin/service.dovecot.sh index d9af03b..8b825f2 100755 --- a/bin/service.dovecot.sh +++ b/bin/service.dovecot.sh @@ -10,8 +10,8 @@ fi export DOVECOT_BINDIR=${DIR}/dovecot/bin case $1 in start) - /bin/rm -rf ${SNAP_COMMON}/dovecot/master.pid - exec $DIR/dovecot/bin/dovecot.sh -F -c ${SNAP_COMMON}/config/dovecot/dovecot.conf + /bin/rm -rf ${SNAP_DATA}/dovecot/master.pid + exec $DIR/dovecot/bin/dovecot.sh -F -c ${SNAP_DATA}/config/dovecot/dovecot.conf ;; *) echo "not valid command" diff --git a/bin/service.nginx.sh b/bin/service.nginx.sh index acb4f08..bc7df92 100755 --- a/bin/service.nginx.sh +++ b/bin/service.nginx.sh @@ -3,4 +3,4 @@ DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) /bin/rm -f ${SNAP_COMMON}/web.socket -exec ${DIR}/nginx/bin/nginx.sh -c ${SNAP_COMMON}/config/nginx/nginx.conf -p ${DIR}/nginx -e stderr +exec ${DIR}/nginx/bin/nginx.sh -c ${SNAP_DATA}/config/nginx/nginx.conf -p ${DIR}/nginx -e stderr diff --git a/bin/service.opendkim.sh b/bin/service.opendkim.sh index bb88ed0..0b06bcc 100755 --- a/bin/service.opendkim.sh +++ b/bin/service.opendkim.sh @@ -9,7 +9,7 @@ fi case $1 in start) - exec $DIR/opendkim/bin/opendkim.sh -x ${SNAP_COMMON}/config/opendkim/opendkim.conf + exec $DIR/opendkim/bin/opendkim.sh -x ${SNAP_DATA}/config/opendkim/opendkim.conf ;; *) echo "not valid command" diff --git a/bin/service.php-fpm.sh b/bin/service.php-fpm.sh index 19bb701..5fc8669 100755 --- a/bin/service.php-fpm.sh +++ b/bin/service.php-fpm.sh @@ -9,10 +9,10 @@ fi case $1 in start) - exec $DIR/php/bin/php-fpm.sh -y ${SNAP_COMMON}/config/php/php-fpm.conf -c ${SNAP_COMMON}/config/php/php.ini + exec $DIR/php/bin/php-fpm.sh -y ${SNAP_DATA}/config/php/php-fpm.conf -c ${SNAP_DATA}/config/php/php.ini ;; post-start) - timeout 5 /bin/bash -c 'until [ -S '${SNAP_COMMON}'/log/php5-fpm.sock ]; do echo "waiting for ${SNAP_COMMON}/log/php5-fpm.sock"; sleep 1; done' + timeout 5 /bin/bash -c 'until [ -S '${SNAP_DATA}'/log/php5-fpm.sock ]; do echo "waiting for ${SNAP_DATA}/log/php5-fpm.sock"; sleep 1; done' ;; *) echo "not valid command" diff --git a/bin/service.postfix.sh b/bin/service.postfix.sh index a2c00ac..3482f8a 100755 --- a/bin/service.postfix.sh +++ b/bin/service.postfix.sh @@ -10,10 +10,10 @@ fi case $1 in start) export SASL_PATH=$DIR/postfix/lib/sasl2 - exec $DIR/postfix/bin/postfix.sh -c ${SNAP_COMMON}/config/postfix start + exec $DIR/postfix/bin/postfix.sh -c ${SNAP_DATA}/config/postfix start ;; stop) - exec $DIR/postfix/bin/postfix.sh -c ${SNAP_COMMON}/config/postfix stop + exec $DIR/postfix/bin/postfix.sh -c ${SNAP_DATA}/config/postfix stop ;; *) echo "not valid command" diff --git a/bin/service.postgresql.sh b/bin/service.postgresql.sh index d135e28..41d5366 100755 --- a/bin/service.postgresql.sh +++ b/bin/service.postgresql.sh @@ -9,10 +9,10 @@ fi case $1 in start) - exec ${DIR}/postgresql/bin/pg_ctl.sh -w -s -D ${SNAP_COMMON}/database start + exec ${DIR}/postgresql/bin/pg_ctl.sh -w -s -D ${SNAP_DATA}/database start ;; stop) - exec ${DIR}/postgresql/bin/pg_ctl.sh -s -D ${SNAP_COMMON}/database stop -m fast + exec ${DIR}/postgresql/bin/pg_ctl.sh -s -D ${SNAP_DATA}/database stop -m fast ;; *) echo "not valid command" diff --git a/bin/service.webui.sh b/bin/service.webui.sh index 45c4361..5e4d368 100755 --- a/bin/service.webui.sh +++ b/bin/service.webui.sh @@ -1,3 +1,3 @@ #!/bin/bash DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -exec $DIR/webui unix ${SNAP_COMMON}/webui.socket +exec $DIR/webui unix ${SNAP_DATA}/webui.socket diff --git a/cli/installer/installer.go b/cli/installer/installer.go index 143cbcb..c09c675 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -31,6 +31,7 @@ var dkimKeyPattern = regexp.MustCompile(`(?s).*p=(.*?)".*`) type Variables struct { AppDir string AppDataDir string + AppCommonDir string DbPsqlPath string DbPsqlPort int DbName string @@ -48,6 +49,7 @@ type Variables struct { type Installer struct { appDir string dataDir string + commonDir string configPath string logDir string opendkimDir string @@ -61,12 +63,14 @@ type Installer struct { func New(logger *zap.Logger) *Installer { appDir := fmt.Sprintf("/snap/%s/current", App) - dataDir := fmt.Sprintf("/var/snap/%s/common", App) + dataDir := fmt.Sprintf("/var/snap/%s/current", App) + commonDir := fmt.Sprintf("/var/snap/%s/common", App) configPath := path.Join(dataDir, "config") executor := NewExecutor(logger) return &Installer{ appDir: appDir, dataDir: dataDir, + commonDir: commonDir, configPath: configPath, logDir: path.Join(dataDir, "log"), opendkimDir: path.Join(dataDir, "opendkim"), @@ -112,6 +116,7 @@ func (i *Installer) RegenerateConfigs() error { variables := Variables{ AppDir: i.appDir, AppDataDir: i.dataDir, + AppCommonDir: i.commonDir, DbPsqlPath: i.database.Dir(), DbPsqlPort: i.database.Port(), DbName: DbName, @@ -138,7 +143,42 @@ func (i *Installer) RegenerateConfigs() error { return linux.Chown(i.configPath, UserName) } +func (i *Installer) MigrateCommonToData() error { + marker := path.Join(i.dataDir, ".migrated_from_common") + if _, err := os.Stat(marker); err == nil { + return nil + } + if err := linux.CreateMissingDirs(i.dataDir); err != nil { + return err + } + entries, err := os.ReadDir(i.commonDir) + if err != nil { + if os.IsNotExist(err) { + return os.WriteFile(marker, []byte{}, 0644) + } + return err + } + for _, entry := range entries { + name := entry.Name() + if name == "web.socket" || strings.HasSuffix(name, ".socket") { + continue + } + dst := path.Join(i.dataDir, name) + if _, err := os.Stat(dst); err == nil { + continue + } + i.logger.Info("migrating to data", zap.String("name", name)) + if err := os.Rename(path.Join(i.commonDir, name), dst); err != nil { + return err + } + } + return os.WriteFile(marker, []byte{}, 0644) +} + func (i *Installer) InitConfig() error { + if err := i.MigrateCommonToData(); err != nil { + return err + } if err := linux.CreateUser("maildrop"); err != nil { return err } diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index b386ddf..6bdae98 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -19,7 +19,7 @@ http { scgi_temp_path {{ .AppDataDir }}/nginx/scgi_temp; server { - listen unix:{{ .AppDataDir }}/web.socket; + listen unix:{{ .AppCommonDir }}/web.socket; server_name localhost; access_log syslog:server=unix:/dev/log ; diff --git a/config/php/php-fpm.conf b/config/php/php-fpm.conf index e602129..fca2ebe 100644 --- a/config/php/php-fpm.conf +++ b/config/php/php-fpm.conf @@ -1,10 +1,10 @@ [global] -pid = /var/snap/mail/common/php-fpm.pid +pid = {{ .AppDataDir }}/php-fpm.pid error_log = syslog syslog.ident = mail.php-fpm [www] -listen = /var/snap/mail/common/log/php5-fpm.sock +listen = {{ .AppDataDir }}/log/php5-fpm.sock pm = dynamic pm.max_children = 10 pm.start_servers = 2 From 77df7e26de7600196cb0a43465a656387f768d2c Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Wed, 15 Jul 2026 19:45:35 +0100 Subject: [PATCH 03/14] Make www/build.sh executable --- www/build.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 www/build.sh diff --git a/www/build.sh b/www/build.sh old mode 100644 new mode 100755 From dcb864f1fc3221cdbf4b0d7a97c5660f8fac36b0 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Wed, 15 Jul 2026 21:00:35 +0100 Subject: [PATCH 04/14] test: point data_dir fixture at $SNAP_DATA after migration --- test/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/conftest.py b/test/conftest.py index 9d826f0..161c169 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,3 +5,7 @@ @pytest.fixture(scope="session") def project_dir(): return join(dirname(__file__), '..') + +@pytest.fixture(scope="session") +def data_dir(snap_data_dir): + return snap_data_dir From a666ebbcfbe47bb5805ce395db59a079d9d4e67d Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 07:33:48 +0100 Subject: [PATCH 05/14] Keep $SNAP_COMMON owned by mail so nginx can bind web.socket After moving app state to $SNAP_DATA, InitConfig only chowned the data dir, leaving $SNAP_COMMON root-owned. The nginx service runs as mail and must create web.socket under $SNAP_COMMON (platform contract), so bind failed with EACCES and nginx crash-looped (platform served 503). Ensure $SNAP_COMMON exists and is owned by mail, as it was before the migration. --- cli/installer/installer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/installer/installer.go b/cli/installer/installer.go index c09c675..e531d08 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -197,6 +197,7 @@ func (i *Installer) InitConfig() error { boxDataDir := path.Join(i.dataDir, "box") if err := linux.CreateMissingDirs( + i.commonDir, path.Join(i.dataDir, "nginx"), path.Join(i.dataDir, "config"), i.logDir, @@ -227,6 +228,9 @@ func (i *Installer) InitConfig() error { if err := linux.Chown(i.dataDir, UserName); err != nil { return err } + if err := linux.Chown(i.commonDir, UserName); err != nil { + return err + } if _, err := i.executor.RunDir("", "chown", "-R", "dovecot:dovecot", boxDataDir); err != nil { return err } From 9efb66c0b0b0e1373485879ee2942de636e01801 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 13:25:00 +0100 Subject: [PATCH 06/14] Fix relay e2e: fill inputs by testid; run webui as root - Element Plus forwards data-testid onto the inner , so target it directly instead of .locator('input') (which searched for an input inside the input and timed out). - The relay save path (ApplyRelay -> RegenerateConfigs + postmap + platform RestartService) needs privilege; run the webui daemon as root like the hooks that already call the platform. The socket is chmod 0666 so nginx (mail) can still connect. --- meta/snap.yaml | 1 - test/e2e/specs/03-relay.spec.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/meta/snap.yaml b/meta/snap.yaml index 47637df..758bc87 100644 --- a/meta/snap.yaml +++ b/meta/snap.yaml @@ -47,7 +47,6 @@ apps: after: [postgresql] before: [nginx] webui: - user: mail command: bin/service.webui.sh daemon: simple plugs: diff --git a/test/e2e/specs/03-relay.spec.ts b/test/e2e/specs/03-relay.spec.ts index 35cbbe5..5152913 100644 --- a/test/e2e/specs/03-relay.spec.ts +++ b/test/e2e/specs/03-relay.spec.ts @@ -8,10 +8,10 @@ test.describe('mail admin relay', () => { await shoot(page, testInfo, 'admin') await page.getByTestId('relay-enabled').click() - await page.getByTestId('relay-host').locator('input').fill('smtp.gmail.com') - await page.getByTestId('relay-port').locator('input').fill('587') - await page.getByTestId('relay-user').locator('input').fill('user@gmail.com') - await page.getByTestId('relay-password').locator('input').fill('app-password') + await page.getByTestId('relay-host').fill('smtp.gmail.com') + await page.getByTestId('relay-port').fill('587') + await page.getByTestId('relay-user').fill('user@gmail.com') + await page.getByTestId('relay-password').fill('app-password') await shoot(page, testInfo, 'relay-filled') await page.getByTestId('relay-save').click() From 94846257b3d79de1e599c0927bc0bf0c35ee7f41 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 13:40:28 +0100 Subject: [PATCH 07/14] Add Admin button to Roundcube taskbar linking to /admin/ Roundcube (the default web UI at /) had no way to reach the new relay admin page. Add a small mailadmin plugin that registers an "Admin" button in the Elastic taskbar (with a gear icon via CSS mask so it matches the skin), linking to /admin/. Packaged into roundcubemail/plugins and enabled in config.inc.php. The e2e now logs into Roundcube and clicks the button to reach the relay form. --- .gitignore | 1 + config/roundcube/config.inc.php | 2 +- package.sh | 1 + .../plugins/mailadmin/localization/en_US.inc | 4 +++ roundcube/plugins/mailadmin/mailadmin.php | 28 +++++++++++++++++++ .../mailadmin/skins/elastic/mailadmin.css | 10 +++++++ test/e2e/specs/03-relay.spec.ts | 11 +++++++- 7 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 roundcube/plugins/mailadmin/localization/en_US.inc create mode 100644 roundcube/plugins/mailadmin/mailadmin.php create mode 100644 roundcube/plugins/mailadmin/skins/elastic/mailadmin.css diff --git a/.gitignore b/.gitignore index 209ce3e..5c5a9d7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ log lib src/version .drone.yml +.amd64watch.py diff --git a/config/roundcube/config.inc.php b/config/roundcube/config.inc.php index e96e39b..1b5ac59 100644 --- a/config/roundcube/config.inc.php +++ b/config/roundcube/config.inc.php @@ -15,7 +15,7 @@ $config['smtp_user'] = '%u'; $config['smtp_pass'] = '%p'; -$config['plugins'] = array(); +$config['plugins'] = array('mailadmin'); $config['imap_host'] = 'tls://localhost'; diff --git a/package.sh b/package.sh index 4ffef31..c36d4bf 100755 --- a/package.sh +++ b/package.sh @@ -22,6 +22,7 @@ cp -r ${DIR}/bin/* ${SNAP_DIR}/bin cp -r ${DIR}/config ${SNAP_DIR} cp -r ${DIR}/meta ${SNAP_DIR} cp ${SNAP_DIR}/roundcubemail/config/defaults.inc.php ${SNAP_DIR}/config/roundcube/ +cp -r ${DIR}/roundcube/plugins/mailadmin ${SNAP_DIR}/roundcubemail/plugins/ cp -r ${SNAP_DIR}/postfix/config/postfix/postfix-files ${SNAP_DIR}/config/postfix echo "version: $VERSION" >> ${SNAP_DIR}/meta/snap.yaml diff --git a/roundcube/plugins/mailadmin/localization/en_US.inc b/roundcube/plugins/mailadmin/localization/en_US.inc new file mode 100644 index 0000000..d51aa32 --- /dev/null +++ b/roundcube/plugins/mailadmin/localization/en_US.inc @@ -0,0 +1,4 @@ +add_texts('localization/', false); + $this->include_stylesheet($this->local_skin_path() . '/mailadmin.css'); + + $this->add_button([ + 'type' => 'link', + 'label' => 'mailadmin.admin', + 'title' => 'mailadmin.admin', + 'href' => '/admin/', + 'class' => 'button-mailadmin', + 'classsel' => 'button-mailadmin button-selected', + 'innerclass' => 'button-inner', + ], 'taskbar'); + + $this->api->output->add_footer( + '' + ); + } +} diff --git a/roundcube/plugins/mailadmin/skins/elastic/mailadmin.css b/roundcube/plugins/mailadmin/skins/elastic/mailadmin.css new file mode 100644 index 0000000..29587f6 --- /dev/null +++ b/roundcube/plugins/mailadmin/skins/elastic/mailadmin.css @@ -0,0 +1,10 @@ +#taskmenu a.button-mailadmin::before { + content: ""; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + background-color: currentColor; +} diff --git a/test/e2e/specs/03-relay.spec.ts b/test/e2e/specs/03-relay.spec.ts index 5152913..51d81b8 100644 --- a/test/e2e/specs/03-relay.spec.ts +++ b/test/e2e/specs/03-relay.spec.ts @@ -1,9 +1,18 @@ import { test, expect } from '@playwright/test' import { shoot } from '../helpers/screenshot' +import { login, expectInbox } from '../helpers/roundcube' + +const user = process.env.PLAYWRIGHT_DEVICE_USER ?? 'user' +const password = process.env.PLAYWRIGHT_DEVICE_PASSWORD ?? 'Password1' test.describe('mail admin relay', () => { test('configure outbound relay', async ({ page }, testInfo) => { - await page.goto('/admin/') + await page.goto('/') + await login(page, user, password) + await expectInbox(page) + await shoot(page, testInfo, 'inbox') + + await page.getByTestId('nav-admin').click() await expect(page.getByTestId('admin-title')).toBeVisible() await shoot(page, testInfo, 'admin') From 087b66ac82850ef9f8b95876e196ebd496c2183f Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 14:45:48 +0100 Subject: [PATCH 08/14] Fix admin button navigation to /admin/ Roundcube rewrote the plugin button href '/admin/' into a skin-relative 'skins/elastic/admin/' (404). Set the correct absolute href and force navigation on click in the footer script, so the taskbar Admin button reliably opens /admin/ regardless of Roundcube's taskbar click handling. --- roundcube/plugins/mailadmin/mailadmin.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roundcube/plugins/mailadmin/mailadmin.php b/roundcube/plugins/mailadmin/mailadmin.php index e4a8310..dea787c 100644 --- a/roundcube/plugins/mailadmin/mailadmin.php +++ b/roundcube/plugins/mailadmin/mailadmin.php @@ -22,7 +22,8 @@ function init() $this->api->output->add_footer( '' + 'if(a){a.setAttribute("href","/admin/");a.setAttribute("data-testid","nav-admin");' . + 'a.addEventListener("click",function(e){e.stopPropagation();e.preventDefault();window.location.assign("/admin/");});}});' ); } } From cca6df508ac78bb16a049a121655e99683d0d3c3 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 15:58:15 +0100 Subject: [PATCH 09/14] Refresh DB postgresql.conf on upgrade so it points at $SNAP_DATA On upgrade the stable DB's postgresql.conf (with unix_socket_directories baked to /var/snap/mail/common/database) is carried to $SNAP_DATA by the migration but still targets the now-gone common path, so postgres fails to create its lock file and the refresh aborts. Rewrite the data dir's postgresql.conf from the template on every InitConfig (extracted as Database.UpdateConfig, reused by Init), so it always matches the current data-dir location. --- cli/installer/database.go | 7 +++++++ cli/installer/installer.go | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/cli/installer/database.go b/cli/installer/database.go index cd8cfcd..344a421 100644 --- a/cli/installer/database.go +++ b/cli/installer/database.go @@ -54,6 +54,13 @@ func (d *Database) Init() error { if _, err := d.executor.RunDir("", "sudo", "-H", "-u", d.user, initdb, d.databaseDir); err != nil { return err } + return d.UpdateConfig() +} + +func (d *Database) UpdateConfig() error { + if _, err := os.Stat(d.databaseDir); os.IsNotExist(err) { + return nil + } src := path.Join(d.configDir, "postgresql", "postgresql.conf") dst := path.Join(d.databaseDir, "postgresql.conf") content, err := os.ReadFile(src) diff --git a/cli/installer/installer.go b/cli/installer/installer.go index e531d08..c1601a8 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -217,6 +217,10 @@ func (i *Installer) InitConfig() error { return err } + if err := i.database.UpdateConfig(); err != nil { + return err + } + dkimKey, err := i.GenerateDkimKey(deviceDomainName, opendkimKeysDomainDir) if err != nil { return err From accf52cd7ad95528a1518aee957fadbc9d005a66 Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Thu, 16 Jul 2026 18:48:43 +0100 Subject: [PATCH 10/14] Run webui as mail (non-root), map-driven relay, e2e relay send via Mailpit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop root from the webui daemon. Relay is now fully lookup-table driven: main.cf statically references sender_dependent_relayhost_maps + sasl_passwd, and the webui (as mail) writes and postmaps those two maps on save — no main.cf regeneration, no postfix reload, no platform RestartService. The mail-owned config dir makes all of this doable without privilege. To make map changes take effect immediately (postfix caches lookup maps for a daemon's lifetime), the trivial-rewrite service recycles per request (master.cf: rewrite -o max_use=1), so a relay toggle applies on the next message without a reload. E2e now covers both modes end to end, purely through the UI: - 02-send: plain local send arrives in the inbox. - 03-relay: log in, open the admin page via the Roundcube button, save relay credentials in the form, return to webmail via a new "Back to mail" link, compose an external message, then poll the relay's REST API until it arrives. The relay target is a Mailpit service (authenticated SMTP + REST), added to the amd64 pipeline. --- .drone.jsonnet | 12 ++++++- cli/installer/installer.go | 9 +----- cli/installer/relay.go | 55 +++++++++++++++++++++++++-------- config/postfix/main.cf | 7 ++--- config/postfix/master.cf | 1 + meta/snap.yaml | 1 + test/e2e/helpers/mailpit.ts | 12 +++++++ test/e2e/run.sh | 5 +++ test/e2e/specs/03-relay.spec.ts | 29 +++++++++++------ www/src/App.vue | 8 ++++- 10 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 test/e2e/helpers/mailpit.ts diff --git a/.drone.jsonnet b/.drone.jsonnet index 00ad695..42b7d85 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -12,6 +12,7 @@ local php = 'php:8.0.30-fpm-bullseye'; local postgres = 'postgres:9.4-alpine'; local platform = '26.04.10'; local playwright = 'mcr.microsoft.com/playwright:v1.48.2-jammy'; +local mailpit = 'axllent/mailpit:latest'; local store_publisher = 'stable-303'; local distros = ['bookworm', 'buster']; @@ -234,7 +235,16 @@ local build(arch, test_ui) = [{ ], } for distro in distros - ], + ] + (if test_ui then [ + { + name: 'mailpit', + image: mailpit, + environment: { + MP_SMTP_AUTH_ACCEPT_ANY: '1', + MP_SMTP_AUTH_ALLOW_INSECURE: '1', + }, + }, + ] else []), volumes: [ { name: 'dbus', host: { path: '/var/run/dbus' } }, { name: 'dev', host: { path: '/dev' } }, diff --git a/cli/installer/installer.go b/cli/installer/installer.go index c1601a8..d6952b9 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -23,7 +23,6 @@ const ( DbUser = "mail" DbPass = "mail" SystemdDovecot = "mail.dovecot" - SystemdPostfix = "mail.postfix" ) var dkimKeyPattern = regexp.MustCompile(`(?s).*p=(.*?)".*`) @@ -41,9 +40,6 @@ type Variables struct { DeviceDomainName string AppDomainName string Timezone string - Relay bool - RelayHost string - RelayPort int } type Installer struct { @@ -126,9 +122,6 @@ func (i *Installer) RegenerateConfigs() error { DeviceDomainName: deviceDomainName, AppDomainName: appDomainName, Timezone: tz, - Relay: relay.Enabled, - RelayHost: relay.Host, - RelayPort: relay.Port, } templatesPath := path.Join(i.appDir, "config") @@ -136,7 +129,7 @@ func (i *Installer) RegenerateConfigs() error { return err } - if err := i.applySasl(relay); err != nil { + if err := i.writeRelayMaps(relay, deviceDomainName); err != nil { return err } diff --git a/cli/installer/relay.go b/cli/installer/relay.go index b46eb5f..c28eb83 100644 --- a/cli/installer/relay.go +++ b/cli/installer/relay.go @@ -5,6 +5,7 @@ import ( "os" "path" "strconv" + "strings" "gopkg.in/ini.v1" ) @@ -49,25 +50,53 @@ func (i *Installer) SetRelay(relay RelayConfig) error { } func (i *Installer) ApplyRelay() error { - if err := i.RegenerateConfigs(); err != nil { + relay, err := i.GetRelay() + if err != nil { return err } - return i.platformClient.RestartService(SystemdPostfix) + domain, err := i.mydomain() + if err != nil { + return err + } + return i.writeRelayMaps(relay, domain) } -func (i *Installer) applySasl(relay RelayConfig) error { - saslFile := path.Join(i.configPath, "postfix", "sasl_passwd") - if !relay.Enabled { - _ = os.Remove(saslFile) - _ = os.Remove(saslFile + ".db") - return nil +func (i *Installer) mydomain() (string, error) { + postconf := path.Join(i.appDir, "postfix", "usr", "sbin", "postconf") + postfixDir := path.Join(i.configPath, "postfix") + out, err := i.executor.RunDir(postfixDir, postconf, "-h", "-c", postfixDir, "mydomain") + if err != nil { + return "", err + } + return strings.TrimSpace(out), nil +} + +func (i *Installer) writeRelayMaps(relay RelayConfig, domain string) error { + postfixDir := path.Join(i.configPath, "postfix") + saslFile := path.Join(postfixDir, "sasl_passwd") + relayFile := path.Join(postfixDir, "relayhost") + + saslContent := "" + relayContent := "" + if relay.Enabled { + host := fmt.Sprintf("[%s]:%d", relay.Host, relay.Port) + saslContent = fmt.Sprintf("%s %s:%s\n", host, relay.User, relay.Password) + relayContent = fmt.Sprintf("@%s %s\n", domain, host) } - line := fmt.Sprintf("[%s]:%d %s:%s\n", relay.Host, relay.Port, relay.User, relay.Password) - if err := os.WriteFile(saslFile, []byte(line), 0600); err != nil { + + if err := os.WriteFile(saslFile, []byte(saslContent), 0600); err != nil { + return err + } + if err := os.WriteFile(relayFile, []byte(relayContent), 0644); err != nil { return err } + postmap := path.Join(i.appDir, "postfix", "usr", "sbin", "postmap") - postfixConfigDir := path.Join(i.configPath, "postfix") - _, err := i.executor.RunDir(postfixConfigDir, postmap, "-c", postfixConfigDir, "hash:"+saslFile) - return err + if _, err := i.executor.RunDir(postfixDir, postmap, "-c", postfixDir, "hash:"+saslFile); err != nil { + return err + } + if _, err := i.executor.RunDir(postfixDir, postmap, "-c", postfixDir, "hash:"+relayFile); err != nil { + return err + } + return nil } diff --git a/config/postfix/main.cf b/config/postfix/main.cf index 8b2ddbe..52c8b8e 100644 --- a/config/postfix/main.cf +++ b/config/postfix/main.cf @@ -330,15 +330,12 @@ mynetworks_style = host # # If you're connected via UUCP, see also the default_transport parameter. # -{{ if .Relay }} -relayhost = [{{ .RelayHost }}]:{{ .RelayPort }} +relayhost = smtp_sasl_auth_enable = yes smtp_sasl_security_options = noanonymous smtp_sasl_password_maps = hash:{{ .AppDataDir }}/config/postfix/sasl_passwd smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt -{{ else }} -relayhost = -{{ end }} +sender_dependent_relayhost_maps = hash:{{ .AppDataDir }}/config/postfix/relayhost #relayhost = [gateway.my.domain] #relayhost = [mailserver.isp.tld] #relayhost = uucphost diff --git a/config/postfix/master.cf b/config/postfix/master.cf index 77e6662..fbc6264 100644 --- a/config/postfix/master.cf +++ b/config/postfix/master.cf @@ -46,6 +46,7 @@ qmgr unix n - n 300 1 qmgr #qmgr unix n - n 300 1 oqmgr tlsmgr unix - - n 1000? 1 tlsmgr rewrite unix - - n - - trivial-rewrite + -o max_use=1 bounce unix - - n - 0 bounce defer unix - - n - 0 bounce trace unix - - n - 0 bounce diff --git a/meta/snap.yaml b/meta/snap.yaml index 758bc87..47637df 100644 --- a/meta/snap.yaml +++ b/meta/snap.yaml @@ -47,6 +47,7 @@ apps: after: [postgresql] before: [nginx] webui: + user: mail command: bin/service.webui.sh daemon: simple plugs: diff --git a/test/e2e/helpers/mailpit.ts b/test/e2e/helpers/mailpit.ts new file mode 100644 index 0000000..ff3077b --- /dev/null +++ b/test/e2e/helpers/mailpit.ts @@ -0,0 +1,12 @@ +import { APIRequestContext, expect } from '@playwright/test' + +const mailpitUrl = process.env.PLAYWRIGHT_MAILPIT_URL ?? 'http://mailpit:8025' + +export async function expectRelayed(request: APIRequestContext, subject: string) { + await expect.poll(async () => { + const res = await request.get(`${mailpitUrl}/api/v1/search?query=${encodeURIComponent(subject)}`) + if (!res.ok()) return 0 + const body = await res.json() + return body.messages?.length ?? 0 + }, { timeout: 60000, intervals: [1000, 2000, 3000, 5000] }).toBeGreaterThan(0) +} diff --git a/test/e2e/run.sh b/test/e2e/run.sh index d48800b..bd9db91 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -15,6 +15,11 @@ export PLAYWRIGHT_SSH_USER=root export PLAYWRIGHT_SSH_PASSWORD=Password1 export PLAYWRIGHT_PROJECT=${PROJECT} export PLAYWRIGHT_ARTIFACT_DIR=/drone/src/artifact/${ARTIFACT_SUBDIR} +export PLAYWRIGHT_MAILPIT_URL=http://mailpit:8025 +export PLAYWRIGHT_RELAY_HOST=mailpit +export PLAYWRIGHT_RELAY_PORT=1025 +export PLAYWRIGHT_RELAY_USER=relayuser +export PLAYWRIGHT_RELAY_PASSWORD=relaypass apt-get update -qq apt-get install -y -qq sshpass openssh-client curl diff --git a/test/e2e/specs/03-relay.spec.ts b/test/e2e/specs/03-relay.spec.ts index 51d81b8..1f76996 100644 --- a/test/e2e/specs/03-relay.spec.ts +++ b/test/e2e/specs/03-relay.spec.ts @@ -1,30 +1,41 @@ import { test, expect } from '@playwright/test' import { shoot } from '../helpers/screenshot' -import { login, expectInbox } from '../helpers/roundcube' +import { login, expectInbox, compose } from '../helpers/roundcube' +import { expectRelayed } from '../helpers/mailpit' const user = process.env.PLAYWRIGHT_DEVICE_USER ?? 'user' const password = process.env.PLAYWRIGHT_DEVICE_PASSWORD ?? 'Password1' +const relayHost = process.env.PLAYWRIGHT_RELAY_HOST ?? 'mailpit' +const relayPort = process.env.PLAYWRIGHT_RELAY_PORT ?? '1025' +const relayUser = process.env.PLAYWRIGHT_RELAY_USER ?? 'relayuser' +const relayPassword = process.env.PLAYWRIGHT_RELAY_PASSWORD ?? 'relaypass' test.describe('mail admin relay', () => { - test('configure outbound relay', async ({ page }, testInfo) => { + test('configure relay and send through it', async ({ page, request }, testInfo) => { await page.goto('/') await login(page, user, password) await expectInbox(page) - await shoot(page, testInfo, 'inbox') await page.getByTestId('nav-admin').click() await expect(page.getByTestId('admin-title')).toBeVisible() await shoot(page, testInfo, 'admin') await page.getByTestId('relay-enabled').click() - await page.getByTestId('relay-host').fill('smtp.gmail.com') - await page.getByTestId('relay-port').fill('587') - await page.getByTestId('relay-user').fill('user@gmail.com') - await page.getByTestId('relay-password').fill('app-password') - await shoot(page, testInfo, 'relay-filled') - + await page.getByTestId('relay-host').fill(relayHost) + await page.getByTestId('relay-port').fill(relayPort) + await page.getByTestId('relay-user').fill(relayUser) + await page.getByTestId('relay-password').fill(relayPassword) await page.getByTestId('relay-save').click() await expect(page.getByText('Saved')).toBeVisible() await shoot(page, testInfo, 'relay-saved') + + await page.getByTestId('nav-mail').click() + await expectInbox(page) + + const subject = `e2e-relay-${Date.now()}` + await compose(page, 'relayed@example.com', subject, 'playwright relay send test') + await shoot(page, testInfo, 'relay-sent') + + await expectRelayed(request, subject) }) }) diff --git a/www/src/App.vue b/www/src/App.vue index 4ad1146..db5b3f3 100644 --- a/www/src/App.vue +++ b/www/src/App.vue @@ -1,7 +1,8 @@