diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e96e24b..571b10d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,5 +6,9 @@ // Should match what is defined in ui/package.json "version": "21.1.0" } - } + }, + "mounts": [ + "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" + ] } + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b31041f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: build image +on: + push: + branches: + - dev + - main + workflow_dispatch: + pull_request_review: + types: [submitted] + +jobs: + build: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Build + if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: v21.1.0 + cache: "npm" + cache-dependency-path: "**/package-lock.json" + - name: Set up Golang + uses: actions/setup-go@v4 + with: + go-version: "1.24.0" + - name: Build frontend + run: | + make frontend + - name: Build application + run: | + make build_dev + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: jetkvm-app + path: bin/jetkvm_app \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..7ec9229 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,37 @@ +--- +name: golangci-lint +on: + push: + paths: + - "go.sum" + - "go.mod" + - "**.go" + - ".github/workflows/golangci-lint.yml" + - ".golangci.yml" + pull_request: + +permissions: # added using https://github.com/step-security/secure-repo + contents: read + +jobs: + golangci: + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Install Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: 1.23.x + - name: Create empty resource directory + run: | + mkdir -p static && touch static/.gitkeep + - name: Lint + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + args: --verbose + version: v1.62.0 diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml new file mode 100644 index 0000000..d5493e7 --- /dev/null +++ b/.github/workflows/smoketest.yml @@ -0,0 +1,122 @@ +name: smoketest +on: + repository_dispatch: + types: [smoketest] + +jobs: + ghbot_payload: + name: Ghbot payload + runs-on: ubuntu-latest + steps: + - name: "GH_CHECK_RUN_ID=${{ github.event.client_payload.check_run_id }}" + run: | + echo "== START GHBOT_PAYLOAD ==" + cat <<'GHPAYLOAD_EOF' | base64 + ${{ toJson(github.event.client_payload) }} + GHPAYLOAD_EOF + echo "== END GHBOT_PAYLOAD ==" + deploy_and_test: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Smoke test + concurrency: + group: smoketest-jk + steps: + - name: Download artifact + run: | + wget -O /tmp/jk.zip "${{ github.event.client_payload.artifact_download_url }}" + unzip /tmp/jk.zip + - name: Configure WireGuard and check connectivity + run: | + WG_KEY_FILE=$(mktemp) + echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ + sudo apt-get update && sudo apt-get install -y wireguard-tools && \ + sudo ip link add dev wg-ci type wireguard && \ + sudo ip addr add $CI_WG_IPS dev wg-ci && \ + sudo wg set wg-ci listen-port 51820 \ + private-key $WG_KEY_FILE \ + peer $CI_WG_PUBLIC \ + allowed-ips $CI_WG_ALLOWED_IPS \ + endpoint $CI_WG_ENDPOINT \ + persistent-keepalive 15 && \ + sudo ip link set up dev wg-ci && \ + sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci + ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} + CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} + CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} + CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} + CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} + CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} + - name: Configure SSH + run: | + # Write SSH private key to a file + SSH_PRIVATE_KEY=$(mktemp) + echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY + chmod 0600 $SSH_PRIVATE_KEY + # Configure SSH + mkdir -p ~/.ssh + cat <> ~/.ssh/config + Host jkci + HostName $CI_HOST + User $CI_USER + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + IdentityFile $SSH_PRIVATE_KEY + EOF + env: + CI_USER: ${{ vars.JETKVM_CI_USER }} + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} + - name: Deploy application + run: | + set -e + # Copy the binary to the remote host + echo "+ Copying the application to the remote host" + cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" + # Deploy and run the application on the remote host + echo "+ Deploying the application on the remote host" + ssh jkci ash < /proc/sys/vm/drop_caches + # Reboot the application + reboot -d 5 -f & + EOF + sleep 10 + echo "Deployment complete, waiting for JetKVM to come back online " + function check_online() { + for i in {1..60}; do + if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then + echo "JetKVM is back online" + return 0 + fi + echo -n "." + sleep 1 + done + echo "JetKVM did not come back online within 60 seconds" + return 1 + } + check_online + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Run smoke tests + run: | + echo "+ Checking the status of the device" + curl -v http://$CI_HOST/device/status && echo + echo "+ Waiting for 10 seconds to allow all services to start" + sleep 10 + echo "+ Collecting logs" + ssh jkci "cat /userdata/jetkvm/last.log" > last.log + cat last.log + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: device-logs + path: last.log diff --git a/.github/workflows/ui-lint.yml b/.github/workflows/ui-lint.yml new file mode 100644 index 0000000..492a5fe --- /dev/null +++ b/.github/workflows/ui-lint.yml @@ -0,0 +1,34 @@ +--- +name: ui-lint +on: + push: + paths: + - "ui/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/ui-lint.yml" + +permissions: + contents: read + +jobs: + ui-lint: + name: UI Lint + runs-on: buildjet-4vcpu-ubuntu-2204 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: v21.1.0 + cache: "npm" + cache-dependency-path: "ui/package-lock.json" + - name: Install dependencies + run: | + cd ui + npm ci + - name: Lint UI + run: | + cd ui + npm run lint diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..95a1cb8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,22 @@ +--- +linters: + enable: + - forbidigo + - goimports + - misspell + # - revive + - whitespace + +issues: + exclude-rules: + - path: _test.go + linters: + - errcheck + +linters-settings: + forbidigo: + forbid: + - p: ^fmt\.Print.*$ + msg: Do not commit print statements. Use logger package. + - p: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ + msg: Do not commit log statements. Use logger package. diff --git a/Makefile b/Makefile index 04c7402..2aefdea 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,31 @@ -VERSION_DEV := 0.3.5-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.3.4 +BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +BUILDDATE ?= $(shell date -u +%FT%T%z) +BUILDTS ?= $(shell date -u +%s) +REVISION ?= $(shell git rev-parse HEAD) +VERSION_DEV := 0.3.9-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.3.8 + +PROMETHEUS_TAG := github.com/prometheus/common/version +KVM_PKG_NAME := github.com/jetkvm/kvm + +GO_LDFLAGS := \ + -s -w \ + -X $(PROMETHEUS_TAG).Branch=$(BRANCH) \ + -X $(PROMETHEUS_TAG).BuildDate=$(BUILDDATE) \ + -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ + -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) hash_resource: @shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256 build_dev: hash_resource @echo "Building..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go + GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go frontend: cd ui && npm ci && npm run build:device -dev_release: build_dev +dev_release: frontend build_dev @echo "Uploading release..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app @@ -19,7 +33,7 @@ dev_release: build_dev build_release: frontend hash_resource @echo "Building release..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go + GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go release: @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \ diff --git a/README.md b/README.md index 1b516d7..5d0e9d7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We welcome contributions from the community! Whether it's improving the firmware ## I need help -The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://discord.gg/8MaAhua7NW). +The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://jetkvm.com/discord). ## I want to report an issue diff --git a/block_device.go b/block_device.go index 1e34884..4a8769e 100644 --- a/block_device.go +++ b/block_device.go @@ -3,7 +3,6 @@ package kvm import ( "context" "errors" - "log" "net" "os" "time" @@ -17,8 +16,8 @@ type remoteImageBackend struct { func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { virtualMediaStateMutex.RLock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debugf("read size: %d, off: %d", len(p), off) + logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") + logger.Debug().Int64("read size", int64(len(p))).Int64("off", off).Msg("read size and off") if currentVirtualMediaState == nil { return 0, errors.New("image not mounted") } @@ -94,7 +93,8 @@ func (d *NBDDevice) Start() error { // Remove the socket file if it already exists if _, err := os.Stat(nbdSocketPath); err == nil { if err := os.Remove(nbdSocketPath); err != nil { - log.Fatalf("Failed to remove existing socket file %s: %v", nbdSocketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", nbdSocketPath).Msg("Failed to remove existing socket file") + os.Exit(1) } } @@ -134,7 +134,7 @@ func (d *NBDDevice) runServerConn() { MaximumBlockSize: uint32(16 * 1024), SupportsMultiConn: false, }) - log.Println("nbd server exited:", err) + nativeLogger.Info().Err(err).Msg("nbd server exited") } func (d *NBDDevice) runClientConn() { @@ -142,14 +142,14 @@ func (d *NBDDevice) runClientConn() { ExportName: "jetkvm", BlockSize: uint32(4 * 1024), }) - log.Println("nbd client exited:", err) + nativeLogger.Info().Err(err).Msg("nbd client exited") } func (d *NBDDevice) Close() { if d.dev != nil { err := client.Disconnect(d.dev) if err != nil { - log.Println("error disconnecting nbd client:", err) + nativeLogger.Warn().Err(err).Msg("error disconnecting nbd client") } _ = d.dev.Close() } diff --git a/cloud.go b/cloud.go index db47727..a999fc8 100644 --- a/cloud.go +++ b/cloud.go @@ -4,16 +4,21 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" - "github.com/coder/websocket/wsjson" + "sync" "time" + "github.com/coder/websocket/wsjson" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/coreos/go-oidc/v3/oidc" - "github.com/gin-gonic/gin" "github.com/coder/websocket" + "github.com/gin-gonic/gin" ) type CloudRegisterRequest struct { @@ -23,6 +28,142 @@ type CloudRegisterRequest struct { ClientId string `json:"clientId"` } +const ( + // CloudWebSocketConnectTimeout is the timeout for the websocket connection to the cloud + CloudWebSocketConnectTimeout = 1 * time.Minute + // CloudAPIRequestTimeout is the timeout for cloud API requests + CloudAPIRequestTimeout = 10 * time.Second + // CloudOidcRequestTimeout is the timeout for OIDC token verification requests + // should be lower than the websocket response timeout set in cloud-api + CloudOidcRequestTimeout = 10 * time.Second + // WebsocketPingInterval is the interval at which the websocket client sends ping messages to the cloud + WebsocketPingInterval = 15 * time.Second +) + +var ( + metricCloudConnectionStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_status", + Help: "The status of the cloud connection", + }, + ) + metricCloudConnectionEstablishedTimestamp = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_established_timestamp", + Help: "The timestamp when the cloud connection was established", + }, + ) + metricConnectionLastPingTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_timestamp", + Help: "The timestamp when the last ping response was received", + }, + []string{"type", "source"}, + ) + metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_received_timestamp", + Help: "The timestamp when the last ping request was received", + }, + []string{"type", "source"}, + ) + metricConnectionLastPingDuration = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_duration", + Help: "The duration of the last ping response", + }, + []string{"type", "source"}, + ) + metricConnectionPingDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_connection_ping_duration", + Help: "The duration of the ping response", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + []string{"type", "source"}, + ) + metricConnectionTotalPingSentCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_total_ping_sent", + Help: "The total number of pings sent to the connection", + }, + []string{"type", "source"}, + ) + metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_total_ping_received", + Help: "The total number of pings received from the connection", + }, + []string{"type", "source"}, + ) + metricConnectionSessionRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_session_total_requests", + Help: "The total number of session requests received", + }, + []string{"type", "source"}, + ) + metricConnectionSessionRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_connection_session_request_duration", + Help: "The duration of session requests", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + []string{"type", "source"}, + ) + metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_session_request_timestamp", + Help: "The timestamp of the last session request", + }, + []string{"type", "source"}, + ) + metricConnectionLastSessionRequestDuration = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_session_request_duration", + Help: "The duration of the last session request", + }, + []string{"type", "source"}, + ) + metricCloudConnectionFailureCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_cloud_connection_failure_count", + Help: "The number of times the cloud connection has failed", + }, + ) +) + +var ( + cloudDisconnectChan chan error + cloudDisconnectLock = &sync.Mutex{} +) + +func wsResetMetrics(established bool, sourceType string, source string) { + metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) + + metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1) + + metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1) + + if sourceType != "cloud" { + return + } + + if established { + metricCloudConnectionEstablishedTimestamp.SetToCurrentTime() + metricCloudConnectionStatus.Set(1) + } else { + metricCloudConnectionEstablishedTimestamp.Set(-1) + metricCloudConnectionStatus.Set(-1) + } +} + func handleCloudRegister(c *gin.Context) { var req CloudRegisterRequest @@ -43,22 +184,31 @@ func handleCloudRegister(c *gin.Context) { return } - resp, err := http.Post(req.CloudAPI+"/devices/token", "application/json", bytes.NewBuffer(jsonPayload)) + client := &http.Client{Timeout: CloudAPIRequestTimeout} + + apiReq, err := http.NewRequest(http.MethodPost, config.CloudURL+"/devices/token", bytes.NewBuffer(jsonPayload)) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to create register request: " + err.Error()}) + return + } + apiReq.Header.Set("Content-Type", "application/json") + + apiResp, err := client.Do(apiReq) if err != nil { c.JSON(500, gin.H{"error": "Failed to exchange token: " + err.Error()}) return } - defer resp.Body.Close() + defer apiResp.Body.Close() - if resp.StatusCode != http.StatusOK { - c.JSON(resp.StatusCode, gin.H{"error": "Failed to exchange token: " + resp.Status}) + if apiResp.StatusCode != http.StatusOK { + c.JSON(apiResp.StatusCode, gin.H{"error": "Failed to exchange token: " + apiResp.Status}) return } var tokenResp struct { SecretToken string `json:"secretToken"` } - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + if err := json.NewDecoder(apiResp.Body).Decode(&tokenResp); err != nil { c.JSON(500, gin.H{"error": "Failed to parse token response: " + err.Error()}) return } @@ -69,7 +219,6 @@ func handleCloudRegister(c *gin.Context) { } config.CloudToken = tokenResp.SecretToken - config.CloudURL = req.CloudAPI provider, err := oidc.NewProvider(c, "https://accounts.google.com") if err != nil { @@ -99,76 +248,91 @@ func handleCloudRegister(c *gin.Context) { c.JSON(200, gin.H{"message": "Cloud registration successful"}) } +func disconnectCloud(reason error) { + cloudDisconnectLock.Lock() + defer cloudDisconnectLock.Unlock() + + if cloudDisconnectChan == nil { + cloudLogger.Trace().Msg("cloud disconnect channel is not set, no need to disconnect") + return + } + + // just in case the channel is closed, we don't want to panic + defer func() { + if r := recover(); r != nil { + cloudLogger.Warn().Interface("reason", r).Msg("cloud disconnect channel is closed, no need to disconnect") + } + }() + cloudDisconnectChan <- reason +} + func runWebsocketClient() error { if config.CloudToken == "" { time.Sleep(5 * time.Second) return fmt.Errorf("cloud token is not set") } + wsURL, err := url.Parse(config.CloudURL) if err != nil { return fmt.Errorf("failed to parse config.CloudURL: %w", err) } + if wsURL.Scheme == "http" { wsURL.Scheme = "ws" } else { wsURL.Scheme = "wss" } + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) + header.Set("X-App-Version", builtAppVersion) header.Set("Authorization", "Bearer "+config.CloudToken) - dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Minute) + dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) + + scopedLogger := websocketLogger.With(). + Str("source", wsURL.Host). + Str("sourceType", "cloud"). + Logger() + defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, + OnPingReceived: func(ctx context.Context, payload []byte) bool { + scopedLogger.Info().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") + + metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() + metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() + + return true + }, }) + // if the context is canceled, we don't want to return an error if err != nil { + if errors.Is(err, context.Canceled) { + cloudLogger.Info().Msg("websocket connection canceled") + return nil + } return err } - defer c.CloseNow() - logger.Infof("WS connected to %v", wsURL.String()) - runCtx, cancelRun := context.WithCancel(context.Background()) - defer cancelRun() - go func() { - for { - time.Sleep(15 * time.Second) - err := c.Ping(runCtx) - if err != nil { - logger.Warnf("websocket ping error: %v", err) - cancelRun() - return - } - } - }() - for { - typ, msg, err := c.Read(runCtx) - if err != nil { - return err - } - if typ != websocket.MessageText { - // ignore non-text messages - continue - } - var req WebRTCSessionRequest - err = json.Unmarshal(msg, &req) - if err != nil { - logger.Warnf("unable to parse ws message: %v", string(msg)) - continue - } + defer c.CloseNow() //nolint:errcheck + cloudLogger.Info().Str("url", wsURL.String()).Msg("websocket connected") - err = handleSessionRequest(runCtx, c, req) - if err != nil { - logger.Infof("error starting new session: %v", err) - continue - } - } + // set the metrics when we successfully connect to the cloud. + wsResetMetrics(true, "cloud", wsURL.Host) + + // we don't have a source for the cloud connection + return handleWebRTCSignalWsMessages(c, true, wsURL.Host, &scopedLogger) } -func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { - oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute) +func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { + oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) defer cancelOIDC() provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") if err != nil { - fmt.Println("Failed to initialize OIDC provider:", err) + _ = wsjson.Write(context.Background(), c, gin.H{ + "error": fmt.Sprintf("failed to initialize OIDC provider: %v", err), + }) + cloudLogger.Warn().Err(err).Msg("failed to initialize OIDC provider") return err } @@ -184,10 +348,40 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess googleIdentity := idToken.Audience[0] + ":" + idToken.Subject if config.GoogleIdentity != googleIdentity { + _ = wsjson.Write(context.Background(), c, gin.H{"error": "google identity mismatch"}) return fmt.Errorf("google identity mismatch") } - session, err := newSession() + return nil +} + +func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest, isCloudConnection bool, source string) error { + var sourceType string + if isCloudConnection { + sourceType = "cloud" + } else { + sourceType = "local" + } + + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v) + metricConnectionSessionRequestDuration.WithLabelValues(sourceType, source).Observe(v) + })) + defer timer.ObserveDuration() + + // If the message is from the cloud, we need to authenticate the session. + if isCloudConnection { + if err := authenticateSession(ctx, c, req); err != nil { + return err + } + } + + session, err := newSession(SessionConfig{ + ws: c, + IsCloud: isCloudConnection, + LocalIP: req.IP, + ICEServers: req.ICEServers, + }) if err != nil { _ = wsjson.Write(context.Background(), c, gin.H{"error": err}) return err @@ -206,16 +400,41 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess _ = peerConn.Close() }() } + + cloudLogger.Info().Interface("session", session).Msg("new session accepted") + cloudLogger.Trace().Interface("session", session).Msg("new session accepted") currentSession = session - _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) + _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil } func RunWebsocketClient() { for { + // If the cloud token is not set, we don't need to run the websocket client. + if config.CloudToken == "" { + time.Sleep(5 * time.Second) + continue + } + + // If the network is not up, well, we can't connect to the cloud. + if !networkState.Up { + cloudLogger.Warn().Msg("waiting for network to be up, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + + // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. + if isTimeSyncNeeded() && !timeSyncSuccess { + cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + err := runWebsocketClient() if err != nil { - fmt.Println("Websocket client error:", err) + cloudLogger.Warn().Err(err).Msg("websocket client error") + metricCloudConnectionStatus.Set(0) + metricCloudConnectionFailureCount.Inc() time.Sleep(5 * time.Second) } } @@ -224,12 +443,14 @@ func RunWebsocketClient() { type CloudState struct { Connected bool `json:"connected"` URL string `json:"url,omitempty"` + AppURL string `json:"appUrl,omitempty"` } func rpcGetCloudState() CloudState { return CloudState{ Connected: config.CloudToken != "" && config.CloudURL != "", URL: config.CloudURL, + AppURL: config.CloudAppURL, } } @@ -244,7 +465,7 @@ func rpcDeregisterDevice() error { } req.Header.Set("Authorization", "Bearer "+config.CloudToken) - client := &http.Client{Timeout: 10 * time.Second} + client := &http.Client{Timeout: CloudAPIRequestTimeout} resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to send deregister request: %w", err) @@ -257,12 +478,15 @@ func rpcDeregisterDevice() error { // (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it. if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) { config.CloudToken = "" - config.CloudURL = "" config.GoogleIdentity = "" + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } + cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") + disconnectCloud(fmt.Errorf("device deregistered")) + return nil } diff --git a/cmd/main.go b/cmd/main.go index 6080aff..ab44ac9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,7 @@ package main import ( - "kvm" + "github.com/jetkvm/kvm" ) func main() { diff --git a/config.go b/config.go index 3657653..51b24e4 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,9 @@ import ( "encoding/json" "fmt" "os" + "sync" + + "github.com/jetkvm/kvm/internal/usbgadget" ) type WakeOnLanDevice struct { @@ -11,56 +14,167 @@ type WakeOnLanDevice struct { MacAddress string `json:"macAddress"` } +// Constants for keyboard macro limits +const ( + MaxMacrosPerDevice = 25 + MaxStepsPerMacro = 10 + MaxKeysPerStep = 10 + MinStepDelay = 50 + MaxStepDelay = 2000 +) + +type KeyboardMacroStep struct { + Keys []string `json:"keys"` + Modifiers []string `json:"modifiers"` + Delay int `json:"delay"` +} + +func (s *KeyboardMacroStep) Validate() error { + if len(s.Keys) > MaxKeysPerStep { + return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep) + } + + if s.Delay < MinStepDelay { + s.Delay = MinStepDelay + } else if s.Delay > MaxStepDelay { + s.Delay = MaxStepDelay + } + + return nil +} + +type KeyboardMacro struct { + ID string `json:"id"` + Name string `json:"name"` + Steps []KeyboardMacroStep `json:"steps"` + SortOrder int `json:"sortOrder,omitempty"` +} + +func (m *KeyboardMacro) Validate() error { + if m.Name == "" { + return fmt.Errorf("macro name cannot be empty") + } + + if len(m.Steps) == 0 { + return fmt.Errorf("macro must have at least one step") + } + + if len(m.Steps) > MaxStepsPerMacro { + return fmt.Errorf("too many steps in macro (max %d)", MaxStepsPerMacro) + } + + for i := range m.Steps { + if err := m.Steps[i].Validate(); err != nil { + return fmt.Errorf("invalid step %d: %w", i+1, err) + } + } + + return nil +} + type Config struct { - CloudURL string `json:"cloud_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - KeyboardLayout string `json:"keyboard_layout"` - KeyboardMappingEnabled bool `json:"keyboard_mapping_enabled"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + CloudURL string `json:"cloud_url"` + CloudAppURL string `json:"cloud_app_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + KeyboardLayout string `json:"keyboard_layout"` + KeyboardMappingEnabled bool `json:"keyboard_mapping_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` + TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" + UsbConfig *usbgadget.Config `json:"usb_config"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", + CloudAppURL: "https://app.jetkvm.com", AutoUpdateEnabled: true, // Set a default value KeyboardLayout: "en-US", KeyboardMappingEnabled: false, + ActiveExtension: "", + KeyboardMacros: []KeyboardMacro{}, + DisplayMaxBrightness: 64, + DisplayDimAfterSec: 120, // 2 minutes + DisplayOffAfterSec: 1800, // 30 minutes + TLSMode: "", + UsbConfig: &usbgadget.Config{ + VendorId: "0x1d6b", //The Linux Foundation + ProductId: "0x0104", //Multifunction Composite Gadget + SerialNumber: "", + Manufacturer: "JetKVM", + Product: "USB Emulation Device", + }, + UsbDevices: &usbgadget.Devices{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, + }, } -var config *Config +var ( + config *Config + configLock = &sync.Mutex{} +) func LoadConfig() { + configLock.Lock() + defer configLock.Unlock() + if config != nil { + logger.Info().Msg("config already loaded, skipping") return } + // load the default config + config = defaultConfig + file, err := os.Open(configPath) if err != nil { - logger.Debug("default config file doesn't exist, using default") - config = defaultConfig + logger.Debug().Msg("default config file doesn't exist, using default") return } defer file.Close() - var loadedConfig Config + // load and merge the default config with the user config + loadedConfig := *defaultConfig if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { - logger.Errorf("config file JSON parsing failed, %v", err) - config = defaultConfig + logger.Warn().Err(err).Msg("config file JSON parsing failed") return } + // merge the user config with the default config + if loadedConfig.UsbConfig == nil { + loadedConfig.UsbConfig = defaultConfig.UsbConfig + } + + if loadedConfig.UsbDevices == nil { + loadedConfig.UsbDevices = defaultConfig.UsbDevices + } + config = &loadedConfig } func SaveConfig() error { + configLock.Lock() + defer configLock.Unlock() + + logger.Trace().Str("path", configPath).Msg("Saving config") + file, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create config file: %w", err) @@ -75,3 +189,9 @@ func SaveConfig() error { return nil } + +func ensureConfigLoaded() { + if config == nil { + LoadConfig() + } +} diff --git a/dev_deploy.sh b/dev_deploy.sh index 72bb6dd..dc18c16 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash +# # Exit immediately if a command exits with a non-zero status set -e @@ -10,17 +12,18 @@ show_help() { echo echo "Optional:" echo " -u, --user Remote username (default: root)" + echo " --skip-ui-build Skip frontend/UI build" echo " --help Display this help message" echo echo "Example:" echo " $0 -r 192.168.0.17" echo " $0 -r 192.168.0.17 -u admin" - exit 0 } # Default values REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" +SKIP_UI_BUILD=false # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -33,6 +36,10 @@ while [[ $# -gt 0 ]]; do REMOTE_USER="$2" shift 2 ;; + --skip-ui-build) + SKIP_UI_BUILD=true + shift + ;; --help) show_help exit 0 @@ -52,17 +59,22 @@ if [ -z "$REMOTE_HOST" ]; then fi # Build the development version on the host -make frontend +if [ "$SKIP_UI_BUILD" = false ]; then + make frontend +fi make build_dev # Change directory to the binary output directory cd bin +# Kill any existing instances of the application +ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + # Copy the binary to the remote host -cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug" +ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app # Deploy and run the application on the remote host -ssh "${REMOTE_USER}@${REMOTE_HOST}" ash < 100 || brightness < 0 { + return errors.New("brightness value out of bounds, must be between 0 and 100") + } + + // Check the display backlight class is available + if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) { + return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware") + } + + // Set the value + bs := []byte(strconv.Itoa(brightness)) + err := os.WriteFile(backlightControlClass, bs, 0644) + if err != nil { + return err + } + + displayLogger.Info().Int("brightness", brightness).Msg("set brightness") + return nil +} + +// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness +// of the display by half of the max brightness. +func tick_displayDim() { + err := setDisplayBrightness(config.DisplayMaxBrightness / 2) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to dim display") + } + + dimTicker.Stop() + + backlightState = 1 +} + +// tick_displayOff() is called when the off ticker expires, it turns off the display +// by setting the brightness to zero. +func tick_displayOff() { + err := setDisplayBrightness(0) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to turn off display") + } + + offTicker.Stop() + + backlightState = 2 +} + +// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display +// last woke, ready for displayTimeoutTick to put the display back in the dim/off states. +// Set force to true to skip the backlight state check, this should be done if altering the tickers. +func wakeDisplay(force bool) { + if backlightState == 0 && !force { + return + } + + // Don't try to wake up if the display is turned off. + if config.DisplayMaxBrightness == 0 { + return + } + + err := setDisplayBrightness(config.DisplayMaxBrightness) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to wake display") + } + + if config.DisplayDimAfterSec != 0 { + dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second) + } + + if config.DisplayOffAfterSec != 0 { + offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second) + } + backlightState = 0 +} + +// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the +// touchscreen interface still works even with LCD dimming/off. +// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight +// control should be hoisted up to jetkvm_native. +func watchTsEvents() { + ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to open touchscreen device") + return + } + + defer ts.Close() + + // This buffer is set to 24 bytes as that's the normal size of events on /dev/input + // Reference: https://www.kernel.org/doc/Documentation/input/input.txt + // This could potentially be set higher, to require multiple events to wake the display. + buf := make([]byte, 24) + for { + _, err := ts.Read(buf) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to read from touchscreen device") + return + } + + wakeDisplay(false) + } +} + +// startBacklightTickers starts the two tickers for dimming and switching off the display +// if they're not already set. This is done separately to the init routine as the "never dim" +// option has the value set to zero, but time.NewTicker only accept positive values. +func startBacklightTickers() { + // Don't start the tickers if the display is switched off. + // Set the display to off if that's the case. + if config.DisplayMaxBrightness == 0 { + _ = setDisplayBrightness(0) + return + } + + if dimTicker == nil && config.DisplayDimAfterSec != 0 { + displayLogger.Info().Msg("dim_ticker has started") + dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) + defer dimTicker.Stop() + + go func() { + for { //nolint:gosimple + select { + case <-dimTicker.C: + tick_displayDim() + } + } + }() + } + + if offTicker == nil && config.DisplayOffAfterSec != 0 { + displayLogger.Info().Msg("off_ticker has started") + offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) + defer offTicker.Stop() + + go func() { + for { //nolint:gosimple + select { + case <-offTicker.C: + tick_displayOff() + } + } + }() + } +} + func init() { + ensureConfigLoaded() + go func() { waitCtrlClientConnected() - fmt.Println("setting initial display contents") + displayLogger.Info().Msg("setting initial display contents") time.Sleep(500 * time.Millisecond) updateStaticContents() displayInited = true - fmt.Println("display inited") + displayLogger.Info().Msg("display inited") + startBacklightTickers() + wakeDisplay(true) requestDisplayUpdate() }() + + go watchTsEvents() } diff --git a/fuse.go b/fuse.go index 6ecc49c..ea50bfd 100644 --- a/fuse.go +++ b/fuse.go @@ -2,7 +2,6 @@ package kvm import ( "context" - "fmt" "os" "sync" "syscall" @@ -104,7 +103,7 @@ func RunFuseServer() { var err error fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts) if err != nil { - fmt.Println("failed to mount fuse: %w", err) + logger.Warn().Err(err).Msg("failed to mount fuse") } fuseServer.Wait() } diff --git a/go.mod b/go.mod index 5ddcfb6..1311a33 100644 --- a/go.mod +++ b/go.mod @@ -1,52 +1,59 @@ -module kvm +module github.com/jetkvm/kvm -go 1.21.0 - -toolchain go1.21.1 +go 1.23.0 require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/beevik/ntp v1.3.1 - github.com/coder/websocket v1.8.12 + github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 - github.com/gin-gonic/gin v1.9.1 + github.com/gin-contrib/logger v1.2.5 + github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf github.com/hanwen/go-fuse/v2 v2.5.1 - github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965 + github.com/hashicorp/go-envparse v0.1.0 github.com/pion/logging v0.2.2 github.com/pion/mdns/v2 v2.0.7 github.com/pion/webrtc/v4 v4.0.0 github.com/pojntfx/go-nbd v0.3.2 + github.com/prometheus/client_golang v1.21.0 + github.com/prometheus/common v0.62.0 github.com/psanford/httpreadat v0.1.0 + github.com/rs/zerolog v1.34.0 github.com/vishvananda/netlink v1.3.0 - golang.org/x/crypto v0.28.0 - golang.org/x/net v0.30.0 + go.bug.st/serial v1.6.2 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.38.0 ) replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pilebones/go-udev v0.9.0 // indirect github.com/pion/datachannel v1.5.9 // indirect github.com/pion/dtls/v3 v3.0.3 // indirect @@ -61,16 +68,16 @@ require ( github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index be21917..565c0cc 100644 --- a/go.sum +++ b/go.sum @@ -2,32 +2,40 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM= github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= +github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -36,12 +44,13 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -49,24 +58,33 @@ github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= @@ -76,10 +94,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965 h1:bZGtUfkOl0dqvem8ltx9KCYied0gSlRuDhaZDxgppN4= -github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965/go.mod h1:6cAIK2c4O3/yETSrRjmNwsBL3yE4Vcu9M9p/Qwx5+gM= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= @@ -114,14 +132,24 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -132,8 +160,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -144,34 +173,33 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/hw.go b/hw.go index efe8f5c..02e4815 100644 --- a/hw.go +++ b/hw.go @@ -14,7 +14,7 @@ func extractSerialNumber() (string, error) { return "", err } - r, err := regexp.Compile("Serial\\s*:\\s*(\\S+)") + r, err := regexp.Compile(`Serial\s*:\s*(\S+)`) if err != nil { return "", fmt.Errorf("failed to compile regex: %w", err) } @@ -27,7 +27,7 @@ func extractSerialNumber() (string, error) { return matches[1], nil } -func readOtpEntropy() ([]byte, error) { +func readOtpEntropy() ([]byte, error) { //nolint:unused content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem") if err != nil { return nil, err @@ -42,7 +42,7 @@ func GetDeviceID() string { deviceIDOnce.Do(func() { serial, err := extractSerialNumber() if err != nil { - logger.Warn("unknown serial number, the program likely not running on RV1106") + logger.Warn().Msg("unknown serial number, the program likely not running on RV1106") deviceID = "unknown_device_id" } else { deviceID = serial @@ -54,7 +54,7 @@ func GetDeviceID() string { func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { - logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err) + logger.Warn().Err(err).Msg("unable to open /dev/watchdog, skipping watchdog reset") return } defer file.Close() @@ -65,13 +65,13 @@ func runWatchdog() { case <-ticker.C: _, err = file.Write([]byte{0}) if err != nil { - logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err) + logger.Warn().Err(err).Msg("error writing to /dev/watchdog, system may reboot") } case <-appCtx.Done(): //disarm watchdog with magic value _, err := file.Write([]byte("V")) if err != nil { - logger.Errorf("failed to disarm watchdog, system may reboot: %v", err) + logger.Warn().Err(err).Msg("failed to disarm watchdog, system may reboot") } return } diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go new file mode 100644 index 0000000..b73d392 --- /dev/null +++ b/internal/usbgadget/config.go @@ -0,0 +1,336 @@ +package usbgadget + +import ( + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "sort" +) + +type gadgetConfigItem struct { + order uint + device string + path []string + attrs gadgetAttributes + configAttrs gadgetAttributes + configPath []string + reportDesc []byte +} + +type gadgetAttributes map[string]string + +type gadgetConfigItemWithKey struct { + key string + item gadgetConfigItem +} + +type orderedGadgetConfigItems []gadgetConfigItemWithKey + +var defaultGadgetConfig = map[string]gadgetConfigItem{ + "base": { + order: 0, + attrs: gadgetAttributes{ + "bcdUSB": "0x0200", // USB 2.0 + "idVendor": "0x1d6b", // The Linux Foundation + "idProduct": "0104", // Multifunction Composite Gadget + "bcdDevice": "0100", + }, + configAttrs: gadgetAttributes{ + "MaxPower": "250", // in unit of 2mA + }, + }, + "base_info": { + order: 1, + path: []string{"strings", "0x409"}, + configPath: []string{"strings", "0x409"}, + attrs: gadgetAttributes{ + "serialnumber": "", + "manufacturer": "JetKVM", + "product": "JetKVM USB Emulation Device", + }, + configAttrs: gadgetAttributes{ + "configuration": "Config 1: HID", + }, + }, + // keyboard HID + "keyboard": keyboardConfig, + // mouse HID + "absolute_mouse": absoluteMouseConfig, + // relative mouse HID + "relative_mouse": relativeMouseConfig, + // mass storage + "mass_storage_base": massStorageBaseConfig, + "mass_storage_lun0": massStorageLun0Config, +} + +func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { + switch itemKey { + case "absolute_mouse": + return u.enabledDevices.AbsoluteMouse + case "relative_mouse": + return u.enabledDevices.RelativeMouse + case "keyboard": + return u.enabledDevices.Keyboard + case "mass_storage_base": + return u.enabledDevices.MassStorage + case "mass_storage_lun0": + return u.enabledDevices.MassStorage + default: + return true + } +} + +func (u *UsbGadget) loadGadgetConfig() { + if u.customConfig.isEmpty { + u.log.Trace().Msg("using default gadget config") + return + } + + u.configMap["base"].attrs["idVendor"] = u.customConfig.VendorId + u.configMap["base"].attrs["idProduct"] = u.customConfig.ProductId + + u.configMap["base_info"].attrs["serialnumber"] = u.customConfig.SerialNumber + u.configMap["base_info"].attrs["manufacturer"] = u.customConfig.Manufacturer + u.configMap["base_info"].attrs["product"] = u.customConfig.Product +} + +func (u *UsbGadget) SetGadgetConfig(config *Config) { + u.configLock.Lock() + defer u.configLock.Unlock() + + if config == nil { + return // nothing to do + } + + u.customConfig = *config + u.loadGadgetConfig() +} + +func (u *UsbGadget) SetGadgetDevices(devices *Devices) { + u.configLock.Lock() + defer u.configLock.Unlock() + + if devices == nil { + return // nothing to do + } + + u.enabledDevices = *devices +} + +// GetConfigPath returns the path to the config item. +func (u *UsbGadget) GetConfigPath(itemKey string) (string, error) { + item, ok := u.configMap[itemKey] + if !ok { + return "", fmt.Errorf("config item %s not found", itemKey) + } + return joinPath(u.kvmGadgetPath, item.configPath), nil +} + +// GetPath returns the path to the item. +func (u *UsbGadget) GetPath(itemKey string) (string, error) { + item, ok := u.configMap[itemKey] + if !ok { + return "", fmt.Errorf("config item %s not found", itemKey) + } + return joinPath(u.kvmGadgetPath, item.path), nil +} + +func mountConfigFS() error { + _, err := os.Stat(gadgetPath) + // TODO: check if it's mounted properly + if err == nil { + return nil + } + + if os.IsNotExist(err) { + err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run() + if err != nil { + return fmt.Errorf("failed to mount configfs: %w", err) + } + } else { + return fmt.Errorf("unable to access usb gadget path: %w", err) + } + return nil +} + +func (u *UsbGadget) Init() error { + u.configLock.Lock() + defer u.configLock.Unlock() + + u.loadGadgetConfig() + + udcs := getUdcs() + if len(udcs) < 1 { + u.log.Error().Msg("no udc found, skipping USB stack init") + return nil + } + + u.udc = udcs[0] + _, err := os.Stat(u.kvmGadgetPath) + if err == nil { + u.log.Info().Msg("usb gadget already exists") + } + + if err := mountConfigFS(); err != nil { + u.log.Error().Err(err).Msg("failed to mount configfs, usb stack might not function properly") + } + + if err := os.MkdirAll(u.configC1Path, 0755); err != nil { + u.log.Error().Err(err).Msg("failed to create config path") + } + + if err := u.writeGadgetConfig(); err != nil { + u.log.Error().Err(err).Msg("failed to start gadget") + } + + return nil +} + +func (u *UsbGadget) UpdateGadgetConfig() error { + u.configLock.Lock() + defer u.configLock.Unlock() + + u.loadGadgetConfig() + + if err := u.writeGadgetConfig(); err != nil { + u.log.Error().Err(err).Msg("failed to update gadget") + } + + return nil +} + +func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems { + items := make([]gadgetConfigItemWithKey, 0) + for key, item := range u.configMap { + items = append(items, gadgetConfigItemWithKey{key, item}) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].item.order < items[j].item.order + }) + + return items +} + +func (u *UsbGadget) writeGadgetConfig() error { + // create kvm gadget path + err := os.MkdirAll(u.kvmGadgetPath, 0755) + if err != nil { + return err + } + + u.log.Trace().Msg("writing gadget config") + for _, val := range u.getOrderedConfigItems() { + key := val.key + item := val.item + + // check if the item is enabled in the config + if !u.isGadgetConfigItemEnabled(key) { + u.log.Trace().Str("key", key).Msg("disabling gadget config") + err = u.disableGadgetItemConfig(item) + if err != nil { + return err + } + continue + } + u.log.Trace().Str("key", key).Msg("writing gadget config") + err = u.writeGadgetItemConfig(item) + if err != nil { + return err + } + } + + if err = u.writeUDC(); err != nil { + u.log.Error().Err(err).Msg("failed to write UDC") + return err + } + + if err = u.rebindUsb(true); err != nil { + u.log.Info().Err(err).Msg("failed to rebind usb") + } + + return nil +} + +func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error { + // remove symlink if exists + if item.configPath == nil { + return nil + } + + configPath := joinPath(u.configC1Path, item.configPath) + + if _, err := os.Lstat(configPath); os.IsNotExist(err) { + u.log.Trace().Str("path", configPath).Msg("symlink does not exist") + return nil + } + + if err := os.Remove(configPath); err != nil { + return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err) + } + + return nil +} + +func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error { + // create directory for the item + gadgetItemPath := joinPath(u.kvmGadgetPath, item.path) + err := os.MkdirAll(gadgetItemPath, 0755) + if err != nil { + return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err) + } + + if len(item.attrs) > 0 { + // write attributes for the item + err = u.writeGadgetAttrs(gadgetItemPath, item.attrs) + if err != nil { + return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err) + } + } + + // write report descriptor if available + if item.reportDesc != nil { + err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644) + if err != nil { + return err + } + } + + // create config directory if configAttrs are set + if len(item.configAttrs) > 0 { + configItemPath := joinPath(u.configC1Path, item.configPath) + err = os.MkdirAll(configItemPath, 0755) + if err != nil { + return fmt.Errorf("failed to create path %s: %w", configItemPath, err) + } + + err = u.writeGadgetAttrs(configItemPath, item.configAttrs) + if err != nil { + return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err) + } + } + + // create symlink if configPath is set + if item.configPath != nil && item.configAttrs == nil { + configPath := joinPath(u.configC1Path, item.configPath) + u.log.Trace().Str("source", configPath).Str("target", gadgetItemPath).Msg("creating symlink") + if err := ensureSymlink(configPath, gadgetItemPath); err != nil { + return err + } + } + + return nil +} + +func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error { + for key, val := range attrs { + filePath := filepath.Join(basePath, key) + err := u.writeIfDifferent(filePath, []byte(val), 0644) + if err != nil { + return fmt.Errorf("failed to write to %s: %w", filePath, err) + } + } + return nil +} diff --git a/internal/usbgadget/consts.go b/internal/usbgadget/consts.go new file mode 100644 index 0000000..8204d0a --- /dev/null +++ b/internal/usbgadget/consts.go @@ -0,0 +1,3 @@ +package usbgadget + +const dwc3Path = "/sys/bus/platform/drivers/dwc3" diff --git a/internal/usbgadget/hid.go b/internal/usbgadget/hid.go new file mode 100644 index 0000000..5faac89 --- /dev/null +++ b/internal/usbgadget/hid.go @@ -0,0 +1,11 @@ +package usbgadget + +import "time" + +func (u *UsbGadget) resetUserInputTime() { + u.lastUserInput = time.Now() +} + +func (u *UsbGadget) GetLastUserInputTime() time.Time { + return u.lastUserInput +} diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go new file mode 100644 index 0000000..de007e4 --- /dev/null +++ b/internal/usbgadget/hid_keyboard.go @@ -0,0 +1,95 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var keyboardConfig = gadgetConfigItem{ + order: 1000, + device: "hid.usb0", + path: []string{"functions", "hid.usb0"}, + configPath: []string{"hid.usb0"}, + attrs: gadgetAttributes{ + "protocol": "1", + "subclass": "1", + "report_length": "8", + }, + reportDesc: keyboardReportDesc, +} + +// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt +var keyboardReportDesc = []byte{ + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x06, /* USAGE (Keyboard) */ + 0xa1, 0x01, /* COLLECTION (Application) */ + 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ + 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ + 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x08, /* REPORT_COUNT (8) */ + 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ + 0x95, 0x05, /* REPORT_COUNT (5) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ + 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x03, /* REPORT_SIZE (3) */ + 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ + 0x95, 0x06, /* REPORT_COUNT (6) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */ + 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ + 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ + 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ + 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ + 0xc0, /* END_COLLECTION */ +} + +func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { + if u.keyboardHidFile == nil { + var err error + u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg0: %w", err) + } + } + + _, err := u.keyboardHidFile.Write(data) + if err != nil { + u.log.Error().Err(err).Msg("failed to write to hidg0") + u.keyboardHidFile.Close() + u.keyboardHidFile = nil + return err + } + + return nil +} + +func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { + u.keyboardLock.Lock() + defer u.keyboardLock.Unlock() + + if len(keys) > 6 { + keys = keys[:6] + } + if len(keys) < 6 { + keys = append(keys, make([]uint8, 6-len(keys))...) + } + + err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]}) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go new file mode 100644 index 0000000..de77b1e --- /dev/null +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -0,0 +1,128 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var absoluteMouseConfig = gadgetConfigItem{ + order: 1001, + device: "hid.usb1", + path: []string{"functions", "hid.usb1"}, + configPath: []string{"hid.usb1"}, + attrs: gadgetAttributes{ + "protocol": "2", + "subclass": "1", + "report_length": "6", + }, + reportDesc: absoluteMouseCombinedReportDesc, +} + +var absoluteMouseCombinedReportDesc = []byte{ + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x02, // Usage (Mouse) + 0xA1, 0x01, // Collection (Application) + + // Report ID 1: Absolute Mouse Movement + 0x85, 0x01, // Report ID (1) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x03, // Usage Maximum (0x03) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x03, // Report Count (3) + 0x81, 0x02, // Input (Data, Var, Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x03, // Input (Cnst, Var, Abs) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x16, 0x00, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x7F, // Logical Maximum (32767) + 0x36, 0x00, 0x00, // Physical Minimum (0) + 0x46, 0xFF, 0x7F, // Physical Maximum (32767) + 0x75, 0x10, // Report Size (16) + 0x95, 0x02, // Report Count (2) + 0x81, 0x02, // Input (Data, Var, Abs) + 0xC0, // End Collection + + // Report ID 2: Relative Wheel Movement + 0x85, 0x02, // Report ID (2) + 0x09, 0x38, // Usage (Wheel) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7F, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x06, // Input (Data, Var, Rel) + + 0xC0, // End Collection +} + +func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { + if u.absMouseHidFile == nil { + var err error + u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg1: %w", err) + } + } + + _, err := u.absMouseHidFile.Write(data) + if err != nil { + u.log.Error().Err(err).Msg("failed to write to hidg1") + u.absMouseHidFile.Close() + u.absMouseHidFile = nil + return err + } + return nil +} + +func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error { + u.absMouseLock.Lock() + defer u.absMouseLock.Unlock() + + err := u.absMouseWriteHidFile([]byte{ + 1, // Report ID 1 + buttons, // Buttons + uint8(x), // X Low Byte + uint8(x >> 8), // X High Byte + uint8(y), // Y Low Byte + uint8(y >> 8), // Y High Byte + }) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} + +func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error { + u.absMouseLock.Lock() + defer u.absMouseLock.Unlock() + + // Accumulate the wheelY value + u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0 + + // Only send a report if the accumulated value is significant + if abs(u.absMouseAccumulatedWheelY) < 1.0 { + return nil + } + + scaledWheelY := int8(u.absMouseAccumulatedWheelY) + + err := u.absMouseWriteHidFile([]byte{ + 2, // Report ID 2 + byte(scaledWheelY), // Scaled Wheel Y (signed) + }) + + // Reset the accumulator, keeping any remainder + u.absMouseAccumulatedWheelY -= float64(scaledWheelY) + + u.resetUserInputTime() + return err +} diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go new file mode 100644 index 0000000..af2d028 --- /dev/null +++ b/internal/usbgadget/hid_mouse_relative.go @@ -0,0 +1,92 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var relativeMouseConfig = gadgetConfigItem{ + order: 1002, + device: "hid.usb2", + path: []string{"functions", "hid.usb2"}, + configPath: []string{"hid.usb2"}, + attrs: gadgetAttributes{ + "protocol": "2", + "subclass": "1", + "report_length": "4", + }, + reportDesc: relativeMouseCombinedReportDesc, +} + +// from: https://github.com/NicoHood/HID/blob/b16be57caef4295c6cd382a7e4c64db5073647f7/src/SingleReport/BootMouse.cpp#L26 +var relativeMouseCombinedReportDesc = []byte{ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 54 + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + + // Pointer and Physical are required by Apple Recovery + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + + // 8 Buttons + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x08, // USAGE_MAXIMUM (Button 8) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + + // X, Y, Wheel + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT (Data,Var,Rel) + + // End + 0xc0, // End Collection (Physical) + 0xc0, // End Collection +} + +func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { + if u.relMouseHidFile == nil { + var err error + u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg1: %w", err) + } + } + + _, err := u.relMouseHidFile.Write(data) + if err != nil { + u.log.Error().Err(err).Msg("failed to write to hidg2") + u.relMouseHidFile.Close() + u.relMouseHidFile = nil + return err + } + return nil +} + +func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error { + u.relMouseLock.Lock() + defer u.relMouseLock.Unlock() + + err := u.relMouseWriteHidFile([]byte{ + buttons, // Buttons + uint8(mx), // X + uint8(my), // Y + 0, // Wheel + }) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go new file mode 100644 index 0000000..f962cb4 --- /dev/null +++ b/internal/usbgadget/mass_storage.go @@ -0,0 +1,23 @@ +package usbgadget + +var massStorageBaseConfig = gadgetConfigItem{ + order: 3000, + device: "mass_storage.usb0", + path: []string{"functions", "mass_storage.usb0"}, + configPath: []string{"mass_storage.usb0"}, + attrs: gadgetAttributes{ + "stall": "1", + }, +} + +var massStorageLun0Config = gadgetConfigItem{ + order: 3001, + path: []string{"functions", "mass_storage.usb0", "lun.0"}, + attrs: gadgetAttributes{ + "cdrom": "1", + "ro": "1", + "removable": "1", + "file": "\n", + "inquiry_string": "JetKVM Virtual Media", + }, +} diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go new file mode 100644 index 0000000..84dfbe4 --- /dev/null +++ b/internal/usbgadget/udc.go @@ -0,0 +1,109 @@ +package usbgadget + +import ( + "fmt" + "os" + "path" + "strings" +) + +func getUdcs() []string { + var udcs []string + + files, err := os.ReadDir("/sys/devices/platform/usbdrd") + if err != nil { + return nil + } + + for _, file := range files { + if !file.IsDir() || !strings.HasSuffix(file.Name(), ".usb") { + continue + } + udcs = append(udcs, file.Name()) + } + + return udcs +} + +func rebindUsb(udc string, ignoreUnbindError bool) error { + err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(udc), 0644) + if err != nil && !ignoreUnbindError { + return err + } + err = os.WriteFile(path.Join(dwc3Path, "bind"), []byte(udc), 0644) + if err != nil { + return err + } + return nil +} + +func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error { + u.log.Info().Str("udc", u.udc).Msg("rebinding USB gadget to UDC") + return rebindUsb(u.udc, ignoreUnbindError) +} + +// RebindUsb rebinds the USB gadget to the UDC. +func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error { + u.configLock.Lock() + defer u.configLock.Unlock() + + return u.rebindUsb(ignoreUnbindError) +} + +func (u *UsbGadget) writeUDC() error { + path := path.Join(u.kvmGadgetPath, "UDC") + + u.log.Trace().Str("udc", u.udc).Str("path", path).Msg("writing UDC") + err := u.writeIfDifferent(path, []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("failed to write UDC: %w", err) + } + + return nil +} + +// GetUsbState returns the current state of the USB gadget +func (u *UsbGadget) GetUsbState() (state string) { + stateFile := path.Join("/sys/class/udc", u.udc, "state") + stateBytes, err := os.ReadFile(stateFile) + if err != nil { + if os.IsNotExist(err) { + return "not attached" + } else { + u.log.Trace().Err(err).Msg("failed to read usb state") + } + return "unknown" + } + return strings.TrimSpace(string(stateBytes)) +} + +// IsUDCBound checks if the UDC state is bound. +func (u *UsbGadget) IsUDCBound() (bool, error) { + udcFilePath := path.Join(dwc3Path, u.udc) + _, err := os.Stat(udcFilePath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("error checking USB emulation state: %w", err) + } + return true, nil +} + +// BindUDC binds the gadget to the UDC. +func (u *UsbGadget) BindUDC() error { + err := os.WriteFile(path.Join(dwc3Path, "bind"), []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("error binding UDC: %w", err) + } + return nil +} + +// UnbindUDC unbinds the gadget from the UDC. +func (u *UsbGadget) UnbindUDC() error { + err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("error unbinding UDC: %w", err) + } + return nil +} diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go new file mode 100644 index 0000000..1dff2f3 --- /dev/null +++ b/internal/usbgadget/usbgadget.go @@ -0,0 +1,110 @@ +// Package usbgadget provides a high-level interface to manage USB gadgets +// THIS PACKAGE IS FOR INTERNAL USE ONLY AND ITS API MAY CHANGE WITHOUT NOTICE +package usbgadget + +import ( + "os" + "path" + "sync" + "time" + + "github.com/rs/zerolog" +) + +// Devices is a struct that represents the USB devices that can be enabled on a USB gadget. +type Devices struct { + AbsoluteMouse bool `json:"absolute_mouse"` + RelativeMouse bool `json:"relative_mouse"` + Keyboard bool `json:"keyboard"` + MassStorage bool `json:"mass_storage"` +} + +// Config is a struct that represents the customizations for a USB gadget. +// TODO: rename to something else that won't confuse with the USB gadget configuration +type Config struct { + VendorId string `json:"vendor_id"` + ProductId string `json:"product_id"` + SerialNumber string `json:"serial_number"` + Manufacturer string `json:"manufacturer"` + Product string `json:"product"` + + isEmpty bool +} + +var defaultUsbGadgetDevices = Devices{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, +} + +// UsbGadget is a struct that represents a USB gadget. +type UsbGadget struct { + name string + udc string + kvmGadgetPath string + configC1Path string + + configMap map[string]gadgetConfigItem + customConfig Config + + configLock sync.Mutex + + keyboardHidFile *os.File + keyboardLock sync.Mutex + absMouseHidFile *os.File + absMouseLock sync.Mutex + relMouseHidFile *os.File + relMouseLock sync.Mutex + + enabledDevices Devices + + absMouseAccumulatedWheelY float64 + + lastUserInput time.Time + + log *zerolog.Logger +} + +const configFSPath = "/sys/kernel/config" +const gadgetPath = "/sys/kernel/config/usb_gadget" + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +// NewUsbGadget creates a new UsbGadget. +func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget { + if logger == nil { + logger = &defaultLogger + } + + if enabledDevices == nil { + enabledDevices = &defaultUsbGadgetDevices + } + + if config == nil { + config = &Config{isEmpty: true} + } + + g := &UsbGadget{ + name: name, + kvmGadgetPath: path.Join(gadgetPath, name), + configC1Path: path.Join(gadgetPath, name, "configs/c.1"), + configMap: defaultGadgetConfig, + customConfig: *config, + configLock: sync.Mutex{}, + keyboardLock: sync.Mutex{}, + absMouseLock: sync.Mutex{}, + relMouseLock: sync.Mutex{}, + enabledDevices: *enabledDevices, + lastUserInput: time.Now(), + log: logger, + + absMouseAccumulatedWheelY: 0, + } + if err := g.Init(); err != nil { + logger.Error().Err(err).Msg("failed to init USB gadget") + return nil + } + + return g +} diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go new file mode 100644 index 0000000..0e796c8 --- /dev/null +++ b/internal/usbgadget/utils.go @@ -0,0 +1,63 @@ +package usbgadget + +import ( + "bytes" + "fmt" + "os" + "path/filepath" +) + +// Helper function to get absolute value of float64 +func abs(x float64) float64 { + if x < 0 { + return -x + } + return x +} + +func joinPath(basePath string, paths []string) string { + pathArr := append([]string{basePath}, paths...) + return filepath.Join(pathArr...) +} + +func ensureSymlink(linkPath string, target string) error { + if _, err := os.Lstat(linkPath); err == nil { + currentTarget, err := os.Readlink(linkPath) + if err != nil || currentTarget != target { + err = os.Remove(linkPath) + if err != nil { + return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err) + } + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to check if symlink exists: %w", err) + } + + if err := os.Symlink(target, linkPath); err != nil { + return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err) + } + + return nil +} + +func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error { + if _, err := os.Stat(filePath); err == nil { + oldContent, err := os.ReadFile(filePath) + if err == nil { + if bytes.Equal(oldContent, content) { + u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content") + return nil + } + + if len(oldContent) == len(content)+1 && + bytes.Equal(oldContent[:len(content)], content) && + oldContent[len(content)] == 10 { + u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content") + return nil + } + + u.log.Trace().Str("path", filePath).Bytes("old", oldContent).Bytes("new", content).Msg("writing to as it has different content") + } + } + return os.WriteFile(filePath, content, permMode) +} diff --git a/internal/websecure/log.go b/internal/websecure/log.go new file mode 100644 index 0000000..f45767e --- /dev/null +++ b/internal/websecure/log.go @@ -0,0 +1,9 @@ +package websecure + +import ( + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger() diff --git a/internal/websecure/selfsign.go b/internal/websecure/selfsign.go new file mode 100644 index 0000000..77efa37 --- /dev/null +++ b/internal/websecure/selfsign.go @@ -0,0 +1,191 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "net" + "strings" + "time" + + "github.com/rs/zerolog" + "golang.org/x/net/idna" +) + +const selfSignerCAMagicName = "__ca__" + +type SelfSigner struct { + store *CertStore + log *zerolog.Logger + + caInfo pkix.Name + + DefaultDomain string + DefaultOrg string + DefaultOU string +} + +func NewSelfSigner( + store *CertStore, + log *zerolog.Logger, + defaultDomain, + defaultOrg, + defaultOU, + caName string, +) *SelfSigner { + return &SelfSigner{ + store: store, + log: log, + DefaultDomain: defaultDomain, + DefaultOrg: defaultOrg, + DefaultOU: defaultOU, + caInfo: pkix.Name{ + CommonName: caName, + Organization: []string{defaultOrg}, + OrganizationalUnit: []string{defaultOU}, + }, + } +} + +func (s *SelfSigner) getCA() *tls.Certificate { + return s.createSelfSignedCert(selfSignerCAMagicName) +} + +func (s *SelfSigner) createSelfSignedCert(hostname string) *tls.Certificate { + if tlsCert := s.store.certificates[hostname]; tlsCert != nil { + return tlsCert + } + + // check if hostname is the CA magic name + var ca *tls.Certificate + if hostname != selfSignerCAMagicName { + ca = s.getCA() + if ca == nil { + s.log.Error().Msg("Failed to get CA certificate") + return nil + } + } + + s.log.Info().Str("hostname", hostname).Msg("Creating self-signed certificate") + + // lock the store while creating the certificate (do not move upwards) + s.store.certLock.Lock() + defer s.store.certLock.Unlock() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate private key") + return nil + } + + notBefore := time.Now() + notAfter := notBefore.AddDate(1, 0, 0) + + serialNumber, err := generateSerialNumber() + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate serial number") + return nil + } + + dnsName := hostname + ip := net.ParseIP(hostname) + if ip != nil { + dnsName = s.DefaultDomain + } + + // set up CSR + isCA := hostname == selfSignerCAMagicName + subject := pkix.Name{ + CommonName: hostname, + Organization: []string{s.DefaultOrg}, + OrganizationalUnit: []string{s.DefaultOU}, + } + keyUsage := x509.KeyUsageDigitalSignature + extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + + // check if hostname is the CA magic name, and if so, set the subject to the CA info + if isCA { + subject = s.caInfo + keyUsage |= x509.KeyUsageCertSign + extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) + notAfter = notBefore.AddDate(10, 0, 0) + } + + cert := x509.Certificate{ + SerialNumber: serialNumber, + Subject: subject, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: isCA, + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + BasicConstraintsValid: true, + } + + // set up DNS names and IP addresses + if !isCA { + cert.DNSNames = []string{dnsName} + if ip != nil { + cert.IPAddresses = []net.IP{ip} + } + } + + // set up parent certificate + parent := &cert + parentPriv := priv + if ca != nil { + parent, err = x509.ParseCertificate(ca.Certificate[0]) + if err != nil { + s.log.Error().Err(err).Msg("Failed to parse parent certificate") + return nil + } + parentPriv = ca.PrivateKey.(*ecdsa.PrivateKey) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &cert, parent, &priv.PublicKey, parentPriv) + if err != nil { + s.log.Error().Err(err).Msg("Failed to create certificate") + return nil + } + + tlsCert := &tls.Certificate{ + Certificate: [][]byte{certBytes}, + PrivateKey: priv, + } + if ca != nil { + tlsCert.Certificate = append(tlsCert.Certificate, ca.Certificate...) + } + + s.store.certificates[hostname] = tlsCert + s.store.saveCertificate(hostname) + + return tlsCert +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *SelfSigner) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + var hostname string + if info.ServerName != "" && info.ServerName != selfSignerCAMagicName { + hostname = info.ServerName + } else { + hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0] + } + + s.log.Info().Str("hostname", hostname).Strs("supported_protos", info.SupportedProtos).Msg("TLS handshake") + + // convert hostname to punycode + h, err := idna.Lookup.ToASCII(hostname) + if err != nil { + s.log.Warn().Str("hostname", hostname).Err(err).Str("remote_addr", info.Conn.RemoteAddr().String()).Msg("Hostname is not valid") + hostname = s.DefaultDomain + } else { + hostname = h + } + + cert := s.createSelfSignedCert(hostname) + return cert, nil +} diff --git a/internal/websecure/store.go b/internal/websecure/store.go new file mode 100644 index 0000000..7da2dee --- /dev/null +++ b/internal/websecure/store.go @@ -0,0 +1,175 @@ +package websecure + +import ( + "crypto/tls" + "fmt" + "os" + "path" + "strings" + "sync" + + "github.com/rs/zerolog" +) + +type CertStore struct { + certificates map[string]*tls.Certificate + certLock *sync.Mutex + + storePath string + + log *zerolog.Logger +} + +func NewCertStore(storePath string, log *zerolog.Logger) *CertStore { + if log == nil { + log = &defaultLogger + } + + return &CertStore{ + certificates: make(map[string]*tls.Certificate), + certLock: &sync.Mutex{}, + + storePath: storePath, + log: log, + } +} + +func (s *CertStore) ensureStorePath() error { + // check if directory exists + stat, err := os.Stat(s.storePath) + if err == nil { + if stat.IsDir() { + return nil + } + + return fmt.Errorf("TLS store path exists but is not a directory: %s", s.storePath) + } + + if os.IsNotExist(err) { + s.log.Trace().Str("path", s.storePath).Msg("TLS store directory does not exist, creating directory") + err = os.MkdirAll(s.storePath, 0755) + if err != nil { + return fmt.Errorf("Failed to create TLS store path: %w", err) + } + return nil + } + + return fmt.Errorf("Failed to check TLS store path: %w", err) +} + +func (s *CertStore) LoadCertificates() { + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + files, err := os.ReadDir(s.storePath) + if err != nil { + s.log.Error().Err(err).Msg("Failed to read TLS directory") + return + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if strings.HasSuffix(file.Name(), ".crt") { + s.loadCertificate(strings.TrimSuffix(file.Name(), ".crt")) + } + } +} + +func (s *CertStore) loadCertificate(hostname string) { + s.certLock.Lock() + defer s.certLock.Unlock() + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + cert, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + s.log.Error().Err(err).Str("hostname", hostname).Msg("Failed to load certificate") + return + } + + s.certificates[hostname] = &cert + + s.log.Info().Str("hostname", hostname).Msg("Loaded certificate") +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *CertStore) GetCertificate(hostname string) *tls.Certificate { + s.certLock.Lock() + defer s.certLock.Unlock() + + return s.certificates[hostname] +} + +// ValidateAndSaveCertificate validates the certificate and saves it to the store +// returns are: +// - error: if the certificate is invalid or if there's any error during saving the certificate +// - error: if there's any warning or error during saving the certificate +func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) { + tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return fmt.Errorf("Failed to parse certificate: %w", err), nil + } + + // this can be skipped as current implementation supports one custom certificate only + if tlsCert.Leaf != nil { + // add recover to avoid panic + defer func() { + if r := recover(); r != nil { + s.log.Error().Interface("recovered", r).Msg("Failed to verify hostname") + } + }() + + if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil { + if !ignoreWarning { + return nil, fmt.Errorf("Certificate does not match hostname: %w", err) + } + s.log.Warn().Err(err).Msg("Certificate does not match hostname") + } + } + + s.certLock.Lock() + s.certificates[hostname] = &tlsCert + s.certLock.Unlock() + + s.saveCertificate(hostname) + + return nil, nil +} + +func (s *CertStore) saveCertificate(hostname string) { + // check if certificate already exists + tlsCert := s.certificates[hostname] + if tlsCert == nil { + s.log.Error().Str("hostname", hostname).Msg("Certificate for hostname does not exist, skipping saving certificate") + return + } + + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + if err := keyToFile(tlsCert, keyFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save key file") + return + } + + if err := certToFile(tlsCert, crtFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save certificate") + return + } + + s.log.Info().Str("hostname", hostname).Msg("Saved certificate") +} diff --git a/internal/websecure/utils.go b/internal/websecure/utils.go new file mode 100644 index 0000000..de29c73 --- /dev/null +++ b/internal/websecure/utils.go @@ -0,0 +1,80 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + "os" +) + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 4096) + +func withSecretFile(filename string, f func(*os.File) error) error { + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer file.Close() + + return f(file) +} + +func keyToFile(cert *tls.Certificate, filename string) error { + var keyBlock pem.Block + switch k := cert.PrivateKey.(type) { + case *rsa.PrivateKey: + keyBlock = pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + } + case *ecdsa.PrivateKey: + b, e := x509.MarshalECPrivateKey(k) + if e != nil { + return fmt.Errorf("Failed to marshal EC private key: %v", e) + } + + keyBlock = pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: b, + } + default: + return fmt.Errorf("Unknown private key type: %T", k) + } + + err := withSecretFile(filename, func(file *os.File) error { + return pem.Encode(file, &keyBlock) + }) + + if err != nil { + return fmt.Errorf("Failed to save private key: %w", err) + } + + return nil +} + +func certToFile(cert *tls.Certificate, filename string) error { + return withSecretFile(filename, func(file *os.File) error { + for _, c := range cert.Certificate { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: c, + } + + err := pem.Encode(file, &block) + if err != nil { + return fmt.Errorf("Failed to save certificate: %w", err) + } + } + + return nil + }) +} + +func generateSerialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, serialNumberLimit) +} diff --git a/jiggler.go b/jiggler.go index 06f2b6c..94f7d15 100644 --- a/jiggler.go +++ b/jiggler.go @@ -6,10 +6,6 @@ import ( var lastUserInput = time.Now() -func resetUserInputTime() { - lastUserInput = time.Now() -} - var jigglerEnabled = false func rpcSetJigglerState(enabled bool) { @@ -20,6 +16,8 @@ func rpcGetJigglerState() bool { } func init() { + ensureConfigLoaded() + go runJiggler() } @@ -30,11 +28,11 @@ func runJiggler() { //TODO: change to rel mouse err := rpcAbsMouseReport(1, 1, 0) if err != nil { - logger.Warnf("Failed to jiggle mouse: %v", err) + logger.Warn().Err(err).Msg("Failed to jiggle mouse") } err = rpcAbsMouseReport(0, 0, 0) if err != nil { - logger.Warnf("Failed to reset mouse position: %v", err) + logger.Warn().Err(err).Msg("Failed to reset mouse position") } } } diff --git a/jsonrpc.go b/jsonrpc.go index c859fc5..c2dfbec 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -5,13 +5,17 @@ import ( "encoding/json" "errors" "fmt" - "log" "os" "os/exec" "path/filepath" "reflect" + "strconv" + "time" "github.com/pion/webrtc/v4" + "go.bug.st/serial" + + "github.com/jetkvm/kvm/internal/usbgadget" ) type JSONRPCRequest struct { @@ -34,15 +38,21 @@ type JSONRPCEvent struct { Params interface{} `json:"params,omitempty"` } +type BacklightSettings struct { + MaxBrightness int `json:"max_brightness"` + DimAfter int `json:"dim_after"` + OffAfter int `json:"off_after"` +} + func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { - log.Println("Error marshalling JSONRPC response:", err) + logger.Warn().Err(err).Msg("Error marshalling JSONRPC response") return } err = session.RPCChannel.SendText(string(responseBytes)) if err != nil { - log.Println("Error sending JSONRPC response:", err) + logger.Warn().Err(err).Msg("Error sending JSONRPC response") return } } @@ -55,16 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) { } requestBytes, err := json.Marshal(request) if err != nil { - log.Println("Error marshalling JSONRPC event:", err) + logger.Warn().Err(err).Msg("Error marshalling JSONRPC event") return } if session == nil || session.RPCChannel == nil { - log.Println("RPC channel not available") + logger.Info().Msg("RPC channel not available") return } err = session.RPCChannel.SendText(string(requestBytes)) if err != nil { - log.Println("Error sending JSONRPC event:", err) + logger.Warn().Err(err).Msg("Error sending JSONRPC event") return } } @@ -85,7 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } - //log.Printf("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID) + logger.Trace().Str("method", request.Method).Interface("params", request.Params).Interface("id", request.ID).Msg("Received RPC request") handler, ok := rpcHandlers[request.Method] if !ok { errorResponse := JSONRPCResponse{ @@ -100,6 +110,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + logger.Trace().Str("method", request.Method).Interface("id", request.ID).Msg("Calling RPC handler") result, err := callRPCHandler(handler, request.Params) if err != nil { errorResponse := JSONRPCResponse{ @@ -115,6 +126,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + logger.Trace().Interface("result", result).Interface("id", request.ID).Msg("RPC handler returned") response := JSONRPCResponse{ JSONRPC: "2.0", Result: result, @@ -155,6 +167,30 @@ func rpcSetKeyboardMappingState(enabled bool) (bool, error) { return enabled, nil } +func rpcReboot(force bool) error { + logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") + + args := []string{} + if force { + args = append(args, "-f") + } + + cmd := exec.Command("reboot", args...) + err := cmd.Start() + if err != nil { + logger.Error().Err(err).Msg("failed to reboot") + return fmt.Errorf("failed to reboot: %w", err) + } + + // If the reboot command is successful, exit the program after 5 seconds + go func() { + time.Sleep(5 * time.Second) + os.Exit(0) + }() + + return nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -162,7 +198,7 @@ func rpcGetStreamQualityFactor() (float64, error) { } func rpcSetStreamQualityFactor(factor float64) error { - log.Printf("Setting stream quality factor to: %f", factor) + logger.Info().Float64("factor", factor).Msg("Setting stream quality factor") var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor}) if err != nil { return err @@ -198,15 +234,19 @@ func rpcGetEDID() (string, error) { func rpcSetEDID(edid string) error { if edid == "" { - log.Println("Restoring EDID to default") + logger.Info().Msg("Restoring EDID to default") edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" } else { - log.Printf("Setting EDID to: %s", edid) + logger.Info().Str("edid", edid).Msg("Setting EDID") } _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid}) if err != nil { return err } + + // Save EDID to config, allowing it to be restored on reboot. + config.EdidString = edid + _ = SaveConfig() return nil } @@ -237,12 +277,58 @@ func rpcTryUpdate() error { go func() { err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Warnf("failed to try update: %v", err) + logger.Warn().Err(err).Msg("failed to try update") } }() return nil } +func rpcSetBacklightSettings(params BacklightSettings) error { + blConfig := params + + // NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with. + if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 { + return fmt.Errorf("maxBrightness must be between 0 and 255") + } + + if blConfig.DimAfter < 0 { + return fmt.Errorf("dimAfter must be a positive integer") + } + + if blConfig.OffAfter < 0 { + return fmt.Errorf("offAfter must be a positive integer") + } + + config.DisplayMaxBrightness = blConfig.MaxBrightness + config.DisplayDimAfterSec = blConfig.DimAfter + config.DisplayOffAfterSec = blConfig.OffAfter + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + logger.Info().Int("max_brightness", config.DisplayMaxBrightness).Int("dim_after", config.DisplayDimAfterSec).Int("off_after", config.DisplayOffAfterSec).Msg("rpc: display: settings applied") + + // If the device started up with auto-dim and/or auto-off set to zero, the display init + // method will not have started the tickers. So in case that has changed, attempt to start the tickers now. + startBacklightTickers() + + // Wake the display after the settings are altered, this ensures the tickers + // are reset to the new settings, and will bring the display up to maxBrightness. + // Calling with force set to true, to ignore the current state of the display, and force + // it to reset the tickers. + wakeDisplay(true) + return nil +} + +func rpcGetBacklightSettings() (*BacklightSettings, error) { + return &BacklightSettings{ + MaxBrightness: config.DisplayMaxBrightness, + DimAfter: int(config.DisplayDimAfterSec), + OffAfter: int(config.DisplayOffAfterSec), + }, nil +} + const ( devModeFile = "/userdata/jetkvm/devmode.enable" sshKeyDir = "/userdata/dropbear/.ssh" @@ -282,7 +368,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to create devmode file: %w", err) } } else { - logger.Debug("dev mode already enabled") + logger.Debug().Msg("dev mode already enabled") return nil } } else { @@ -291,7 +377,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to remove devmode file: %w", err) } } else if os.IsNotExist(err) { - logger.Debug("dev mode already disabled") + logger.Debug().Msg("dev mode already disabled") return nil } else { return fmt.Errorf("error checking dev mode file: %w", err) @@ -301,7 +387,7 @@ func rpcSetDevModeState(enabled bool) error { cmd := exec.Command("dropbear.sh") output, err := cmd.CombinedOutput() if err != nil { - logger.Warnf("Failed to start/stop SSH: %v, %v", err, output) + logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH") return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect") } @@ -339,6 +425,23 @@ func rpcSetSSHKeyState(sshKey string) error { return nil } +func rpcGetTLSState() TLSState { + return getTLSState() +} + +func rpcSetTLSState(state TLSState) error { + err := setTLSState(state) + if err != nil { + return fmt.Errorf("failed to set TLS state: %w", err) + } + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) { handlerValue := reflect.ValueOf(handler.Func) handlerType := handlerValue.Type() @@ -403,7 +506,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac } args[i] = reflect.ValueOf(newStruct).Elem() } else { - return nil, fmt.Errorf("invalid parameter type for: %s", paramName) + return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind()) } } else { args[i] = convertedValue.Convert(paramType) @@ -442,23 +545,23 @@ type RPCHandler struct { } func rpcSetMassStorageMode(mode string) (string, error) { - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") var cdrom bool if mode == "cdrom" { cdrom = true } else if mode != "file" { - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode) + logger.Info().Str("mode", mode).Msg("Invalid mode provided") return "", fmt.Errorf("invalid mode: %s", mode) } - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") err := setMassStorageMode(cdrom) if err != nil { return "", fmt.Errorf("failed to set mass storage mode: %w", err) } - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode) + logger.Info().Str("mode", mode).Msg("Mass storage mode set") // Get the updated mode after setting return rpcGetMassStorageMode() @@ -481,29 +584,31 @@ func rpcIsUpdatePending() (bool, error) { return IsUpdatePending(), nil } -var udcFilePath = filepath.Join("/sys/bus/platform/drivers/dwc3", udc) - func rpcGetUsbEmulationState() (bool, error) { - _, err := os.Stat(udcFilePath) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, fmt.Errorf("error checking USB emulation state: %w", err) - } - return true, nil + return gadget.IsUDCBound() } func rpcSetUsbEmulationState(enabled bool) error { if enabled { - return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644) + return gadget.BindUDC() } else { - return os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644) + return gadget.UnbindUDC() } } -func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { +func rpcGetUsbConfig() (usbgadget.Config, error) { LoadConfig() + return *config.UsbConfig, nil +} + +func rpcSetUsbConfig(usbConfig usbgadget.Config) error { + LoadConfig() + config.UsbConfig = &usbConfig + gadget.SetGadgetConfig(config.UsbConfig) + return updateUsbRelatedConfig() +} + +func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { if config.WakeOnLanDevices == nil { return []WakeOnLanDevice{}, nil } @@ -515,30 +620,352 @@ type SetWakeOnLanDevicesParams struct { } func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error { - LoadConfig() config.WakeOnLanDevices = params.Devices return SaveConfig() } func rpcResetConfig() error { - LoadConfig() config = defaultConfig if err := SaveConfig(); err != nil { return fmt.Errorf("failed to reset config: %w", err) } - log.Println("Configuration reset to default") + logger.Info().Msg("Configuration reset to default") return nil } -// TODO: replace this crap with code generator +type DCPowerState struct { + IsOn bool `json:"isOn"` + Voltage float64 `json:"voltage"` + Current float64 `json:"current"` + Power float64 `json:"power"` +} + +func rpcGetDCPowerState() (DCPowerState, error) { + return dcState, nil +} + +func rpcSetDCPowerState(enabled bool) error { + logger.Info().Bool("enabled", enabled).Msg("Setting DC power state") + err := setDCPowerState(enabled) + if err != nil { + return fmt.Errorf("failed to set DC power state: %w", err) + } + return nil +} + +func rpcGetActiveExtension() (string, error) { + return config.ActiveExtension, nil +} + +func rpcSetActiveExtension(extensionId string) error { + if config.ActiveExtension == extensionId { + return nil + } + if config.ActiveExtension == "atx-power" { + _ = unmountATXControl() + } else if config.ActiveExtension == "dc-power" { + _ = unmountDCControl() + } + config.ActiveExtension = extensionId + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + if extensionId == "atx-power" { + _ = mountATXControl() + } else if extensionId == "dc-power" { + _ = mountDCControl() + } + return nil +} + +func rpcSetATXPowerAction(action string) error { + logger.Debug().Str("action", action).Msg("Executing ATX power action") + switch action { + case "power-short": + logger.Debug().Msg("Simulating short power button press") + return pressATXPowerButton(200 * time.Millisecond) + case "power-long": + logger.Debug().Msg("Simulating long power button press") + return pressATXPowerButton(5 * time.Second) + case "reset": + logger.Debug().Msg("Simulating reset button press") + return pressATXResetButton(200 * time.Millisecond) + default: + return fmt.Errorf("invalid action: %s", action) + } +} + +type ATXState struct { + Power bool `json:"power"` + HDD bool `json:"hdd"` +} + +func rpcGetATXState() (ATXState, error) { + state := ATXState{ + Power: ledPWRState, + HDD: ledHDDState, + } + return state, nil +} + +type SerialSettings struct { + BaudRate string `json:"baudRate"` + DataBits string `json:"dataBits"` + StopBits string `json:"stopBits"` + Parity string `json:"parity"` +} + +func rpcGetSerialSettings() (SerialSettings, error) { + settings := SerialSettings{ + BaudRate: strconv.Itoa(serialPortMode.BaudRate), + DataBits: strconv.Itoa(serialPortMode.DataBits), + StopBits: "1", + Parity: "none", + } + + switch serialPortMode.StopBits { + case serial.OneStopBit: + settings.StopBits = "1" + case serial.OnePointFiveStopBits: + settings.StopBits = "1.5" + case serial.TwoStopBits: + settings.StopBits = "2" + } + + switch serialPortMode.Parity { + case serial.NoParity: + settings.Parity = "none" + case serial.OddParity: + settings.Parity = "odd" + case serial.EvenParity: + settings.Parity = "even" + case serial.MarkParity: + settings.Parity = "mark" + case serial.SpaceParity: + settings.Parity = "space" + } + + return settings, nil +} + +var serialPortMode = defaultMode + +func rpcSetSerialSettings(settings SerialSettings) error { + baudRate, err := strconv.Atoi(settings.BaudRate) + if err != nil { + return fmt.Errorf("invalid baud rate: %v", err) + } + dataBits, err := strconv.Atoi(settings.DataBits) + if err != nil { + return fmt.Errorf("invalid data bits: %v", err) + } + + var stopBits serial.StopBits + switch settings.StopBits { + case "1": + stopBits = serial.OneStopBit + case "1.5": + stopBits = serial.OnePointFiveStopBits + case "2": + stopBits = serial.TwoStopBits + default: + return fmt.Errorf("invalid stop bits: %s", settings.StopBits) + } + + var parity serial.Parity + switch settings.Parity { + case "none": + parity = serial.NoParity + case "odd": + parity = serial.OddParity + case "even": + parity = serial.EvenParity + case "mark": + parity = serial.MarkParity + case "space": + parity = serial.SpaceParity + default: + return fmt.Errorf("invalid parity: %s", settings.Parity) + } + serialPortMode = &serial.Mode{ + BaudRate: baudRate, + DataBits: dataBits, + StopBits: stopBits, + Parity: parity, + } + + _ = port.SetMode(serialPortMode) + + return nil +} + +func rpcGetUsbDevices() (usbgadget.Devices, error) { + return *config.UsbDevices, nil +} + +func updateUsbRelatedConfig() error { + if err := gadget.UpdateGadgetConfig(); err != nil { + return fmt.Errorf("failed to write gadget config: %w", err) + } + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { + config.UsbDevices = &usbDevices + gadget.SetGadgetDevices(config.UsbDevices) + return updateUsbRelatedConfig() +} + +func rpcSetUsbDeviceState(device string, enabled bool) error { + switch device { + case "absoluteMouse": + config.UsbDevices.AbsoluteMouse = enabled + case "relativeMouse": + config.UsbDevices.RelativeMouse = enabled + case "keyboard": + config.UsbDevices.Keyboard = enabled + case "massStorage": + config.UsbDevices.MassStorage = enabled + default: + return fmt.Errorf("invalid device: %s", device) + } + gadget.SetGadgetDevices(config.UsbDevices) + return updateUsbRelatedConfig() +} + +func rpcSetCloudUrl(apiUrl string, appUrl string) error { + currentCloudURL := config.CloudURL + config.CloudURL = apiUrl + config.CloudAppURL = appUrl + + if currentCloudURL != apiUrl { + disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl)) + } + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + +var currentScrollSensitivity string = "default" + +func rpcGetScrollSensitivity() (string, error) { + return currentScrollSensitivity, nil +} + +func rpcSetScrollSensitivity(sensitivity string) error { + currentScrollSensitivity = sensitivity + return nil +} + +func getKeyboardMacros() (interface{}, error) { + macros := make([]KeyboardMacro, len(config.KeyboardMacros)) + copy(macros, config.KeyboardMacros) + + return macros, nil +} + +type KeyboardMacrosParams struct { + Macros []interface{} `json:"macros"` +} + +func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { + if params.Macros == nil { + return nil, fmt.Errorf("missing or invalid macros parameter") + } + + newMacros := make([]KeyboardMacro, 0, len(params.Macros)) + + for i, item := range params.Macros { + macroMap, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid macro at index %d", i) + } + + id, _ := macroMap["id"].(string) + if id == "" { + id = fmt.Sprintf("macro-%d", time.Now().UnixNano()) + } + + name, _ := macroMap["name"].(string) + + sortOrder := i + 1 + if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok { + sortOrder = int(sortOrderFloat) + } + + steps := []KeyboardMacroStep{} + if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + for _, stepItem := range stepsArray { + stepMap, ok := stepItem.(map[string]interface{}) + if !ok { + continue + } + + step := KeyboardMacroStep{} + + if keysArray, ok := stepMap["keys"].([]interface{}); ok { + for _, k := range keysArray { + if keyStr, ok := k.(string); ok { + step.Keys = append(step.Keys, keyStr) + } + } + } + + if modsArray, ok := stepMap["modifiers"].([]interface{}); ok { + for _, m := range modsArray { + if modStr, ok := m.(string); ok { + step.Modifiers = append(step.Modifiers, modStr) + } + } + } + + if delay, ok := stepMap["delay"].(float64); ok { + step.Delay = int(delay) + } + + steps = append(steps, step) + } + } + + macro := KeyboardMacro{ + ID: id, + Name: name, + Steps: steps, + SortOrder: sortOrder, + } + + if err := macro.Validate(); err != nil { + return nil, fmt.Errorf("invalid macro at index %d: %w", i, err) + } + + newMacros = append(newMacros, macro) + } + + config.KeyboardMacros = newMacros + + if err := SaveConfig(); err != nil { + return nil, err + } + + return nil, nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, "getVideoState": {Func: rpcGetVideoState}, "getUSBState": {Func: rpcGetUSBState}, @@ -565,11 +992,15 @@ var rpcHandlers = map[string]RPCHandler{ "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState}, "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, "getMassStorageMode": {Func: rpcGetMassStorageMode}, "isUpdatePending": {Func: rpcIsUpdatePending}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "getStorageSpace": {Func: rpcGetStorageSpace}, @@ -582,4 +1013,22 @@ var rpcHandlers = map[string]RPCHandler{ "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, + "getDCPowerState": {Func: rpcGetDCPowerState}, + "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, + "getActiveExtension": {Func: rpcGetActiveExtension}, + "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, + "getATXState": {Func: rpcGetATXState}, + "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, + "getSerialSettings": {Func: rpcGetSerialSettings}, + "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, + "getUsbDevices": {Func: rpcGetUsbDevices}, + "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, + "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, + "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, + "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, } diff --git a/log.go b/log.go index 89ad1d2..5dac1f6 100644 --- a/log.go +++ b/log.go @@ -1,8 +1,170 @@ package kvm -import "github.com/pion/logging" +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" -// we use logging framework from pion -// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC -var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm") -var usbLogger = logging.NewDefaultLoggerFactory().NewLogger("usb") + "github.com/pion/logging" + "github.com/rs/zerolog" +) + +var ( + defaultLogOutput io.Writer = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + PartsOrder: []string{"time", "level", "scope", "component", "message"}, + FieldsExclude: []string{"scope", "component"}, + FormatPartValueByName: func(value interface{}, name string) string { + val := fmt.Sprintf("%s", value) + if name == "component" { + if value == nil { + return "-" + } + } + return val + }, + } + defaultLogLevel = zerolog.ErrorLevel + rootLogger = zerolog.New(defaultLogOutput).With(). + Str("scope", "jetkvm"). + Timestamp(). + Stack(). + Logger() +) + +var ( + scopeLevels map[string]zerolog.Level + scopeLevelMutex = sync.Mutex{} +) + +var ( + logger = getLogger("jetkvm") + cloudLogger = getLogger("cloud") + websocketLogger = getLogger("websocket") + nativeLogger = getLogger("native") + ntpLogger = getLogger("ntp") + displayLogger = getLogger("display") + usbLogger = getLogger("usb") + ginLogger = getLogger("gin") + websecureLogger = getLogger("websecure") +) + +func updateLogLevel() { + scopeLevelMutex.Lock() + defer scopeLevelMutex.Unlock() + + logLevels := map[string]zerolog.Level{ + "DISABLE": zerolog.Disabled, + "NOLEVEL": zerolog.NoLevel, + "PANIC": zerolog.PanicLevel, + "FATAL": zerolog.FatalLevel, + "ERROR": zerolog.ErrorLevel, + "WARN": zerolog.WarnLevel, + "INFO": zerolog.InfoLevel, + "DEBUG": zerolog.DebugLevel, + "TRACE": zerolog.TraceLevel, + } + + scopeLevels = make(map[string]zerolog.Level) + + for name, level := range logLevels { + env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + + if env == "" { + env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) + } + + if env == "" { + env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) + } + + if env == "" { + continue + } + + if strings.ToLower(env) == "all" { + if defaultLogLevel > level { + defaultLogLevel = level + } + + continue + } + + scopes := strings.Split(strings.ToLower(env), ",") + for _, scope := range scopes { + scopeLevels[scope] = level + } + } +} + +func getLogger(scope string) zerolog.Logger { + if scopeLevels == nil { + updateLogLevel() + } + + l := rootLogger.With().Str("component", scope).Logger() + + // if the scope is not in the map, use the default level from the root logger + if level, ok := scopeLevels[scope]; ok { + return l.Level(level) + } + + return l.Level(defaultLogLevel) +} + +type pionLogger struct { + logger *zerolog.Logger +} + +// Print all messages except trace. +func (c pionLogger) Trace(msg string) { + c.logger.Trace().Msg(msg) +} +func (c pionLogger) Tracef(format string, args ...interface{}) { + c.logger.Trace().Msgf(format, args...) +} + +func (c pionLogger) Debug(msg string) { + c.logger.Debug().Msg(msg) +} +func (c pionLogger) Debugf(format string, args ...interface{}) { + c.logger.Debug().Msgf(format, args...) +} +func (c pionLogger) Info(msg string) { + c.logger.Info().Msg(msg) +} +func (c pionLogger) Infof(format string, args ...interface{}) { + c.logger.Info().Msgf(format, args...) +} +func (c pionLogger) Warn(msg string) { + c.logger.Warn().Msg(msg) +} +func (c pionLogger) Warnf(format string, args ...interface{}) { + c.logger.Warn().Msgf(format, args...) +} +func (c pionLogger) Error(msg string) { + c.logger.Error().Msg(msg) +} +func (c pionLogger) Errorf(format string, args ...interface{}) { + c.logger.Error().Msgf(format, args...) +} + +// customLoggerFactory satisfies the interface logging.LoggerFactory +// This allows us to create different loggers per subsystem. So we can +// add custom behavior. +type pionLoggerFactory struct{} + +func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { + logger := getLogger(subsystem).With(). + Str("scope", "pion"). + Str("component", subsystem). + Logger() + + return pionLogger{logger: &logger} +} + +var defaultLoggerFactory = &pionLoggerFactory{} diff --git a/main.go b/main.go index f94b24e..d74b1ef 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package kvm import ( "context" - "log" "net/http" "os" "os/signal" @@ -18,17 +17,17 @@ func Main() { var cancel context.CancelFunc appCtx, cancel = context.WithCancel(context.Background()) defer cancel() - logger.Info("Starting JetKvm") + logger.Info().Msg("Starting JetKvm") go runWatchdog() go confirmCurrentSystem() http.DefaultClient.Timeout = 1 * time.Minute LoadConfig() - logger.Debug("config loaded") + logger.Debug().Msg("config loaded") err := rootcerts.UpdateDefaultTransport() if err != nil { - logger.Errorf("failed to load CA certs: %v", err) + logger.Warn().Err(err).Msg("failed to load CA certs") } go TimeSyncLoop() @@ -36,49 +35,63 @@ func Main() { StartNativeCtrlSocketServer() StartNativeVideoSocketServer() + initPrometheus() + go func() { err = ExtractAndRunNativeBin() if err != nil { - logger.Errorf("failed to extract and run native bin: %v", err) + logger.Warn().Err(err).Msg("failed to extract and run native bin") //TODO: prepare an error message screen buffer to show on kvm screen } }() + initUsbGadget() + go func() { time.Sleep(15 * time.Minute) for { - logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled) - if config.AutoUpdateEnabled == false { + logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") + if !config.AutoUpdateEnabled { return } if currentSession != nil { - logger.Debugf("skipping update since a session is active") + logger.Debug().Msg("skipping update since a session is active") time.Sleep(1 * time.Minute) continue } includePreRelease := config.IncludePreRelease err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Errorf("failed to auto update: %v", err) + logger.Warn().Err(err).Msg("failed to auto update") } time.Sleep(1 * time.Hour) } }() //go RunFuseServer() go RunWebServer() + + go RunWebSecureServer() + // Web secure server is started only if TLS mode is enabled + if config.TLSMode != "" { + startWebSecureServer() + } + + // As websocket client already checks if the cloud token is set, we can start it here. go RunWebsocketClient() + + initSerialPort() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - log.Println("JetKVM Shutting Down") + logger.Info().Msg("JetKVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { - // log.Printf("Failed to unmount mass storage image: %v", err) + // logger.Infof("Failed to unmount mass storage image: %v", err) // } // err = fuseServer.Unmount() // if err != nil { - // log.Printf("Failed to unmount fuse: %v", err) + // logger.Infof("Failed to unmount fuse: %v", err) // } // os.Exit(0) diff --git a/native.go b/native.go index 89e6803..630093b 100644 --- a/native.go +++ b/native.go @@ -5,14 +5,16 @@ import ( "encoding/json" "fmt" "io" - "kvm/resource" - "log" "net" "os" "os/exec" "sync" + "syscall" "time" + "github.com/jetkvm/kvm/resource" + "github.com/rs/zerolog" + "github.com/pion/webrtc/v4/pkg/media" ) @@ -33,6 +35,19 @@ type CtrlResponse struct { Data json.RawMessage `json:"data,omitempty"` } +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + type EventHandler func(event CtrlResponse) var seq int32 = 1 @@ -60,7 +75,7 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - fmt.Println("sending ctrl action", string(jsonData)) + nativeLogger.Info().Str("action", ctrlAction.Action).Msg("sending ctrl action") err = WriteCtrlMessage(jsonData) if err != nil { @@ -90,8 +105,8 @@ func WriteCtrlMessage(message []byte) error { return err } -var nativeCtrlSocketListener net.Listener -var nativeVideoSocketListener net.Listener +var nativeCtrlSocketListener net.Listener //nolint:unused +var nativeVideoSocketListener net.Listener //nolint:unused var ctrlClientConnected = make(chan struct{}) @@ -103,26 +118,28 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC // Remove the socket file if it already exists if _, err := os.Stat(socketPath); err == nil { if err := os.Remove(socketPath); err != nil { - log.Fatalf("Failed to remove existing socket file %s: %v", socketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("Failed to remove existing socket file") + os.Exit(1) } } listener, err := net.Listen("unixpacket", socketPath) if err != nil { - log.Fatalf("Failed to start server on %s: %v", socketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("Failed to start server") + os.Exit(1) } - log.Printf("Server listening on %s", socketPath) + nativeLogger.Info().Str("socket_path", socketPath).Msg("Server listening") go func() { conn, err := listener.Accept() listener.Close() if err != nil { - logger.Errorf("failed to accept sock: %v", err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("failed to accept sock") } if isCtrl { close(ctrlClientConnected) - logger.Debug("first native ctrl socket client connected") + nativeLogger.Debug().Msg("first native ctrl socket client connected") } handleClient(conn) }() @@ -132,40 +149,45 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC func StartNativeCtrlSocketServer() { nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) - logger.Debug("native app ctrl sock started") + nativeLogger.Debug().Msg("native app ctrl sock started") } func StartNativeVideoSocketServer() { nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) - logger.Debug("native app video sock started") + nativeLogger.Debug().Msg("native app video sock started") } func handleCtrlClient(conn net.Conn) { defer conn.Close() - logger.Debug("native socket client connected") + nativeLogger.Debug().Msg("native socket client connected") if ctrlSocketConn != nil { - logger.Debugf("closing existing native socket connection") + nativeLogger.Debug().Msg("closing existing native socket connection") ctrlSocketConn.Close() } ctrlSocketConn = conn + // Restore HDMI EDID if applicable + go restoreHdmiEdid() + readBuf := make([]byte, 4096) for { n, err := conn.Read(readBuf) if err != nil { - logger.Errorf("error reading from ctrl sock: %v", err) + nativeLogger.Warn().Err(err).Msg("error reading from ctrl sock") break } readMsg := string(readBuf[:n]) - logger.Tracef("ctrl sock msg: %v", readMsg) + ctrlResp := CtrlResponse{} err = json.Unmarshal([]byte(readMsg), &ctrlResp) if err != nil { - logger.Warnf("error parsing ctrl sock msg: %v", err) + nativeLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg") continue } + nativeLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg") + if ctrlResp.Seq != 0 { responseChan, ok := ongoingRequests[ctrlResp.Seq] if ok { @@ -178,30 +200,29 @@ func handleCtrlClient(conn net.Conn) { } } - logger.Debug("ctrl sock disconnected") + nativeLogger.Debug().Msg("ctrl sock disconnected") } func handleVideoClient(conn net.Conn) { defer conn.Close() - log.Printf("Native video socket client connected: %v", conn.RemoteAddr()) + nativeLogger.Info().Str("addr", conn.RemoteAddr().String()).Msg("Native video socket client connected") inboundPacket := make([]byte, maxFrameSize) lastFrame := time.Now() for { n, err := conn.Read(inboundPacket) if err != nil { - log.Println("error during read: %s", err) + nativeLogger.Warn().Err(err).Msg("error during read") return } now := time.Now() sinceLastFrame := now.Sub(lastFrame) lastFrame = now - //fmt.Println("Video packet received", n, sinceLastFrame) if currentSession != nil { err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { - log.Println("Error writing sample", err) + nativeLogger.Warn().Err(err).Msg("error writing sample") } } } @@ -220,9 +241,25 @@ func ExtractAndRunNativeBin() error { // Run the binary in the background cmd := exec.Command(binaryPath) + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + // Redirect stdout and stderr to the current process - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr + + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } // Start the command if err := cmd.Start(); err != nil { @@ -232,28 +269,28 @@ func ExtractAndRunNativeBin() error { //TODO: add auto restart go func() { <-appCtx.Done() - logger.Infof("killing process PID: %d", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") err := cmd.Process.Kill() if err != nil { - logger.Errorf("failed to kill process: %v", err) + nativeLogger.Warn().Err(err).Msg("failed to kill process") return } }() - fmt.Printf("Binary started with PID: %d\n", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("Binary started") return nil } func shouldOverwrite(destPath string, srcHash []byte) bool { if srcHash == nil { - logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting") return true } dstHash, err := os.ReadFile(destPath + ".sha256") if err != nil { - logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting") return true } @@ -269,13 +306,13 @@ func ensureBinaryUpdated(destPath string) error { srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") if err != nil { - logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update") srcHash = nil } _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - logger.Info("writing jetkvm_native") + nativeLogger.Info().Msg("writing jetkvm_native") _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { @@ -292,8 +329,20 @@ func ensureBinaryUpdated(destPath string) error { return err } } - logger.Info("jetkvm_native updated") + nativeLogger.Info().Msg("jetkvm_native updated") } return nil } + +// Restore the HDMI EDID value from the config. +// Called after successful connection to jetkvm_native. +func restoreHdmiEdid() { + if config.EdidString != "" { + nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") + _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) + if err != nil { + nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") + } + } +} diff --git a/network.go b/network.go index f461e45..4051c06 100644 --- a/network.go +++ b/network.go @@ -1,22 +1,35 @@ package kvm import ( + "bytes" "fmt" + "net" + "os" + "strings" + "time" + + "os/exec" + + "github.com/hashicorp/go-envparse" "github.com/pion/mdns/v2" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "net" - "time" "github.com/vishvananda/netlink" "github.com/vishvananda/netlink/nl" ) -var networkState struct { +var mDNSConn *mdns.Conn + +var networkState NetworkState + +type NetworkState struct { Up bool IPv4 string IPv6 string MAC string + + checked bool } type LocalIpInfo struct { @@ -25,44 +38,93 @@ type LocalIpInfo struct { MAC string } +const ( + NetIfName = "eth0" + DHCPLeaseFile = "/run/udhcpc.%s.info" +) + +// setDhcpClientState sends signals to udhcpc to change it's current mode +// of operation. Setting active to true will force udhcpc to renew the DHCP lease. +// Setting active to false will put udhcpc into idle mode. +func setDhcpClientState(active bool) { + var signal string + if active { + signal = "-SIGUSR1" + } else { + signal = "-SIGUSR2" + } + + cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") + if err := cmd.Run(); err != nil { + logger.Warn().Err(err).Msg("network: setDhcpClientState: failed to change udhcpc state") + } +} + func checkNetworkState() { - iface, err := netlink.LinkByName("eth0") + iface, err := netlink.LinkByName(NetIfName) if err != nil { - fmt.Printf("failed to get eth0 interface: %v\n", err) + logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get interface") return } - newState := struct { - Up bool - IPv4 string - IPv6 string - MAC string - }{ + newState := NetworkState{ Up: iface.Attrs().OperState == netlink.OperUp, MAC: iface.Attrs().HardwareAddr.String(), + + checked: true, } addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) if err != nil { - fmt.Printf("failed to get addresses for eth0: %v\n", err) + logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get addresses") + } + + // If the link is going down, put udhcpc into idle mode. + // If the link is coming back up, activate udhcpc and force it to renew the lease. + if newState.Up != networkState.Up { + setDhcpClientState(newState.Up) } for _, addr := range addrs { if addr.IP.To4() != nil { - newState.IPv4 = addr.IP.String() + if !newState.Up && networkState.Up { + // If the network is going down, remove all IPv4 addresses from the interface. + logger.Info().Str("address", addr.IP.String()).Msg("network: state transitioned to down, removing IPv4 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("network: failed to delete address") + } + + newState.IPv4 = "..." + } else { + newState.IPv4 = addr.IP.String() + } } else if addr.IP.To16() != nil && newState.IPv6 == "" { newState.IPv6 = addr.IP.String() } } if newState != networkState { + logger.Info().Msg("network state changed") + // restart MDNS + _ = startMDNS() networkState = newState - fmt.Println("network state changed") requestDisplayUpdate() } } func startMDNS() error { + // If server was previously running, stop it + if mDNSConn != nil { + logger.Info().Msg("Stopping mDNS server") + err := mDNSConn.Close() + if err != nil { + logger.Warn().Err(err).Msg("failed to stop mDNS server") + } + } + + // Start a new server + logger.Info().Msg("Starting mDNS server on jetkvm.local") addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { return err @@ -83,22 +145,59 @@ func startMDNS() error { return err } - _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable + mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ + LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable + LoggerFactory: defaultLoggerFactory, }) if err != nil { + mDNSConn = nil return err } //defer server.Close() return nil } +func getNTPServersFromDHCPInfo() ([]string, error) { + buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName)) + if err != nil { + // do not return error if file does not exist + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to load udhcpc info: %w", err) + } + + // parse udhcpc info + env, err := envparse.Parse(bytes.NewReader(buf)) + if err != nil { + return nil, fmt.Errorf("failed to parse udhcpc info: %w", err) + } + + val, ok := env["ntpsrv"] + if !ok { + return nil, nil + } + + var servers []string + + for _, server := range strings.Fields(val) { + if net.ParseIP(server) == nil { + logger.Info().Str("server", server).Msg("invalid NTP server IP, ignoring") + } + servers = append(servers, server) + } + + return servers, nil +} + func init() { + ensureConfigLoaded() + updates := make(chan netlink.LinkUpdate) done := make(chan struct{}) if err := netlink.LinkSubscribe(updates, done); err != nil { - fmt.Println("failed to subscribe to link updates: %v", err) + logger.Warn().Err(err).Msg("failed to subscribe to link updates") return } @@ -111,8 +210,8 @@ func init() { for { select { case update := <-updates: - if update.Link.Attrs().Name == "eth0" { - fmt.Printf("link update: %+v\n", update) + if update.Link.Attrs().Name == NetIfName { + logger.Info().Interface("update", update).Msg("link update") checkNetworkState() } case <-ticker.C: @@ -122,9 +221,8 @@ func init() { } } }() - fmt.Println("Starting mDNS server") err := startMDNS() if err != nil { - fmt.Println("failed to run mDNS: %v", err) + logger.Warn().Err(err).Msg("failed to run mDNS") } } diff --git a/ntp.go b/ntp.go index f785d96..aa7d17a 100644 --- a/ntp.go +++ b/ntp.go @@ -3,28 +3,96 @@ package kvm import ( "errors" "fmt" - "log" "net/http" "os/exec" + "strconv" "time" "github.com/beevik/ntp" ) -var timeSynced = false +const ( + timeSyncRetryStep = 5 * time.Second + timeSyncRetryMaxInt = 1 * time.Minute + timeSyncWaitNetChkInt = 100 * time.Millisecond + timeSyncWaitNetUpInt = 3 * time.Second + timeSyncInterval = 1 * time.Hour + timeSyncTimeout = 2 * time.Second +) + +var ( + builtTimestamp string + timeSyncRetryInterval = 0 * time.Second + timeSyncSuccess = false + defaultNTPServers = []string{ + "time.cloudflare.com", + "time.apple.com", + } +) + +func isTimeSyncNeeded() bool { + if builtTimestamp == "" { + ntpLogger.Warn().Msg("Built timestamp is not set, time sync is needed") + return true + } + + ts, err := strconv.Atoi(builtTimestamp) + if err != nil { + ntpLogger.Warn().Str("error", err.Error()).Msg("Failed to parse built timestamp") + return true + } + + // builtTimestamp is UNIX timestamp in seconds + builtTime := time.Unix(int64(ts), 0) + now := time.Now() + + ntpLogger.Debug().Str("built_time", builtTime.Format(time.RFC3339)).Str("now", now.Format(time.RFC3339)).Msg("Built time and now") + + if now.Sub(builtTime) < 0 { + ntpLogger.Warn().Msg("System time is behind the built time, time sync is needed") + return true + } + + return false +} func TimeSyncLoop() { for { - fmt.Println("Syncing system time") + if !networkState.checked { + time.Sleep(timeSyncWaitNetChkInt) + continue + } + + if !networkState.Up { + ntpLogger.Info().Msg("Waiting for network to come up") + time.Sleep(timeSyncWaitNetUpInt) + continue + } + + // check if time sync is needed, but do nothing for now + isTimeSyncNeeded() + + ntpLogger.Info().Msg("Syncing system time") start := time.Now() err := SyncSystemTime() if err != nil { - log.Printf("Failed to sync system time: %v", err) + ntpLogger.Error().Str("error", err.Error()).Msg("Failed to sync system time") + + // retry after a delay + timeSyncRetryInterval += timeSyncRetryStep + time.Sleep(timeSyncRetryInterval) + // reset the retry interval if it exceeds the max interval + if timeSyncRetryInterval > timeSyncRetryMaxInt { + timeSyncRetryInterval = 0 + } + continue } - log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) - timeSynced = true - time.Sleep(1 * time.Hour) //once the first sync is done, sync every hour + timeSyncSuccess = true + ntpLogger.Info().Str("now", time.Now().Format(time.RFC3339)). + Str("time_taken", time.Since(start).String()). + Msg("Time sync successful") + time.Sleep(timeSyncInterval) // after the first sync is done } } @@ -41,13 +109,22 @@ func SyncSystemTime() (err error) { } func queryNetworkTime() (*time.Time, error) { - ntpServers := []string{ - "time.cloudflare.com", - "time.apple.com", + ntpServers, err := getNTPServersFromDHCPInfo() + if err != nil { + ntpLogger.Error().Str("error", err.Error()).Msg("failed to get NTP servers from DHCP info") } + + if ntpServers == nil { + ntpServers = defaultNTPServers + ntpLogger.Info().Str("ntp_servers", fmt.Sprintf("%v", ntpServers)).Msg("Using default NTP servers") + } else { + ntpLogger.Info().Str("ntp_servers", fmt.Sprintf("%v", ntpServers)).Msg("Using NTP servers from DHCP") + } + for _, server := range ntpServers { - now, err := queryNtpServer(server, 2*time.Second) + now, err := queryNtpServer(server, timeSyncTimeout) if err == nil { + ntpLogger.Info().Str("ntp_server", server).Str("time", now.Format(time.RFC3339)).Msg("NTP server returned time") return now, nil } } @@ -56,11 +133,13 @@ func queryNetworkTime() (*time.Time, error) { "http://cloudflare.com", } for _, url := range httpUrls { - now, err := queryHttpTime(url, 2*time.Second) + now, err := queryHttpTime(url, timeSyncTimeout) if err == nil { + ntpLogger.Info().Str("http_url", url).Str("time", now.Format(time.RFC3339)).Msg("HTTP server returned time") return now, nil } } + ntpLogger.Error().Msg("failed to query network time") return nil, errors.New("failed to query network time") } diff --git a/ota.go b/ota.go index 9f9cb6f..64d7a26 100644 --- a/ota.go +++ b/ota.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "os" @@ -77,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) updateUrl.RawQuery = query.Encode() - fmt.Println("Checking for updates at:", updateUrl.String()) + logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates") req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) if err != nil { @@ -127,7 +126,13 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } - resp, err := http.DefaultClient.Do(req) + // TODO: set a separate timeout for the download but keep the TLS handshake short + // use Transport here will cause CA certificate validation failure so we temporarily removed it + client := http.Client{ + Timeout: 10 * time.Minute, + } + + resp, err := client.Do(req) if err != nil { return fmt.Errorf("error downloading file: %w", err) } @@ -230,7 +235,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error } hashSum := hash.Sum(nil) - fmt.Printf("SHA256 hash of %s: %x\n", path, hashSum) + logger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") if hex.EncodeToString(hashSum) != expectedHash { return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) @@ -272,7 +277,7 @@ var otaState = OTAState{} func triggerOTAStateUpdate() { go func() { if currentSession == nil { - log.Println("No active RPC session, skipping update state update") + logger.Info().Msg("No active RPC session, skipping update state update") return } writeJSONRPCEvent("otaState", otaState, currentSession) @@ -280,7 +285,7 @@ func triggerOTAStateUpdate() { } func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { - log.Println("Trying to update...") + logger.Info().Msg("Trying to update...") if otaState.Updating { return fmt.Errorf("update already in progress") } @@ -315,7 +320,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err rebootNeeded := false if appUpdateAvailable { - fmt.Printf("App update available: %s -> %s\n", local.AppVersion, remote.AppVersion) + logger.Info().Str("local", local.AppVersion).Str("remote", remote.AppVersion).Msg("App update available") err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) if err != nil { @@ -341,14 +346,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppUpdateProgress = 1 triggerOTAStateUpdate() - fmt.Println("App update downloaded") + logger.Info().Msg("App update downloaded") rebootNeeded = true } else { - fmt.Println("App is up to date") + logger.Info().Msg("App is up to date") } if systemUpdateAvailable { - fmt.Printf("System update available: %s -> %s\n", local.SystemVersion, remote.SystemVersion) + logger.Info().Str("local", local.SystemVersion).Str("remote", remote.SystemVersion).Msg("System update available") + err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) @@ -366,7 +372,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err triggerOTAStateUpdate() return err } - fmt.Println("System update downloaded") + logger.Info().Msg("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerificationProgress = 1 @@ -413,17 +419,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) } - fmt.Printf("rk_ota success, output: %s\n", output) + logger.Info().Str("output", output).Msg("rk_ota success") otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished triggerOTAStateUpdate() rebootNeeded = true } else { - fmt.Println("System is up to date") + logger.Info().Msg("System is up to date") } if rebootNeeded { - fmt.Println("System Rebooting in 10s...") + logger.Info().Msg("System Rebooting in 10s") time.Sleep(10 * time.Second) cmd := exec.Command("reboot") err := cmd.Start() @@ -498,6 +504,6 @@ func IsUpdatePending() bool { func confirmCurrentSystem() { output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() if err != nil { - logger.Warnf("failed to set current partition in A/B setup: %s", string(output)) + logger.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup") } } diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..5d4c5e7 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,13 @@ +package kvm + +import ( + "github.com/prometheus/client_golang/prometheus" + versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" + "github.com/prometheus/common/version" +) + +func initPrometheus() { + // A Prometheus metrics endpoint. + version.Version = builtAppVersion + prometheus.MustRegister(versioncollector.NewCollector("jetkvm")) +} diff --git a/publish_source.sh b/publish_source.sh index 432d5cd..e5c133d 100755 --- a/publish_source.sh +++ b/publish_source.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Check if a commit message was provided if [ -z "$1" ]; then @@ -26,7 +26,7 @@ git checkout -b release-temp if git ls-remote --heads public main | grep -q 'refs/heads/main'; then git reset --soft public/main else - git reset --soft $(git rev-list --max-parents=0 HEAD) + git reset --soft "$(git rev-list --max-parents=0 HEAD)" fi # Merge changes from main diff --git a/remote_mount.go b/remote_mount.go index e6e7322..befffcb 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -44,12 +44,12 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( return nil, errors.New("not active session") } - logger.Debugf("reading from webrtc %v", string(jsonBytes)) + logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc") err = currentSession.DiskChannel.SendText(string(jsonBytes)) if err != nil { return nil, err } - buf := make([]byte, 0) + var buf []byte for { select { case data := <-diskReadChan: diff --git a/serial.go b/serial.go new file mode 100644 index 0000000..732c022 --- /dev/null +++ b/serial.go @@ -0,0 +1,267 @@ +package kvm + +import ( + "bufio" + "io" + "strconv" + "strings" + "time" + + "github.com/pion/webrtc/v4" + "go.bug.st/serial" +) + +const serialPortPath = "/dev/ttyS3" + +var port serial.Port + +func mountATXControl() error { + _ = port.SetMode(defaultMode) + go runATXControl() + + return nil +} + +func unmountATXControl() error { + _ = reopenSerialPort() + return nil +} + +var ( + ledHDDState bool + ledPWRState bool + btnRSTState bool + btnPWRState bool +) + +func runATXControl() { + reader := bufio.NewReader(port) + for { + line, err := reader.ReadString('\n') + if err != nil { + logger.Warn().Err(err).Msg("Error reading from serial port") + return + } + + // Each line should be 4 binary digits + newline + if len(line) != 5 { + logger.Warn().Int("length", len(line)).Msg("Invalid line length") + continue + } + + // Parse new states + newLedHDDState := line[0] == '0' + newLedPWRState := line[1] == '0' + newBtnRSTState := line[2] == '1' + newBtnPWRState := line[3] == '1' + + if currentSession != nil { + writeJSONRPCEvent("atxState", ATXState{ + Power: newLedPWRState, + HDD: newLedHDDState, + }, currentSession) + } + + if newLedHDDState != ledHDDState || + newLedPWRState != ledPWRState || + newBtnRSTState != btnRSTState || + newBtnPWRState != btnPWRState { + logger.Debug().Bool("hdd", newLedHDDState).Bool("pwr", newLedPWRState).Bool("rst", newBtnRSTState).Bool("pwr", newBtnPWRState).Msg("Status changed") + + // Update states + ledHDDState = newLedHDDState + ledPWRState = newLedPWRState + btnRSTState = newBtnRSTState + btnPWRState = newBtnPWRState + } + } +} + +func pressATXPowerButton(duration time.Duration) error { + _, err := port.Write([]byte("\n")) + if err != nil { + return err + } + + _, err = port.Write([]byte("BTN_PWR_ON\n")) + if err != nil { + return err + } + + time.Sleep(duration) + + _, err = port.Write([]byte("BTN_PWR_OFF\n")) + if err != nil { + return err + } + + return nil +} + +func pressATXResetButton(duration time.Duration) error { + _, err := port.Write([]byte("\n")) + if err != nil { + return err + } + + _, err = port.Write([]byte("BTN_RST_ON\n")) + if err != nil { + return err + } + + time.Sleep(duration) + + _, err = port.Write([]byte("BTN_RST_OFF\n")) + if err != nil { + return err + } + + return nil +} + +func mountDCControl() error { + _ = port.SetMode(defaultMode) + go runDCControl() + return nil +} + +func unmountDCControl() error { + _ = reopenSerialPort() + return nil +} + +var dcState DCPowerState + +func runDCControl() { + reader := bufio.NewReader(port) + for { + line, err := reader.ReadString('\n') + if err != nil { + logger.Warn().Err(err).Msg("Error reading from serial port") + return + } + + // Split the line by semicolon + parts := strings.Split(strings.TrimSpace(line), ";") + if len(parts) != 4 { + logger.Warn().Str("line", line).Msg("Invalid line") + continue + } + + // Parse new states + powerState, err := strconv.Atoi(parts[0]) + if err != nil { + logger.Warn().Err(err).Msg("Invalid power state") + continue + } + dcState.IsOn = powerState == 1 + milliVolts, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + logger.Warn().Err(err).Msg("Invalid voltage") + continue + } + volts := milliVolts / 1000 // Convert mV to V + + milliAmps, err := strconv.ParseFloat(parts[2], 64) + if err != nil { + logger.Warn().Err(err).Msg("Invalid current") + continue + } + amps := milliAmps / 1000 // Convert mA to A + + milliWatts, err := strconv.ParseFloat(parts[3], 64) + if err != nil { + logger.Warn().Err(err).Msg("Invalid power") + continue + } + watts := milliWatts / 1000 // Convert mW to W + + dcState.Voltage = volts + dcState.Current = amps + dcState.Power = watts + + if currentSession != nil { + writeJSONRPCEvent("dcState", dcState, currentSession) + } + } +} + +func setDCPowerState(on bool) error { + _, err := port.Write([]byte("\n")) + if err != nil { + return err + } + command := "PWR_OFF\n" + if on { + command = "PWR_ON\n" + } + _, err = port.Write([]byte(command)) + if err != nil { + return err + } + return nil +} + +var defaultMode = &serial.Mode{ + BaudRate: 115200, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, +} + +func initSerialPort() { + _ = reopenSerialPort() + if config.ActiveExtension == "atx-power" { + _ = mountATXControl() + } else if config.ActiveExtension == "dc-power" { + _ = mountDCControl() + } +} + +func reopenSerialPort() error { + if port != nil { + port.Close() + } + var err error + port, err = serial.Open(serialPortPath, defaultMode) + if err != nil { + logger.Warn().Err(err).Msg("Error opening serial port") + } + return nil +} + +func handleSerialChannel(d *webrtc.DataChannel) { + d.OnOpen(func() { + go func() { + buf := make([]byte, 1024) + for { + n, err := port.Read(buf) + if err != nil { + if err != io.EOF { + logger.Warn().Err(err).Msg("Failed to read from serial port") + } + break + } + err = d.Send(buf[:n]) + if err != nil { + logger.Warn().Err(err).Msg("Failed to send serial output") + break + } + } + }() + }) + + d.OnMessage(func(msg webrtc.DataChannelMessage) { + if port == nil { + return + } + _, err := port.Write(msg.Data) + if err != nil { + logger.Warn().Err(err).Msg("Failed to write to serial") + } + }) + + d.OnClose(func() { + + }) +} diff --git a/terminal.go b/terminal.go index 1a1ac1c..2200064 100644 --- a/terminal.go +++ b/terminal.go @@ -23,7 +23,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var err error ptmx, err = pty.Start(cmd) if err != nil { - logger.Errorf("Failed to start pty: %v", err) + logger.Warn().Err(err).Msg("Failed to start pty") d.Close() return } @@ -34,13 +34,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) { n, err := ptmx.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from pty: %v", err) + logger.Warn().Err(err).Msg("Failed to read from pty") } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send pty output: %v", err) + logger.Warn().Err(err).Msg("Failed to send pty output") break } } @@ -55,17 +55,19 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var size TerminalSize err := json.Unmarshal([]byte(msg.Data), &size) if err == nil { - pty.Setsize(ptmx, &pty.Winsize{ + err = pty.Setsize(ptmx, &pty.Winsize{ Rows: uint16(size.Rows), Cols: uint16(size.Cols), }) - return + if err == nil { + return + } } - logger.Errorf("Failed to parse terminal size: %v", err) + logger.Warn().Err(err).Msg("Failed to parse terminal size") } _, err := ptmx.Write(msg.Data) if err != nil { - logger.Errorf("Failed to write to pty: %v", err) + logger.Warn().Err(err).Msg("Failed to write to pty") } }) @@ -74,7 +76,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { ptmx.Close() } if cmd != nil && cmd.Process != nil { - cmd.Process.Kill() + _ = cmd.Process.Kill() } }) } diff --git a/ui/.env.cloud-development b/ui/.env.cloud-development new file mode 100644 index 0000000..471e280 --- /dev/null +++ b/ui/.env.cloud-development @@ -0,0 +1,4 @@ +# No need for VITE_CLOUD_APP it's only needed for the device build + +# We use this for all the cloud API requests from the browser +VITE_CLOUD_API=http://localhost:3000 diff --git a/ui/.env.cloud-production b/ui/.env.cloud-production new file mode 100644 index 0000000..d9895d2 --- /dev/null +++ b/ui/.env.cloud-production @@ -0,0 +1,4 @@ +# No need for VITE_CLOUD_APP it's only needed for the device build + +# We use this for all the cloud API requests from the browser +VITE_CLOUD_API=https://api.jetkvm.com diff --git a/ui/.env.cloud-staging b/ui/.env.cloud-staging new file mode 100644 index 0000000..bc5c14c --- /dev/null +++ b/ui/.env.cloud-staging @@ -0,0 +1,4 @@ +# No need for VITE_CLOUD_APP it's only needed for the device build + +# We use this for all the cloud API requests from the browser +VITE_CLOUD_API=https://staging-api.jetkvm.com diff --git a/ui/.env.development b/ui/.env.development deleted file mode 100644 index 4534cc5..0000000 --- a/ui/.env.development +++ /dev/null @@ -1,4 +0,0 @@ -VITE_SIGNAL_API=http://localhost:3000 - -VITE_CLOUD_APP=http://localhost:5173 -VITE_CLOUD_API=http://localhost:3000 diff --git a/ui/.env.device b/ui/.env.device deleted file mode 100644 index f13097c..0000000 --- a/ui/.env.device +++ /dev/null @@ -1,4 +0,0 @@ -VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint - -VITE_CLOUD_APP=https://app.jetkvm.com -VITE_CLOUD_API=https://api.jetkvm.com diff --git a/ui/.env.production b/ui/.env.production deleted file mode 100644 index de65a37..0000000 --- a/ui/.env.production +++ /dev/null @@ -1,4 +0,0 @@ -VITE_SIGNAL_API=https://api.jetkvm.com - -VITE_CLOUD_APP=https://app.jetkvm.com -VITE_CLOUD_API=https://api.jetkvm.com \ No newline at end of file diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs index 671054c..568fbd9 100644 --- a/ui/.eslintrc.cjs +++ b/ui/.eslintrc.cjs @@ -8,6 +8,8 @@ module.exports = { "plugin:react-hooks/recommended", "plugin:react/recommended", "plugin:react/jsx-runtime", + "plugin:import/recommended", + "prettier", ], ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"], parser: "@typescript-eslint/parser", @@ -20,5 +22,45 @@ module.exports = { }, rules: { "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "import/order": [ + "error", + { + /** + * @description + * + * This keeps imports separate from one another, ensuring that imports are separated + * by their relative groups. As you move through the groups, imports become closer + * to the current file. + * + * @example + * ``` + * import fs from 'fs'; + * + * import package from 'npm-package'; + * + * import xyz from '~/project-file'; + * + * import index from '../'; + * + * import sibling from './foo'; + * ``` + */ + groups: ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + }, + ], + }, + settings: { + "import/resolver": { + alias: { + map: [ + ["@components", "./src/components"], + ["@routes", "./src/routes"], + ["@assets", "./src/assets"], + ["@", "./src"], + ], + extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], + }, + }, }, }; diff --git a/ui/.prettierrc b/ui/.prettierrc index 0fa9a7c..65b362d 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -5,11 +5,7 @@ "useTabs": false, "arrowParens": "avoid", "singleQuote": false, - "plugins": [ - "prettier-plugin-tailwindcss" - ], - "tailwindFunctions": [ - "clsx" - ], + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindFunctions": ["clsx"], "printWidth": 90 -} \ No newline at end of file +} diff --git a/ui/dev_device.sh b/ui/dev_device.sh new file mode 100755 index 0000000..650cadd --- /dev/null +++ b/ui/dev_device.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Check if an IP address was provided as an argument +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +ip_address="$1" + +# Print header +echo "┌──────────────────────────────────────┐" +echo "│ JetKVM Development Setup │" +echo "└──────────────────────────────────────┘" + +# Set the environment variable and run Vite +echo "Starting development server with JetKVM device at: $ip_address" +sleep 1 +JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device diff --git a/ui/package-lock.json b/ui/package-lock.json index e60ce6f..ebce148 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,17 +8,19 @@ "name": "kvm-ui", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.1.10", - "@headlessui/tailwindcss": "^0.2.0", - "@heroicons/react": "^2.1.3", + "@headlessui/react": "^2.2.0", + "@headlessui/tailwindcss": "^0.2.1", + "@heroicons/react": "^2.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", - "framer-motion": "^11.0.28", + "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^18.2.0", @@ -28,34 +30,39 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", - "recharts": "^2.12.6", - "tailwind-merge": "^2.2.2", + "react-use-websocket": "^4.13.0", + "react-xtermjs": "^1.0.9", + "recharts": "^2.15.0", + "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", "xterm": "^5.3.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.12", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@types/react": "^18.2.66", "@types/react-dom": "^18.3.0", + "@types/semver": "^7.5.8", "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "prettier": "^3.2.5", - "prettier-plugin-tailwindcss": "^0.5.13", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", + "eslint": "^8.20.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", "vite": "^5.2.0", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^5.1.4" }, "engines": { "node": "21.1.0" @@ -65,7 +72,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -82,9 +88,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", - "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -464,7 +470,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -479,7 +484,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -488,7 +492,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -511,7 +514,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -521,7 +523,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -533,7 +534,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -587,9 +587,9 @@ "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" }, "node_modules/@headlessui/react": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.10.tgz", - "integrity": "sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", @@ -600,34 +600,33 @@ "node": ">=10" }, "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@headlessui/tailwindcss": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz", - "integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", + "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", "engines": { "node": ">=10" }, "peerDependencies": { - "tailwindcss": "^3.0" + "tailwindcss": "^3.0 || ^4.0" } }, "node_modules/@heroicons/react": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz", - "integrity": "sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", "peerDependencies": { - "react": ">= 16" + "react": ">= 16 || ^19.0.0-rc" } }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -641,7 +640,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -651,7 +649,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -663,7 +660,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -675,8 +671,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -1083,15 +1078,20 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + }, "node_modules/@swc/core": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.12.tgz", - "integrity": "sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.4.tgz", + "integrity": "sha512-EHl6eNod/914xDRK4nu7gr78riK2cfi4DkAMvJt6COdaNGOnbR5eKrLe3SnRizyzzrPcxUMhflDL5hrcXS8rAQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.19" }, "engines": { "node": ">=10" @@ -1101,19 +1101,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.12", - "@swc/core-darwin-x64": "1.4.12", - "@swc/core-linux-arm-gnueabihf": "1.4.12", - "@swc/core-linux-arm64-gnu": "1.4.12", - "@swc/core-linux-arm64-musl": "1.4.12", - "@swc/core-linux-x64-gnu": "1.4.12", - "@swc/core-linux-x64-musl": "1.4.12", - "@swc/core-win32-arm64-msvc": "1.4.12", - "@swc/core-win32-ia32-msvc": "1.4.12", - "@swc/core-win32-x64-msvc": "1.4.12" + "@swc/core-darwin-arm64": "1.11.4", + "@swc/core-darwin-x64": "1.11.4", + "@swc/core-linux-arm-gnueabihf": "1.11.4", + "@swc/core-linux-arm64-gnu": "1.11.4", + "@swc/core-linux-arm64-musl": "1.11.4", + "@swc/core-linux-x64-gnu": "1.11.4", + "@swc/core-linux-x64-musl": "1.11.4", + "@swc/core-win32-arm64-msvc": "1.11.4", + "@swc/core-win32-ia32-msvc": "1.11.4", + "@swc/core-win32-x64-msvc": "1.11.4" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1122,9 +1122,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.12.tgz", - "integrity": "sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.4.tgz", + "integrity": "sha512-Oi4lt4wqjpp80pcCh+vzvpsESJ8XXozYCE5EM/dDpr+9m2oRpkseds7Gq4ulzgdbUDPo1jJ1PonjjrKpfKY+sQ==", "cpu": [ "arm64" ], @@ -1138,9 +1138,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.12.tgz", - "integrity": "sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.4.tgz", + "integrity": "sha512-Tb7ez94DXxhX5iJ5slnAlT2gwJinQk3pMnQ46Npi6adKr3ZXM5Bdk0jpRUp8XjEcgNXkQRV1DtrySgCz6YlEnQ==", "cpu": [ "x64" ], @@ -1154,9 +1154,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.12.tgz", - "integrity": "sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.4.tgz", + "integrity": "sha512-p1uV+6Mi+0M+1kL7qL206ZaohomYMW7yroXSLDTJXbIylx7wG2xrUQL6AFtz2DwqDoX/E8jMNBjp+GcEy8r8Ig==", "cpu": [ "arm" ], @@ -1170,9 +1170,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.12.tgz", - "integrity": "sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.4.tgz", + "integrity": "sha512-4ijX4bWf9oc7kWkT6xUhugVGzEJ7U9c7CHNmt/xhI/yWsQdfM11+HECqWh7ay3m+aaEoVdvTeU5gykeF5jSxDA==", "cpu": [ "arm64" ], @@ -1186,9 +1186,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.12.tgz", - "integrity": "sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.4.tgz", + "integrity": "sha512-XI+gOgcuSanejbAC5QXKTjNA3GUJi7bzHmeJbNhKpX9d349RdVwan0k9okHmhMBY7BywAg3LK0ovF9PmOLgMHg==", "cpu": [ "arm64" ], @@ -1202,9 +1202,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.12.tgz", - "integrity": "sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.4.tgz", + "integrity": "sha512-wyD6noaCPFayKOvl9mTxuiQoEULAagGuO0od2VkW7h4HvlgpOAZNekZYX73WEP/b+WuePNHurZ9KGpom43IzmA==", "cpu": [ "x64" ], @@ -1218,9 +1218,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.12.tgz", - "integrity": "sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.4.tgz", + "integrity": "sha512-e2vG9gUF1BRX0BWqSEHop6u14l5BtV3VS2Pmr+oquc0Ycs/zj81xhYc3ML4ByK5OxDkAaKBWryAOKTLaJA/DVg==", "cpu": [ "x64" ], @@ -1234,9 +1234,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.12.tgz", - "integrity": "sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.4.tgz", + "integrity": "sha512-rm51iljNqjCA/41gxYameuyjX1ENaTlvdxmaoPPYeUDt6hfypG93IxMJJCewaeHN9XfNxqZU7d4cupNqk+8nng==", "cpu": [ "arm64" ], @@ -1250,9 +1250,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.12.tgz", - "integrity": "sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.4.tgz", + "integrity": "sha512-PHy3N6zlyU8te7Umi0ggXNbcx2VUkwpE59PW9FQQy9MBZM1Qn+OEGnO/4KLWjGFABw+9CwIeaRYgq6uCi1ry6A==", "cpu": [ "ia32" ], @@ -1266,9 +1266,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.12.tgz", - "integrity": "sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.4.tgz", + "integrity": "sha512-0TiriDGl7Dr4ObfMBk07PS4Ql5hgQH0QnU3E8I+fbs45hqfwC5OrN47HOsXx4ZbEw8XYxp2NM8SGnVoTIm4J8w==", "cpu": [ "x64" ], @@ -1296,30 +1296,30 @@ } }, "node_modules/@swc/types": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", - "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", + "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", "dev": true, "dependencies": { "@swc/counter": "^0.1.3" } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", - "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "dev": true, "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz", - "integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", "dev": true, "dependencies": { "lodash.castarray": "^4.4.0", @@ -1328,7 +1328,7 @@ "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { @@ -1429,11 +1429,10 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/prop-types": { "version": "15.7.12", @@ -1473,79 +1472,69 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz", - "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/type-utils": "7.5.0", - "@typescript-eslint/utils": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", - "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1553,39 +1542,35 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", - "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/utils": "7.5.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz", - "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1593,91 +1578,113 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz", - "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", - "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz", - "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.5.0", - "eslint-visitor-keys": "^3.4.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz", - "integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", + "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", "dev": true, "dependencies": { - "@swc/core": "^1.3.107" + "@swc/core": "^1.10.15" }, "peerDependencies": { - "vite": "^4 || ^5" + "vite": "^4 || ^5 || ^6" } }, "node_modules/@xterm/addon-clipboard": { @@ -1726,14 +1733,12 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "peer": true + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1745,7 +1750,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1754,7 +1758,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1813,17 +1816,15 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -1836,7 +1837,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1852,15 +1852,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -1881,11 +1872,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -1900,15 +1910,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1917,45 +1926,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -1964,10 +1962,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -1984,11 +1990,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2005,7 +2011,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2041,20 +2046,20 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -2071,10 +2076,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2084,16 +2089,41 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -2106,7 +2136,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -2120,9 +2149,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001666", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", - "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -2143,7 +2172,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2224,8 +2252,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -2386,14 +2413,13 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2403,29 +2429,27 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -2440,7 +2464,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2461,14 +2484,12 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2485,7 +2506,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2503,18 +2523,6 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2524,7 +2532,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -2541,15 +2548,28 @@ "csstype": "^3.0.2" } }, + "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==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.4.729", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz", - "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==", + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", "dev": true }, "node_modules/emoji-regex": { @@ -2558,57 +2578,61 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -2618,13 +2642,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -2633,41 +2653,41 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", - "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -2676,37 +2696,38 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "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==", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2754,9 +2775,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -2766,7 +2787,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2778,7 +2798,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2829,57 +2848,220 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "node_modules/eslint-config-prettier": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-alias": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", + "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "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/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz", - "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", "dev": true, "peerDependencies": { - "eslint": ">=7" + "eslint": ">=8.40" } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { @@ -2929,7 +3111,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2945,7 +3126,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2957,7 +3137,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2967,7 +3146,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2979,7 +3157,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -2996,7 +3173,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3008,7 +3184,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3020,7 +3195,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -3029,7 +3203,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3042,13 +3215,12 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", "engines": { "node": ">=6.0.0" } @@ -3082,14 +3254,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastq": { "version": "1.17.1", @@ -3103,7 +3273,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -3112,9 +3281,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3126,7 +3295,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3142,7 +3310,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3155,8 +3322,7 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/focus-trap": { "version": "7.5.4", @@ -3181,12 +3347,17 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { @@ -3218,16 +3389,18 @@ } }, "node_modules/framer-motion": { - "version": "11.0.28", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.28.tgz", - "integrity": "sha512-j/vNYTCH5MX5sY/3dwMs00z1+qAqKX3iIHF762bwqlU814ooD5dDbuj3pA0LmIT5YqyryCkXEb/q+zRblin0lw==", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -3244,8 +3417,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -3269,15 +3441,16 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -3290,22 +3463,25 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "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", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "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" @@ -3314,15 +3490,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, + "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==", "dependencies": { - "call-bind": "^1.0.5", + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dependencies": { + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3335,7 +3522,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3366,7 +3552,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3376,7 +3561,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3388,7 +3572,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -3400,12 +3583,12 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -3414,26 +3597,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -3441,20 +3604,19 @@ "dev": true }, "node_modules/goober": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", - "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", "peerDependencies": { "csstype": "^3.0.10" } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3463,14 +3625,15 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3479,7 +3642,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3488,7 +3650,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -3497,10 +3658,12 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3509,10 +3672,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -3524,7 +3686,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -3550,7 +3711,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -3559,7 +3719,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3575,7 +3734,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -3584,7 +3742,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3593,18 +3750,16 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3619,13 +3774,13 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3635,12 +3790,15 @@ } }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dependencies": { - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3650,12 +3808,14 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3673,13 +3833,12 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3692,7 +3851,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3701,22 +3859,26 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -3727,12 +3889,12 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3750,12 +3912,14 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3770,12 +3934,14 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3799,19 +3965,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3828,12 +3981,12 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3846,19 +3999,19 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3871,7 +4024,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3880,12 +4032,11 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3895,12 +4046,12 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3910,12 +4061,13 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3925,12 +4077,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -3943,7 +4094,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3952,25 +4102,26 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3982,8 +4133,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -3991,16 +4141,20 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jackspeak": { @@ -4021,9 +4175,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "bin": { "jiti": "bin/jiti.js" } @@ -4042,7 +4196,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4053,20 +4206,28 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } }, "node_modules/jsx-ast-utils": { "version": "3.3.5", @@ -4087,7 +4248,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4096,7 +4256,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4106,11 +4265,14 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -4122,7 +4284,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -4158,8 +4319,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/lodash.throttle": { "version": "4.1.1", @@ -4177,16 +4337,12 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "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==", "engines": { - "node": ">=10" + "node": ">= 0.4" } }, "node_modules/merge2": { @@ -4198,11 +4354,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4231,6 +4387,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -4239,11 +4403,20 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", + "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==" + }, + "node_modules/motion-utils": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", + "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4256,9 +4429,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -4275,13 +4448,12 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/normalize-path": { @@ -4318,10 +4490,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4330,20 +4504,20 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -4371,7 +4545,6 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4385,30 +4558,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dependencies": { + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -4423,7 +4592,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4432,7 +4600,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -4445,11 +4612,26 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4464,7 +4646,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -4479,7 +4660,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4491,7 +4671,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -4500,7 +4679,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4541,19 +4719,10 @@ "node": "14 || >=16.14" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4583,18 +4752,17 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -4610,9 +4778,9 @@ } ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4702,39 +4870,34 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4752,15 +4915,14 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -4773,9 +4935,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz", - "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==", + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", + "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", "dev": true, "engines": { "node": ">=14.21.3" @@ -4785,13 +4947,14 @@ "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig-melody": "*", + "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", @@ -4811,7 +4974,7 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, - "@zackad/prettier-plugin-twig-melody": { + "@zackad/prettier-plugin-twig": { "optional": true }, "prettier-plugin-astro": { @@ -4829,6 +4992,9 @@ "prettier-plugin-marko": { "optional": true }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, "prettier-plugin-organize-attributes": { "optional": true }, @@ -4860,7 +5026,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4920,11 +5085,12 @@ } }, "node_modules/react-hot-toast": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", - "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", "dependencies": { - "goober": "^2.1.10" + "csstype": "^3.1.3", + "goober": "^2.1.16" }, "engines": { "node": ">=10" @@ -4935,9 +5101,9 @@ } }, "node_modules/react-icons": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", - "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", "peerDependencies": { "react": "*" } @@ -4987,17 +5153,17 @@ } }, "node_modules/react-smooth": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", - "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-transition-group": { @@ -5015,6 +5181,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==" + }, + "node_modules/react-xtermjs": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz", + "integrity": "sha512-lrK1xiWfgxAC+4shtMHh0Irxg2t5t7JbTtpP0W7GIf1gQ9SHW/djmyiLpQSA75mN1DpT0bKeqj1fOKd0XX8RBA==", + "peerDependencies": { + "@xterm/xterm": "^5.5.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5035,15 +5214,15 @@ } }, "node_modules/recharts": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz", - "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", - "react-smooth": "^4.0.0", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" @@ -5052,8 +5231,8 @@ "node": ">=14" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/recharts-scale": { @@ -5064,19 +5243,24 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", - "dev": true, + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -5091,15 +5275,16 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5129,7 +5314,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -5147,7 +5331,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -5215,14 +5398,14 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -5232,15 +5415,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dependencies": { - "call-bind": "^1.0.6", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -5258,13 +5455,10 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5276,7 +5470,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5293,7 +5486,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5304,6 +5496,19 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5324,15 +5529,65 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5352,19 +5607,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "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==", "engines": { "node": ">=0.10.0" } @@ -5430,23 +5676,24 @@ } }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5455,16 +5702,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5474,15 +5733,18 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5491,7 +5753,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5527,11 +5788,18 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -5585,7 +5853,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5610,44 +5877,41 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwind-merge": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz", - "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==", - "dependencies": { - "@babel/runtime": "^7.24.0" - }, + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -5676,8 +5940,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -5715,15 +5978,15 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -5751,6 +6014,17 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -5760,7 +6034,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5772,7 +6045,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -5781,30 +6053,28 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5814,17 +6084,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -5834,17 +6104,16 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -5854,9 +6123,9 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -5867,24 +6136,26 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -5901,8 +6172,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -5915,7 +6186,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -5929,9 +6199,9 @@ } }, "node_modules/usehooks-ts": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", - "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", "dependencies": { "lodash.debounce": "^4.0.8" }, @@ -5939,7 +6209,7 @@ "node": ">=16.15.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "node_modules/util-deprecate": { @@ -6032,9 +6302,9 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -6065,39 +6335,41 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -6110,7 +6382,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -6125,15 +6396,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -6233,8 +6505,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xterm": { "version": "5.3.0", @@ -6242,12 +6513,6 @@ "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yaml": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", @@ -6263,7 +6528,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/ui/package.json b/ui/package.json index 592a300..a248616 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,24 +7,30 @@ "node": "21.1.0" }, "scripts": { - "dev": "vite dev --mode=development", + "dev": "./dev_device.sh", + "dev:cloud": "vite dev --mode=cloud-development", "build": "npm run build:prod", "build:device": "tsc && vite build --mode=device --emptyOutDir", - "build:prod": "tsc && vite build --mode=production", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "build:staging": "tsc && vite build --mode=cloud-staging", + "build:prod": "tsc && vite build --mode=cloud-production", + "lint": "eslint './src/**/*.{ts,tsx}'", + "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix", + "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.1.10", - "@headlessui/tailwindcss": "^0.2.0", - "@heroicons/react": "^2.1.3", + "@headlessui/react": "^2.2.0", + "@headlessui/tailwindcss": "^0.2.1", + "@heroicons/react": "^2.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", - "framer-motion": "^11.0.28", + "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^18.2.0", @@ -34,33 +40,38 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", - "recharts": "^2.12.6", - "tailwind-merge": "^2.2.2", + "react-use-websocket": "^4.13.0", + "react-xtermjs": "^1.0.9", + "recharts": "^2.15.0", + "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", "xterm": "^5.3.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.12", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@types/react": "^18.2.66", "@types/react-dom": "^18.3.0", + "@types/semver": "^7.5.8", "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "prettier": "^3.2.5", - "prettier-plugin-tailwindcss": "^0.5.13", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", + "eslint": "^8.20.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", "vite": "^5.2.0", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/ui/src/assets/netboot-icon.svg b/ui/src/assets/netboot-icon.svg index e341c99..39b5fb2 100644 --- a/ui/src/assets/netboot-icon.svg +++ b/ui/src/assets/netboot-icon.svg @@ -1,18 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index cd5432c..1afef63 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -1,34 +1,38 @@ +import { MdOutlineContentPasteGo } from "react-icons/md"; +import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; +import { FaKeyboard } from "react-icons/fa6"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; +import { Fragment, useCallback, useRef } from "react"; +import { CommandLineIcon } from "@heroicons/react/20/solid"; + import { Button } from "@components/Button"; import { useHidStore, useMountMediaStore, - useUiStore, useSettingsStore, + useUiStore, } from "@/hooks/stores"; -import { MdOutlineContentPasteGo } from "react-icons/md"; import Container from "@components/Container"; -import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; import { cx } from "@/cva.config"; import PasteModal from "@/components/popovers/PasteModal"; -import { FaKeyboard } from "react-icons/fa6"; import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import MountPopopover from "./popovers/MountPopover"; -import { Fragment, useCallback, useRef } from "react"; -import { CommandLineIcon } from "@heroicons/react/20/solid"; +import MountPopopover from "@/components/popovers/MountPopover"; +import ExtensionPopover from "@/components/popovers/ExtensionPopover"; +import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; export default function Actionbar({ requestFullscreen, }: { requestFullscreen: () => Promise; }) { + const { navigateTo } = useDeviceUiNavigation(); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); const toggleSidebarView = useUiStore(state => state.toggleSidebarView); const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - const enableTerminal = useUiStore(state => state.enableTerminal); - const setEnableTerminal = useUiStore(state => state.setEnableTerminal); + const terminalType = useUiStore(state => state.terminalType); + const setTerminalType = useUiStore(state => state.setTerminalType); const remoteVirtualMediaState = useMountMediaStore( state => state.remoteVirtualMediaState, ); @@ -53,7 +57,7 @@ export default function Actionbar({ ); return ( - +
e.stopPropagation()} onKeyDown={e => e.stopPropagation()} @@ -66,7 +70,7 @@ export default function Actionbar({ theme="light" text="Web Terminal" LeadingIcon={({ className }) => } - onClick={() => setEnableTerminal(!enableTerminal)} + onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")} /> )} @@ -92,7 +96,7 @@ export default function Actionbar({ {({ open }) => { checkIfStateChanged(open); return ( -
+
); @@ -134,7 +138,7 @@ export default function Actionbar({ {({ open }) => { checkIfStateChanged(open); return ( -
+
); @@ -148,7 +152,7 @@ export default function Actionbar({
-
+
-
+ +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/ui/src/components/Container.tsx b/ui/src/components/Container.tsx index ba02e64..a759ca5 100644 --- a/ui/src/components/Container.tsx +++ b/ui/src/components/Container.tsx @@ -1,4 +1,6 @@ +/* eslint-disable react-refresh/only-export-components */ import React, { ReactNode } from "react"; + import { cx } from "@/cva.config"; function Container({ children, className }: { children: ReactNode; className?: string }) { diff --git a/ui/src/components/CustomTooltip.tsx b/ui/src/components/CustomTooltip.tsx index 8ca214c..a27f607 100644 --- a/ui/src/components/CustomTooltip.tsx +++ b/ui/src/components/CustomTooltip.tsx @@ -1,8 +1,8 @@ import Card from "@components/Card"; -export type CustomTooltipProps = { +export interface CustomTooltipProps { payload: { payload: { date: number; stat: number }; unit: string }[]; -}; +} export default function CustomTooltip({ payload }: CustomTooltipProps) { if (payload?.length) { diff --git a/ui/src/components/EmptyCard.tsx b/ui/src/components/EmptyCard.tsx index 0b467b9..d8ba782 100644 --- a/ui/src/components/EmptyCard.tsx +++ b/ui/src/components/EmptyCard.tsx @@ -1,14 +1,16 @@ -import { GridCard } from "@/components/Card"; import React from "react"; + +import { GridCard } from "@/components/Card"; + import { cx } from "../cva.config"; -type Props = { - IconElm?: React.FC; +interface Props { + IconElm?: React.FC<{ className: string | undefined }>; headline: string; description?: string | React.ReactNode; BtnElm?: React.ReactNode; className?: string; -}; +} export default function EmptyCard({ IconElm, @@ -27,10 +29,16 @@ export default function EmptyCard({ >
- {IconElm && } -

{headline}

+ {IconElm && ( + + )} +

+ {headline} +

-

{description}

+

+ {description} +

{BtnElm}
diff --git a/ui/src/components/ExtLink.tsx b/ui/src/components/ExtLink.tsx index 09c5f4e..79eec8c 100644 --- a/ui/src/components/ExtLink.tsx +++ b/ui/src/components/ExtLink.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { cx } from "@/cva.config"; export default function ExtLink({ diff --git a/ui/src/components/FeatureFlag.tsx b/ui/src/components/FeatureFlag.tsx new file mode 100644 index 0000000..cc0c7c5 --- /dev/null +++ b/ui/src/components/FeatureFlag.tsx @@ -0,0 +1,28 @@ +import { useEffect } from "react"; + +import { useFeatureFlag } from "../hooks/useFeatureFlag"; + +export function FeatureFlag({ + minAppVersion, + name = "unnamed", + fallback = null, + children, +}: { + minAppVersion: string; + name?: string; + fallback?: React.ReactNode; + children: React.ReactNode; +}) { + const { isEnabled, appVersion } = useFeatureFlag(minAppVersion); + + useEffect(() => { + if (!appVersion) return; + console.log( + `Feature '${name}' ${isEnabled ? "ENABLED" : "DISABLED"}: ` + + `Current version: ${appVersion}, ` + + `Required min version: ${minAppVersion || "N/A"}`, + ); + }, [isEnabled, name, minAppVersion, appVersion]); + + return isEnabled ? children : fallback; +} diff --git a/ui/src/components/FieldLabel.tsx b/ui/src/components/FieldLabel.tsx index 3df932a..f9065a1 100644 --- a/ui/src/components/FieldLabel.tsx +++ b/ui/src/components/FieldLabel.tsx @@ -1,13 +1,14 @@ import React from "react"; + import { cx } from "@/cva.config"; -type Props = { +interface Props { label: string | React.ReactNode; id?: string; as?: "label" | "span"; description?: string | React.ReactNode | null; disabled?: boolean; -}; +} export default function FieldLabel({ label, id, @@ -26,7 +27,7 @@ export default function FieldLabel({ > {label} {description && ( - + {description} )} @@ -34,12 +35,12 @@ export default function FieldLabel({ ); } else if (as === "span") { return ( -
- +
+ {label} {description && ( - + {description} )} @@ -48,4 +49,4 @@ export default function FieldLabel({ } else { return <>; } -} +} \ No newline at end of file diff --git a/ui/src/components/Fieldset.tsx b/ui/src/components/Fieldset.tsx index edfa823..9a37e79 100644 --- a/ui/src/components/Fieldset.tsx +++ b/ui/src/components/Fieldset.tsx @@ -9,7 +9,7 @@ export default function Fieldset({ disabled, }: { children: React.ReactNode; - fetcher?: FetcherWithComponents; + fetcher?: FetcherWithComponents; className?: string; disabled?: boolean; }) { diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index a5fe429..19e9652 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -1,18 +1,22 @@ import { Fragment, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid"; -import { Menu, MenuButton, Transition } from "@headlessui/react"; +import { Menu, MenuButton } from "@headlessui/react"; +import { LuMonitorSmartphone } from "react-icons/lu"; + import Container from "@/components/Container"; import Card from "@/components/Card"; -import { LuMonitorSmartphone } from "react-icons/lu"; import { cx } from "@/cva.config"; import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores"; import LogoBlueIcon from "@/assets/logo-blue.svg"; import LogoWhiteIcon from "@/assets/logo-white.svg"; import USBStateStatus from "@components/USBStateStatus"; import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard"; +import { CLOUD_API, DEVICE_API } from "@/ui.config"; + import api from "../api"; import { isOnDevice } from "../main"; + import { Button, LinkButton } from "./Button"; interface NavbarProps { @@ -32,28 +36,26 @@ export default function DashboardNavbar({ picture, kvmName, }: NavbarProps) { - const peerConnectionState = useRTCStore(state => state.peerConnection?.connectionState); + const peerConnectionState = useRTCStore(state => state.peerConnectionState); const setUser = useUserStore(state => state.setUser); const navigate = useNavigate(); const onLogout = useCallback(async () => { - const logoutUrl = isOnDevice - ? `${import.meta.env.VITE_SIGNAL_API}/auth/logout` - : `${import.meta.env.VITE_CLOUD_API}/logout`; + const logoutUrl = isOnDevice ? `${DEVICE_API}/auth/logout` : `${CLOUD_API}/logout`; const res = await api.POST(logoutUrl); if (!res.ok) return; setUser(null); - // The root route will redirect to appropiate login page, be it the local one or the cloud one + // The root route will redirect to appropriate login page, be it the local one or the cloud one navigate("/"); }, [navigate, setUser]); const usbState = useHidStore(state => state.usbState); return ( -
+
-
-
+
+
@@ -74,10 +76,10 @@ export default function DashboardNavbar({ })}
-
-
+
+
{showConnectionStatus && ( -
+
@@ -104,66 +106,55 @@ export default function DashboardNavbar({ text={ <> {picture ? <> : userEmail} - + } - LeadingIcon={({ className }) => ( + LeadingIcon={({ className }) => picture && ( Avatar ) - )} + } />
- - - -
- {userEmail && ( -
- -
-
- Logged in as -
-
- {userEmail} -
-
-
-
- )} -
+ + + +
+ {userEmail && ( +
-
- +
+
Logged in as
+
+ {userEmail} +
+ )} +
+ +
+ +
+
- - - +
+ + ) : null} diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index dbbaa40..d70d590 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; + import { cx } from "@/cva.config"; import { useHidStore, @@ -7,7 +9,6 @@ import { useVideoStore, useKeyboardMappingsStore, } from "@/hooks/stores"; -import { useEffect, useState } from "react"; export default function InfoBar() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); @@ -25,6 +26,7 @@ export default function InfoBar() { const activeModifiers = useHidStore(state => state.activeModifiers); const mouseX = useMouseStore(state => state.mouseX); const mouseY = useMouseStore(state => state.mouseY); + const mouseMove = useMouseStore(state => state.mouseMove); const videoClientSize = useVideoStore( state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, @@ -73,7 +75,7 @@ export default function InfoBar() {
) : null} - {settings.debugMode ? ( + {(settings.debugMode && settings.mouseMode == "absolute") ? (
Pointer: @@ -82,6 +84,17 @@ export default function InfoBar() {
) : null} + {(settings.debugMode && settings.mouseMode == "relative") ? ( +
+ Last Move: + + {mouseMove ? + `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : + "N/A"} + +
+ ) : null} + {settings.debugMode && (
USB State: diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx index 57db7d8..2f580a0 100644 --- a/ui/src/components/InputField.tsx +++ b/ui/src/components/InputField.tsx @@ -1,7 +1,8 @@ import type { Ref } from "react"; import React, { forwardRef } from "react"; -import FieldLabel from "@/components/FieldLabel"; import clsx from "clsx"; + +import FieldLabel from "@/components/FieldLabel"; import Card from "@/components/Card"; import { cva } from "@/cva.config"; @@ -84,7 +85,7 @@ const InputFieldWithLabel = forwardRef )} - +
); }, diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index ec7fc62..c680a37 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -1,10 +1,11 @@ -import { Button, LinkButton } from "@components/Button"; -import Card from "@components/Card"; import { MdConnectWithoutContact } from "react-icons/md"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Link } from "react-router-dom"; import { LuEllipsisVertical } from "react-icons/lu"; +import Card from "@components/Card"; +import { Button, LinkButton } from "@components/Button"; + function getRelativeTimeString(date: Date | number, lang = navigator.language): string { // Allow dates or times to be passed const timeMs = typeof date === "number" ? date : date.getTime(); @@ -12,7 +13,7 @@ function getRelativeTimeString(date: Date | number, lang = navigator.language): // Get the amount of seconds between the given date and now const deltaSeconds = Math.round((timeMs - Date.now()) / 1000); - // Array reprsenting one minute, hour, day, week, month, etc in seconds + // Array representing one minute, hour, day, week, month, etc in seconds const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; // Array equivalent to the above but in the string representation of the units @@ -52,7 +53,7 @@ export default function KvmCard({ return (
-
+
{title} diff --git a/ui/src/components/MacroBar.tsx b/ui/src/components/MacroBar.tsx new file mode 100644 index 0000000..066c21f --- /dev/null +++ b/ui/src/components/MacroBar.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { LuCommand } from "react-icons/lu"; + +import { Button } from "@components/Button"; +import Container from "@components/Container"; +import { useMacrosStore } from "@/hooks/stores"; +import useKeyboard from "@/hooks/useKeyboard"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; + +export default function MacroBar() { + const { macros, initialized, loadMacros, setSendFn } = useMacrosStore(); + const { executeMacro } = useKeyboard(); + const [send] = useJsonRpc(); + + useEffect(() => { + setSendFn(send); + + if (!initialized) { + loadMacros(); + } + }, [initialized, loadMacros, setSendFn, send]); + + if (macros.length === 0) { + return null; + } + + return ( + +
+
+ +
+
+ {macros.map(macro => ( +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/MacroForm.tsx new file mode 100644 index 0000000..135817c --- /dev/null +++ b/ui/src/components/MacroForm.tsx @@ -0,0 +1,271 @@ +import { useState } from "react"; + +import { LuPlus } from "react-icons/lu"; + +import { KeySequence } from "@/hooks/stores"; +import { Button } from "@/components/Button"; +import { InputFieldWithLabel, FieldError } from "@/components/InputField"; +import Fieldset from "@/components/Fieldset"; +import { MacroStepCard } from "@/components/MacroStepCard"; +import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +interface ValidationErrors { + name?: string; + steps?: Record; +} + +interface MacroFormProps { + initialData: Partial; + onSubmit: (macro: Partial) => Promise; + onCancel: () => void; + isSubmitting?: boolean; + submitText?: string; +} + +export function MacroForm({ + initialData, + onSubmit, + onCancel, + isSubmitting = false, + submitText = "Save Macro", +}: MacroFormProps) { + const [macro, setMacro] = useState>(initialData); + const [keyQueries, setKeyQueries] = useState>({}); + const [errors, setErrors] = useState({}); + const [errorMessage, setErrorMessage] = useState(null); + + const showTemporaryError = (message: string) => { + setErrorMessage(message); + setTimeout(() => setErrorMessage(null), 3000); + }; + + const validateForm = (): boolean => { + const newErrors: ValidationErrors = {}; + + // Name validation + if (!macro.name?.trim()) { + newErrors.name = "Name is required"; + } else if (macro.name.trim().length > 50) { + newErrors.name = "Name must be less than 50 characters"; + } + + if (!macro.steps?.length) { + newErrors.steps = { 0: { keys: "At least one step is required" } }; + } else { + const hasKeyOrModifier = macro.steps.some(step => + (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0 + ); + + if (!hasKeyOrModifier) { + newErrors.steps = { 0: { keys: "At least one step must have keys or modifiers" } }; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + showTemporaryError("Please fix the validation errors"); + return; + } + + try { + await onSubmit(macro); + } catch (error) { + if (error instanceof Error) { + showTemporaryError(error.message); + } else { + showTemporaryError("An error occurred while saving"); + } + } + }; + + const handleKeySelect = (stepIndex: number, option: { value: string | null; keys?: string[] }) => { + const newSteps = [...(macro.steps || [])]; + if (!newSteps[stepIndex]) return; + + if (option.keys) { + newSteps[stepIndex].keys = option.keys; + } else if (option.value) { + if (!newSteps[stepIndex].keys) { + newSteps[stepIndex].keys = []; + } + const keysArray = Array.isArray(newSteps[stepIndex].keys) ? newSteps[stepIndex].keys : []; + if (keysArray.length >= MAX_KEYS_PER_STEP) { + showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`); + return; + } + newSteps[stepIndex].keys = [...keysArray, option.value]; + } + setMacro({ ...macro, steps: newSteps }); + + if (errors.steps?.[stepIndex]?.keys) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleKeyQueryChange = (stepIndex: number, query: string) => { + setKeyQueries(prev => ({ ...prev, [stepIndex]: query })); + }; + + const handleModifierChange = (stepIndex: number, modifiers: string[]) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].modifiers = modifiers; + setMacro({ ...macro, steps: newSteps }); + + // Clear step errors when modifiers are added + if (errors.steps?.[stepIndex]?.keys && modifiers.length > 0) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleDelayChange = (stepIndex: number, delay: number) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].delay = delay; + setMacro({ ...macro, steps: newSteps }); + }; + + const handleStepMove = (stepIndex: number, direction: 'up' | 'down') => { + const newSteps = [...(macro.steps || [])]; + const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1; + [newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]]; + setMacro({ ...macro, steps: newSteps }); + }; + + const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO; + + return ( + <> +
+
+ { + setMacro(prev => ({ ...prev, name: e.target.value })); + if (errors.name) { + const newErrors = { ...errors }; + delete newErrors.name; + setErrors(newErrors); + } + }} + /> +
+ +
+
+
+ +
+ + {macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps + +
+ {errors.steps && errors.steps[0]?.keys && ( +
+ +
+ )} +
+
+ {(macro.steps || []).map((step, stepIndex) => ( + 1 ? () => { + const newSteps = [...(macro.steps || [])]; + newSteps.splice(stepIndex, 1); + setMacro(prev => ({ ...prev, steps: newSteps })); + } : undefined} + onMoveUp={() => handleStepMove(stepIndex, 'up')} + onMoveDown={() => handleStepMove(stepIndex, 'down')} + onKeySelect={(option) => handleKeySelect(stepIndex, option)} + onKeyQueryChange={(query) => handleKeyQueryChange(stepIndex, query)} + keyQuery={keyQueries[stepIndex] || ''} + onModifierChange={(modifiers) => handleModifierChange(stepIndex, modifiers)} + onDelayChange={(delay) => handleDelayChange(stepIndex, delay)} + isLastStep={stepIndex === (macro.steps?.length || 0) - 1} + /> + ))} +
+
+ +
+
+ + {errorMessage && ( +
+ +
+ )} + +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx new file mode 100644 index 0000000..8642c28 --- /dev/null +++ b/ui/src/components/MacroStepCard.tsx @@ -0,0 +1,235 @@ +import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import { Combobox } from "@/components/Combobox"; +import { SelectMenuBasic } from "@/components/SelectMenuBasic"; +import Card from "@/components/Card"; +import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; +import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +// Filter out modifier keys since they're handled in the modifiers section +const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; + +const keyOptions = Object.keys(keys) + .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) + .map(key => ({ + value: key, + label: keyDisplayMap[key] || key, + })); + +const modifierOptions = Object.keys(modifiers).map(modifier => ({ + value: modifier, + label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), +})); + +const groupedModifiers: Record = { + Control: modifierOptions.filter(mod => mod.value.startsWith('Control')), + Shift: modifierOptions.filter(mod => mod.value.startsWith('Shift')), + Alt: modifierOptions.filter(mod => mod.value.startsWith('Alt')), + Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')), +}; + +const basePresetDelays = [ + { value: "50", label: "50ms" }, + { value: "100", label: "100ms" }, + { value: "200", label: "200ms" }, + { value: "300", label: "300ms" }, + { value: "500", label: "500ms" }, + { value: "750", label: "750ms" }, + { value: "1000", label: "1000ms" }, + { value: "1500", label: "1500ms" }, + { value: "2000", label: "2000ms" }, +]; + +const PRESET_DELAYS = basePresetDelays.map(delay => { + if (parseInt(delay.value, 10) === DEFAULT_DELAY) { + return { ...delay, label: "Default" }; + } + return delay; +}); + +interface MacroStep { + keys: string[]; + modifiers: string[]; + delay: number; +} + +interface MacroStepCardProps { + step: MacroStep; + stepIndex: number; + onDelete?: () => void; + onMoveUp?: () => void; + onMoveDown?: () => void; + onKeySelect: (option: { value: string | null; keys?: string[] }) => void; + onKeyQueryChange: (query: string) => void; + keyQuery: string; + onModifierChange: (modifiers: string[]) => void; + onDelayChange: (delay: number) => void; + isLastStep: boolean; +} + +const ensureArray = (arr: T[] | null | undefined): T[] => { + return Array.isArray(arr) ? arr : []; +}; + +export function MacroStepCard({ + step, + stepIndex, + onDelete, + onMoveUp, + onMoveDown, + onKeySelect, + onKeyQueryChange, + keyQuery, + onModifierChange, + onDelayChange, + isLastStep +}: MacroStepCardProps) { + const getFilteredKeys = () => { + const selectedKeys = ensureArray(step.keys); + const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); + + if (keyQuery === '') { + return availableKeys; + } else { + return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); + } + }; + + return ( + +
+
+ + {stepIndex + 1} + +
+ +
+
+
+ {onDelete && ( +
+
+ +
+
+ +
+ {Object.entries(groupedModifiers).map(([group, mods]) => ( +
+ + {group} + +
+ {mods.map(option => ( +
+
+ ))} +
+
+ +
+
+ +
+ {ensureArray(step.keys) && step.keys.length > 0 && ( +
+ {step.keys.map((key, keyIndex) => ( + + + {keyDisplayMap[key] || key} + +
+ )} +
+ { + onKeySelect(value); + onKeyQueryChange(''); + }} + displayValue={() => keyQuery} + onInputChange={onKeyQueryChange} + options={getFilteredKeys} + disabledMessage="Max keys reached" + size="SM" + immediate + disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP} + placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."} + emptyMessage="No matching keys found" + /> +
+
+ +
+
+ +
+
+ onDelayChange(parseInt(e.target.value, 10))} + options={PRESET_DELAYS} + /> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx index 886469d..e395445 100644 --- a/ui/src/components/Modal.tsx +++ b/ui/src/components/Modal.tsx @@ -1,8 +1,9 @@ import React from "react"; import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react"; + import { cx } from "@/cva.config"; -export default function Modal({ +const Modal = React.memo(function Modal({ children, className, open, @@ -14,25 +15,28 @@ export default function Modal({ onClose: () => void; }) { return ( - + - -
-
+
+ {/* TODO: This doesn't work well with other-sessions */} +
-
+
-
e.stopPropagation()}> +
e.stopPropagation()} + > {children}
@@ -42,4 +46,6 @@ export default function Modal({
); -} +}); + +export default Modal; diff --git a/ui/src/components/NotFoundPage.tsx b/ui/src/components/NotFoundPage.tsx index c89b618..b499b11 100644 --- a/ui/src/components/NotFoundPage.tsx +++ b/ui/src/components/NotFoundPage.tsx @@ -1,4 +1,5 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; + import EmptyCard from "@/components/EmptyCard"; export default function NotFoundPage() { diff --git a/ui/src/components/PeerConnectionStatusCard.tsx b/ui/src/components/PeerConnectionStatusCard.tsx index ca0621f..98025cd 100644 --- a/ui/src/components/PeerConnectionStatusCard.tsx +++ b/ui/src/components/PeerConnectionStatusCard.tsx @@ -9,21 +9,22 @@ const PeerConnectionStatusMap = { failed: "Connection failed", closed: "Closed", new: "Connecting", -}; +} as Record; export type PeerConnections = keyof typeof PeerConnectionStatusMap; -type StatusProps = { - [key in PeerConnections]: { +type StatusProps = Record< + PeerConnections, + { statusIndicatorClassName: string; - }; -}; + } +>; export default function PeerConnectionStatusCard({ state, title, }: { - state?: PeerConnections; + state?: RTCPeerConnectionState | null; title?: string; }) { if (!state) return null; diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index f515eb3..066603a 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -1,9 +1,12 @@ import React from "react"; -import FieldLabel from "@/components/FieldLabel"; import clsx from "clsx"; -import Card from "./Card"; + +import FieldLabel from "@/components/FieldLabel"; import { cva } from "@/cva.config"; +import Card from "./Card"; + + type SelectMenuProps = Pick< JSX.IntrinsicElements["select"], "disabled" | "onChange" | "name" | "value" @@ -19,7 +22,7 @@ type SelectMenuProps = Pick< direction?: "vertical" | "horizontal"; error?: string; fullWidth?: boolean; -} & React.ComponentProps; +} & Partial>; const sizes = { XS: "h-[24.5px] pl-3 pr-8 text-xs", @@ -61,7 +64,7 @@ export const SelectMenuBasic = React.forwardRef {label && } - +